diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f2730a3..821b7de7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,12 +23,14 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + # No macOS on PRs, main or nightly: the org's 5 macOS slots are kept for release builds, + # and release.yml builds macOS (a failing release is fixed and rebuilt). + os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v4 # The optional craft-fonts build input (Japanese fonts), so the Japanese font tests run on - # Linux; macOS and Windows test the build without it. See craftrules standards/fonts.md. + # Linux; Windows tests the build without it. See craftrules standards/fonts.md. - name: Check out craft-fonts (Linux) if: runner.os == 'Linux' uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 4e3221a9..afe30062 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -4,7 +4,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE). -## In this repository (249) +## In this repository (257) | Asset | Title | Author | Licence | Source | Used for | |---|---|---|---|---|---| @@ -14,7 +14,13 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `crates/ui-egui/src/i18n/pt-br.tsv` | Brazilian Portuguese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/es.tsv` | Spanish interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/fr.tsv` | French interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/de.tsv` | German interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/te.tsv` | Telugu interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/ru.tsv` | Russian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/hu.tsv` | Hungarian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/bg.tsv` | Bulgarian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `crates/ui-egui/src/i18n/ja.tsv` | Japanese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/uk.tsv` | Ukrainian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `assets/icons/accessibility.svg` | Lucide icon "accessibility" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/accessibility.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/align-left.svg` | Lucide icon "align-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/align-center.svg` | Lucide icon "align-center" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-center.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | @@ -257,6 +263,8 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `crates/sign/tests/data/openssl-signed.pdf` | One-page PDF signed with OpenSSL CMS (adbe.pkcs7.detached) by the RSA test key | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: hand-written PDF objects; signature made with `openssl cms -sign` over its byte ranges (see crates/sign/tests/data/README.md) | Signature validation tests (crates/sign/tests/pdf.rs): a signature PdfCraft did not make | | `assets/icons/clipboard-paste.svg` | Lucide icon "clipboard-paste" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/clipboard-paste.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/fonts/DancingScript.ttf` | Dancing Script (variable) | The Dancing Script Project Authors (Pablo Impallari) | OFL-1.1 | https://github.com/google/fonts/blob/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/dancingscript/DancingScript%5Bwght%5D.ttf | Fill & Sign typed signatures and initials: the typed name's glyph outlines, drawn as filled paths (the font file itself is not embedded in PDFs) | +| `packaging/macos/dmg/background.svg` | PdfCraft macOS DMG window background | @XusBadia | MIT OR Apache-2.0 | Original work for this repository, derived from the PdfCraft app icon (assets/app-icon/pdfcraft.svg, referenced via an SVG image element, not copied) | Finder window of the macOS DMG; rendered to background.tiff by packaging/macos/dmg/generate.py | +| `packaging/macos/dmg/background.tiff` | PdfCraft macOS DMG window background (rendered) | @XusBadia | MIT OR Apache-2.0 | Rendered from packaging/macos/dmg/background.svg by packaging/macos/dmg/generate.py | 1x + 2x HiDPI 16-colour palette TIFF copied into the macOS DMG by packaging/macos/package.sh; includes the app icon and the free LittleCMS built-in sRGB profile | ## Compiled in through dependencies (6) diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 36087581..b33510f3 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -19,7 +19,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "5da2aa2944fcad4720972062f0f97800f73b8bfa5dd0fff6c28f4ec0f8ee19f9" +sha256 = "2d56f8464f3c25efe9737b204db34cef012ea3716f4d78209d9abeaf0ea9393b" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" @@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "39f83ab539f6380df995a855c1c8e8c036bcd060e06b9d1b027771e1e7d6e7ef" +sha256 = "b0ca648560c45e554447aae8dba37c1c9c636c86b277d00fceeb2a21b474c8f7" [[asset]] path = "crates/ui-egui/src/i18n/cs.tsv" @@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "accf181cff1f0ea86bb33528f6158d1aec18c9c3ff54de62dc758536a24cb677" +sha256 = "8bab483a3f5ba3a724365c2a5a892291a10f41fdc443eaceb66f49c8a20cae20" [[asset]] path = "crates/ui-egui/src/i18n/pt-br.tsv" @@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "0ee4d2f630623ef7e06413a7c34a4a0269a2600fba682f06988a1346be4d07a5" +sha256 = "2cd5dcc6cf7e94914b955ee0f2f5c3e8951b16e26ab49fcb7111246a6e31ee2a" [[asset]] path = "crates/ui-egui/src/i18n/es.tsv" @@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "4c409434bd3385b56b561c36ff819e19e574bda8979d16cd53232cb5bea3c65f" +sha256 = "e9a9bad1252afdcb42c36b24ca85411f8c63df7debada94d4423bd48761ecb05" [[asset]] path = "crates/ui-egui/src/i18n/fr.tsv" @@ -74,7 +74,62 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "d322ac956218961d22828726a7fe0752b0a68603679c2dcf17c5aa840995e124" +sha256 = "0351382a91d2df51da3d45e25a09af87a081bc88e3496c3512ad89f5d4460a3a" + +[[asset]] +path = "crates/ui-egui/src/i18n/de.tsv" +title = "German interface translation catalog" +author = "PdfCraft contributors" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +licence = "MIT OR Apache-2.0" +licence_file = "LICENSE-MIT" +kind = "translation" +usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" +sha256 = "57f52e3671a322938a160ac11b73b1f55fb6311140f20d89ae12c9a91a0500d6" + +[[asset]] +path = "crates/ui-egui/src/i18n/te.tsv" +title = "Telugu interface translation catalog" +author = "PdfCraft contributors" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +licence = "MIT OR Apache-2.0" +licence_file = "LICENSE-MIT" +kind = "translation" +usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" +sha256 = "d3630f989e9725c22444dc27765142674b66f72436502ca969b2851fc38d703d" + +[[asset]] +path = "crates/ui-egui/src/i18n/ru.tsv" +title = "Russian interface translation catalog" +author = "PdfCraft contributors" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +licence = "MIT OR Apache-2.0" +licence_file = "LICENSE-MIT" +kind = "translation" +usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" +sha256 = "e505c00bcc8b8af6aec27abf84bebac7a1925c632130411000a6c57241816193" + +[[asset]] +path = "crates/ui-egui/src/i18n/hu.tsv" +title = "Hungarian interface translation catalog" +author = "PdfCraft contributors" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +licence = "MIT OR Apache-2.0" +licence_file = "LICENSE-MIT" +kind = "translation" +usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" +sha256 = "c198f1de831d7f9c853baa6977894ccc34d94e6b36c772c2e53fc5b0e0fe708b" + +[[asset]] +path = "crates/ui-egui/src/i18n/bg.tsv" +title = "Bulgarian interface translation catalog" +author = "PdfCraft contributors" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +licence = "MIT OR Apache-2.0" +licence_file = "LICENSE-MIT" +kind = "translation" +usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" +sha256 = "9682b2c3c28449dc63637cb5962b309df3ad7db7e8d96bf8bb0f0bc386c9c611" [[asset]] path = "crates/ui-egui/src/i18n/ja.tsv" @@ -85,7 +140,18 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "4b14f85bc0890823c30bff37723ecb05bc5ea5a8583334644168b08043ab6bde" +sha256 = "319b032d2c50097ec14e2c7eab92e8696d33e198aeac3585ff46e98c61dec90f" + +[[asset]] +path = "crates/ui-egui/src/i18n/uk.tsv" +title = "Ukrainian interface translation catalog" +author = "PdfCraft contributors" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +licence = "MIT OR Apache-2.0" +licence_file = "LICENSE-MIT" +kind = "translation" +usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" +sha256 = "f3a03fc466c1f956cbd44a5cad101c53db5ffc80b3b7551a273ac41f43263dd5" [[asset]] path = "assets/icons/accessibility.svg" @@ -3083,6 +3149,29 @@ kind = "font" usage = "Fill & Sign typed signatures and initials: the typed name's glyph outlines, drawn as filled paths (the font file itself is not embedded in PDFs)" sha256 = "21808625578fe8d8cd10cb684be546dca077b27cd03a53a2f1ec11dc743c924c" +[[asset]] +path = "packaging/macos/dmg/background.svg" +title = "PdfCraft macOS DMG window background" +author = "@XusBadia" +source = "Original work for this repository, derived from the PdfCraft app icon (assets/app-icon/pdfcraft.svg, referenced via an SVG image element, not copied)" +licence = "MIT OR Apache-2.0" +licence_file = "assets/app-icon/LICENSE.txt" +kind = "image" +usage = "Finder window of the macOS DMG; rendered to background.tiff by packaging/macos/dmg/generate.py" +sha256 = "e8b9ea9d5e64b7b741c977543c1693825e77d5c4c579eeb674bf5dcd3764cd8a" + + +[[asset]] +path = "packaging/macos/dmg/background.tiff" +title = "PdfCraft macOS DMG window background (rendered)" +author = "@XusBadia" +source = "Rendered from packaging/macos/dmg/background.svg by packaging/macos/dmg/generate.py" +licence = "MIT OR Apache-2.0" +licence_file = "assets/app-icon/LICENSE.txt" +kind = "image" +usage = "1x + 2x HiDPI 16-colour palette TIFF copied into the macOS DMG by packaging/macos/package.sh; includes the app icon and the free LittleCMS built-in sRGB profile" +sha256 = "f46881724a6621f3686a612a79dbdce4f7dbe9e2025610e44c9a094341334c2c" + [[fetched]] file = "text-detection.rten" url = "https://ocrs-models.s3-accelerate.amazonaws.com/text-detection.rten" diff --git a/Cargo.lock b/Cargo.lock index 2520b60a..879c1cfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3477,6 +3477,7 @@ dependencies = [ "hayro-syntax", "pdfcraft-cos", "pdfcraft-fonts", + "pdfcraft-model", "thiserror 2.0.21", ] @@ -3486,8 +3487,11 @@ version = "0.4.0" dependencies = [ "base64 0.22.1", "image", + "pdfcraft-cos", "pdfcraft-engine", + "pdfcraft-model", "pdfcraft-organize", + "pdfcraft-platform", "pdfcraft-render", "pdfcraft-xfa", "png 0.18.1", @@ -3566,6 +3570,7 @@ dependencies = [ "pdfcraft-fonts", "pdfcraft-model", "thiserror 2.0.21", + "unicode-bidi", ] [[package]] @@ -3573,6 +3578,8 @@ name = "pdfcraft-engine" version = "0.4.0" dependencies = [ "chrono", + "icu_properties", + "idna", "image", "pdfcraft-a11y", "pdfcraft-annot", @@ -3598,6 +3605,7 @@ dependencies = [ "pdfcraft-xfdf", "png 0.18.1", "thiserror 2.0.21", + "url", ] [[package]] @@ -3621,6 +3629,7 @@ dependencies = [ name = "pdfcraft-fonts" version = "0.4.0" dependencies = [ + "harfrust", "pdfcraft-cos", "skrifa 0.44.0", ] @@ -3634,6 +3643,7 @@ dependencies = [ "pdfcraft-cos", "pdfcraft-fonts", "pdfcraft-model", + "pdfcraft-render", "thiserror 2.0.21", ] @@ -3700,6 +3710,10 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "pdfcraft-platform" +version = "0.4.0" + [[package]] name = "pdfcraft-preflight" version = "0.4.0" @@ -3736,12 +3750,15 @@ dependencies = [ name = "pdfcraft-render" version = "0.4.0" dependencies = [ + "bytemuck", "getrandom 0.4.3", "hayro", "hayro-syntax", "kurbo 0.13.1", "log", "lopdf", + "pdfcraft-fonts", + "skrifa 0.44.0", "thiserror 2.0.21", ] @@ -3777,6 +3794,7 @@ version = "0.4.0" dependencies = [ "arboard", "base64 0.22.1", + "bytemuck", "eframe", "egui", "egui_extras", @@ -3785,10 +3803,14 @@ dependencies = [ "image", "js-sys", "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", "pdfcraft-cos", "pdfcraft-engine", "pdfcraft-fonts", "pdfcraft-model", + "pdfcraft-platform", "pdfcraft-render", "pdfcraft-xfa", "png 0.18.1", @@ -3822,9 +3844,12 @@ name = "pdfcraft-xfa" version = "0.4.0" dependencies = [ "base64 0.22.1", + "image", "pdfcraft-cos", "pdfcraft-fonts", + "png 0.18.1", "roxmltree", + "sha2", "thiserror 2.0.21", ] @@ -6571,8 +6596,6 @@ 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", diff --git a/Cargo.toml b/Cargo.toml index 8a1c3b65..e80a0b46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ pdfcraft-forms = { path = "crates/forms", version = "0.4.0" } pdfcraft-xfa = { path = "crates/xfa", version = "0.4.0" } pdfcraft-render = { path = "crates/render", version = "0.4.0" } pdfcraft-engine = { path = "crates/engine", version = "0.4.0" } +pdfcraft-platform = { path = "crates/platform", version = "0.4.0" } pdfcraft-ui-egui = { path = "crates/ui-egui", version = "0.4.0" } pdfcraft-automation = { path = "crates/automation", version = "0.4.0", default-features = false } @@ -74,8 +75,14 @@ unsafe_code = "forbid" dbg_macro = "warn" todo = "warn" +# `"*"` matches every non-workspace package: crates.io dependencies and the +# vendored crates under `vendor/` (excluded from the workspace). Our own crates +# keep full debug info. Dependencies keep only line tables, so backtraces still +# show file and line, but debug info for the whole dependency graph stays out of +# every test binary (each UI test binary had a 500-600 MB PDB on Windows, #454). [profile.dev.package."*"] opt-level = 2 +debug = "line-tables-only" [profile.dev] opt-level = 1 @@ -91,3 +98,4 @@ hayro = { path = "vendor/hayro" } hayro-syntax = { path = "vendor/hayro-syntax" } hayro-jbig2 = { path = "vendor/hayro-jbig2" } lopdf = { path = "vendor/lopdf" } +winit = { path = "vendor/winit" } diff --git a/README.md b/README.md index e08d5551..43342478 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ Search the whole document as you type, step through matches with ⌘G ## Navigate long documents -Bookmarks, page thumbnails and the document's own page labels (i, ii, 1, 2…) keep you oriented in long documents. +Bookmarks, page thumbnails and the document's own page labels (i, ii, 1, 2…) keep you oriented in long documents. Search bookmark titles in the Bookmarks panel to find nested entries even when their parents are collapsed. Matches keep their ancestors for context; Clear restores the unfiltered tree without changing its expansion state. @@ -172,6 +172,8 @@ Open **Organize pages** to see every page at once: - *Atomic:* the file is written to a temporary copy, then swapped in. - *Verified:* independently checked with qpdf. +When open documents exceed the window width, scroll over the tab strip with the mouse wheel or trackpad, or use its horizontal scrollbar. Opening or switching to a document brings its tab into view. + Unsaved documents carry a dot on their tab, and closing or quitting asks before anything is lost. Changes are autosaved every minute. If PdfCraft ever quits unexpectedly, it offers to recover your work the next time it opens. Encrypted documents stay encrypted on disk. @@ -291,6 +293,8 @@ Every engine feature is reachable without the GUI, through one table of JSON-Sch { "mcpServers": { "pdfcraft": { "command": "pdfcraft-cli", "args": ["mcp", "--root", "/path/to/your/pdfs"] } } } ``` + `--compact` shrinks the tool list the agent has to read: `tools/list` returns about ten core tools plus `tool_search` and `tool_call`, which find and run every other tool, so the list costs far fewer tokens. Every tool still works. + `--root` confines every file the agent can read or write to one directory. Builds that should not include the server at all can use `cargo build -p pdfcraft-cli --no-default-features`. - **The Rust API** (`pdfcraft_automation::Automation::call`), for embedding. @@ -418,6 +422,10 @@ The honest assessment by area, what's lacking and where we're going are in **[RO Installers and executables are code-signed. +The portable zip runs from any folder, a USB stick included. Its `portable.txt` keeps the settings, +logs and crash recovery in a `PdfCraftData` folder next to `pdfcraft.exe`, so nothing is written to +`%APPDATA%`; delete that file to use the normal per-user folders. + The MSI installs for all users and requires administrator privileges. For unattended deployment without a desktop shortcut, run from an elevated terminal: diff --git a/ROADMAP.md b/ROADMAP.md index ecd96d48..ce510d9c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -79,7 +79,8 @@ The gaps that matter most, in priority order. Agents: prefer these over adding P 3. **Measure fidelity against Acrobat.** Build a side-by-side harness (render, text, form behaviour) over synthetic fixtures (`plan/acrobat/`, clean-room rules apply). Until then, parity numbers count features, not quality. 4. **Editing existing content (M7).** Fonts, subsets, reflow and CJK. The most visible gap for Pro users. 5. **Pro workflows.** Signatures with timestamps and LTV (M9), OCR beyond Latin (M10), Office import/export (M10), preflight and PDF/A/X/UA (M11), XFA (M12). -6. **1.0 polish (M14).** Code-signed installers, localization (the catalog system is in place and French, Spanish, Japanese and both Chinese catalogs cover the UI; Czech and Brazilian Portuguese still cover menus only), keyboard-only operation, a full screen-reader audit. +6. **1.0 polish (M14).** Code-signed installers, localization (the catalog system is in place and French, German, Spanish, Japanese, Russian and both Chinese catalogs cover the UI; Czech and Brazilian Portuguese still cover menus only), keyboard-only operation, a full screen-reader audit. +6. **1.0 polish (M14).** Code-signed installers, localization (the catalog system is in place and French, Spanish, Japanese, Russian, Ukrainian and both Chinese catalogs cover the UI; Czech and Brazilian Portuguese still cover menus only), keyboard-only operation, a full screen-reader audit. Decided against for now (owner, 2026-10-05): self-installing updates and an update check at start. Help ▸ Check for updates is manual only. @@ -90,20 +91,21 @@ Hours are for a single agent (low–high). "Done" is the estimated fraction of t | M | Milestone | Est. hours | Done | Remaining (h) | Notes | |---|---|---|---|---|---| | M0 | Skeleton: workspace, xtask gates, CI | 15–30 | 92% | 2–4 | GitHub workflow, `deny.toml` (licence audit of every dependency), parity checklist (826 features, `xtask parity`) done. Missing: remaining crate stubs, testkit/oracle crates | -| M1 | COS: filters, crypt, parser, xref, writer | 120–200 | 78% | 25–45 | Done:
- filters and crypt: every standard-security revision R2–R6 (RC4, AES-128/256), SASLprep, permissions, creating encryption;
- cos: parse and repair (including invalid xref object-number ranges), decrypt on load, re-encrypt on save, incremental and full writing.
Corpus: open/edit/save passes on 958 files, and all 7 password-protected files open.
Full saves now pack objects into compressed object streams. Fuzzing runs nightly (`xtask fuzz`). Missing: ≥ 250 tests, own image codecs | -| M2 | Model, render, text | 200–350 | 15% | 170–300 | hayro bootstrap renderer (vendored patches). Text extraction reaches word-F1 0.98 against pdftotext. Missing: model crate, fonts, DisplayList, renderer independent of hayro | +| M2 | Model, render, text | 200–350 | 15% | 170–300 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input. Text extraction reaches word-F1 0.98 against pdftotext. Missing: model crate, fonts, DisplayList, renderer independent of hayro | +| M1 | COS: filters, crypt, parser, xref, writer | 120–200 | 78% | 25–45 | Done:
- filters and crypt: every standard-security revision R2–R6 (RC4, AES-128/256), SASLprep, permissions, creating encryption;
- cos: parse and repair (including invalid xref object-number ranges), decoded-stream limits including unfiltered data, decrypt on load, re-encrypt on save, incremental and full writing.
Corpus: open/edit/save passes on 958 files, and all 7 password-protected files open.
Full saves now pack objects into compressed object streams. Fuzzing runs nightly (`xtask fuzz`). Missing: ≥ 250 tests, own image codecs | | M3 | Viewer app (native + web) | 80–150 | 88% | 10–19 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel. | -| M4 | Engine, history, save, organize | 100–180 | 95% | 9–17 | Done:
- command registry (menus, shortcuts and palette all use it);
- undo/redo; incremental, atomic and encrypted saves;
- autosave and crash recovery;
- organize, combine, extract, split and insert-from-file, with identical fonts and images stored once;
- CLI `edit/combine/extract/split`.
Done since: bookmark editing, page labels (Number pages), CLI `run`, Set Page Boxes, Crop tool, Duplicate pages, Ctrl+A / Cmd+A to select all pages in Organize. Missing: Replace pages, page transitions, recovery on the web | -| M5 | Comments (all annotation types, XFDF) | 120–200 | 88% | 15–25 | Done: notes, highlight/underline/strikeout/squiggly, text boxes, ink, lines, arrows, rectangles, ovals, stamps (dynamic, Sign Here, Standard Business), with appearance streams; replies, status, checkmarks, locking, move/resize/restyle/delete; properties dialog; panel filter and sort; hide all; summaries (comments only); XFDF/FDF import and export; flatten; Fill & Sign; agent tools. Done since: polygons, connected lines, clouds, callouts, inserted text, colour and checkmark filters. Missing: custom stamps, replace-text proposals, summary layouts with the page | +| M4 | Engine, history, save, organize | 100–180 | 95% | 9–17 | Done:
- command registry (menus, shortcuts and palette all use it);
- undo/redo; incremental, atomic and encrypted saves;
- autosave and crash recovery;
- organize, combine, extract, split and insert-from-file, with identical fonts and images stored once;
- CLI `edit/combine/extract/split`.
Done since: Combine files in its own tab (sortable, resizable columns; folders; early warnings; unlocking protected files), bookmark editing, page labels (Number pages), CLI `run`, Set Page Boxes, Crop tool, Duplicate pages, Ctrl+A / Cmd+A to select all pages in Organize. Missing: Replace pages, page transitions, recovery on the web | +| M5 | Comments (all annotation types, XFDF) | 120–200 | 88% | 15–25 | Done: notes, highlight/underline/strikeout/squiggly, text boxes, ink, lines, arrows, rectangles, ovals, stamps (dynamic, Sign Here, Standard Business), with appearance streams; replies, status, checkmarks, locking, move/resize/restyle/delete; properties dialog; panel filter and sort; hide all; summaries (comments only); XFDF/FDF import and export; flatten; Fill & Sign; agent tools. Done since: polygons, connected lines, clouds, callouts, inserted text, colour and checkmark filters; image signature pointer previews, selection handles and live image movement/resizing with proportional corners and independent edge handles. Missing: custom stamps, replace-text proposals, summary layouts with the page | | M6 | Forms + JavaScript | 160–320 | 45% | 95–185 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done. Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA | | M7 | Content editing (text, images, header/footer, watermark) | 250–500 | 17% | 210–420 | Done: header & footer, watermarks, backgrounds, Bates; added text and images that stay editable (move, resize, format, rotate, flip, crop, replace); links (Link tool, Link Properties, create from URLs, remove all). Missing: editing existing text and images in place (the longest pole), image/PDF watermarks | | M8 | Security + redaction | 100–180 | 66% | 35–65 | Done: opening protected documents, permissions, Protect Using Password, Remove security; redaction (mark text/areas/pages, Search & Redact with patterns, apply removing glyphs, image pixels, vectors, XObject content, annotations and fields, verification, full rewrite on save); Remove hidden information and Sanitize. Missing: certificate security, redaction codes and pattern locales, DCT re-encoding | | M9 | Signatures (PAdES, validation) | 160–280 | 55% | 70–125 | Done: new `sign` crate (DER, X.509, CMS, PKCS #12 on RustCrypto; aws-lc-rs for RSA private keys); PAdES B-B signing (visible, invisible, existing fields, certification with DocMDP); validation with trust store and changes-after-signing classification; self-signed digital IDs; Signatures panel, message bar, sign dialogs; agent tools. Checked with pdfsig and OpenSSL. Done since: macOS Keychain and Windows Current User Personal store signing (software-backed CNG keys), certificate viewer, signed documents protected from rewrites. Missing: timestamps (B-T), LTV (DSS, OCSP, CRL), FieldMDP, smart cards, PKCS #11 | -| M10 | OCR, create, export, print | 200–350 | 33% | 130–235 | Done: create from blank/text/PNG/JPEG/TIFF (multi-page)/GIF/BMP; export PNG/JPEG/TIFF and text; Print (Acrobat's sizing, n-up, booklet, poster, comments & forms, preview, CUPS spooler, print-ready PDF). Done since: embedded/72/custom DPI choices for image imports; export all images; OCR (searchable image for pages, ranges and multiple files); single-sided cut-and-stack imposition with cut marks, through Print and doc_print. Missing: OCR languages beyond Latin, editable-text OCR output, Office export/import, Windows/web printing | +| M10 | OCR, create, export, print | 200–350 | 33% | 130–235 | Done: create from blank/text/PNG/JPEG/TIFF (multi-page)/GIF/BMP; export PNG/JPEG/TIFF and text; Print (Acrobat's sizing, n-up, booklet, poster, comments & forms, preview, CUPS spooler, print-ready PDF). Done since: embedded/72/custom DPI choices for image imports; export all images; OCR (searchable image for pages, ranges and multiple files); single-sided cut-and-stack imposition with cut marks, through Print and doc_print; Create PDF from multiple files (PDFs, images and text, edited as pages in the grid). Missing: OCR languages beyond Latin, editable-text OCR output, Office export/import, Windows/web printing | | M11 | Optimize, preflight, PDF/A/X/UA, print production | 200–350 | 18% | 165–290 | Done: new `optimize` crate: Reduce File Size and the PDF Optimizer (images measured where drawn, bicubic downsampling, JPEG/ZIP recompression only when smaller, discard objects and user data, Flate clean-up, resource merging, object streams). Missing: fonts and transparency panels, space audit, preflight, PDF/A/X/UA | | M12 | Accessibility, compare, measure, search, XFA | 200–380 | 23% | 155–295 | Done: new `a11y` crate with the Accessibility Checker (all 32 rules, report, Fix/Skip/Explain, options dialog and results panel, agent tools). Done since: 2D distance, perimeter and area measurements with persistent viewport calibration, snapping, live information and CSV export. Missing: autotag, Tags/Order/Content panels, Reading Order tool, alt-text workflow, compare, geospatial/3D measurement, search index, XFA | | M13 | Automation (MCP, Action Wizard, CLI) + AI providers | 60–120 | 45% | 33–66 | Done: MCP resources (document info, text, page images); headless tool table (123 tools incl. signing, optimizing, initial view, links, stamps, data exchange, comment review, forms authoring and scripts, redaction, sanitize, print, add content), opt-in MCP server over stdio, CLI `run`/`tools` (closed stdout pipes exit cleanly), UI control channel with drag. Missing: Action Wizard, AI providers | -| M14 | 1.0 polish: performance, localization, installers | 120–250 | 5% | 115–240 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, strict catalog tests); every dialog, panel and notice goes through `tl!`; Japanese, Traditional Chinese, Simplified Chinese, Spanish and French catalogs have ≈1,800 entries each; Simplified Chinese and French coverage is checked against the UI source; Czech and Brazilian Portuguese for menus. French locales (`fr`, `fr_FR`, `fr_CA`) follow the French catalog. Missing: more catalogs filled in, installers, performance budgets | +| M14 | 1.0 polish: performance, localization, installers | 120–250 | 5% | 115–240 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, strict catalog tests); every dialog, panel and notice goes through `tl!`; Japanese, Traditional Chinese, Simplified Chinese, Russian, German, Spanish and French catalogs have ≈1,800 entries each; Simplified Chinese, French and German coverage is checked against the UI source; Czech and Brazilian Portuguese for menus. French locales (`fr`, `fr_FR`, `fr_CA`) follow the French catalog. Missing: more catalogs filled in, installers, performance budgets | +| M14 | 1.0 polish: performance, localization, installers | 120–250 | 5% | 115–240 | Done: PhotoCraft's translation system (`i18n/`: TSV catalogs, `tl!`, command-id and plural entries, system-language detection, strict catalog tests); every dialog, panel and notice goes through `tl!`; Japanese, Traditional Chinese, Simplified Chinese, Russian, Spanish, French and Ukrainian catalogs have ≈1,800–2,000 entries each; Simplified Chinese, French and Ukrainian coverage is checked against the UI source; Czech and Brazilian Portuguese for menus. French locales (`fr`, `fr_FR`, `fr_CA`) follow the French catalog; Ukrainian locales (`uk`, `uk_UA`) follow the Ukrainian one. Missing: more catalogs filled in, installers, performance budgets | | | **Total (original plan sizing)** | **2,085–3,840** | **≈ 35%** | **≈ 1,350–2,500 at the planned rate; ≈ 600–1,100 at the measured rate** | | **Overall progress: about 30–35% of the effort (51.0% of features shipped).** The viewer and the core are far ahead of the editing features, because the viewer was built first so progress could be seen; rendering itself is still borrowed from `hayro`. See [Honest assessment](#honest-assessment-2026-10-05). @@ -125,14 +127,48 @@ M0 → M1 → M2 → M3 → M4 must happen in order. After M4, M5–M12 can run ## Log +- **2026-10-09 (M5, line endings):** Lines, connected lines and callouts draw all ten standard line-ending styles. `comment_add` accepts `endings`, and Comment properties can change them in one undo step. An unknown name is left undrawn, and a non-finite direction is rejected. Cloudy borders remain polygon-only, so `comment.line-endings` stays partial. Overall estimate unchanged (about 30–35%). +- **2026-10-09 (M6, field rotation):** Regenerated form appearances honour `/MK /R` (0, 90, 180, 270). Filling or restyling a rotated field keeps the text, check mark and button caption turned, and Field Properties ▸ Position and `form_set_props` `rotation` set it, swapping width and height around the field's center when the axis changes. `form.appearance-regeneration` stays partial (Helvetica metrics are still approximate). Overall estimate unchanged (about 30–35%). +- **2026-10-09 (M5.7, #434 partial):** Natural-size image stamps placed at a click or through `stamp_custom` remain upright on rotated pages, with a displayed-space-sized rectangle and a counterrotated standard appearance matrix. Move, resize, restyle and undo/redo preserve that orientation. Native and external Poppler save/reopen rendering cover all four quarter-turns; UI tests verify the displayed bounds. Explicit rectangle placement, image signatures, PDF-page stamps and image-file orientation metadata remain outside this repair. #434 stays open; milestone table and effort estimates remain unchanged. +- **2026-10-09 (M1, raw-stream decoding limits):** COS checks unfiltered decoded data against the caller and per-stream limits before copying it, including empty filter arrays and streams whose Crypt filter was applied on load. Strict decoding uses the same raw-data guard. Tiny synthetic regressions cover exceeded, exact and zero-byte limits while retaining the filtered decoder behavior. Milestone and overall effort estimates unchanged (about 30–35%). + +- **2026-10-09 (M7, grouped artwork):** Edit text/images selects page-drawn Form XObjects as whole figures, including nested raster/vector/text content. Their BBox and optional Matrix determine the selection; move/resize/rotate/flip/delete rewrite only the selected page placement, preserving shared Forms and other placements. Engine, automation and UI core regressions cover geometry, rendered movement, save/reopen, undo and selection/drag/Delete. Bitmap replacement/extraction remain raster-only; inline images and individual elements inside a Form are not editable through this tool. Overall estimate unchanged (about 30–35%). + +- **2026-10-08 (M5.7, signature resize proportions):** Image signature corner handles preserve the embedded image's original aspect ratio and anchor the opposite corner; edge midpoint handles stretch width or height independently. The live preview and committed edit share the constrained geometry, including page/view rotation and reopened PDFs whose rectangles were stretched previously. Synthetic UI tests cover all corners, edge resizing, undo and redo. Milestone and effort estimates unchanged. +- **2026-10-08 (M5.7, live signature adjustment):** Embedded image signatures resize and move with every pointer frame. Movement hides the frame and handles, which return on release; Escape cancels without committing. A cached background preserves the page beneath the image, and only the completed gesture enters undo history. The read-only engine preview and `comment_image_preview` tool share bounded image decoding and render layers. Synthetic pixel tests cover live geometry, alpha, coloured page content, rotation, cancellation and encrypted save/reopen. Milestone and effort estimates unchanged. +- **2026-10-08 (M10/M4, create from multiple files):** Create a PDF ▸ Multiple files is live. The picked PDFs, images and `.txt` files open as one unsaved document in the Organize page grid, where pages are removed and reordered and Save pages writes what is shown. The grid gained a "+" at every gap that inserts files right there, and on desktop files dropped on the grid go to the gap under the pointer; Insert from file now takes several files in order, converts images and text, and selects what it inserted. File-type detection moved from the shell into `pdfcraft-create` (`source_kind`), with one engine entry point (`Session::convert_to_pdf`) shared by Open, Insert, Create and the new `doc_create_multiple` tool (which can also write one PDF per file). Not yet: Office/HTML/Markdown inputs, PDF Portfolios, progress for big batches, dropping onto the grid in the web build. Overall estimate unchanged (about 30–35%). + +- **2026-10-08 (M5.7, signature placement):** Image signatures and initials preview at the pointer at their actual page size, including zoom and document/view rotation. A successful placement switches to Select with the new annotation selected; stamp resize handles use the existing engine edit, automation tool and undo history while preserving the appearance. Synthetic rendered UI and automation tests verify positioning, one placement per selection, resizing, undo/redo and save/reopen. Milestone and overall effort estimates unchanged. +- **2026-10-08 (M5.7, image signatures):** Fill & Sign imports local PNG/JPEG signatures and initials through the Image tab, with previews, local persistence and undoable placement. The shared engine validates bounded images and preserves transparency and orientation; `fill_sign_add` accepts an image path. Synthetic UI and automation tests cover cancellation, invalid input, settings, undo/redo and save/reopen rendering. Overall effort estimate unchanged (about 30–35%). + - **2026-10-08 (M9, #179):** Windows Current User Personal certificate store identities join file-based digital IDs, signing through CNG without exporting keys. RSA-2048 and ECDSA P-256 round trips use temporary certificates with cleanup guards; enumeration, missing identities, automation errors and the password-free UI have regression coverage. Smart cards and PKCS #11 remain planned; overall effort estimate unchanged. Newest first. One line per session: the date, what moved, and the new overall percentage. +- **2026-10-09 (M14, German interface follow-up):** Follow the merged German catalog with shorter search/action labels, corrected callout/accessibility wording and a distinct certificate Subject context. Localized tool names retain their capitalization and comment composer titles go through the catalog. Search hints reserve shortcut space and Home cards bound text with full hover text; layout, context and German Preferences/search control regression tests cover these changes. Overall effort estimate remains about 30–35%. + +- **2026-10-09 (community, destinations):** Bookmarks and links go to their destination's position, not just its page (ISO 32000-2 §12.3.2.2): `/XYZ` puts (left, top) at the window's top-left and applies a non-zero zoom; `/Fit`, `/FitH` and `/FitV` switch to fit page, width and height with `top` or `left` at the window's edge; `/FitR` zooms to fit the rectangle, centred. Named destinations resolve the same way; crop-box origin, `/Rotate` and view rotation are accounted for; malformed or non-finite operands fall back to the top of the page. Not yet: the `/FitB*` content box (read as the page). Overall estimate unchanged (about 30–35%). + +- **2026-10-09 (community, M14, Hungarian):** Magyar (`hu`) is a complete interface catalog (2,036 entries) covering every command, All tools entry, dialog, panel and notice, selected automatically for `hu` and `hu_HU`, and from Preferences. Informal "te"; commands are nouns (Mentés, Megnyitás). After a numeral the noun stays singular, so both plural forms match. Coverage tests check commands, All tools labels and every `tl!` literal; history labels keep captured names; the bundled Latin faces cover Hungarian diacritics including ő and ű. Overall estimate unchanged (about 30–35%). +- **2026-10-09 (M14, Windows Auto):** Neutral launcher locales (`C`, `C.UTF-8`, `POSIX`) no longer select English ahead of the Windows preferred display-language list. Explicit supported locale overrides retain their priority. Regression tests use existing catalogs and cover neutral/mixed overrides, unsupported first display tags and multiple preferred Windows languages. Catalogs and other platforms are unchanged; overall effort estimate remains about 30–35%. + +- **2026-10-09 (community, M14, German):** Deutsch (`de`) is a complete interface catalog (2,035 entries) covering every command, All tools entry, dialog, panel and notice, selected automatically for `de`, `de_DE`, `de_AT`, `de_CH` and other German locales, and from Preferences. Informal "du", infinitive for commands. Coverage tests check commands, All tools labels and every `tl!` literal; history labels keep captured names; the bundled Latin faces cover umlauts, ß and German quotes. Overall estimate unchanged (about 30–35%). +- **2026-10-09 (community, M7, Arabic added text):** Add text writes Arabic into the PDF instead of `?`. The text is shaped with the craft-fonts `Arab` face through harfrust (already in the tree via egui): joining forms, ligatures and mark positions. Each line is laid out in display order by the Unicode bidirectional algorithm, so Arabic mixed with Latin words, European or Arabic-Indic digits and brackets reads correctly; justified right-to-left paragraphs end at the right edge. Each cluster (a letter with its dots or marks) becomes one glyph of a Type 3 font built from the face's outlines, with a ToUnicode entry, so `text_extract`, copy and search get every letter once and no font file is embedded. Wrapping measures shaped words. Each item has its own fonts under names free on the page (a full save renumbers objects but not resource names); updating or deleting the item removes them. Without the face, adding Arabic text returns a clear error and changes nothing, while an existing item holding Arabic still moves and retypes, drawn as `?` as before. Tests cover shaping, display order, ToUnicode, font names and clean-up on one page, and odd input (empty lines, direction controls, lone marks, 18,000 characters). Not yet: bold/italic Arabic, Hebrew, right-to-left typing in the box (misc.rtl-ui), editing existing Arabic text (edit.rtl-cjk-editing). The craft-fonts revision pinned for releases predates its Arabic face, so release builds report the missing font until the pin moves. Overall estimate unchanged (about 30–35%). +- **2026-10-09 (M2, predictor hardening):** Empty PNG predictor frames return without row buffers. Packed TIFF Predictor 2 rows reject overflowing dimensions and size per-component scratch to the samples present in the input, aligned with upstream lopdf#589. Synthetic regressions cover malformed dimensions, partial rows, 1/2/4-bit decoding and padding. Overall estimate unchanged (about 30–35%). +- **2026-10-09 (community, document inspection):** Annotation dates preserve non-date Unicode text instead of panicking while opening; failed auxiliary structure inspection keeps renderer page geometry and reports a warning. Synthetic UTF16BE annotation, UTF8-boundary and independent panic regressions cover the fix. Overall estimate unchanged (about 30–35%). + +- **2026-10-09 (community, M14, #503):** Ukrainian (Українська, `uk`) adds 2,047 original catalog entries covering commands, tools, dialogs, panels, notices, history and contributor controls. Auto resolves `uk`/`uk_UA`; the manual preference persists. Integer plurals distinguish one/few/many, including 11–14 and large counts; fixed-count templates use count-neutral wording. Calendar dates use genitive month names; Home cards fit wrapped text and the search hint keeps clear of its shortcut. Coverage checks scan UI literals, and font checks use bundled Inter and JetBrains Mono, including ґ/є/і/ї. Control tests check translated Preferences, command search, error framing and language switching with unsaved documents. Overall estimate unchanged (about 30–35%). + +- **2026-10-09 (community, Home pinned folders):** Home gets a Folders section: File ▸ Pin folder to Home… (or "Pin a folder…" on Home) pins up to 8 folders, and each lists its newest PDFs (name, how long ago, size; 8 at first, up to 100 with Show more), one click to open, Unpin to remove. Folders are read on a worker thread and listed again every few seconds while Home shows, so new scans appear without reopening anything; a folder that can't be read (moved, offline synced drive) stays pinned and says so. Each folder is described by where it is ("iCloud Drive", "OneDrive · Contoso", "Documents › Work") rather than its full path, so two folders called "Scans" can be told apart. Cloud drives work through their local synced folder; nothing is read through a provider's API. Settings keep only absolute paths, without duplicates. Command label translated in every catalog; Home strings in French, Spanish, Japanese and both Chinese catalogs. Not yet: subfolders, search inside a pinned folder, the web build. Overall estimate unchanged (about 30–35%). + +- **2026-10-09 (community, M14, Telugu):** Telugu (తెలుగు, `te`) as a complete catalog of 1,984 entries covering every command, All tools entry, dialog, panel and notice; `te` and `te_IN` locales follow it. Telugu text is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, storytold/craft-fonts#10), kept in the web build too, after the CJK and Arabic faces; egui shapes its conjuncts. Commands use the plain imperative and sentences the polite form. The craft-fonts revision pinned for releases has no Telugu face yet, so release builds show Telugu as replacement boxes until the pin moves. Overall estimate unchanged (about 30–35%). + - **2026-10-09 (M14, Simplified Chinese About):** The About tabs, contributor controls and statistics, and model table now have Simplified Chinese translations, along with multiline link-privacy and cut-and-stack guidance. Coverage checks include direct `i18n::t` calls, multiline literals and dynamic credit labels; UI control checks exercise the translated About tabs and tables. Chinese font delivery remains incomplete; overall ≈ 30–35% (unchanged). - **2026-10-09 (M14, #305):** Windows MSI rejects per-user installation overrides while allowing removal of legacy per-user installs and keeps its progress message visible. README documents unattended deployment without a desktop shortcut. Compiled x64 MSI checks pass; ARM64 install regressions added for CI. Overall estimate unchanged (about 30–35%). +- **2026-10-08 (M4, Combine files):** Combine files is now its own tab instead of a dialog: a table of the files (name, pages, size, modified, warnings) whose columns sort, resize and move, with the layout kept in the settings. Files, folders (optionally with subfolders) and open documents can be added, or dropped on the tab; rows select like a file manager (Ctrl/Shift), move as a block by toolbar, Alt+arrows or drag, and every change undoes with ⌘Z. Problems show before Combine: password-protected files, security settings that withhold page assembly, bad page ranges (checked as typed), duplicates; protected files unlock with their open or permissions password, also headlessly (`doc_combine` `passwords`). The result is never encrypted, and the list says so. `organize.combine-sort` shipped; `organize.combine-options` partial (open files). Still missing: protecting the result in the same step, file-size and PDF/A options. Overall estimate unchanged (about 30–35%). + - **2026-10-08 (M14, French):** Français (`fr`) is a complete interface catalog (about 1,880 entries), selected automatically for `fr`, `fr_FR`, `fr_CA` and other French locales, and from Preferences. Coverage tests check commands, All tools labels and every `tl!` literal; history labels keep captured names. Latin UI faces cover the catalog. Localization remains partial for Czech and Brazilian Portuguese; overall ≈ 30–35% (unchanged). - **2026-10-08 (M14, right-to-left file names):** A tab with an Arabic or Hebrew file name showed replacement boxes. The interface now takes an `Arab` face from craft-fonts when the build input has one, and on desktop falls back to one font installed on the machine for characters no embedded face has (`PDFCRAFT_SYSTEM_FONTS=0` turns that off; README screenshots do). File names and paths in the tab strip, the recent-files list and Properties are put in display order, since egui joins the letters but does not reorder runs. Still missing: multi-line text (bookmarks, comments, search results), text input and a mirrored layout, so `misc.rtl-ui` stays planned; the craft-fonts revision pinned for releases predates its Arabic face, so release builds rely on the installed font until the pin moves. Overall estimate unchanged (about 30–35%). @@ -238,3 +274,5 @@ Newest first. One line per session: the date, what moved, and the new overall pe - Robustness sweep (963 of 983 files open, 0 crashes), text layer, find and select, tiles, web build, polish. - Overall ≈ 3–4%. - **2026-09-30 (session 1):** planning complete; viewer vertical slice. + +- M13: common command, batch, inspection and bounded preview tools share the existing confined automation path. MCP tools carry complete hints and reject unknown top-level keys as invalid parameters. diff --git a/apps/pdfcraft-cli/src/main.rs b/apps/pdfcraft-cli/src/main.rs index 011d8320..1940d63e 100644 --- a/apps/pdfcraft-cli/src/main.rs +++ b/apps/pdfcraft-cli/src/main.rs @@ -1,22 +1,6 @@ //! pdfcraft-cli — headless PdfCraft. //! -//! ```text -//! pdfcraft-cli info [--password PW] document summary as JSON -//! pdfcraft-cli render --page N [--dpi 96] --out x.png (.png, .jpg, .tif or .pam) -//! pdfcraft-cli text [--page N] extracted text (pages separated by form feeds) -//! pdfcraft-cli edit --out out.pdf [--rotate 1,3:90] [--delete 2,4] [--move 5:1] -//! [--insert-blank 1] [--title T] [--author A] [--full] -//! pdfcraft-cli combine … --out combined.pdf -//! pdfcraft-cli extract --pages 1,3,5 --out out.pdf -//! pdfcraft-cli split (--every N | --before 3,7) [--out-dir DIR] -//! pdfcraft-cli check [--timeout 20] [--dpi 36] [--json out.json] -//! pdfcraft-cli tools automation tools and their JSON Schemas -//! pdfcraft-cli run [key=value …] [--root DIR] [--out image.png] -//! pdfcraft-cli run --script steps.json [--root DIR] [{"tool": "doc_open", "args": {…}}, …] -//! pdfcraft-cli mcp [--root DIR] MCP server on stdin/stdout (opt-in) -//! pdfcraft-cli ui --control FILE [key=value …] [--out shot.png] -//! drive a running app started with --control FILE -//! ``` +//! Usage: `pdfcraft-cli --help`, which prints `USAGE` below. //! //! `run` and `mcp` drive the same tool table (`pdfcraft-automation`). In `run`, values parse as //! JSON when they can (`pages=[1,3]`, `degrees=90`) and are strings otherwise. A script runs its @@ -42,28 +26,75 @@ use std::time::{Duration, Instant}; use pdfcraft_engine::export::ImageFormat; use pdfcraft_render::{PageRenderer, RenderConfig, RenderRequest, RequestKind, inspect}; +/// The `mcp` lines of [`USAGE`]: only a build with the `mcp` feature has the command. +#[cfg(feature = "mcp")] +macro_rules! mcp_usage { + () => { + "\ +pdfcraft-cli mcp [--root DIR] [--compact] MCP server on stdin/stdout (opt-in) + --compact lists a core set of tools plus tool_search and tool_call +" + }; +} +#[cfg(not(feature = "mcp"))] +macro_rules! mcp_usage { + () => { + "" + }; +} + +/// The full usage, printed by `--help` (also after a command, e.g. `render --help`). +const USAGE: &str = concat!( + "\ +pdfcraft-cli info [--password PW] document summary as JSON +pdfcraft-cli render --page N [--dpi 96] --out x.png (.png, .jpg, .tif or .pam) +pdfcraft-cli text [--page N] extracted text (pages separated by form feeds) +pdfcraft-cli edit --out out.pdf [--rotate 1,3:90] [--delete 2,4] [--move 5:1] + [--insert-blank 1] [--title T] [--author A] [--full] +pdfcraft-cli combine … --out combined.pdf +pdfcraft-cli extract --pages 1,3,5 --out out.pdf +pdfcraft-cli split (--every N | --before 3,7) [--out-dir DIR] +pdfcraft-cli check [--timeout 20] [--dpi 36] [--json out.json] +pdfcraft-cli tools automation tools and their JSON Schemas +pdfcraft-cli run [key=value …] [--root DIR] [--out image.png] +pdfcraft-cli run --script steps.json [--root DIR] [{\"tool\": \"doc_open\", \"args\": {…}}, …] +", + mcp_usage!(), + "\ +pdfcraft-cli ui --control FILE [key=value …] [--out shot.png] + drive a running app started with --control FILE +help and feedback: https://discord.gg/artcraft" +); + fn main() -> ExitCode { let args: Vec = std::env::args().skip(1).collect(); - let result = - match args.first().map(String::as_str) { - Some("info") => info(&args[1..]), - Some("render") => render(&args[1..]), - Some("text") => text(&args[1..]), - Some("edit") => edit(&args[1..]), - Some("combine") => combine(&args[1..]), - Some("extract") => extract(&args[1..]), - Some("split") => split(&args[1..]), - Some("check") => check(&args[1..]), - Some("check-one") => check_one(&args[1..]), - Some("tools") => tools(), - Some("run") => run(&args[1..]), - Some("ui") => ui(&args[1..]), - #[cfg(feature = "mcp")] - Some("mcp") => mcp(&args[1..]), - Some("--version") => version(), - _ => Err("usage: pdfcraft-cli … (see source header for options)\nhelp and feedback: https://discord.gg/artcraft" - .into()), - }; + // ` --help` too; only right after the command, so a later `-h` (a value) still reaches it. + let command = match args.get(1).map(String::as_str) { + Some("--help" | "-h") => Some("--help"), + _ => args.first().map(String::as_str), + }; + let result = match command { + Some("--help" | "-h" | "help") => stdout_line(format_args!("{USAGE}")), + None => Err(format!("usage:\n{USAGE}").into()), + Some("info") => info(&args[1..]), + Some("render") => render(&args[1..]), + Some("text") => text(&args[1..]), + Some("edit") => edit(&args[1..]), + Some("combine") => combine(&args[1..]), + Some("extract") => extract(&args[1..]), + Some("split") => split(&args[1..]), + Some("check") => check(&args[1..]), + Some("check-one") => check_one(&args[1..]), + Some("tools") => tools(), + Some("run") => run(&args[1..]), + Some("ui") => ui(&args[1..]), + #[cfg(feature = "mcp")] + Some("mcp") => mcp(&args[1..]), + #[cfg(not(feature = "mcp"))] + Some("mcp") => Err("mcp: this build leaves out the MCP server (built without the `mcp` feature)".into()), + Some("--version") => version(), + Some(other) => Err(format!("unknown command '{other}'; see pdfcraft-cli --help").into()), + }; match result { Ok(()) => ExitCode::SUCCESS, Err(CliError::Stdout(e)) if e.kind() == std::io::ErrorKind::BrokenPipe => ExitCode::SUCCESS, @@ -175,6 +206,22 @@ fn info(args: &[String]) -> Result<(), CliError> { fn text(args: &[String]) -> Result<(), CliError> { let path = *positional(args).first().ok_or("text: missing file")?; + let mut selected_page = None; + // Validate every supplied value before reading, retaining the first valid selection. + let mut options = args.iter(); + while let Some(option) = options.next() { + if option == "--page" { + let page = options + .next() + .and_then(|value| value.parse::().ok()) + .filter(|page| *page > 0) + .ok_or("bad --page: expected a positive page number")?; + selected_page.get_or_insert(page - 1); + } else if option.starts_with("--") { + // Match positional(): an option's operand is not itself another option. + options.next(); + } + } let password = flag(args, "--password"); let bytes = read(path)?; let mut r = PageRenderer::new(bytes.clone(), RenderConfig { password: password.map(Arc::from), ..Default::default() }); @@ -187,15 +234,15 @@ fn text(args: &[String]) -> Result<(), CliError> { return Err("text: the document could not be parsed".into()); } } - let pages: Vec = match flag(args, "--page") { - Some(p) => vec![p.parse::().map_err(|_| "bad --page")?.saturating_sub(1)], + let pages: Vec = match selected_page { + Some(page) => vec![page], None => (0..r.page_count()).collect(), }; let mut failed: Vec = Vec::new(); for (n, p) in pages.iter().enumerate() { let out = r.render(RenderRequest { page: *p, kind: RequestKind::Text, tile: None, scale: 1.0, tag: 0 }); if let Some(e) = out.error { - eprintln!("page {}: {e}", p + 1); + let _ = writeln!(std::io::stderr().lock(), "page {}: {e}", p + 1); failed.push(p + 1); continue; } @@ -262,11 +309,11 @@ fn edit(args: &[String]) -> Result<(), CliError> { "--move" => { let (pages, to) = value.split_once(':').ok_or("--move PAGES:TO")?; let to: usize = to.parse().map_err(|_| "bad target")?; - edits.push(Edit::MovePages { pages: page_list(pages)?, to: to.saturating_sub(1) }); + edits.push(Edit::MovePages { pages: page_list(pages)?, to: to.checked_sub(1).ok_or("--move target must be at least 1")? }); } "--insert-blank" => { let at: usize = value.parse().map_err(|_| "bad position")?; - edits.push(Edit::InsertBlankPage { at: at.saturating_sub(1), width: 612.0, height: 792.0 }); + edits.push(Edit::InsertBlankPage { at: at.checked_sub(1).ok_or("--insert-blank must be at least 1")?, width: 612.0, height: 792.0 }); } "--title" => edits.push(Edit::SetInfo { key: "Title".into(), value: value.into() }), "--author" => edits.push(Edit::SetInfo { key: "Author".into(), value: value.into() }), @@ -337,6 +384,7 @@ fn split(args: &[String]) -> Result<(), CliError> { fn render(args: &[String]) -> Result<(), CliError> { let path = *positional(args).first().ok_or("render: missing file")?; let page: usize = flag(args, "--page").unwrap_or("1").parse().map_err(|_| "bad --page")?; + let page_index = page.checked_sub(1).ok_or("--page must be at least 1")?; let dpi: f32 = flag(args, "--dpi").unwrap_or("96").parse().map_err(|_| "bad --dpi")?; let out = flag(args, "--out").ok_or("render: missing --out (.png, .jpg, .tif or .pam)")?; // The file is what its name says (#248): a `.png` used to get a netpbm PAM stream. @@ -348,7 +396,7 @@ fn render(args: &[String]) -> Result<(), CliError> { _ => return Err(format!("render: --out {out}: use a .png, .jpg, .tif or .pam name").into()), }; let mut r = PageRenderer::new(read(path)?, RenderConfig { password: flag(args, "--password").map(Arc::from), ..Default::default() }); - let p = r.render(RenderRequest { page: page.saturating_sub(1), kind: RequestKind::Pixels, tile: None, scale: dpi / 72.0, tag: 0 }); + let p = r.render(RenderRequest { page: page_index, kind: RequestKind::Pixels, tile: None, scale: dpi / 72.0, tag: 0 }); if let Some(e) = p.error { return Err(e.into()); } @@ -362,7 +410,7 @@ fn render(args: &[String]) -> Result<(), CliError> { } }; std::fs::write(out, bytes).map_err(|e| format!("{out}: {e}"))?; - eprintln!("rendered page {page} at {dpi} dpi: {}×{} px in {} ms", p.width, p.height, p.millis); + let _ = writeln!(std::io::stderr().lock(), "rendered page {page} at {dpi} dpi: {}×{} px in {} ms", p.width, p.height, p.millis); Ok(()) } @@ -579,8 +627,14 @@ fn run(args: &[String]) -> Result<(), CliError> { #[cfg(feature = "mcp")] fn mcp(args: &[String]) -> Result<(), CliError> { - let mut server = pdfcraft_automation::mcp::McpServer::new(automation(args)?); - eprintln!("pdfcraft-cli: MCP server on stdio (protocol {}); close stdin to stop", pdfcraft_automation::mcp::PROTOCOL_VERSIONS[0]); + let compact = args.iter().any(|a| a == "--compact"); + let mut server = pdfcraft_automation::mcp::McpServer::new(automation(args)?).with_compact(compact); + let _ = writeln!( + std::io::stderr().lock(), + "pdfcraft-cli: MCP server on stdio (protocol {}{}); close stdin to stop", + pdfcraft_automation::mcp::PROTOCOL_VERSIONS[0], + if compact { ", compact tool list" } else { "" } + ); server.serve(std::io::stdin().lock(), std::io::stdout().lock()).map_err(|e| CliError::Message(e.to_string())) } diff --git a/apps/pdfcraft-cli/tests/edit.rs b/apps/pdfcraft-cli/tests/edit.rs index 8cd47cfb..15fdb68c 100644 --- a/apps/pdfcraft-cli/tests/edit.rs +++ b/apps/pdfcraft-cli/tests/edit.rs @@ -91,3 +91,91 @@ fn the_documented_options_still_edit_and_save() { assert_eq!(info.title.as_deref(), Some("Rotated"), "{name}"); } } + +mod one_based_arguments { + use super::*; + use std::sync::atomic::{AtomicU64, Ordering}; + + struct TestDir(PathBuf); + impl TestDir { + fn new() -> Self { + static NEXT: AtomicU64 = AtomicU64::new(0); + for _ in 0..128 { + let path = + std::env::temp_dir().join(format!("pdfcraft-cli-one-based-{}-{}", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed))); + match std::fs::create_dir(&path) { + Ok(()) => return Self(path), + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(e) => panic!("cannot create test directory: {e}"), + } + } + panic!("cannot reserve test directory"); + } + } + impl Drop for TestDir { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + + #[test] + fn zero_render_page_and_edit_positions_fail_before_io() { + let dir = TestDir::new(); + let source = fixture(); + std::fs::write(dir.0.join("input.pdf"), &source).unwrap(); + for input in ["input.pdf", "missing.pdf"] { + for destination in ["output.pam", "new.pam", "input.pdf"] { + std::fs::write(dir.0.join("output.pam"), b"keep destination").unwrap(); + for (command, option, value, diagnostic) in [ + ("render", "--page", "0", "--page must be at least 1"), + ("edit", "--move", "1:0", "--move target must be at least 1"), + ("edit", "--insert-blank", "0", "--insert-blank must be at least 1"), + ] { + let out = Command::new(BIN).current_dir(&dir.0).args([command, input, "--out", destination, option, value]).output().unwrap(); + assert_eq!(out.status.code(), Some(1), "{command} {input} {option} {value}"); + assert_eq!(out.stderr, format!("pdfcraft-cli: {diagnostic}\n").as_bytes()); + assert!(out.stdout.is_empty()); + assert_eq!(std::fs::read(dir.0.join("input.pdf")).unwrap(), source); + assert_eq!(std::fs::read(dir.0.join("output.pam")).unwrap(), b"keep destination"); + assert!(!dir.0.join("new.pam").exists()); + assert!(!dir.0.join("missing.pdf").exists()); + } + } + } + assert_eq!(std::fs::read_dir(&dir.0).unwrap().count(), 2); + } + + #[test] + fn first_render_page_move_target_and_insertion_position_remain_usable() { + let dir = TestDir::new(); + let source = fixture(); + std::fs::write(dir.0.join("input.pdf"), &source).unwrap(); + for output in ["page.pam", "page.png"] { + let out = + Command::new(BIN).current_dir(&dir.0).args(["render", "input.pdf", "--page", "1", "--dpi", "18", "--out", output]).output().unwrap(); + assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); + let bytes = std::fs::read(dir.0.join(output)).unwrap(); + if output.ends_with(".pam") { + let header = b"P7\nWIDTH 50\nHEIGHT 75\nDEPTH 4\nMAXVAL 255\nTUPLTYPE RGB_ALPHA\nENDHDR\n"; + assert!(bytes.starts_with(header)); + assert_eq!(bytes.len(), header.len() + 50 * 75 * 4); + } else { + assert!(bytes.starts_with(b"\x89PNG\r\n\x1a\n")); + assert_eq!(u32::from_be_bytes(bytes[16..20].try_into().unwrap()), 50); + assert_eq!(u32::from_be_bytes(bytes[20..24].try_into().unwrap()), 75); + } + } + let out = Command::new(BIN) + .current_dir(&dir.0) + .args(["edit", "input.pdf", "--out", "edited.pdf", "--move", "1:1", "--rotate", "1:90", "--insert-blank", "1"]) + .output() + .unwrap(); + assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); + assert!(out.stdout.is_empty() && out.stderr.is_empty()); + let info = pdfcraft_render::inspect(std::sync::Arc::new(std::fs::read(dir.0.join("edited.pdf")).unwrap()), None).unwrap(); + assert_eq!(info.pages.len(), 2); + assert_eq!((info.pages[0].width, info.pages[0].height, info.pages[0].rotation), (612.0, 792.0, 0)); + assert_eq!((info.pages[1].width, info.pages[1].height, info.pages[1].rotation), (300.0, 200.0, 90)); + assert_eq!(std::fs::read(dir.0.join("input.pdf")).unwrap(), source); + } +} diff --git a/apps/pdfcraft-cli/tests/help.rs b/apps/pdfcraft-cli/tests/help.rs new file mode 100644 index 00000000..9a86150f --- /dev/null +++ b/apps/pdfcraft-cli/tests/help.rs @@ -0,0 +1,65 @@ +//! `--help` prints the full usage to stdout and succeeds; a missing or unknown command fails with +//! the usage (or a pointer to `--help`) on stderr. + +use std::process::{Command, Output}; + +const BIN: &str = env!("CARGO_BIN_EXE_pdfcraft-cli"); + +fn run(args: &[&str]) -> Output { + Command::new(BIN).args(args).output().unwrap() +} + +fn text(bytes: &[u8]) -> String { + String::from_utf8_lossy(bytes).into_owned() +} + +#[test] +fn help_flags_print_full_usage_to_stdout() { + for args in [&["--help"][..], &["-h"], &["help"], &["render", "--help"], &["check", "-h"]] { + let out = run(args); + assert!(out.status.success(), "{args:?} exited {:?}", out.status); + let stdout = text(&out.stdout); + // Per-command options, not just the command list. + assert!(stdout.contains("pdfcraft-cli render --page N"), "{args:?}: {stdout}"); + assert!(stdout.contains("--every N"), "{args:?}: {stdout}"); + assert!(out.stderr.is_empty(), "{args:?}: {}", text(&out.stderr)); + } +} + +#[test] +fn no_command_fails_with_usage_on_stderr() { + let out = run(&[]); + assert!(!out.status.success()); + assert!(out.stdout.is_empty()); + assert!(text(&out.stderr).contains("pdfcraft-cli render --page N")); +} + +/// `mcp` is listed only when the build has it (`--no-default-features` leaves it out). +#[test] +fn usage_lists_mcp_only_when_built_with_it() { + let out = run(&["--help"]); + let stdout = text(&out.stdout); + assert_eq!(stdout.contains("pdfcraft-cli mcp"), cfg!(feature = "mcp"), "{stdout}"); + assert_eq!(stdout.contains("--compact"), cfg!(feature = "mcp"), "{stdout}"); + // The lines around it are still there. + assert!(stdout.contains("pdfcraft-cli run --script steps.json"), "{stdout}"); + assert!(stdout.contains("pdfcraft-cli ui --control FILE"), "{stdout}"); +} + +#[cfg(not(feature = "mcp"))] +#[test] +fn mcp_without_the_feature_says_it_was_left_out() { + let out = run(&["mcp"]); + assert!(!out.status.success()); + let stderr = text(&out.stderr); + assert!(stderr.contains("without the `mcp` feature"), "{stderr}"); +} + +#[test] +fn unknown_command_names_it_and_points_to_help() { + let out = run(&["frobnicate"]); + assert!(!out.status.success()); + let stderr = text(&out.stderr); + assert!(stderr.contains("unknown command 'frobnicate'"), "{stderr}"); + assert!(stderr.contains("pdfcraft-cli --help"), "{stderr}"); +} diff --git a/apps/pdfcraft-cli/tests/mcp_compact.rs b/apps/pdfcraft-cli/tests/mcp_compact.rs new file mode 100644 index 00000000..48692068 --- /dev/null +++ b/apps/pdfcraft-cli/tests/mcp_compact.rs @@ -0,0 +1,76 @@ +//! `pdfcraft-cli mcp --compact`: the flag reaches the server, `--root` is still honoured, and plain `mcp` still lists every tool. +#![cfg(feature = "mcp")] + +use std::io::Write as _; +use std::process::{Command, Stdio}; + +use serde_json::{Value, json}; + +/// Run `pdfcraft-cli mcp ` over an initialize request plus `requests` and return the replies to `requests`. +fn session(flags: &[&str], requests: &[Value]) -> Vec { + let mut child = Command::new(env!("CARGO_BIN_EXE_pdfcraft-cli")) + .arg("mcp") + .args(flags) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .unwrap(); + let mut input = String::from("{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"initialize\",\"params\":{}}\n"); + for r in requests { + input.push_str(&r.to_string()); + input.push('\n'); + } + child.stdin.take().unwrap().write_all(input.as_bytes()).unwrap(); + let out = child.wait_with_output().unwrap(); + assert!(out.status.success()); + String::from_utf8(out.stdout).unwrap().lines().skip(1).map(|l| serde_json::from_str(l).unwrap()).collect() +} + +fn request(id: u64, method: &str, params: Value) -> Value { + json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params }) +} + +/// The `tools/list` tool names of `pdfcraft-cli mcp `. +fn listed_tools(flags: &[&str]) -> Vec { + let reply = session(flags, &[request(1, "tools/list", json!({}))]).remove(0); + reply["result"]["tools"].as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap().to_string()).collect() +} + +#[test] +fn compact_flag_shortens_the_tool_list() { + let full = listed_tools(&[]); + let compact = listed_tools(&["--compact"]); + assert_eq!(compact.len(), 17, "{compact:?}"); + assert!(compact.iter().any(|n| n == "tool_search") && compact.iter().any(|n| n == "tool_call")); + assert!(compact.iter().all(|n| full.contains(n) || n == "tool_search" || n == "tool_call")); + assert!(full.len() > 100 && !full.iter().any(|n| n == "tool_search"), "{} tools", full.len()); + // The flag can sit on either side of --root. + assert_eq!(listed_tools(&["--root", ".", "--compact"]), compact); + assert_eq!(listed_tools(&["--compact", "--root", "."]), compact); +} + +#[test] +fn compact_flag_keeps_the_root() { + let root = std::env::temp_dir().join(format!("pdfcraft-cli-compact-root-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir_all(&root).unwrap(); + let outside = std::env::temp_dir().join(format!("pdfcraft-cli-compact-outside-{}.pdf", std::process::id())); + let outside = outside.to_string_lossy().into_owned(); + let root_arg = root.to_string_lossy().into_owned(); + let open_outside = + request(1, "tools/call", json!({ "name": "tool_call", "arguments": { "name": "doc_open", "arguments": { "path": outside } } })); + let text = |reply: &Value| reply["result"]["content"][0]["text"].as_str().unwrap_or_default().to_string(); + + // With the root, in either flag order, a path outside it is refused with the root's own message. + for flags in [vec!["--root", root_arg.as_str(), "--compact"], vec!["--compact", "--root", root_arg.as_str()]] { + let reply = session(&flags, std::slice::from_ref(&open_outside)).remove(0); + assert_eq!(reply["result"]["isError"], true, "{flags:?}: {reply}"); + assert!(text(&reply).contains("is outside the allowed directory"), "{flags:?}: {}", text(&reply)); + } + // Without a root the same call gets past that check (the file does not exist, so it fails for that reason instead). + let reply = session(&["--compact"], std::slice::from_ref(&open_outside)).remove(0); + assert_eq!(reply["result"]["isError"], true, "{reply}"); + assert!(!text(&reply).contains("is outside the allowed directory"), "{}", text(&reply)); + let _ = std::fs::remove_dir_all(&root); +} diff --git a/apps/pdfcraft-cli/tests/run_root.rs b/apps/pdfcraft-cli/tests/run_root.rs index 2f9fb69b..511621cc 100644 --- a/apps/pdfcraft-cli/tests/run_root.rs +++ b/apps/pdfcraft-cli/tests/run_root.rs @@ -61,11 +61,19 @@ fn script_image_output_is_confined_to_the_root() { assert!(!base.join("outside/escaped.png").exists() && !base.join("outside/new").exists()); assert_eq!(std::fs::read_to_string(base.join("outside/existing.txt")).unwrap(), "SENTINEL\n"); - // "." is the root itself: refused, without staging a temporary file beside the root. + // "." is the root itself: refused, without staging a temporary file beside the root. The + // file at the old fixed staging name and the listing are canaries; staging names are random. std::fs::write(base.join(".root.pdfcraft-tmp"), "SENTINEL\n").unwrap(); + let listing = || { + let mut names: Vec<_> = std::fs::read_dir(&base).unwrap().map(|e| e.unwrap().file_name()).collect(); + names.sort(); + names + }; + let before = listing(); let o = render_to(&base, &base, Some(&root), "."); assert!(!o.status.success() && String::from_utf8_lossy(&o.stderr).contains("is a folder"), "{}", String::from_utf8_lossy(&o.stderr)); assert_eq!(std::fs::read_to_string(base.join(".root.pdfcraft-tmp")).unwrap(), "SENTINEL\n"); + assert_eq!(listing(), before, "nothing was left beside the root"); // A relative path resolves inside the root, not in the working directory. let o = render_to(&base, &base, Some(&root), "relative.png"); diff --git a/apps/pdfcraft-cli/tests/stderr.rs b/apps/pdfcraft-cli/tests/stderr.rs new file mode 100644 index 00000000..9991bc1c --- /dev/null +++ b/apps/pdfcraft-cli/tests/stderr.rs @@ -0,0 +1,132 @@ +//! Diagnostic stderr failures must not replace a command's real result. +#![cfg(target_os = "linux")] + +use std::io::Write as _; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Output, Stdio}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{Duration, Instant}; + +const BIN: &str = env!("CARGO_BIN_EXE_pdfcraft-cli"); + +struct Scratch(PathBuf); +impl Scratch { + fn new() -> Self { + static NEXT: AtomicU64 = AtomicU64::new(0); + for _ in 0..128 { + let path = std::env::temp_dir().join(format!("pdfcraft-stderr-{}-{}", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed))); + match std::fs::create_dir(&path) { + Ok(()) => return Self(path), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(error) => panic!("creating stderr test directory: {error}"), + } + } + panic!("no unused stderr test directory after 128 attempts"); + } +} +impl Drop for Scratch { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } +} + +// Only this test's newly spawned child is ever terminated, including assertion unwinding. +struct OwnedChild(Option); +impl Drop for OwnedChild { + fn drop(&mut self) { + if let Some(child) = &mut self.0 { + let _ = child.kill(); + let _ = child.wait(); + } + } +} +fn run(command: &mut Command, input: Option<&str>) -> Output { + command.stdin(if input.is_some() { Stdio::piped() } else { Stdio::null() }); + let mut owned = OwnedChild(Some(command.spawn().unwrap())); + if let Some(input) = input { + owned.0.as_mut().unwrap().stdin.take().unwrap().write_all(input.as_bytes()).unwrap(); + } + // Every fixture emits only a few short lines, below the pipe capacity. + let deadline = Instant::now() + Duration::from_secs(30); + while owned.0.as_mut().unwrap().try_wait().unwrap().is_none() { + assert!(Instant::now() < deadline, "owned CLI child exceeded thirty seconds"); + std::thread::sleep(Duration::from_millis(10)); + } + owned.0.take().unwrap().wait_with_output().unwrap() +} +fn full() -> Stdio { + std::fs::OpenOptions::new().write(true).open("/dev/full").unwrap().into() +} +fn render(pdf: &Path, out: &Path, stderr: Stdio) -> Output { + run(Command::new(BIN).arg("render").arg(pdf).args(["--page", "1", "--dpi", "18", "--out"]).arg(out).stdout(Stdio::piped()).stderr(stderr), None) +} + +#[test] +fn failing_diagnostics_preserve_render_success_and_real_text_or_file_errors() { + let dir = Scratch::new(); + let bytes = pdfcraft_engine::Session::new().create_from_text("Diagnostic test", "Ordinary diagnostic text").unwrap(); + let pdf = dir.0.join("input.pdf"); + std::fs::write(&pdf, bytes.as_slice()).unwrap(); + let image_path = dir.0.join("page.pam"); + let failed_sink = render(&pdf, &image_path, full()); + assert!(failed_sink.status.success(), "completed render must survive failed diagnostics: {}", failed_sink.status); + assert!(failed_sink.stdout.is_empty()); + let image = std::fs::read(&image_path).unwrap(); + assert!(image.starts_with(b"P7\nWIDTH ") && image.windows(7).any(|part| part == b"ENDHDR\n")); + let ordinary = render(&pdf, &image_path, Stdio::piped()); + assert!(ordinary.status.success()); + assert!(String::from_utf8_lossy(&ordinary.stderr).contains("rendered page 1 at 18 dpi")); + assert_eq!(std::fs::read(&image_path).unwrap(), image, "diagnostic destination must not change rendered bytes"); + + let invalid = run(Command::new(BIN).arg("text").arg(&pdf).args(["--page", "2"]).stdout(Stdio::piped()).stderr(full()), None); + assert_eq!(invalid.status.code(), Some(1), "page errors remain normal failures rather than stderr panics"); + assert!(invalid.stdout.is_empty()); + let valid = run(Command::new(BIN).arg("text").arg(&pdf).stdout(Stdio::piped()).stderr(full()), None); + assert!(valid.status.success()); + assert!(String::from_utf8_lossy(&valid.stdout).contains("Ordinary diagnostic text")); + + // An existing directory with a supported extension reaches the real file writer. + // Using /dev/full as the render path would only test upstream's extension check. + let folder = dir.0.join("folder.pam"); + std::fs::create_dir(&folder).unwrap(); + std::fs::write(folder.join("keep"), b"sentinel").unwrap(); + assert_eq!(render(&pdf, &folder, full()).status.code(), Some(1)); + assert_eq!(std::fs::read(folder.join("keep")).unwrap(), b"sentinel"); + assert_eq!(std::fs::read(&image_path).unwrap(), image); + assert_eq!(std::fs::read(&pdf).unwrap(), bytes.as_slice()); +} + +#[cfg(feature = "mcp")] +#[test] +fn failing_startup_diagnostics_preserve_mcp_replies_and_protocol_output_errors() { + let requests = [ + serde_json::json!({ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18" } }), + serde_json::json!({ "jsonrpc": "2.0", "id": 2, "method": "ping" }), + serde_json::json!({ "jsonrpc": "2.0", "id": 3, "method": "ordinary-unknown-method" }), + ]; + let input = requests.iter().map(|request| format!("{request}\n")).collect::(); + let output = run(Command::new(BIN).arg("mcp").stdout(Stdio::piped()).stderr(full()), Some(&input)); + assert!(output.status.success(), "failed startup diagnostics must not stop stdio: {}", output.status); + let replies: Vec = String::from_utf8(output.stdout).unwrap().lines().map(|line| serde_json::from_str(line).unwrap()).collect(); + assert_eq!(replies.len(), 3); + assert_eq!(replies[0]["id"], 1); + assert_eq!(replies[0]["result"]["protocolVersion"], "2025-06-18"); + assert_eq!(replies[1]["id"], 2); + assert_eq!(replies[1]["result"], serde_json::json!({})); + assert_eq!(replies[2]["id"], 3); + assert_eq!(replies[2]["error"]["code"], -32601); + // Upstream's compact mode still reaches stdio even when its expanded diagnostic fails. + let ping = "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"ping\"}\n"; + for (stderr, diagnostic_expected) in [(full(), false), (Stdio::piped(), true)] { + let output = run(Command::new(BIN).args(["mcp", "--compact"]).stdout(Stdio::piped()).stderr(stderr), Some(ping)); + assert!(output.status.success()); + let reply: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(reply["id"], 4); + assert_eq!(reply["result"], serde_json::json!({})); + if diagnostic_expected { + assert!(String::from_utf8_lossy(&output.stderr).contains(", compact tool list); close stdin to stop")); + } + } + let output = run(Command::new(BIN).arg("mcp").stdout(full()).stderr(full()), Some("{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"ping\"}\n")); + assert_eq!(output.status.code(), Some(1), "actual protocol output failure must still fail"); +} diff --git a/apps/pdfcraft-cli/tests/text.rs b/apps/pdfcraft-cli/tests/text.rs index c573393b..5ac2e07c 100644 --- a/apps/pdfcraft-cli/tests/text.rs +++ b/apps/pdfcraft-cli/tests/text.rs @@ -95,3 +95,117 @@ fn text_of_a_protected_document_with_the_password_succeeds() { let stdout = String::from_utf8_lossy(&out.stdout); assert!(stdout.contains("Page 1") && stdout.contains("Page 3"), "{stdout}"); } + +mod page_argument_tests { + use super::*; + use std::path::PathBuf; + use std::sync::atomic::{AtomicU64, Ordering}; + + struct PageDir(PathBuf); + + impl PageDir { + fn new() -> Self { + static NEXT: AtomicU64 = AtomicU64::new(0); + for _ in 0..128 { + let serial = NEXT.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!("pdfcraft-page-{}-{serial}", std::process::id())); + match std::fs::create_dir(&path) { + Ok(()) => return Self(path), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(error) => panic!("creating page test directory: {error}"), + } + } + panic!("no unused page test directory after 128 attempts"); + } + } + + impl Drop for PageDir { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + + #[test] + fn malformed_page_values_fail_before_reading_or_printing_document_text() { + let dir = PageDir::new(); + let path = dir.0.join("three.pdf"); + let bytes = fixture(3); + std::fs::write(&path, &bytes).unwrap(); + let missing = dir.0.join("missing.pdf"); + let invalid = [ + vec!["--page", "0"], + vec!["--page", ""], + vec!["--page", "bad"], + vec!["--page", "-1"], + vec!["--page", "1.5"], + vec!["--page", "18446744073709551616"], + vec!["--page"], + vec!["--page", "--password", "unused"], + vec!["--page", "2", "--page"], + ]; + for input in [&path, &missing] { + for args in &invalid { + let out = Command::new(BIN).arg("text").arg(input).args(args).output().unwrap(); + assert_eq!(out.status.code(), Some(1), "{args:?}: {out:?}"); + assert!(out.stdout.is_empty(), "{args:?}: no text may be printed"); + assert_eq!(String::from_utf8(out.stderr).unwrap().trim(), "pdfcraft-cli: bad --page: expected a positive page number"); + assert_eq!(std::fs::read(&path).unwrap(), bytes); + assert!(!missing.exists()); + } + } + } + + #[test] + fn valid_page_values_preserve_all_page_selection_and_first_duplicate_semantics() { + let dir = PageDir::new(); + let path = dir.0.join("three.pdf"); + let bytes = fixture(3); + std::fs::write(&path, &bytes).unwrap(); + for (args, expected) in [ + (vec![], vec!["Page 1", "Page 2", "Page 3"]), + (vec!["--page", "1"], vec!["Page 1"]), + (vec!["--page", "2"], vec!["Page 2"]), + (vec!["--page", "3"], vec!["Page 3"]), + (vec!["--page", "2", "--page", "1"], vec!["Page 2"]), + ] { + let out = Command::new(BIN).arg("text").arg(&path).args(args).output().unwrap(); + assert!(out.status.success(), "{out:?}"); + assert!(out.stderr.is_empty()); + let text = String::from_utf8(out.stdout).unwrap(); + assert_eq!(text.split(' ').map(str::trim).collect::>(), expected); + assert_eq!(std::fs::read(&path).unwrap(), bytes); + } + } + + #[test] + fn an_option_shaped_password_is_not_parsed_as_a_page_flag() { + let dir = PageDir::new(); + let source = dir.0.join("three.pdf"); + let bytes = fixture(3); + std::fs::write(&source, &bytes).unwrap(); + let protected = dir.0.join("protected.pdf"); + let script = dir.0.join("protect.json"); + let steps = serde_json::json!([ + { "tool": "doc_open", "args": { "path": source } }, + { "tool": "doc_protect", "args": { "doc": 1, "open_password": "--page" } }, + { "tool": "doc_save", "args": { "doc": 1, "path": protected } }, + ]); + std::fs::write(&script, steps.to_string()).unwrap(); + let out = Command::new(BIN).args(["run", "--script"]).arg(&script).output().unwrap(); + assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); + let protected_bytes = std::fs::read(&protected).unwrap(); + for (args, expected) in [ + (vec!["--password", "--page"], vec!["Page 1", "Page 2", "Page 3"]), + (vec!["--page", "2", "--password", "--page"], vec!["Page 2"]), + (vec!["--password", "--page", "--page", "2"], vec!["Page 2"]), + ] { + let out = Command::new(BIN).arg("text").arg(&protected).args(args).output().unwrap(); + assert!(out.status.success(), "{out:?}"); + assert!(out.stderr.is_empty()); + let text = String::from_utf8(out.stdout).unwrap(); + assert_eq!(text.split(' ').map(str::trim).collect::>(), expected); + assert_eq!(std::fs::read(&protected).unwrap(), protected_bytes); + assert_eq!(std::fs::read(&source).unwrap(), bytes); + } + } +} diff --git a/apps/pdfcraft/Cargo.toml b/apps/pdfcraft/Cargo.toml index 78d9fb5c..2400fe9e 100644 --- a/apps/pdfcraft/Cargo.toml +++ b/apps/pdfcraft/Cargo.toml @@ -14,7 +14,9 @@ workspace = true ureq = { version = "3.4", default-features = false, features = ["rustls-no-provider", "_ring"] } rustls-native-certs = "0.8" pdfcraft-ui-egui = { workspace = true } -eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11", "persistence", "links"] } +# wgpu draws; OpenGL (glow) is the fallback when wgpu can't start on an old or unusual GPU or +# driver (#461, #435, #392), and can be chosen with PDFCRAFT_RENDERER=gl. +eframe = { workspace = true, features = ["default_fonts", "wgpu", "glow", "accesskit", "wayland", "x11", "persistence", "links"] } egui = { workspace = true } serde_json = { workspace = true } # Records from every crate go to standard error and the log file (src/logging.rs). diff --git a/apps/pdfcraft/src/main.rs b/apps/pdfcraft/src/main.rs index 8fae5b58..5d567b81 100644 --- a/apps/pdfcraft/src/main.rs +++ b/apps/pdfcraft/src/main.rs @@ -19,6 +19,9 @@ #![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")] #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] +use std::cell::Cell; +use std::rc::Rc; + use pdfcraft_ui_egui::PdfCraftApp; #[cfg(target_os = "macos")] @@ -39,16 +42,24 @@ const APP_ICON_PNG: &[u8] = include_bytes!("../../../assets/app-icon/hicolor/256 /// The app was called PrintCraft before; settings saved then are under this key. const LEGACY_STORAGE_KEY: &str = "printcraft"; -/// The settings folder: `app.ron` and the `logs` folder (docs/development.md). eframe would -/// otherwise derive it from the app id; keep it under "PdfCraft". +/// The settings folder: `app.ron` and the `logs` folder (docs/development.md). In portable mode +/// it is `PdfCraftData` beside the executable (#157). eframe would otherwise derive it from the +/// app id; keep it under "PdfCraft". fn settings_dir() -> Option { + if let Some(dir) = pdfcraft_ui_egui::portable::data_dir() { + return Some(dir.to_path_buf()); + } eframe::storage_dir("PdfCraft") } /// Move the settings and crash-recovery folders of the app's former name, PrintCraft, to the new /// name once, so an upgrade keeps recent files, preferences and unsaved work. Best effort: a -/// folder is left alone when the new one already exists or the move fails. +/// folder is left alone when the new one already exists or the move fails. A portable copy leaves +/// the per-user folders alone. fn migrate_legacy_folders() { + if pdfcraft_ui_egui::portable::data_dir().is_some() { + return; + } let mut moves = vec![(eframe::storage_dir("PrintCraft"), settings_dir())]; // Recovery lives in the settings folder except on Windows, where it is under %LOCALAPPDATA%. if cfg!(windows) { @@ -70,6 +81,37 @@ fn migrate_legacy_folders() { } } +/// Desktop launchers (GNOME Files, KDE Dolphin…) only recognise an app as the default handler for a +/// mime type if its `Exec` takes URIs (`%u`/`%U`), not just paths (`%F`); the packaged `.desktop` +/// file uses `%U` accordingly (packaging/linux/ai.storyteller.pdfcraft.desktop). Decode a local +/// `file://` argument (`file:///path` or `file://localhost/path`) to a plain path here so the rest +/// of the app, which only ever opens paths, is unaffected. Other schemes (`http://`, `mailto:`…), +/// URIs naming another host, and plain paths pass through untouched. +fn path_from_arg(arg: String) -> String { + let Some(rest) = arg.strip_prefix("file://") else { return arg }; + let rest = rest.strip_prefix("localhost").unwrap_or(rest); + if !rest.starts_with('/') { + return arg; + } + // `file:///C:/x.pdf` on Windows names `C:/x.pdf`. `rest` starts with the one-byte '/', so + // byte 1 is a char boundary. + let rest = if cfg!(windows) && rest.as_bytes().get(2) == Some(&b':') { &rest[1..] } else { rest }; + let mut out = Vec::with_capacity(rest.len()); + let mut bytes = rest.bytes(); + while let Some(b) = bytes.next() { + if b == b'%' { + let hex = bytes.clone().take(2).collect::>(); + if let Some(byte) = std::str::from_utf8(&hex).ok().filter(|h| h.len() == 2).and_then(|h| u8::from_str_radix(h, 16).ok()) { + out.push(byte); + bytes.nth(1); + continue; + } + } + out.push(b); + } + String::from_utf8(out).unwrap_or(arg) +} + fn main() -> eframe::Result { // First, so the panic hook and every start-up warning are recorded (`logging`). let logger = logging::install(); @@ -107,10 +149,89 @@ fn main() -> eframe::Result { let value = args.next().unwrap_or_default(); options.push((flag.trim_start_matches("--").to_string(), value)); } - _ => files.push(a), + _ => files.push(path_from_arg(a)), } } let integrated = cfg!(target_os = "macos"); + migrate_legacy_folders(); + // The log file lives in the settings folder; opened after the arguments (so `--version` leaves + // no file behind) and after the PrintCraft migration (which a fresh folder would block). + // Records logged until now are written to it first. + if let (Some(logger), Some(dir)) = (logger, settings_dir()) { + match logger.attach_dir(&dir.join("logs")) { + Ok(path) => log::info!("PdfCraft {}, log file {}", env!("CARGO_PKG_VERSION"), path.display()), + // Standard error only by now (`attach_dir` gave up on the file); unlike `eprintln!`, never panics. + Err(e) => log::warn!("no log file: {e}"), + } + } + let choice = renderer_choice(std::env::var("PDFCRAFT_RENDERER").ok().as_deref()); + let launch = Launch { files, options, control_file, create_images, integrated }; + // Finder, Open With and the Dock deliver files as Apple events, not arguments; catch the one + // that launched us as well as later ones. Lives until the event loop returns. + #[cfg(target_os = "macos")] + let apple_events = apple_events::AppleEvents::install(); + // Set once the app is created, which is after the renderer has started. + let started = Rc::new(Cell::new(false)); + let first = if choice == RendererChoice::Gl { eframe::Renderer::Glow } else { eframe::Renderer::Wgpu }; + let result = eframe::run_native( + "PdfCraft", + native_options(integrated, first), + app_creator( + launch.clone(), + Rc::clone(&started), + #[cfg(target_os = "macos")] + &apple_events, + ), + ); + match result { + // Only a renderer that couldn't start: without a window or display at all, OpenGL can't + // help either, and winit's own error says more. + Err(e @ eframe::Error::Wgpu(_)) if retry_with_gl(choice, started.get()) => { + // Old or unusual GPUs and drivers (#461, #435, #392) can't give wgpu a device; OpenGL + // usually still works there, so that's better than quitting. + log::error!("the GPU renderer (wgpu) didn't start: {e}. Starting with OpenGL instead; set PDFCRAFT_RENDERER=gl to skip wgpu."); + eframe::run_native( + "PdfCraft", + native_options(integrated, eframe::Renderer::Glow), + app_creator( + launch, + started, + #[cfg(target_os = "macos")] + &apple_events, + ), + ) + } + other => other, + } +} + +/// Which renderer to start with, from `PDFCRAFT_RENDERER`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RendererChoice { + /// wgpu, then OpenGL if wgpu can't start (unset, or anything unrecognised). + Auto, + /// wgpu only (`wgpu`): a failure is reported, not worked around. + Wgpu, + /// OpenGL only (`gl`, `opengl` or `glow`): for drivers where wgpu starts but misbehaves. + Gl, +} + +fn renderer_choice(value: Option<&str>) -> RendererChoice { + match value.map(|v| v.trim().to_ascii_lowercase()).as_deref() { + Some("wgpu") => RendererChoice::Wgpu, + Some("gl" | "opengl" | "glow") => RendererChoice::Gl, + _ => RendererChoice::Auto, + } +} + +/// Whether a failed run should be retried with OpenGL: only when wgpu was tried first by choice of +/// nobody, and it failed before the app was created (so while starting the renderer, not later). +fn retry_with_gl(choice: RendererChoice, app_started: bool) -> bool { + choice == RendererChoice::Auto && !app_started +} + +/// The window and renderer settings for one run. +fn native_options(integrated: bool, renderer: eframe::Renderer) -> eframe::NativeOptions { let mut viewport = egui::ViewportBuilder::default() .with_title("PdfCraft") .with_inner_size([1440.0, 920.0]) @@ -126,90 +247,130 @@ fn main() -> eframe::Result { if integrated { viewport = viewport.with_fullsize_content_view(true).with_titlebar_shown(false).with_title_shown(false); } - migrate_legacy_folders(); - // The log file lives in the settings folder; opened after the arguments (so `--version` leaves - // no file behind) and after the PrintCraft migration (which a fresh folder would block). - // Records logged until now are written to it first. - if let (Some(logger), Some(dir)) = (logger, settings_dir()) { - match logger.attach_dir(&dir.join("logs")) { - Ok(path) => log::info!("PdfCraft {}, log file {}", env!("CARGO_PKG_VERSION"), path.display()), - // Standard error only by now (`attach_dir` gave up on the file); unlike `eprintln!`, never panics. - Err(e) => log::warn!("no log file: {e}"), - } - } let persistence_path = settings_dir().map(|d| d.join("app.ron")); - let mut native = eframe::NativeOptions { viewport, persistence_path, ..Default::default() }; - configure_gpu(&mut native); - // Finder, Open With and the Dock deliver files as Apple events, not arguments; catch the one - // that launched us as well as later ones. Lives until the event loop returns. - #[cfg(target_os = "macos")] - let apple_events = apple_events::AppleEvents::install(); - #[cfg(target_os = "macos")] - let apple_events = &apple_events; - eframe::run_native( - "PdfCraft", - native, - Box::new(move |cc| { - let mut app = PdfCraftApp::new(); - if let Some(json) = cc.storage.and_then(|s| s.get_string("pdfcraft").or_else(|| s.get_string(LEGACY_STORAGE_KEY))) { - app.restore(&json); + // Set explicitly, so the renderer never depends on which one eframe defaults to. + let mut native = eframe::NativeOptions { viewport, persistence_path, renderer, ..Default::default() }; + if renderer == eframe::Renderer::Wgpu { + configure_gpu(&mut native); + } + native +} + +/// What the command line asked for, kept so a second run (with OpenGL) can start the same way. +#[derive(Clone)] +struct Launch { + files: Vec, + options: Vec<(String, String)>, + control_file: Option, + create_images: bool, + integrated: bool, +} + +fn app_creator<'a>( + launch: Launch, + started: Rc>, + #[cfg(target_os = "macos")] apple_events: &'a apple_events::AppleEvents, +) -> eframe::AppCreator<'a> { + let Launch { files, options, control_file, create_images, integrated } = launch; + Box::new(move |cc| { + started.set(true); + let mut app = PdfCraftApp::new(); + if let Some(json) = cc.storage.and_then(|s| s.get_string("pdfcraft").or_else(|| s.get_string(LEGACY_STORAGE_KEY))) { + app.restore(&json); + } + app.integrated_titlebar = integrated; + app.update_source = Some(std::sync::Arc::new(updates::latest_release)); + app.os_key_store_ids = cfg!(any(target_os = "macos", target_os = "windows")); + #[cfg(target_os = "macos")] + { + app.os_events = Some(apple_events.connect(&cc.egui_ctx)); + } + if let Some(file) = &control_file { + let client = app.attach_control(&cc.egui_ctx); + match pdfcraft_ui_egui::control::serve(client).and_then(|ep| write_control_file(file, ep.port, &ep.token).map(|()| ep.port)) { + // Never the token (AGENTS.md §3): it stays in the owner-only file. + Ok(port) => log::info!("UI control channel on 127.0.0.1:{port} (connection details in {file})"), + Err(e) => log::error!("--control {file}: {e}"), } - app.integrated_titlebar = integrated; - app.update_source = Some(std::sync::Arc::new(updates::latest_release)); - app.os_key_store_ids = cfg!(any(target_os = "macos", target_os = "windows")); - #[cfg(target_os = "macos")] - { - app.os_events = Some(apple_events.connect(&cc.egui_ctx)); + } + // Autosave unsaved changes; offer to recover documents a crashed session left behind. + if let Some(dir) = pdfcraft_ui_egui::RecoveryStore::default_dir() { + app.enable_recovery(pdfcraft_ui_egui::RecoveryStore::new(dir)); + } + // A portable marker whose data folder can't be written (#157): say where settings went. + if let Some(w) = &pdfcraft_ui_egui::portable::current().unwritable { + app.notify_fmt( + "Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead.", + &[("folder", &w.folder.display().to_string()), ("error", &w.error)], + ); + } + if create_images { + if let Err(e) = app.begin_image_import_paths(&files) { + app.notify(e); } - if let Some(file) = &control_file { - let client = app.attach_control(&cc.egui_ctx); - match pdfcraft_ui_egui::control::serve(client).and_then(|ep| write_control_file(file, ep.port, &ep.token).map(|()| ep.port)) { - // Never the token (AGENTS.md §3): it stays in the owner-only file. - Ok(port) => log::info!("UI control channel on 127.0.0.1:{port} (connection details in {file})"), - Err(e) => log::error!("--control {file}: {e}"), - } + } else { + // With the preference on, last session's files come back first; files named on + // the command line open after them, in front (#442). + app.reopen_last_files(&files); + for f in files { + app.open_path(&f); } - // Autosave unsaved changes; offer to recover documents a crashed session left behind. - if let Some(dir) = pdfcraft_ui_egui::RecoveryStore::default_dir() { - app.enable_recovery(pdfcraft_ui_egui::RecoveryStore::new(dir)); + } + for (k, v) in options { + if let Err(e) = app.set_option(&k, &v) { + log::warn!("--{k} {v}: {e}"); } - if create_images { - if let Err(e) = app.begin_image_import_paths(&files) { - app.notify(e); - } - } else { - for f in files { - app.open_path(&f); - } - } - for (k, v) in options { - if let Err(e) = app.set_option(&k, &v) { - log::warn!("--{k} {v}: {e}"); - } - } - Ok(Box::new(app)) - }), - ) + } + Ok(Box::new(app)) + }) } /// Write the control endpoint so that only the current user can read the token. +/// +/// The JSON goes to a new file next to `path`, created fresh (owner-only on Unix), which then +/// replaces `path`. Opening `path` itself would follow a link planted there and truncate whatever +/// it points at; a rename replaces the link and leaves its target alone. A reader polling for the +/// file also never sees it half-written. fn write_control_file(path: &str, port: u16, token: &str) -> std::io::Result<()> { + use std::io::{ErrorKind, Write}; let json = serde_json::json!({ "port": port, "token": token, "pid": std::process::id() }).to_string(); - let mut opts = std::fs::OpenOptions::new(); - opts.write(true).create(true).truncate(true); - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - opts.mode(0o600); + let path = std::path::Path::new(path); + let name = path.file_name().ok_or_else(|| std::io::Error::new(ErrorKind::InvalidInput, "not a file name"))?; + // Unpredictable suffixes, so the name can't be planted in advance. `RandomState` is keyed from + // the operating system's random source. + let random = std::hash::RandomState::new(); + for i in 0..16u32 { + let mut staged = std::ffi::OsString::from("."); + staged.push(name); + staged.push(format!(".{:016x}.tmp", std::hash::BuildHasher::hash_one(&random, i))); + let staged = path.with_file_name(staged); + let mut opts = std::fs::OpenOptions::new(); + opts.write(true).create_new(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + opts.mode(0o600); + } + let file = match opts.open(&staged) { + Ok(f) => f, + Err(e) if e.kind() == ErrorKind::AlreadyExists => continue, + // Windows refuses `create_new` on a folder with "access denied"; the name is taken all + // the same. A folder we can't write to, with nothing at the name, still fails here. + Err(e) if e.kind() == ErrorKind::PermissionDenied && staged.symlink_metadata().is_ok() => continue, + Err(e) => return Err(e), + }; + // The block closes the file before it is renamed. + let written = { + let mut file = file; + file.write_all(json.as_bytes()) + }; + let done = written.and_then(|()| std::fs::rename(&staged, path)); + if done.is_err() { + let _ = std::fs::remove_file(&staged); + } + return done; } - use std::io::Write; - let mut f = opts.open(path)?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - f.set_permissions(std::fs::Permissions::from_mode(0o600))?; - } - f.write_all(json.as_bytes()) + Err(std::io::Error::new(ErrorKind::AlreadyExists, "no free name for the control file's temporary copy")) } /// How wgpu finds a GPU. Each choice yields to its wgpu environment variable. @@ -220,7 +381,9 @@ fn write_control_file(path: &str, port: u16, token: &str) -> std::io::Result<()> /// It also saves battery. Machines with one GPU are unaffected. /// - On Linux, draw on a GPU that a monitor is plugged into. On a desktop whose monitors all hang /// off the discrete GPU, drawing on the integrated one leaves the window black under Wayland -/// compositors on NVIDIA. Among the GPUs that drive a display, the integrated one still wins. +/// compositors on NVIDIA. When several GPUs drive a display, the one driving a built-in panel +/// wins (a hybrid laptop, issue #8); without a built-in panel (a desktop with a monitor on each +/// GPU, issue #445) the discrete one wins, as the integrated one may fail to present there. /// - On Windows, use Direct3D 12, falling back to OpenGL, and never load Vulkan drivers unless /// `WGPU_BACKEND` asks for them. Creating a Vulkan instance loads every installed Vulkan driver /// into the process, and a faulty one (an Intel driver in issue #37) crashed PdfCraft before @@ -256,58 +419,125 @@ fn configure_gpu(native: &mut eframe::NativeOptions) { } } -/// PCI `(vendor, device)` ids of the GPUs with a connected monitor, read from the DRM connectors -/// under `drm` (`card1-DP-3/status` is `connected`, `card1/device/{vendor,device}` hold `0x10de`). +/// A GPU with a connected monitor. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(not(target_os = "linux"), allow(dead_code))] +struct DisplayGpu { + /// PCI `(vendor, device)` ids. + pci: (u32, u32), + /// Whether one of its connected connectors is a built-in panel (`eDP`, `LVDS` or `DSI`). + internal_panel: bool, +} + +/// Whether a DRM connector name (`eDP-1`, `LVDS-1`, `DSI-1`) is a laptop's built-in panel. +#[cfg_attr(not(target_os = "linux"), allow(dead_code))] +fn is_internal_panel(connector: &str) -> bool { + ["eDP", "LVDS", "DSI"].iter().any(|kind| connector.starts_with(kind)) +} + +/// The GPUs with a connected monitor, read from the DRM connectors under `drm` (`card1-DP-3/status` +/// is `connected`, `card1/device/{vendor,device}` hold `0x10de`). #[cfg(target_os = "linux")] -fn linux_display_gpus(drm: &std::path::Path) -> Vec<(u32, u32)> { +fn linux_display_gpus(drm: &std::path::Path) -> Vec { let read_hex = |p: std::path::PathBuf| -> Option { let s = std::fs::read_to_string(p).ok()?; u32::from_str_radix(s.trim().trim_start_matches("0x"), 16).ok() }; - let mut gpus = Vec::new(); + let mut gpus: Vec = Vec::new(); let Ok(entries) = std::fs::read_dir(drm) else { return gpus }; // A machine has a handful of connectors; the cap only bounds a pathological sysfs. for entry in entries.flatten().take(256) { let name = entry.file_name(); - let Some((card, _connector)) = name.to_str().and_then(|n| n.split_once('-')) else { continue }; + let Some((card, connector)) = name.to_str().and_then(|n| n.split_once('-')) else { continue }; let connected = std::fs::read_to_string(entry.path().join("status")).is_ok_and(|s| s.trim() == "connected"); if !connected { continue; } let device = drm.join(card).join("device"); - if let (Some(v), Some(d)) = (read_hex(device.join("vendor")), read_hex(device.join("device"))) - && !gpus.contains(&(v, d)) - { - gpus.push((v, d)); + let (Some(v), Some(d)) = (read_hex(device.join("vendor")), read_hex(device.join("device"))) else { continue }; + let internal_panel = is_internal_panel(connector); + match gpus.iter_mut().find(|g| g.pci == (v, d)) { + Some(gpu) => gpu.internal_panel |= internal_panel, + None => gpus.push(DisplayGpu { pci: (v, d), internal_panel }), } } gpus } -/// Index of the adapter to draw with: one that drives a display (by PCI ids) first, then the most -/// frugal kind — integrated, discrete, other, virtual, software. +/// Index of the adapter to draw with: one that drives a display (by PCI ids) first. Among several +/// of those, the one driving a built-in panel (a hybrid laptop, #8), or else, with no built-in +/// panel anywhere (a desktop with a monitor on each GPU, #445), the discrete one. Otherwise the +/// most frugal kind: integrated, discrete, other, virtual, software. #[cfg_attr(not(target_os = "linux"), allow(dead_code))] -fn pick_adapter(adapters: &[(u32, u32, eframe::wgpu::DeviceType)], displays: &[(u32, u32)]) -> Option { +fn pick_adapter(adapters: &[(u32, u32, eframe::wgpu::DeviceType)], displays: &[DisplayGpu]) -> Option { use eframe::wgpu::DeviceType; + let display = |pci: (u32, u32)| displays.iter().find(|g| g.pci == pci); + let multi_gpu_desktop = displays.len() > 1 && !displays.iter().any(|g| g.internal_panel); adapters .iter() .enumerate() .min_by_key(|(_, (vendor, device, kind))| { - let drives_display = displays.contains(&(*vendor, *device)); - let frugality = match kind { - DeviceType::IntegratedGpu => 0, - DeviceType::DiscreteGpu => 1, - DeviceType::Other => 2, - DeviceType::VirtualGpu => 3, - DeviceType::Cpu => 4, + let shown = display((*vendor, *device)); + let drives_display = shown.is_some(); + let drives_panel = shown.is_some_and(|g| g.internal_panel); + let rank = match kind { + // On a desktop with monitors on both, the integrated GPU can accept the window and + // still fail to present to it (#445). + DeviceType::DiscreteGpu if drives_display && multi_gpu_desktop => 0, + DeviceType::IntegratedGpu => 1, + DeviceType::DiscreteGpu => 2, + DeviceType::Other => 3, + DeviceType::VirtualGpu => 4, + DeviceType::Cpu => 5, }; - (!drives_display, frugality) + (!drives_display, !drives_panel, rank) }) .map(|(i, _)| i) } #[cfg(test)] mod tests { + #[test] + fn path_from_arg_decodes_file_uris() { + assert_eq!(super::path_from_arg("file:///home/alice/report.pdf".to_string()), "/home/alice/report.pdf"); + assert_eq!(super::path_from_arg("file:///home/alice/my%20report.pdf".to_string()), "/home/alice/my report.pdf"); + assert_eq!(super::path_from_arg("file://localhost/tmp/a%C3%A9.pdf".to_string()), "/tmp/aé.pdf"); + // Malformed or truncated escapes are kept as written; bytes that aren't UTF-8 keep the URI. + assert_eq!(super::path_from_arg("file:///tmp/100%.pdf".to_string()), "/tmp/100%.pdf"); + assert_eq!(super::path_from_arg("file:///tmp/a%2".to_string()), "/tmp/a%2"); + assert_eq!(super::path_from_arg("file:///tmp/%zz%".to_string()), "/tmp/%zz%"); + assert_eq!(super::path_from_arg("file:///tmp/%FF.pdf".to_string()), "file:///tmp/%FF.pdf"); + } + + #[test] + fn path_from_arg_leaves_plain_paths_and_other_schemes_alone() { + assert_eq!(super::path_from_arg("report.pdf".to_string()), "report.pdf"); + assert_eq!(super::path_from_arg("/home/alice/report.pdf".to_string()), "/home/alice/report.pdf"); + assert_eq!(super::path_from_arg("https://example.com/report.pdf".to_string()), "https://example.com/report.pdf"); + assert_eq!(super::path_from_arg("file://server/share/a.pdf".to_string()), "file://server/share/a.pdf"); + assert_eq!(super::path_from_arg("file://".to_string()), "file://"); + } + + #[test] + fn opengl_is_the_fallback_unless_a_renderer_was_chosen() { + use super::{RendererChoice, native_options, renderer_choice, retry_with_gl}; + assert_eq!(renderer_choice(None), RendererChoice::Auto); + assert_eq!(renderer_choice(Some("")), RendererChoice::Auto); + assert_eq!(renderer_choice(Some("vulkan")), RendererChoice::Auto); + assert_eq!(renderer_choice(Some(" WGPU ")), RendererChoice::Wgpu); + for gl in ["gl", "OpenGL", "glow"] { + assert_eq!(renderer_choice(Some(gl)), RendererChoice::Gl); + } + // Retried with OpenGL only when wgpu failed to start by default, never after the app ran. + assert!(retry_with_gl(RendererChoice::Auto, false)); + assert!(!retry_with_gl(RendererChoice::Auto, true)); + assert!(!retry_with_gl(RendererChoice::Wgpu, false)); + assert!(!retry_with_gl(RendererChoice::Gl, false)); + // Each run states its renderer rather than relying on eframe's default. + assert_eq!(native_options(false, eframe::Renderer::Wgpu).renderer, eframe::Renderer::Wgpu); + assert_eq!(native_options(false, eframe::Renderer::Glow).renderer, eframe::Renderer::Glow); + } + #[test] fn gpu_backends_avoid_vulkan_on_windows_and_prefer_low_power() { let mut native = eframe::NativeOptions::default(); @@ -325,6 +555,23 @@ mod tests { } } + #[test] + fn winit_carries_the_windows_11_monitor_scale_fix() { + // Issue #324: winit 0.30.13 as released nudges a window dragged onto a monitor with another + // scale factor back onto the one it is leaving, so on Windows 11 it ends up on the wrong + // monitor, at the wrong size and scale. vendor/winit carries the fix from winit master. A + // dependency bump that resolves winit from crates.io again, or a re-vendored copy without + // the patch, would silently bring the bug back: re-apply the patch, or drop the copy once a + // winit 0.30 release has the fix (vendor/README.md). + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let lock = std::fs::read_to_string(root.join("Cargo.lock")).unwrap(); + let winit = lock.split("[[package]]").find(|p| p.contains("\nname = \"winit\"\n")).expect("winit is in Cargo.lock"); + assert!(!winit.contains("\nsource = "), "winit must resolve to vendor/winit, not:{winit}"); + let dpi_changed = std::fs::read_to_string(root.join("vendor/winit/src/platform_impl/windows/event_loop.rs")).unwrap(); + let patch = "if !WIN10_BUILD_VERSION.is_some_and(|build| build < 22000) {\n new_outer_rect = suggested_rect;"; + assert!(dpi_changed.contains(patch), "vendor/winit lost its WM_DPICHANGED patch"); + } + const NVIDIA: (u32, u32) = (0x10de, 0x2684); const AMD_IGPU: (u32, u32) = (0x1002, 0x164e); @@ -333,29 +580,65 @@ mod tests { vec![(NVIDIA.0, NVIDIA.1, DeviceType::DiscreteGpu), (AMD_IGPU.0, AMD_IGPU.1, DeviceType::IntegratedGpu), (0, 0, DeviceType::Cpu)] } + const INTEL_IGPU: (u32, u32) = (0x8086, 0xa780); + + fn monitor(pci: (u32, u32)) -> super::DisplayGpu { + super::DisplayGpu { pci, internal_panel: false } + } + + fn panel(pci: (u32, u32)) -> super::DisplayGpu { + super::DisplayGpu { pci, internal_panel: true } + } + #[test] fn pick_adapter_prefers_the_gpu_driving_the_monitors() { // A desktop whose monitors are all on the discrete GPU: the integrated one shows black. - assert_eq!(super::pick_adapter(&adapters(), &[NVIDIA]), Some(0)); + assert_eq!(super::pick_adapter(&adapters(), &[monitor(NVIDIA)]), Some(0)); } #[test] fn pick_adapter_keeps_the_integrated_gpu_on_hybrid_laptops() { // Issue #8: the panel is on the integrated GPU, an external monitor on the discrete one. - assert_eq!(super::pick_adapter(&adapters(), &[NVIDIA, AMD_IGPU]), Some(1)); - assert_eq!(super::pick_adapter(&adapters(), &[AMD_IGPU]), Some(1)); + assert_eq!(super::pick_adapter(&adapters(), &[monitor(NVIDIA), panel(AMD_IGPU)]), Some(1)); + assert_eq!(super::pick_adapter(&adapters(), &[panel(AMD_IGPU), monitor(NVIDIA)]), Some(1)); + assert_eq!(super::pick_adapter(&adapters(), &[panel(AMD_IGPU)]), Some(1)); + assert_eq!(super::pick_adapter(&adapters(), &[monitor(AMD_IGPU)]), Some(1)); + } + + #[test] + fn pick_adapter_prefers_the_discrete_gpu_on_desktops_with_a_monitor_on_each() { + // Issue #445: one monitor on the Intel iGPU, one on the NVIDIA card, no built-in panel. + use eframe::wgpu::DeviceType; + let desktop = + [(INTEL_IGPU.0, INTEL_IGPU.1, DeviceType::IntegratedGpu), (NVIDIA.0, NVIDIA.1, DeviceType::DiscreteGpu), (0, 0, DeviceType::Cpu)]; + assert_eq!(super::pick_adapter(&desktop, &[monitor(INTEL_IGPU), monitor(NVIDIA)]), Some(1)); + assert_eq!(super::pick_adapter(&desktop, &[monitor(NVIDIA), monitor(INTEL_IGPU)]), Some(1)); + // A GPU driving no display still loses to one that does. + assert_eq!(super::pick_adapter(&adapters(), &[monitor(AMD_IGPU), monitor(INTEL_IGPU)]), Some(1)); } #[test] fn pick_adapter_falls_back_to_low_power_without_a_match() { - assert_eq!(super::pick_adapter(&adapters(), &[(0x8086, 0x1234)]), Some(1)); - assert_eq!(super::pick_adapter(&[], &[NVIDIA]), None); + assert_eq!(super::pick_adapter(&adapters(), &[monitor((0x8086, 0x1234))]), Some(1)); + assert_eq!(super::pick_adapter(&adapters(), &[monitor((0x8086, 0x1234)), monitor((0x8086, 0x5678))]), Some(1)); + assert_eq!(super::pick_adapter(&[], &[monitor(NVIDIA)]), None); + } + + #[test] + fn internal_panels_are_edp_lvds_and_dsi_connectors() { + for name in ["eDP-1", "LVDS-1", "DSI-1"] { + assert!(super::is_internal_panel(name), "{name}"); + } + for name in ["DP-3", "HDMI-A-1", "DVI-D-1", "VGA-1", "Writeback-1", ""] { + assert!(!super::is_internal_panel(name), "{name}"); + } } #[cfg(target_os = "linux")] #[test] fn linux_display_gpus_reads_connected_connectors() -> std::io::Result<()> { let dir = std::env::temp_dir().join(format!("pdfcraft-drm-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); let card = |name: &str, (vendor, device): (u32, u32)| -> std::io::Result<()> { std::fs::create_dir_all(dir.join(name).join("device"))?; std::fs::write(dir.join(name).join("device/vendor"), format!("{vendor:#06x}\n"))?; @@ -371,10 +654,92 @@ mod tests { connector("card1-DP-4", "connected")?; connector("card2-HDMI-A-1", "disconnected")?; connector("card2-Writeback-1", "unknown")?; - let gpus = super::linux_display_gpus(&dir); + let desktop = super::linux_display_gpus(&dir); + // Issue #445: a monitor on the integrated GPU too, still no built-in panel. + connector("card2-HDMI-A-1", "connected")?; + let mut both = super::linux_display_gpus(&dir); + // A hybrid laptop: the integrated GPU drives the built-in panel as well. + connector("card2-eDP-1", "connected")?; + let mut laptop = super::linux_display_gpus(&dir); std::fs::remove_dir_all(&dir)?; - assert_eq!(gpus, vec![NVIDIA]); + assert_eq!(desktop, vec![monitor(NVIDIA)]); + both.sort_by_key(|g| g.pci); + assert_eq!(both, vec![monitor(AMD_IGPU), monitor(NVIDIA)]); + laptop.sort_by_key(|g| g.pci); + assert_eq!(laptop, vec![panel(AMD_IGPU), monitor(NVIDIA)]); assert!(super::linux_display_gpus(std::path::Path::new("/nonexistent/drm")).is_empty()); Ok(()) } + + /// A fresh folder per call: tests run in parallel. + fn scratch() -> std::path::PathBuf { + static N: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); + let n = N.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let d = std::env::temp_dir().join(format!("pdfcraft-control-file-{}-{n}", std::process::id())); + let _ = std::fs::remove_dir_all(&d); + std::fs::create_dir_all(&d).unwrap(); + d + } + + /// The control file's port and token, and every name in its folder. + fn written(dir: &std::path::Path) -> (serde_json::Value, Vec) { + let json = serde_json::from_str(&std::fs::read_to_string(dir.join("ctl.json")).unwrap()).unwrap(); + let mut names: Vec = std::fs::read_dir(dir).unwrap().map(|e| e.unwrap().file_name().to_string_lossy().into_owned()).collect(); + names.sort(); + (json, names) + } + + #[test] + fn the_control_file_replaces_a_hard_link_instead_of_writing_through_it() { + // The file used to be opened with create + truncate, so a link planted at the path had its + // target truncated and overwritten with the token. + let dir = scratch(); + std::fs::write(dir.join("victim.txt"), "keep me").unwrap(); + std::fs::hard_link(dir.join("victim.txt"), dir.join("ctl.json")).unwrap(); + super::write_control_file(&dir.join("ctl.json").to_string_lossy(), 4242, "t0ken").unwrap(); + assert_eq!(std::fs::read_to_string(dir.join("victim.txt")).unwrap(), "keep me"); + let (json, names) = written(&dir); + assert_eq!((json["port"].as_u64(), json["token"].as_str()), (Some(4242), Some("t0ken"))); + assert_eq!(names, ["ctl.json", "victim.txt"], "no temporary file left behind"); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn the_control_file_replaces_a_symlink_instead_of_writing_through_it() { + let dir = scratch(); + let (link, target) = (dir.join("ctl.json"), dir.join("victim.txt")); + std::fs::write(&target, "keep me").unwrap(); + #[cfg(unix)] + let made = std::os::unix::fs::symlink(&target, &link); + #[cfg(windows)] + let made = std::os::windows::fs::symlink_file(&target, &link); + if let Err(e) = made { + // Windows needs Developer Mode (or admin) for symlinks. + eprintln!("skipped: can't create a symlink here: {e}"); + let _ = std::fs::remove_dir_all(&dir); + return; + } + super::write_control_file(&link.to_string_lossy(), 4242, "t0ken").unwrap(); + assert_eq!(std::fs::read_to_string(&target).unwrap(), "keep me"); + assert!(!std::fs::symlink_metadata(&link).unwrap().file_type().is_symlink()); + assert_eq!(written(&dir).0["token"].as_str(), Some("t0ken")); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn the_control_file_replaces_the_one_a_previous_run_left() { + let dir = scratch(); + std::fs::write(dir.join("ctl.json"), r#"{"port":1,"token":"old"}"#).unwrap(); + super::write_control_file(&dir.join("ctl.json").to_string_lossy(), 4242, "t0ken").unwrap(); + let (json, names) = written(&dir); + assert_eq!(json["token"].as_str(), Some("t0ken")); + assert_eq!(names, ["ctl.json"]); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = std::fs::metadata(dir.join("ctl.json")).unwrap().permissions().mode(); + assert_eq!(mode & 0o077, 0, "only the owner can read the token: {mode:o}"); + } + let _ = std::fs::remove_dir_all(&dir); + } } diff --git a/contributors/people.toml b/contributors/people.toml index c0549e9d..ade46dac 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -22,6 +22,11 @@ real_name = "Brandon Thomas" display_name = "Brandon Thomas" sync_github_name = false +[people.jcl-bit] +real_name = "James Locke" +display_name = "James Locke" +sync_github_name = false + [people.switch87] real_name = "Gert Pellin" display_name = "Gert Pellin" @@ -36,3 +41,26 @@ sync_github_name = false real_name = "Hal Goff" display_name = "Hal Goff, HG Studio" sync_github_name = false + +[people.mlkonrad] +real_name = "Marlon Konrad" +display_name = "Marlon Konrad" +sync_github_name = false + +[people.chiranjeevi-max] +real_name = "Chiranjeevi Uddanti" +display_name = "Chiranjeevi U" +sync_github_name = false + +[people.cyfung1031] +real_name = "Chester Fung" +display_name = "Chester Fung" +sync_github_name = true + +[people.amitabhthakur] +sync_github_name = true + +[people.chent11] +real_name = "Tao Chen" +display_name = "Tao Chen" +sync_github_name = false diff --git a/crates/a11y/src/alt.rs b/crates/a11y/src/alt.rs index 95e272d3..38f0c9f2 100644 --- a/crates/a11y/src/alt.rs +++ b/crates/a11y/src/alt.rs @@ -144,10 +144,8 @@ fn content_bbox(doc: &Document, page: &Dict, ids: &HashSet) -> Option<[f64; let top = ctm.last().copied().unwrap_or(Matrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0])); match op.op.as_slice() { b"q" => ctm.push(top), - b"Q" => { - if ctm.len() > 1 { - ctm.pop(); - } + b"Q" if ctm.len() > 1 => { + ctm.pop(); } b"cm" => { if let Some(m) = Matrix::from_operands(&op.operands) diff --git a/crates/annot/Cargo.toml b/crates/annot/Cargo.toml index 9603e572..8a313814 100644 --- a/crates/annot/Cargo.toml +++ b/crates/annot/Cargo.toml @@ -11,6 +11,7 @@ workspace = true [dependencies] pdfcraft-cos = { workspace = true } pdfcraft-fonts = { workspace = true } +pdfcraft-model = { workspace = true } thiserror = { workspace = true } [dev-dependencies] diff --git a/crates/annot/README.md b/crates/annot/README.md index b3224b7b..4d0fa7c9 100644 --- a/crates/annot/README.md +++ b/crates/annot/README.md @@ -1,6 +1,6 @@ # pdfcraft-annot -Comments (annotations), ISO 32000-2 §12.5. Layer L3, depends only on `pdfcraft-cos`. +Comments (annotations), ISO 32000-2 §12.5. Layer L3, depends on `pdfcraft-cos`, `pdfcraft-fonts` and `pdfcraft-model`. ## What it does @@ -23,6 +23,7 @@ set_review_state(&mut doc, page, index, ReviewState::Accepted, "Ada", &meta)?; move_annotation(&mut doc, page, index, dx, dy, &meta)?; set_style(&mut doc, page, index, Some(rgb), Some(0.5), Some(2.0), &meta)?; delete_annotation(&mut doc, page, index)?; +signature_image(&doc, page, index)?; // embedded image XObject of Fill & Sign signature/initials ``` A comment is addressed by `(page, index in /Annots)`, the same pair @@ -37,6 +38,12 @@ deterministic; the engine supplies them. cloudy borders or callouts, instead of leaving a stale appearance. - Replies get the parent's `/Rect` and an empty appearance: they appear in comment lists but never paint a second icon. +- Resizing a stamp changes its `/Rect` and preserves its original appearance and resources; + viewers scale that appearance into the new rectangle. Locked stamps refuse the edit. +- Natural-size image stamps placed by the engine counterrotate their appearance on rotated + pages. Image restyling retains that appearance matrix. Explicit rectangles, image signatures + and PDF-page stamps still use their existing placement behavior; page-rotation support for + those paths is not complete. ## Not yet diff --git a/crates/annot/src/appearance.rs b/crates/annot/src/appearance.rs index ecb8549b..9a026069 100644 --- a/crates/annot/src/appearance.rs +++ b/crates/annot/src/appearance.rs @@ -26,23 +26,98 @@ pub fn cloud_radius(w: f64) -> f64 { 6.0 + 1.5 * w.max(0.0) } -/// Draw a line ending (`/LE`) at `tip`, for a line arriving from `from`. Open and closed arrows only. -fn line_end(c: &mut String, kind: &[u8], tip: (f64, f64), from: (f64, f64), w: f64) { +/// One of the ten `/LE` names (ISO 32000-2 Table 217). Unknown names are not drawn. +pub(crate) fn known_ending(kind: &[u8]) -> bool { + std::str::from_utf8(kind).ok().and_then(crate::LineEnding::parse).is_some() +} + +/// Draw a line ending (`/LE`) at `tip`, for a line arriving from `from`. +/// +/// Returns false for an unknown style or a non-finite direction, so the caller keeps the +/// previous appearance instead of writing `NaN`. `None` and a zero-length direction draw nothing. +fn line_end(c: &mut String, kind: &[u8], tip: (f64, f64), from: (f64, f64), w: f64) -> bool { if kind == b"None" { - return; + return true; + } + if !known_ending(kind) { + return false; } let (dx, dy) = (from.0 - tip.0, from.1 - tip.1); - let len = dx.hypot(dy); - if len == 0.0 { - return; + if !dx.is_finite() || !dy.is_finite() { + return false; + } + let len = dx.hypot(dy); + if !len.is_finite() { + return false; + } + if len == 0.0 { + return true; } - let (ux, uy) = (dx / len, dy / len); let s = arrow_size(w); - let (cos, sin) = (30f64.to_radians().cos(), 30f64.to_radians().sin()); - let a = (tip.0 + s * (ux * cos - uy * sin), tip.1 + s * (ux * sin + uy * cos)); - let b = (tip.0 + s * (ux * cos + uy * sin), tip.1 + s * (-ux * sin + uy * cos)); - let op = if kind == b"ClosedArrow" { "h B" } else { "S" }; - c.push_str(&format!("{} {} m {} {} l {} {} l {op}\n", n(a.0), n(a.1), n(tip.0), n(tip.1), n(b.0), n(b.1))); + if !s.is_finite() { + return false; + } + let (ix, iy) = (dx / len, dy / len); + match kind { + b"OpenArrow" | b"ClosedArrow" | b"ROpenArrow" | b"RClosedArrow" => { + // Open and closed arrows keep their original geometry. A reverse arrow uses the same + // head, pointing back along the line. + let (ux, uy) = if kind.starts_with(b"R") { (-ix, -iy) } else { (ix, iy) }; + let (cos, sin) = (30f64.to_radians().cos(), 30f64.to_radians().sin()); + let a = (tip.0 + s * (ux * cos - uy * sin), tip.1 + s * (ux * sin + uy * cos)); + let b = (tip.0 + s * (ux * cos + uy * sin), tip.1 + s * (-ux * sin + uy * cos)); + let op = if kind.ends_with(b"ClosedArrow") { "h B" } else { "S" }; + c.push_str(&format!("{} {} m {} {} l {} {} l {op}\n", n(a.0), n(a.1), n(tip.0), n(tip.1), n(b.0), n(b.1))); + } + b"Square" | b"Circle" | b"Diamond" => { + let h = s / 2.0; + if kind == b"Square" { + c.push_str(&format!("{} {} {} {} re h B\n", n(tip.0 - h), n(tip.1 - h), n(s), n(s))); + } else if kind == b"Circle" { + c.push_str(&ellipse(tip.0 - h, tip.1 - h, tip.0 + h, tip.1 + h)); + c.push_str("B\n"); + } else { + let (px, py) = (-iy, ix); + let v = |along: f64, across: f64| (tip.0 + ix * along + px * across, tip.1 + iy * along + py * across); + let (a, b, d, e) = (v(h, 0.0), v(0.0, h), v(-h, 0.0), v(0.0, -h)); + c.push_str(&format!("{} {} m {} {} l {} {} l {} {} l h B\n", n(a.0), n(a.1), n(b.0), n(b.1), n(d.0), n(d.1), n(e.0), n(e.1))); + } + } + _ => { + // Butt is perpendicular to the line. Slash is that stroke turned 45 degrees. + let (px, py) = (-iy, ix); + let (sx, sy) = if kind == b"Slash" { + let q = std::f64::consts::FRAC_1_SQRT_2; + (px * q - py * q, px * q + py * q) + } else { + (px, py) + }; + let h = s / 2.0; + c.push_str(&format!("{} {} m {} {} l S\n", n(tip.0 - sx * h), n(tip.1 - sy * h), n(tip.0 + sx * h), n(tip.1 + sy * h))); + } + } + true +} + +/// The cubic Bézier segments (first control point, second control point, end) of a smooth curve +/// through every point of `pts`, from the first: a Catmull-Rom spline, so the curve leaves each +/// point parallel to the chord between its neighbours (an end uses itself as the missing +/// neighbour). Ink is drawn so, as Acrobat draws it, rather than as straight segments. The curve +/// stays within its points and these control points. +pub(crate) fn smooth_segments(pts: &[(f64, f64)]) -> Vec<[(f64, f64); 3]> { + let Some(&last) = pts.last() else { return Vec::new() }; + let at = |i: usize| pts.get(i).copied().unwrap_or(last); + (0..pts.len().saturating_sub(1)) + .map(|i| { + let (p0, p1, p2, p3) = (at(i.saturating_sub(1)), at(i), at(i + 1), at(i + 2)); + [(p1.0 + (p2.0 - p0.0) / 6.0, p1.1 + (p2.1 - p0.1) / 6.0), (p2.0 - (p3.0 - p1.0) / 6.0, p2.1 - (p3.1 - p1.1) / 6.0), p2] + }) + .collect() +} + +/// `c` operators for [`smooth_segments`] (the path already starts at the first point). +fn smooth_curve(pts: &[(f64, f64)]) -> String { + smooth_segments(pts).iter().map(|[a, b, e]| format!("{} {} {} {} {} {} c\n", n(a.0), n(a.1), n(b.0), n(b.1), n(e.0), n(e.1))).collect() } /// A closed cloudy outline through `pts` (§12.5.4 `/BE /S /C`): each edge becomes a row of @@ -179,6 +254,12 @@ fn form(bbox: [f64; 4], content: &[u8], resources: Dict) -> Stream { Stream::flate(d, content) } +/// The page `/Rotate` an image signature's picture is turned back by (`/PCPictureRotate`, written +/// when the signature is added to a turned page): 0, 90, 180 or 270. +pub(crate) fn picture_turn(d: &Dict) -> i64 { + d.get(b"PCPictureRotate").and_then(Object::as_int).unwrap_or(0).rem_euclid(360) / 90 * 90 +} + /// Draw the normal appearance of an annotation, or `None` if this subtype/variant isn't supported. pub fn build(d: &Dict) -> Option { // Imported measurement appearances may include leaders, captions and formatting that @@ -313,32 +394,45 @@ pub fn build(d: &Dict) -> Option { }); } b"Line" => { - let l = nums(d, b"L").filter(|l| l.len() == 4)?; + let l = nums(d, b"L").filter(|l| l.len() == 4 && l.iter().all(|x| x.is_finite()))?; let col = stroke?; let ends: Vec> = match d.get(b"LE") { None => vec![b"None".to_vec(), b"None".to_vec()], Some(o) => o.as_array()?.iter().map(|e| e.as_name().map(<[u8]>::to_vec)).collect::>()?, }; - if ends.len() != 2 || ends.iter().any(|e| !matches!(e.as_slice(), b"None" | b"OpenArrow" | b"ClosedArrow")) { + if ends.len() != 2 || ends.iter().any(|e| !known_ending(e)) { return None; } let fill = color(d, b"IC")?.unwrap_or(col); c.push_str(&format!("{}{}{} w 1 J 1 j\n{}", rg_stroke(col), rg(fill), n(w), dash(d))); c.push_str(&format!("{} {} m {} {} l S\n[] 0 d\n", n(l[0]), n(l[1]), n(l[2]), n(l[3]))); for (end, (tip, from)) in ends.iter().zip([((l[0], l[1]), (l[2], l[3])), ((l[2], l[3]), (l[0], l[1]))]) { - line_end(&mut c, end, tip, from, w); + if !line_end(&mut c, end, tip, from, w) { + return None; + } } } b"Polygon" | b"PolyLine" => { - let v = nums(d, b"Vertices").filter(|v| v.len() >= 4 && v.len() % 2 == 0)?; + let v = nums(d, b"Vertices").filter(|v| v.len() >= 4 && v.len() % 2 == 0 && v.iter().all(|x| x.is_finite()))?; let pts: Vec<(f64, f64)> = v.as_chunks::<2>().0.iter().map(|p| (p[0], p[1])).collect(); let col = stroke; let closed = subtype == b"Polygon"; let fill = if closed { color(d, b"IC")? } else { None }; - if subtype == b"PolyLine" && d.get(b"LE").is_some_and(|e| e.as_array().is_none_or(|a| a.iter().any(|x| x.as_name() != Some(b"None")))) { - // Line endings on connected lines aren't drawn yet. - return None; - } + let ends = if subtype == b"PolyLine" { + match d.get(b"LE") { + None => None, + Some(o) => { + let names: Vec> = + o.as_array().filter(|a| a.len() == 2)?.iter().map(|e| e.as_name().map(<[u8]>::to_vec)).collect::>()?; + if names.iter().any(|e| !known_ending(e)) { + return None; + } + Some(names) + } + } + } else { + None + }; if let Some(f) = fill { c.push_str(&rg(f)); } @@ -362,6 +456,17 @@ pub fn build(d: &Dict) -> Option { (false, true) => "S\n", (false, false) => "n\n", }); + if let Some(ends) = ends { + let col = col?; + let (Some(start), Some(end)) = (ends.first(), ends.get(1)) else { return None }; + let (Some(&tip0), Some(&from0)) = (pts.first(), pts.get(1)) else { return None }; + let (Some(&tip1), Some(&from1)) = (pts.last(), pts.get(pts.len().saturating_sub(2))) else { return None }; + let fill = color(d, b"IC")?.unwrap_or(col); + c.push_str(&format!("[] 0 d\n{}", rg(fill))); + if !line_end(&mut c, start, tip0, from0, w) || !line_end(&mut c, end, tip1, from1, w) { + return None; + } + } } b"Caret" => { // A filled caret: two curved flanks meeting at the top centre. @@ -396,11 +501,10 @@ pub fn build(d: &Dict) -> Option { let pts: Vec<(f64, f64)> = pts.as_chunks::<2>().0.iter().map(|p| (p[0], p[1])).collect(); let Some(first) = pts.first() else { continue }; c.push_str(&format!("{} {} m\n", n(first.0), n(first.1))); - if pts.len() == 1 { - c.push_str(&format!("{} {} l\n", n(first.0 + 0.01), n(first.1))); - } - for p in &pts[1..] { - c.push_str(&format!("{} {} l\n", n(p.0), n(p.1))); + match pts.as_slice() { + [_] => c.push_str(&format!("{} {} l\n", n(first.0 + 0.01), n(first.1))), + [_, end] => c.push_str(&format!("{} {} l\n", n(end.0), n(end.1))), + _ => c.push_str(&smooth_curve(&pts)), } c.push_str("S\n"); } @@ -424,11 +528,14 @@ pub fn build(d: &Dict) -> Option { return Some(form(rect, c.as_bytes(), res)); } // A custom stamp: its picture (an image, or a form mapped to /PCPictureSize) fills - // the rectangle. + // the rectangle. An image signature added to a turned page (`/PCPictureRotate`) gets + // the standard counter-rotation as its form `/Matrix`: viewers fit the turned + // bounding box to `/Rect` (ISO 32000-2 §12.5.5), so it reads upright as displayed. if let Some(pic) = d.get(b"PCPicture").and_then(Object::as_ref) { let [x0, y0, x1, y1] = rect; let (w, h) = (x1 - x0, y1 - y0); - let place = if matches!(d.get(b"PCPictureImage"), Some(Object::Bool(true))) { + let image = matches!(d.get(b"PCPictureImage"), Some(Object::Bool(true))); + let place = if image { format!("{} 0 0 {} {} {} cm", n(w), n(h), n(x0), n(y0)) } else { let size = nums(d, b"PCPictureSize").filter(|s| s.len() == 2 && s[0] > 0.0 && s[1] > 0.0)?; @@ -438,7 +545,15 @@ pub fn build(d: &Dict) -> Option { let mut xo = Dict::new(); xo.set(b"Pic".to_vec(), Object::Ref(pic)); res.set(b"XObject".to_vec(), Object::Dict(xo)); - return Some(form(rect, c.as_bytes(), res)); + let mut stream = form(rect, c.as_bytes(), res); + let turn = picture_turn(d); + if image && turn != 0 { + // The page's view matrix without its offset: display axes in user space. + let m = pdfcraft_model::view_matrix_for(turn, [0.0; 4]); + let matrix = [m[0], m[1], m[2], m[3], 0.0, 0.0]; + stream.dict.set(b"Matrix".to_vec(), Object::Array(matrix.iter().map(|x| Object::Real(*x)).collect())); + } + return Some(stream); } // Only PdfCraft's own Fill & Sign marks are drawn here. let name = d.name(b"Name")?; @@ -516,17 +631,22 @@ pub fn build(d: &Dict) -> Option { None => b"None".to_vec(), Some(o) => o.as_name()?.to_vec(), }; - if !matches!(end.as_slice(), b"None" | b"OpenArrow" | b"ClosedArrow") { + if !known_ending(&end) { return None; } let lw = bw.max(0.5); let pts: Vec<(f64, f64)> = cl.as_chunks::<2>().0.iter().map(|p| (p[0], p[1])).collect(); + if pts.len() < 2 || pts.iter().any(|p| !p.0.is_finite() || !p.1.is_finite()) { + return None; + } c.push_str(&format!("{}{}{} w 1 J 1 j\n", rg_stroke(text_color), rg(bg.unwrap_or([1.0; 3])), n(lw))); for (i, p) in pts.iter().enumerate() { c.push_str(&format!("{} {} {}\n", n(p.0), n(p.1), if i == 0 { "m" } else { "l" })); } c.push_str("S\n"); - line_end(&mut c, &end, pts[0], pts[1], lw); + if !line_end(&mut c, &end, pts[0], pts[1], lw) { + return None; + } } if let Some(bg) = bg { c.push_str(&format!("{}{} {} {} {} re f\n", rg(bg), n(rect[0]), n(rect[1]), n(rect[2] - rect[0]), n(rect[3] - rect[1]))); diff --git a/crates/annot/src/lib.rs b/crates/annot/src/lib.rs index a0f76306..9eb34651 100644 --- a/crates/annot/src/lib.rs +++ b/crates/annot/src/lib.rs @@ -274,6 +274,55 @@ impl StampKind { } } +/// A line ending (`/LE`): the ten styles in ISO 32000-2 Table 217. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LineEnding { + None, + Square, + Circle, + Diamond, + OpenArrow, + ClosedArrow, + Butt, + ROpenArrow, + RClosedArrow, + Slash, +} + +impl LineEnding { + pub const ALL: [Self; 10] = [ + Self::None, + Self::Square, + Self::Circle, + Self::Diamond, + Self::OpenArrow, + Self::ClosedArrow, + Self::Butt, + Self::ROpenArrow, + Self::RClosedArrow, + Self::Slash, + ]; + + pub fn name(self) -> &'static str { + match self { + Self::None => "None", + Self::Square => "Square", + Self::Circle => "Circle", + Self::Diamond => "Diamond", + Self::OpenArrow => "OpenArrow", + Self::ClosedArrow => "ClosedArrow", + Self::Butt => "Butt", + Self::ROpenArrow => "ROpenArrow", + Self::RClosedArrow => "RClosedArrow", + Self::Slash => "Slash", + } + } + + pub fn parse(name: &str) -> Option { + Self::ALL.into_iter().find(|ending| ending.name() == name) + } +} + /// Geometry of a new comment, in PDF user space of its page. #[derive(Clone, Debug, PartialEq)] pub enum Shape { @@ -294,11 +343,12 @@ pub enum Shape { Oval { rect: [f64; 4], }, - /// A line, with an open arrowhead at `to` when `arrow` is set. + /// A line. An open arrow at `to` is `start: None`, `end: OpenArrow` (the Arrow tool). Line { from: [f64; 2], to: [f64; 2], - arrow: bool, + start: LineEnding, + end: LineEnding, }, /// Freehand strokes (Draw tool). Ink { @@ -357,9 +407,11 @@ pub enum Shape { vertices: Vec<[f64; 2]>, cloud: bool, }, - /// Connected lines (Polygonal Line tool). + /// Connected lines (Polygonal Line tool). `start` is the first vertex, `end` the last. PolyLine { vertices: Vec<[f64; 2]>, + start: LineEnding, + end: LineEnding, }, /// A text callout: a text box at `rect` with a leader line from `point` (arrowhead) via /// `knee` to the box (FreeText, `/IT /FreeTextCallout`, `/CL`). @@ -368,6 +420,7 @@ pub enum Shape { knee: [f64; 2], point: [f64; 2], font_size: f64, + ending: LineEnding, }, /// Insert text: a caret in `rect` (its point at the top centre). Caret { @@ -662,6 +715,31 @@ fn annot_dict(doc: &Document, r: ObjRef) -> Dict { doc.get(r).as_dict().cloned().unwrap_or_default() } +/// The embedded image of a Fill & Sign image signature or initials (0-based target). +/// Other stamps have appearances that can't be represented by this image alone. +pub fn signature_image(doc: &Document, page: usize, index: usize) -> Result, AnnotError> { + let p = page_ref(doc, page)?; + let list = annots(doc, p); + let entry = list.get(index).ok_or(AnnotError::NoSuchAnnotation { page, index })?; + let obj = doc.resolve(entry); + let Some(d) = obj.as_dict() else { return Ok(None) }; + Ok((d.name(b"Subtype") == Some(b"Stamp") + && matches!(d.name(b"Name"), Some(b"PCCustomSignature" | b"PCCustomInitials")) + && matches!(d.get(b"PCPictureImage"), Some(Object::Bool(true)))) + .then(|| d.reference(b"PCPicture")) + .flatten()) +} + +/// The page `/Rotate` the picture of the image signature at `(page, index)` is drawn turned back +/// by, so it reads upright on a page shown that way: 0 for one added to an unturned page, or by +/// another app. The picture appears turned by the page's current rotation less this. +pub fn picture_rotation(doc: &Document, page: usize, index: usize) -> Result { + let p = page_ref(doc, page)?; + let list = annots(doc, p); + let entry = list.get(index).ok_or(AnnotError::NoSuchAnnotation { page, index })?; + Ok(doc.resolve(entry).as_dict().map_or(0, appearance::picture_turn)) +} + // ── building ──────────────────────────────────────────────────────────────────────────────── /// Annotation flags (§12.5.3). @@ -701,6 +779,14 @@ fn grow(r: [f64; 4], by: f64) -> [f64; 4] { [r[0] - by, r[1] - by, r[2] + by, r[3] + by] } +fn ending_draws(ending: LineEnding) -> bool { + ending != LineEnding::None +} + +fn ending_pair(start: LineEnding, end: LineEnding) -> Object { + Object::Array(vec![Object::name(start.name()), Object::name(end.name())]) +} + /// Validate and compute `/Rect` for a new comment. fn rect_for(shape: &Shape, style: &Style) -> Result<[f64; 4], AnnotError> { let bad = |what: &str| AnnotError::Invalid(format!("invalid {what}")); @@ -724,7 +810,6 @@ fn rect_for(shape: &Shape, style: &Style) -> Result<[f64; 4], AnnotError> { | Shape::TextBox { rect, .. } | Shape::Typewriter { rect, .. } | Shape::Stamp { rect, .. } - | Shape::CustomStamp { rect, .. } | Shape::TypedSignature { rect, .. } | Shape::Mark { rect, .. } => { let r = normalize(*rect); @@ -733,18 +818,32 @@ fn rect_for(shape: &Shape, style: &Style) -> Result<[f64; 4], AnnotError> { } r } - Shape::Line { from, to, arrow } => { + Shape::CustomStamp { rect, .. } => { + let r = normalize(*rect); + // Image signatures may be very thin; a positive PDF appearance box still works. + if !finite(rect) || r[2] <= r[0] || r[3] <= r[1] { + return Err(bad("rectangle (empty)")); + } + r + } + Shape::Line { from, to, start, end } => { if !finite(from) || !finite(to) || (from[0] - to[0]).hypot(from[1] - to[1]) < 1.0 { return Err(bad("line (too short)")); } - let pad = half + if *arrow { appearance::arrow_size(style.width) } else { 0.0 }; + let pad = half + if ending_draws(*start) || ending_draws(*end) { appearance::arrow_size(style.width) } else { 0.0 }; grow(bounds([*from, *to].into_iter()).unwrap_or_default(), pad + 1.0) } Shape::Ink { strokes } | Shape::Signature { strokes } => { if strokes.iter().all(|s| s.is_empty()) || !strokes.iter().flatten().all(|p| finite(p)) { return Err(bad("drawing (no points)")); } - grow(bounds(strokes.iter().flatten().copied()).unwrap_or_default(), half + 1.0) + // Strokes of three or more points are drawn as curves, which stay within their + // points and control points. + let controls = strokes.iter().filter(|s| s.len() > 2).flat_map(|s| { + let pts: Vec<(f64, f64)> = s.iter().map(|p| (p[0], p[1])).collect(); + appearance::smooth_segments(&pts).into_iter().flat_map(|[a, b, _]| [[a.0, a.1], [b.0, b.1]]) + }); + grow(bounds(strokes.iter().flatten().copied().chain(controls)).unwrap_or_default(), half + 1.0) } Shape::Polygon { vertices, cloud } => { let b = bounds(vertices.iter().copied()) @@ -752,10 +851,11 @@ fn rect_for(shape: &Shape, style: &Style) -> Result<[f64; 4], AnnotError> { let b = b.ok_or_else(|| bad("polygon (it needs three points)"))?; grow(b, half + 1.0 + if *cloud { 1.5 * appearance::cloud_radius(style.width) } else { 0.0 }) } - Shape::PolyLine { vertices } => { + Shape::PolyLine { vertices, start, end } => { let b = bounds(vertices.iter().copied()) .filter(|b| vertices.len() >= 2 && vertices.iter().all(|p| finite(p)) && (b[2] - b[0]).max(b[3] - b[1]) >= 1.0); - grow(b.ok_or_else(|| bad("connected lines (they need two points)"))?, half + 1.0) + let extra = if ending_draws(*start) || ending_draws(*end) { appearance::arrow_size(style.width) } else { 0.0 }; + grow(b.ok_or_else(|| bad("connected lines (they need two points)"))?, half + 1.0 + extra) } Shape::Callout { rect, knee, point, .. } => { let r = normalize(*rect); @@ -808,7 +908,7 @@ fn subject(shape: &Shape) -> &'static str { Shape::TextMarkup { kind: Markup::Squiggly, .. } => "Squiggly", Shape::Rectangle { .. } => "Rectangle", Shape::Oval { .. } => "Oval", - Shape::Line { arrow: true, .. } => "Arrow", + Shape::Line { start: LineEnding::None, end: LineEnding::OpenArrow, .. } => "Arrow", Shape::Line { .. } => "Line", Shape::Ink { .. } => "Pencil", Shape::TextBox { .. } => "Text Box", @@ -905,11 +1005,11 @@ pub fn add_annotation(doc: &mut Document, new: &NewAnnotation, meta: &Meta) -> R } border(&mut d); } - Shape::Line { from, to, arrow } => { + Shape::Line { from, to, start, end } => { d.set(b"C".to_vec(), rgb(style.color)); d.set(b"L".to_vec(), num_array(&[from[0], from[1], to[0], to[1]])); - if *arrow { - d.set(b"LE".to_vec(), Object::Array(vec![Object::name("None"), Object::name("OpenArrow")])); + if ending_draws(*start) || ending_draws(*end) { + d.set(b"LE".to_vec(), ending_pair(*start, *end)); } border(&mut d); } @@ -940,6 +1040,14 @@ pub fn add_annotation(doc: &mut Document, new: &NewAnnotation, meta: &Meta) -> R d.set(b"PCPicture".to_vec(), Object::Ref(*picture)); d.set(b"PCPictureImage".to_vec(), Object::Bool(*image)); d.set(b"PCPictureSize".to_vec(), num_array(&[size.0, size.1])); + // An image signature's rectangle is in user space; on a turned page its appearance is + // turned back (see `appearance::build`), so it reads upright as displayed. + if *image && matches!(clean.as_str(), "Signature" | "Initials") { + let turn = pdfcraft_model::pages(doc).get(new.page).map_or(0, |p| p.rotation(doc)); + if turn != 0 { + d.set(b"PCPictureRotate".to_vec(), Object::Int(turn)); + } + } } Shape::Ink { strokes } | Shape::Signature { strokes } => { d.set(b"C".to_vec(), rgb(style.color)); @@ -962,9 +1070,12 @@ pub fn add_annotation(doc: &mut Document, new: &NewAnnotation, meta: &Meta) -> R } border(&mut d); } - Shape::PolyLine { vertices } => { + Shape::PolyLine { vertices, start, end } => { d.set(b"C".to_vec(), rgb(style.color)); d.set(b"Vertices".to_vec(), num_array(&vertices.concat())); + if ending_draws(*start) || ending_draws(*end) { + d.set(b"LE".to_vec(), ending_pair(*start, *end)); + } border(&mut d); } Shape::Caret { .. } => { @@ -1001,12 +1112,12 @@ pub fn add_annotation(doc: &mut Document, new: &NewAnnotation, meta: &Meta) -> R d.set(b"Contents".to_vec(), PdfString::text(file.trim())); } } - Shape::Callout { rect: tb, knee, point, .. } => { + Shape::Callout { rect: tb, knee, point, ending, .. } => { let tb = normalize(*tb); let attach = callout_attach(tb, *knee); d.set(b"IT".to_vec(), Object::name("FreeTextCallout")); d.set(b"CL".to_vec(), num_array(&[point[0], point[1], knee[0], knee[1], attach[0], attach[1]])); - d.set(b"LE".to_vec(), Object::name("OpenArrow")); + d.set(b"LE".to_vec(), Object::name(ending.name())); // The text box inside /Rect (§12.5.6.6 /RD). d.set(b"RD".to_vec(), num_array(&[tb[0] - rect[0], tb[1] - rect[1], rect[2] - tb[2], rect[3] - tb[3]])); } @@ -1050,14 +1161,57 @@ pub fn add_annotation(doc: &mut Document, new: &NewAnnotation, meta: &Meta) -> R Ok(index) } +/// Counterrotate a newly placed image stamp's appearance into displayed-page axes. +/// The normal appearance's transformed bounding box is fitted to `/Rect` by PDF viewers. +pub fn orient_image_stamp(doc: &mut Document, page: usize, index: usize, rotation: i64) -> Result<(), AnnotError> { + let matrix = match rotation { + 90 => [0.0, 1.0, -1.0, 0.0, 0.0, 0.0], + 180 => [-1.0, 0.0, 0.0, -1.0, 0.0, 0.0], + 270 => [0.0, -1.0, 1.0, 0.0, 0.0, 0.0], + _ => return Ok(()), + }; + let (_, r) = annot_ref(doc, page, index)?; + let d = annot_dict(doc, r); + if d.name(b"Subtype") != Some(b"Stamp") || !matches!(d.get(b"PCPictureImage"), Some(Object::Bool(true))) { + return Err(AnnotError::Invalid("only an image stamp can be oriented".into())); + } + let normal = d + .get(b"AP") + .map(|ap| doc.resolve(ap)) + .and_then(|ap| ap.as_dict().and_then(|ap| ap.reference(b"N"))) + .ok_or_else(|| AnnotError::Invalid("the stamp has no normal appearance".into()))?; + let Object::Stream(mut stream) = doc.get(normal).as_ref().clone() else { + return Err(AnnotError::Invalid("the stamp's normal appearance is not a stream".into())); + }; + stream.dict.set(b"Matrix".to_vec(), num_array(&matrix)); + doc.set(normal, Object::Stream(stream)); + Ok(()) +} + /// (Re)generate `/AP /N` for the annotation `r` from its dictionary. /// Regenerate an annotation's normal appearance from its dictionary. pub fn set_appearance(doc: &mut Document, r: ObjRef) -> Result<(), AnnotError> { let d = annot_dict(doc, r); let subtype = String::from_utf8_lossy(d.name(b"Subtype").unwrap_or_default()).into_owned(); - let Some(stream) = appearance::build(&d) else { return Err(AnnotError::Unsupported(subtype)) }; + let Some(mut stream) = appearance::build(&d) else { return Err(AnnotError::Unsupported(subtype)) }; + // Image stamp restyling must retain the placement's page-axis correction. + if matches!(d.get(b"PCPictureImage"), Some(Object::Bool(true))) { + let matrix = d.get(b"AP").and_then(|ap| { + let ap = doc.resolve(ap); + let normal = doc.resolve(ap.as_dict()?.get(b"N")?); + let Object::Stream(normal) = normal.as_ref() else { return None }; + normal.dict.get(b"Matrix").cloned() + }); + if let Some(matrix) = matrix { + stream.dict.set(b"Matrix".to_vec(), matrix); + } + } let ap = doc.add(Object::Stream(stream)); - let mut apd = Dict::new(); + // A copy (a shared /AP is left alone) that keeps unknown entries; the old down and rollover + // appearances would show the previous look on press or hover, so they go with the old /N. + let mut apd = d.get(b"AP").map(|a| doc.resolve(a)).and_then(|a| a.as_dict().cloned()).unwrap_or_default(); + apd.remove(b"D"); + apd.remove(b"R"); apd.set(b"N".to_vec(), Object::Ref(ap)); doc.update_dict(r, |d| { d.set(b"AP".to_vec(), Object::Dict(apd)); @@ -1066,6 +1220,30 @@ pub fn set_appearance(doc: &mut Document, r: ObjRef) -> Result<(), AnnotError> { Ok(()) } +/// A copy of `doc` in which every annotation without a normal appearance (`/AP /N`) has the one +/// [`appearance::build`] draws from its dictionary, for displaying the document (`None` when no +/// annotation needs one). Many files carry comments without appearances (FreeText, Ink, notes, +/// stamps, …) that viewers draw from the dictionary. `doc` is not changed: saving writes the file +/// as it was. Links, form fields and pop-ups keep their own handling. +pub fn with_missing_appearances(doc: &Document) -> Option { + let mut copy: Option = None; + for page in page_refs(doc).ok()? { + for r in annots(doc, page).iter().filter_map(Object::as_ref) { + let d = annot_dict(doc, r); + if matches!(d.name(b"Subtype"), None | Some(b"Link" | b"Widget" | b"Popup")) { + continue; + } + let has_normal = d.get(b"AP").is_some_and(|ap| doc.resolve(ap).as_dict().is_some_and(|ap| ap.contains(b"N"))); + if has_normal || appearance::build(&d).is_none() { + continue; + } + // Fails only when `r` isn't a dictionary, which `build` above has just ruled out. + set_appearance(copy.get_or_insert_with(|| doc.clone()), r).ok(); + } + } + copy +} + /// Format a number for content streams and DA strings. pub(crate) fn n(v: f64) -> String { let s = format!("{:.3}", if v.abs() < 5e-4 { 0.0 } else { v }); @@ -1353,12 +1531,24 @@ pub fn move_annotation(doc: &mut Document, page: usize, index: usize, dx: f64, d Ok(()) } -/// Resize a rectangle, oval or text box to `rect`; its appearance is redrawn. +/// Resize a rectangle, oval, text box or stamp to `rect`. Stamps keep their appearance, +/// which PDF viewers scale from its bounding box into the new rectangle. pub fn set_rect(doc: &mut Document, page: usize, index: usize, rect: [f64; 4], meta: &Meta) -> Result<(), AnnotError> { let (_, r) = annot_ref(doc, page, index)?; unlocked(doc, r)?; let d = annot_dict(doc, r); let subtype = String::from_utf8_lossy(d.name(b"Subtype").unwrap_or_default()).into_owned(); + if subtype == "Stamp" { + let rect = normalize(rect); + if !finite(&rect) || rect[2] <= rect[0] || rect[3] <= rect[1] { + return Err(AnnotError::Invalid("invalid rectangle (too small)".into())); + } + doc.update_dict(r, |d| { + d.set(b"Rect".to_vec(), num_array(&rect)); + touch(d, meta); + })?; + return Ok(()); + } if !matches!(subtype.as_str(), "Square" | "Circle" | "FreeText") { return Err(AnnotError::Invalid(format!("{subtype} comments can't be resized"))); } @@ -1405,7 +1595,8 @@ fn border_width_of(d: &Dict) -> f64 { d.get(b"BS").and_then(|b| b.as_dict()).and_then(|b| b.get(b"W")).and_then(|w| w.as_f64()).unwrap_or(1.0).max(0.0) } -/// Change a comment's colour, opacity and/or line width, and redraw it. +/// Change a comment's colour, opacity, line width and/or line endings, and redraw it. +#[allow(clippy::too_many_arguments)] pub fn set_style( doc: &mut Document, page: usize, @@ -1413,6 +1604,7 @@ pub fn set_style( color: Option, opacity: Option, width: Option, + endings: Option<&[LineEnding]>, meta: &Meta, ) -> Result<(), AnnotError> { let (_, r) = annot_ref(doc, page, index)?; @@ -1421,11 +1613,18 @@ pub fn set_style( let subtype = String::from_utf8_lossy(d.name(b"Subtype").unwrap_or_default()).into_owned(); // Check before changing anything: a stale appearance would contradict the new style. if appearance::build(&d).is_none() { - return Err(AnnotError::Unsupported(subtype)); + return Err(AnnotError::Unsupported(subtype.clone())); } if color.is_some_and(|c| !finite(&c)) || opacity.is_some_and(|o| !o.is_finite()) || width.is_some_and(|w| !w.is_finite()) { return Err(AnnotError::Invalid("invalid style".into())); } + if let Some(ends) = endings { + let callout = subtype == "FreeText" && d.contains(b"CL"); + let pair = matches!(subtype.as_str(), "Line" | "PolyLine"); + if (callout && ends.len() != 1) || (pair && ends.len() != 2) || (!callout && !pair) { + return Err(AnnotError::Invalid("this comment has no line endings to change".into())); + } + } let free_text = subtype == "FreeText"; doc.update_dict(r, |d| { if let Some(c) = color { @@ -1452,11 +1651,55 @@ pub fn set_style( d.set(b"BS".to_vec(), Object::Dict(bs)); d.remove(b"Border"); } + if let Some(ends) = endings { + if ends.len() == 1 { + if let Some(ending) = ends.first() { + d.set(b"LE".to_vec(), Object::name(ending.name())); + } + } else if let (Some(start), Some(end)) = (ends.first(), ends.get(1)) { + d.set(b"LE".to_vec(), ending_pair(*start, *end)); + } + } + ensure_ending_room(d); touch(d, meta); })?; set_appearance(doc, r) } +/// Grow a line or polyline's rectangle so a non-`None` ending is not clipped. Idempotent. +fn ensure_ending_room(d: &mut Dict) { + let subtype = d.name(b"Subtype").unwrap_or_default(); + if !matches!(subtype, b"Line" | b"PolyLine") { + return; + } + if !line_endings_of(d).is_some_and(|ends| ends.iter().any(|e| ending_draws(*e))) { + return; + } + let key: &[u8] = if subtype == b"Line" { b"L" } else { b"Vertices" }; + let Some(pts) = pair_points(d, key) else { return }; + let Some(bounds) = bounds(pts.iter().copied()) else { return }; + let w = border_width_of(d); + let need = grow(bounds, w / 2.0 + appearance::arrow_size(w) + 1.0); + let vals: Vec = d.get(b"Rect").and_then(|o| o.as_array()).map(|a| a.iter().filter_map(|x| x.as_f64()).collect()).unwrap_or_default(); + let Some(rect) = four(&vals) else { return }; + let union = [rect[0].min(need[0]), rect[1].min(need[1]), rect[2].max(need[2]), rect[3].max(need[3])]; + if union != rect { + d.set(b"Rect".to_vec(), num_array(&union)); + } +} + +fn four(v: &[f64]) -> Option<[f64; 4]> { + Some([*v.first()?, *v.get(1)?, *v.get(2)?, *v.get(3)?]) +} + +fn pair_points(d: &Dict, key: &[u8]) -> Option> { + let v: Vec = d.get(key)?.as_array()?.iter().filter_map(|o| o.as_f64()).collect(); + if v.len() < 4 || !v.len().is_multiple_of(2) || v.iter().any(|x| !x.is_finite()) { + return None; + } + Some(v.as_chunks::<2>().0.to_vec()) +} + // ── reading ───────────────────────────────────────────────────────────────────────────────── /// One comment as the viewer lists it (the same fields and rules as @@ -1602,6 +1845,8 @@ pub struct Props { pub restylable: bool, /// The Locked flag. pub locked: bool, + /// `/LE`: two names for a line or polyline (`None` when unset), one for a callout. + pub endings: Option>, } /// The current properties of the comment at `(page, index)`. @@ -1626,9 +1871,23 @@ pub fn props(doc: &Document, page: usize, index: usize) -> Option { restylable: appearance::build(d).is_some(), locked: d.get(b"F").and_then(|f| doc.resolve(f).as_int()).unwrap_or(0) & FLAG_LOCKED != 0, subtype, + endings: line_endings_of(d), }) } +/// `/LE` for a line, polyline or callout. Unknown names yield `None` so the control stays hidden. +fn line_endings_of(d: &Dict) -> Option> { + let parse = |name: &[u8]| std::str::from_utf8(name).ok().and_then(LineEnding::parse); + match d.name(b"Subtype")? { + b"Line" | b"PolyLine" => match d.get(b"LE") { + None => Some(vec![LineEnding::None, LineEnding::None]), + Some(o) => o.as_array().filter(|a| a.len() == 2)?.iter().map(|e| parse(e.as_name()?)).collect(), + }, + b"FreeText" if d.contains(b"CL") => Some(vec![parse(d.name(b"LE").unwrap_or(b"None"))?]), + _ => None, + } +} + /// Replace Text (Acrobat's proposal): strike out `quads` and add a caret at the end of the /// struck text holding `replacement`, grouped with the strikeout (`/IRT` + `/RT /Group`) so they /// move, list and delete as one. Returns the strikeout's index. diff --git a/crates/annot/src/tests.rs b/crates/annot/src/tests.rs index afcee4bb..5990e0d0 100644 --- a/crates/annot/src/tests.rs +++ b/crates/annot/src/tests.rs @@ -60,7 +60,7 @@ fn list(doc: &Document, page: usize) -> Vec { } fn ap_content(doc: &Document, d: &Dict) -> String { - let n = d.get(b"AP").and_then(|a| a.as_dict()).and_then(|a| a.reference(b"N")).expect("has /AP /N"); + let n = d.get(b"AP").map(|a| doc.resolve(a)).and_then(|a| a.as_dict().and_then(|a| a.reference(b"N"))).expect("has /AP /N"); let obj = doc.get(n); let Object::Stream(s) = &*obj else { panic!("AP is not a stream") }; String::from_utf8_lossy(&s.decoded().unwrap()).into_owned() @@ -84,7 +84,7 @@ fn every_tool_creates_a_drawable_comment() { Shape::TextMarkup { kind: Markup::Squiggly, quads: vec![[72.0, 412.0, 200.0, 412.0, 72.0, 400.0, 200.0, 400.0]] }, Shape::Rectangle { rect: [300.0, 300.0, 200.0, 200.0] }, Shape::Oval { rect: [300.0, 300.0, 400.0, 350.0] }, - Shape::Line { from: [50.0, 50.0], to: [150.0, 100.0], arrow: true }, + Shape::Line { from: [50.0, 50.0], to: [150.0, 100.0], start: LineEnding::None, end: LineEnding::OpenArrow }, Shape::Ink { strokes: vec![vec![[10.0, 10.0], [20.0, 30.0], [30.0, 10.0]], vec![[40.0, 40.0]]] }, Shape::TextBox { rect: [300.0, 600.0, 500.0, 650.0], font_size: 14.0 }, ]; @@ -141,6 +141,28 @@ fn every_tool_creates_a_drawable_comment() { assert_eq!(all[1].reference(b"Popup"), entries[2].as_ref()); } +/// #260 (page 7): Acrobat draws an Ink annotation as a smooth curve through its `/InkList` +/// points, where PdfCraft drew straight segments between them. Three or more points now make a +/// Catmull-Rom spline through every point (as cubic Béziers); two points stay a line, one a dot. +#[test] +fn ink_strokes_are_smooth_curves_through_their_points() { + let mut doc = fixture(); + let strokes = vec![vec![[0.0, 0.0], [10.0, 10.0], [20.0, 0.0]], vec![[30.0, 0.0], [40.0, 10.0]], vec![[50.0, 50.0]]]; + let i = add_annotation(&mut doc, &new(0, Shape::Ink { strokes }), &meta("ink")).unwrap(); + let ap = ap_content(&doc, &list(&doc, 0)[i]); + // Tangents at each point run parallel to the chord between its neighbours (the ends use + // their own point): (0,0)→(10,10) leaves along (10,10) and arrives along (20,0). + assert!(ap.contains("0 0 m\n1.667 1.667 6.667 10 10 10 c\n13.333 10 18.333 1.667 20 0 c\nS\n"), "{ap}"); + assert!(ap.contains("30 0 m\n40 10 l\nS\n"), "{ap}"); + assert!(ap.contains("50 50 m\n50.01 50 l\nS\n"), "{ap}"); + // /Rect holds the whole curve: rising to (20, 20) and dropping to (21, 0), it swings above + // y 20 (its control point is at y 21.667), beyond the points' own bounds plus the margin. + let rise = Shape::Ink { strokes: vec![vec![[0.0, 0.0], [10.0, 10.0], [20.0, 20.0], [21.0, 0.0]]] }; + let j = add_annotation(&mut doc, &new(0, rise), &meta("rise")).unwrap(); + let top = rect(&list(&doc, 0)[j])[3]; + assert!(top >= 21.667 + 2.0 - 0.01, "/Rect top {top}"); +} + #[test] fn indirect_annots_array_is_updated_in_place() { let mut doc = fixture(); @@ -157,7 +179,7 @@ fn invalid_geometry_is_rejected() { let bad = [ Shape::TextMarkup { kind: Markup::Highlight, quads: vec![] }, Shape::Rectangle { rect: [0.0, 0.0, 0.5, 10.0] }, - Shape::Line { from: [1.0, 1.0], to: [1.0, 1.0], arrow: false }, + Shape::Line { from: [1.0, 1.0], to: [1.0, 1.0], start: LineEnding::None, end: LineEnding::None }, Shape::Ink { strokes: vec![vec![]] }, Shape::Note { at: [f64::NAN, 0.0], icon: NoteIcon::Note }, ]; @@ -239,7 +261,7 @@ fn move_shifts_all_geometry_and_the_popup() { fn restyle_and_resize_redraw_the_appearance() { let mut doc = fixture(); let r = add_annotation(&mut doc, &new(0, Shape::Rectangle { rect: [10.0, 10.0, 110.0, 60.0] }), &meta("r")).unwrap(); - set_style(&mut doc, 0, r, Some([1.0, 0.0, 0.0]), Some(0.5), Some(4.0), &meta("")).unwrap(); + set_style(&mut doc, 0, r, Some([1.0, 0.0, 0.0]), Some(0.5), Some(4.0), None, &meta("")).unwrap(); set_rect(&mut doc, 0, r, [0.0, 0.0, 200.0, 100.0], &meta("")).unwrap(); let doc2 = reopen(&doc); let d = &list(&doc2, 0)[r]; @@ -249,10 +271,15 @@ fn restyle_and_resize_redraw_the_appearance() { assert_eq!(d.get(b"CA").and_then(|o| o.as_f64()), Some(0.5)); // A stamp's appearance can't be regenerated, so restyling it changes nothing. let before = list(&doc, 1)[0].clone(); - assert_eq!(set_style(&mut doc, 1, 0, Some([0.0; 3]), None, None, &meta("")), Err(AnnotError::Unsupported("Stamp".into()))); + assert_eq!(set_style(&mut doc, 1, 0, Some([0.0; 3]), None, None, None, &meta("")), Err(AnnotError::Unsupported("Stamp".into()))); assert_eq!(list(&doc, 1)[0], before); // Lines can't be resized as rectangles. - let l = add_annotation(&mut doc, &new(0, Shape::Line { from: [0.0, 0.0], to: [10.0, 10.0], arrow: false }), &meta("l")).unwrap(); + let l = add_annotation( + &mut doc, + &new(0, Shape::Line { from: [0.0, 0.0], to: [10.0, 10.0], start: LineEnding::None, end: LineEnding::None }), + &meta("l"), + ) + .unwrap(); assert!(matches!(set_rect(&mut doc, 0, l, [0.0, 0.0, 5.0, 5.0], &meta("")), Err(AnnotError::Invalid(_)))); } @@ -261,7 +288,7 @@ fn text_box_text_and_colour_changes_are_drawn() { let mut doc = fixture(); let t = add_annotation(&mut doc, &new(0, Shape::TextBox { rect: [0.0, 0.0, 300.0, 100.0], font_size: 12.0 }), &meta("t")).unwrap(); set_contents(&mut doc, 0, t, "Caf\u{e9} (draft) \u{2014} 100%", &meta("")).unwrap(); - set_style(&mut doc, 0, t, Some([1.0, 0.0, 0.0]), None, None, &meta("")).unwrap(); + set_style(&mut doc, 0, t, Some([1.0, 0.0, 0.0]), None, None, None, &meta("")).unwrap(); let doc = reopen(&doc); let d = &list(&doc, 0)[t]; assert_eq!(text(d, b"Contents"), "Caf\u{e9} (draft) \u{2014} 100%"); @@ -315,7 +342,16 @@ fn editing_a_callout_refits_the_box_and_keeps_the_leader() { let mut doc = fixture(); let c = add_annotation( &mut doc, - &new(0, Shape::Callout { rect: [300.0, 600.0, 500.0, 612.0], knee: [260.0, 560.0], point: [200.0, 520.0], font_size: 12.0 }), + &new( + 0, + Shape::Callout { + rect: [300.0, 600.0, 500.0, 612.0], + knee: [260.0, 560.0], + point: [200.0, 520.0], + font_size: 12.0, + ending: LineEnding::OpenArrow, + }, + ), &meta("c"), ) .unwrap(); @@ -407,8 +443,8 @@ fn fill_and_sign_items_are_drawn() { assert_eq!(text(&all[s], b"Subj"), "Signature"); // Other stamps still can't be restyled; ours can. let mut doc = doc; - set_style(&mut doc, 1, marks[0], Some([0.0, 0.0, 1.0]), None, None, &meta("")).unwrap(); - assert!(set_style(&mut doc, 1, 0, Some([0.0; 3]), None, None, &meta("")).is_err()); + set_style(&mut doc, 1, marks[0], Some([0.0, 0.0, 1.0]), None, None, None, &meta("")).unwrap(); + assert!(set_style(&mut doc, 1, 0, Some([0.0; 3]), None, None, None, &meta("")).is_err()); } #[test] @@ -493,11 +529,24 @@ fn polygons_clouds_connected_lines_callouts_and_carets_are_drawn() { let tri = vec![[100.0, 100.0], [200.0, 100.0], [150.0, 180.0]]; let i = add_annotation(&mut doc, &new(0, Shape::Polygon { vertices: tri.clone(), cloud: false }), &meta("p")).unwrap(); let c = add_annotation(&mut doc, &new(0, Shape::Polygon { vertices: tri.clone(), cloud: true }), &meta("c")).unwrap(); - let l = - add_annotation(&mut doc, &new(0, Shape::PolyLine { vertices: vec![[300.0, 300.0], [350.0, 320.0], [400.0, 300.0]] }), &meta("l")).unwrap(); + let l = add_annotation( + &mut doc, + &new(0, Shape::PolyLine { vertices: vec![[300.0, 300.0], [350.0, 320.0], [400.0, 300.0]], start: LineEnding::None, end: LineEnding::None }), + &meta("l"), + ) + .unwrap(); let co = add_annotation( &mut doc, - &new(0, Shape::Callout { rect: [300.0, 500.0, 420.0, 540.0], knee: [260.0, 520.0], point: [220.0, 460.0], font_size: 10.0 }), + &new( + 0, + Shape::Callout { + rect: [300.0, 500.0, 420.0, 540.0], + knee: [260.0, 520.0], + point: [220.0, 460.0], + font_size: 10.0, + ending: LineEnding::OpenArrow, + }, + ), &meta("co"), ) .unwrap(); @@ -554,7 +603,7 @@ fn polygons_clouds_connected_lines_callouts_and_carets_are_drawn() { for bad in [ Shape::Polygon { vertices: vec![[0.0, 0.0], [10.0, 10.0]], cloud: false }, - Shape::PolyLine { vertices: vec![[0.0, 0.0]] }, + Shape::PolyLine { vertices: vec![[0.0, 0.0]], start: LineEnding::None, end: LineEnding::None }, Shape::Caret { rect: [0.0, 0.0, 0.0, 10.0] }, ] { assert!(add_annotation(&mut doc, &new(0, bad), &meta("b")).is_err()); @@ -657,3 +706,216 @@ fn the_eraser_cuts_strokes_and_removes_empty_drawings() { erase_ink(&mut doc, 0, i, &all[21..], 8.0, &meta("e")).unwrap(); assert!(!list(&doc, 0).iter().any(|d| d.name(b"Subtype") == Some(b"Ink"))); } + +/// A one-page document from object bodies (object 1 is the catalog, 3 the page). +fn document_of(objs: &[&[u8]]) -> Document { + let mut out = b"%PDF-1.7\n".to_vec(); + let mut offsets = Vec::new(); + for (i, o) in objs.iter().enumerate() { + offsets.push(out.len()); + out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes()); + out.extend_from_slice(o); + out.extend_from_slice(b"\nendobj\n"); + } + let xref = out.len(); + out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes()); + for o in offsets { + out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes()); + } + out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes()); + Document::open(Arc::new(out)).expect("opens") +} + +#[test] +fn comments_without_appearances_get_one_for_display_only() { + let doc = document_of(&[ + b"<< /Type /Catalog /Pages 2 0 R >>", + b"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 612 792] >>", + b"<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R] >>", + b"<< /Type /Annot /Subtype /FreeText /Rect [48 582 285 632] /Contents (FreeText annotation with valid /DA string.) /DA (/Helv 10 Tf 0 0 1 rg) /Border [0 0 1] /C [.85 .47 .02] >>", + b"<< /Type /Annot /Subtype /Ink /Rect [70 392 275 472] /InkList [[80 402 120 452 160 412 210 462 260 417]] /Border [0 0 2] /C [.49 .18 .07] >>", + b"<< /Type /Annot /Subtype /Text /Rect [400 717 420 737] /C [1 1 0] /Contents (A note) >>", + b"<< /Type /Annot /Subtype /Square /Rect [10 10 50 50] /C [1 0 0] /AP << /N 11 0 R >> >>", + b"<< /Type /Annot /Subtype /Link /Rect [48 717 308 741] /Border [0 0 1] /C [0 0 1] >>", + b"<< /Type /Annot /Subtype /Widget /FT /Tx /T (f) /Rect [10 100 100 120] >>", + b"<< /Type /Annot /Subtype /Sound /Rect [10 200 30 220] >>", + b"<< /Type /XObject /Subtype /Form /BBox [10 10 50 50] /Length 0 >>\nstream\n\nendstream", + ]); + let display = with_missing_appearances(&doc).expect("three comments need an appearance"); + let has_ap = |doc: &Document| list(doc, 0).iter().map(|d| d.get(b"AP").is_some()).collect::>(); + assert_eq!(has_ap(&doc), [false, false, false, true, false, false, false], "the document itself is untouched"); + assert_eq!(has_ap(&display), [true, true, true, true, false, false, false], "FreeText, Ink and Text get one; the rest don't"); + let shown = list(&display, 0); + let free_text = ap_content(&display, &shown[0]); + assert!(free_text.contains("re f") && free_text.contains("Tj"), "background box and text: {free_text}"); + assert_eq!(shown[3].get(b"AP"), list(&doc, 0)[3].get(b"AP"), "an existing appearance is kept"); + // The display copy survives a save and reopen (what the renderer reads). + assert_eq!(has_ap(&reopen(&display)), [true, true, true, true, false, false, false]); + // Nothing to add: no copy. + let complete = document_of(&[ + b"<< /Type /Catalog /Pages 2 0 R >>", + b"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 612 792] >>", + b"<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R] >>", + b"<< /Type /Annot /Subtype /Link /Rect [48 717 308 741] >>", + b"<< /Type /Annot /Subtype /Square /Rect [10 10 50 50] /C [1 0 0] /AP << /N 6 0 R >> >>", + b"<< /Type /XObject /Subtype /Form /BBox [10 10 50 50] /Length 0 >>\nstream\n\nendstream", + ]); + assert!(with_missing_appearances(&complete).is_none()); +} + +fn line_dict(coords: [f64; 4], ending: &str) -> pdfcraft_cos::Dict { + let mut d = pdfcraft_cos::Dict::new(); + d.set(b"Subtype".to_vec(), Object::name("Line")); + d.set(b"Rect".to_vec(), Object::Array([0.0, 0.0, 300.0, 300.0].into_iter().map(Object::Real).collect())); + d.set(b"C".to_vec(), Object::Array([1.0, 0.0, 0.0].into_iter().map(Object::Real).collect())); + d.set(b"L".to_vec(), Object::Array(coords.into_iter().map(Object::Real).collect())); + d.set(b"LE".to_vec(), Object::Array(vec![Object::name("None"), Object::name(ending)])); + d +} + +#[test] +fn every_line_ending_is_drawn() { + let filled = [LineEnding::Square, LineEnding::Circle, LineEnding::Diamond, LineEnding::ClosedArrow, LineEnding::RClosedArrow]; + let (mut open, mut reverse) = (String::new(), String::new()); + for ending in LineEnding::ALL { + let mut doc = fixture(); + add_annotation( + &mut doc, + &new(0, Shape::Line { from: [80.0, 100.0], to: [200.0, 160.0], start: LineEnding::None, end: ending }), + &meta("ending"), + ) + .unwrap(); + let doc = reopen(&doc); + let line = list(&doc, 0).into_iter().find(|d| d.name(b"Subtype") == Some(b"Line")).expect("line"); + let content = ap_content(&doc, &line); + assert!(!content.is_empty(), "{ending:?}"); + let paints = content.contains("h B") || content.contains("B\n"); + assert_eq!(paints, filled.contains(&ending), "{ending:?} fill: {content}"); + if ending != LineEnding::None { + let r = rect(&line); + assert!(r[0] < 70.0 && r[2] > 210.0, "{ending:?} rectangle includes the ending: {r:?}"); + } + if ending == LineEnding::OpenArrow { + open = content.clone(); + } + if ending == LineEnding::ROpenArrow { + reverse = content; + } + } + assert_ne!(open, reverse, "a reverse arrow points the other way"); + + let mut doc = fixture(); + let index = add_annotation( + &mut doc, + &new(0, Shape::Line { from: [40.0, 40.0], to: [140.0, 40.0], start: LineEnding::None, end: LineEnding::None }), + &meta("plain"), + ) + .unwrap(); + let before = rect(&list(&doc, 0).into_iter().find(|d| d.name(b"Subtype") == Some(b"Line")).expect("line")); + set_style(&mut doc, 0, index, None, None, None, Some(&[LineEnding::None, LineEnding::Square]), &meta("style")).unwrap(); + let mut doc = reopen(&doc); + let line = list(&doc, 0).into_iter().find(|d| d.name(b"Subtype") == Some(b"Line")).expect("line"); + let after = rect(&line); + assert!(after[1] < before[1] - 5.0 && after[3] > before[3] + 5.0, "the square is not clipped: {before:?} → {after:?}"); + assert!(ap_content(&doc, &line).contains("re h B"), "square ending"); + + add_annotation( + &mut doc, + &new(0, Shape::PolyLine { vertices: vec![[40.0, 200.0], [90.0, 260.0], [150.0, 200.0]], start: LineEnding::Circle, end: LineEnding::Slash }), + &meta("poly"), + ) + .unwrap(); + add_annotation( + &mut doc, + &new( + 0, + Shape::Callout { + rect: [300.0, 500.0, 420.0, 540.0], + knee: [260.0, 520.0], + point: [220.0, 460.0], + font_size: 10.0, + ending: LineEnding::Diamond, + }, + ), + &meta("call"), + ) + .unwrap(); + let doc = reopen(&doc); + let all = list(&doc, 0); + let poly = all.iter().find(|d| d.name(b"Subtype") == Some(b"PolyLine")).expect("polyline"); + let poly_ap = ap_content(&doc, poly); + assert!(poly_ap.contains("B\n") && poly_ap.contains(" l S"), "circle fills and the slash only strokes: {poly_ap}"); + let call = all.iter().find(|d| d.name(b"IT") == Some(b"FreeTextCallout")).expect("callout"); + assert_eq!(call.name(b"LE"), Some(b"Diamond".as_slice())); + assert!(ap_content(&doc, call).contains("h B"), "diamond callout"); + + assert!(appearance::build(&line_dict([f64::NAN, 0.0, 10.0, 0.0], "OpenArrow")).is_none(), "a non-finite endpoint is rejected"); + assert!(appearance::build(&line_dict([0.0, 0.0, 10.0, 0.0], "Foo")).is_none(), "an unknown ending is not replaced"); +} + +#[test] +fn redraw_keeps_shared_annotation_appearances_and_drops_stale_alternates() { + let alternates: [(&[u8], &[u8]); 2] = [(b"R", b"0 0 20 10 re f\n"), (b"D", b"1 1 18 8 re S\n")]; + for indirect in [false, true] { + let mut doc = fixture(); + let first = add_annotation(&mut doc, &new(0, Shape::Rectangle { rect: [10.0, 10.0, 110.0, 60.0] }), &meta("first")).unwrap(); + let other = add_annotation(&mut doc, &new(0, Shape::Rectangle { rect: [120.0, 10.0, 220.0, 60.0] }), &meta("other")).unwrap(); + let (_, first_ref) = annot_ref(&mut doc, 0, first).unwrap(); + let (_, other_ref) = annot_ref(&mut doc, 0, other).unwrap(); + let mut entries = annot_dict(&doc, first_ref).get(b"AP").unwrap().as_dict().unwrap().clone(); + for (key, bytes) in alternates { + let mut d = Dict::new(); + d.set(b"Type".to_vec(), Object::name("XObject")); + d.set(b"Subtype".to_vec(), Object::name("Form")); + d.set(b"BBox".to_vec(), num_array(&[0.0, 0.0, 20.0, 10.0])); + let r = doc.add(Object::Stream(pdfcraft_cos::Stream::from_raw(d, bytes.to_vec()))); + entries.set(key.to_vec(), Object::Ref(r)); + } + entries.set(b"VendorState".to_vec(), Object::name("Retained")); + let shared = if indirect { Object::Ref(doc.add(Object::Dict(entries))) } else { Object::Dict(entries) }; + for r in [first_ref, other_ref] { + doc.update_dict(r, |d| d.set(b"AP".to_vec(), shared.clone())).unwrap(); + } + let mut doc = reopen(&doc); + let other_dict = list(&doc, 0)[other].clone(); + let source = other_dict.get(b"AP").unwrap(); + let before = doc.resolve(source); + set_style(&mut doc, 0, first, Some([1.0, 0.0, 0.0]), None, None, None, &meta("")).unwrap(); + assert_eq!(doc.resolve(source), before, "a shared AP must not change"); + for full in [false, true] { + let bytes = if full { + pdfcraft_cos::write_full(&doc, &SaveOptions::default()).unwrap() + } else { + assert!(!doc.revisions().is_empty(), "incremental save needs an existing revision"); + assert!(!doc.encryption_changed() && !doc.full_save_required(), "this edit must not require a full rewrite"); + let original = doc.bytes(); + let bytes = write_incremental(&doc, &SaveOptions::default()).unwrap(); + assert!(bytes.len() > original.len(), "incremental save appends the changed appearance"); + assert!(bytes.starts_with(original.as_slice()), "incremental save preserves the complete original prefix"); + bytes + }; + hayro_syntax::Pdf::new(bytes.clone()).unwrap(); + let doc = Document::open(Arc::new(bytes)).unwrap(); + let all = list(&doc, 0); + let first_ap = doc.resolve(all[first].get(b"AP").unwrap()); + let other_ap = doc.resolve(all[other].get(b"AP").unwrap()); + let first_ap = first_ap.as_dict().unwrap(); + let other_ap = other_ap.as_dict().unwrap(); + assert_ne!(first_ap.get(b"N"), other_ap.get(b"N")); + // The restyled annotation keeps unknown entries, but not down/rollover looks in its + // old colour. + assert_eq!(first_ap.len(), 2); + assert_eq!(other_ap.len(), 4); + for (key, bytes) in alternates { + assert!(first_ap.get(key).is_none(), "stale /{}", String::from_utf8_lossy(key)); + let object = doc.resolve(other_ap.get(key).unwrap()); + let Object::Stream(stream) = &*object else { panic!("alternate appearance") }; + assert_eq!(stream.decoded().unwrap(), bytes); + } + assert_eq!(first_ap.name(b"VendorState"), Some(&b"Retained"[..])); + assert_eq!(other_ap.name(b"VendorState"), Some(&b"Retained"[..])); + assert!(ap_content(&doc, &all[first]).contains("1 0 0 RG")); + assert!(ap_content(&doc, &all[other]).contains("0 0.4 1 RG")); + } + } +} diff --git a/crates/automation/Cargo.toml b/crates/automation/Cargo.toml index 556e9da0..3f8d63e2 100644 --- a/crates/automation/Cargo.toml +++ b/crates/automation/Cargo.toml @@ -12,6 +12,7 @@ workspace = true pdfcraft-engine = { workspace = true } pdfcraft-render = { workspace = true } pdfcraft-organize = { workspace = true } +pdfcraft-platform = { workspace = true } serde_json = { workspace = true } png = "0.18" base64 = { version = "0.22", optional = true } @@ -23,5 +24,7 @@ default = ["mcp"] mcp = ["dep:base64"] [dev-dependencies] +pdfcraft-cos = { workspace = true } +pdfcraft-model = { workspace = true } pdfcraft-xfa = { workspace = true } image = { version = "0.25", default-features = false, features = ["jpeg"] } diff --git a/crates/automation/README.md b/crates/automation/README.md index d1d60f07..3e220b67 100644 --- a/crates/automation/README.md +++ b/crates/automation/README.md @@ -23,6 +23,8 @@ mcp::McpServer::new(a).serve(stdin, stdout)? // newline-delimited JS - Pages and positions are **1-based**. Rectangles are PDF points with the origin at the top-left of the displayed page. - Unknown arguments are rejected, so typos fail loudly. - Tools that change a document return its summary (`doc`, `pages`, `dirty`, `undo`, `redo`, …). +- `comment_image_preview` returns JSON and a PNG: `layer: "background"` (the default) gives the page without the selected image signature/initials; `layer: "image"` gives its embedded image with alpha. The metadata supplies its displayed rectangle, document rotation, `image_rotation` (how far clockwise the image is shown turned: 0 for one placed by PdfCraft, which keeps it upright as displayed) and annotation opacity. Each result can be saved with the CLI's `--out`. Cache the background and transform the image for live movement/resizing; `comment_edit` commits the final geometry in one undo step. Previewing never changes the document. +- For proportional image signature resizing, use the embedded image layer's width/height ratio rather than a previously stretched annotation rectangle. The GUI preserves this ratio at corners with the opposite corner anchored; edge midpoint handles change only their own axis. `comment_edit` accepts the resulting rectangle. - `doc_close` refuses to drop unsaved changes unless `discard_changes: true`. `doc_save` writes atomically, incrementally in place, and in full for a new path. - With a root set, every read and write path must resolve inside it (symlinks and `..` included). Relative paths resolve inside the root, and `..` is resolved by name before the check. Every path outside the root gets the same refusal (` is outside the allowed directory `), whether or not it exists, so a confined agent can't probe the rest of the disk; on Windows another network share or device path (`\\host\share`, `\\?\UNC\…`, `\\.\…`) is refused without being contacted. "Not found" and other filesystem errors are reported only for paths inside the root. Tools that write several files into a folder name them after the document or input file, with separators, colons and control characters replaced by `_`, so a name can't lead them out of the folder. The root itself must be a folder. @@ -30,6 +32,8 @@ mcp::McpServer::new(a).serve(stdin, stdout)? // newline-delimited JS **Opt-in only.** Nothing starts it automatically, and it opens no port. It runs while `pdfcraft-cli mcp [--root DIR]` runs, normally launched by an agent from its MCP configuration. It stops when stdin closes. The CLI's `mcp` Cargo feature (on by default) compiles it out entirely when disabled. +`pdfcraft-cli mcp --compact` (or `McpServer::with_compact(true)`) keeps `tools/list` short for agents with small context budgets. It lists only the core tools (`mcp::COMPACT_CORE_TOOLS`: open, info, save, close, render, text extract and find, combine, split, undo) plus the common command/inspection/preview tools and two meta tools: `tool_search` (optional `query` and `category`, the name prefix; or `name` for one tool's full `input_schema`) and `tool_call` (`name` and `arguments`, run through the same `Automation::call`). `tools/call` by name keeps working for every tool, and the server instructions mention the meta tools. Without the flag nothing changes. + Implemented: `initialize` (protocol 2025-06-18, 2025-03-26, 2024-11-05), `ping`, `tools/list` (with `readOnlyHint`/`destructiveHint` annotations), `tools/call` (JSON results also returned as `structuredContent`; images as `image/png`), and `resources/list`, `resources/templates/list` and `resources/read`. The resources expose the open documents read-only: `pdfcraft://doc/{doc}/info` (JSON), `…/text`, `…/page/{page}/text` and `…/page/{page}/image{?dpi}` (PNG, 1–600 dpi). Tool failures come back as `isError: true` results, so the agent can read and recover from them. ## Adding a tool @@ -38,3 +42,29 @@ Implemented: `initialize` (protocol 2025-06-18, 2025-03-26, 2024-11-05), `ping`, 2. Add a `ToolDef` in `src/tools.rs`: a precise description, the schema, `ro()`/`destructive()`, and `cmd()` if a registry command exists. 3. Handle it in `Automation::call`, validating pages and positions with the existing helpers. 4. Add an end-to-end test in `tests/automation.rs`. `tool_table_is_well_formed` checks names, schemas and command links. + + +## Common command tools + +Following FilmCraft's MCP conventions, `command_list {doc?, filter?, enabled_only?}` keeps its +`{commands: [...]}` result and adds each mapped tool's parameter schema. `command_run {id, params?}` +runs that command through its existing headless tool: include `doc` and other tool arguments in +`params`. Commands without a headless mapping return a tool error. Unknown parameter keys are +ignored with a `warnings` array; required values and paths still go through the original validation. + +`command_batch {steps: [{id, params?}], stop_on_error?}` returns `completed`, `failed`, and per-step +`{ok, result?, error?}` entries. The default is to stop at the first error; earlier edits remain. +MCP marks a batch with any failed steps as `isError`, including through compact `tool_call`. +`doc_inspect {doc?}` returns one document's info, or `{documents: [...]}` for all open documents. +`render_preview {doc?, page?, max_side?}` returns a PNG (page defaults to 1, maximum side defaults +to 1024, allowed 1–4096). Omit `doc` only when exactly one document is open. + +All task tools remain available; compact mode includes the five common tools. Every MCP tool has +all four annotation hints. Unknown top-level argument keys return JSON-RPC `-32602` with the +accepted keys; failures in the invoked tool remain `isError` results. An escaped tool panic is +reported as an internal tool error, and the server continues serving. + +Exports complete synchronously. `progressToken` and cancellation notifications are harmlessly +ignored; no background-job or cancellation behavior is added. + +Session resources `pdfcraft://document` and `pdfcraft://commands`, and the refusal of unknown argument keys, are described in [docs/mcp.md](../../docs/mcp.md). diff --git a/crates/automation/src/comments.rs b/crates/automation/src/comments.rs index 7b09e4d4..ebeab262 100644 --- a/crates/automation/src/comments.rs +++ b/crates/automation/src/comments.rs @@ -3,11 +3,13 @@ //! Geometry follows the automation convention: points from the top-left of the displayed page, //! y down. It is converted to PDF user space (crop box, `/Rotate`) here. -use pdfcraft_engine::{Edit, Markup, NOTE_SIZE, NewAnnotation, NoteIcon, ReviewState, Rgb, Shape, StampGroup, StampKind, Style, SummarySort}; +use pdfcraft_engine::{ + Edit, LineEnding, Markup, NOTE_SIZE, NewAnnotation, NoteIcon, ReviewState, Rgb, Shape, StampGroup, StampKind, Style, SummarySort, +}; use pdfcraft_render::{Annotation, PageInfo}; use serde_json::{Value, json}; -use crate::{Args, Automation, Result, ToolError, failed}; +use crate::{Args, Automation, Content, DEFAULT_DPI, MAX_DPI, Result, ToolError, encode_png, failed}; /// Author used when a tool call names none. pub(crate) const DEFAULT_AUTHOR: &str = "PdfCraft"; @@ -34,6 +36,33 @@ pub(crate) fn parse_color(s: &str) -> Result { Ok([c(0), c(2), c(4)]) } +const ENDING_NAMES: &str = "None, Square, Circle, Diamond, OpenArrow, ClosedArrow, Butt, ROpenArrow, RClosedArrow, Slash"; + +fn line_endings(a: &Args) -> Result>> { + let Some(v) = a.get("endings") else { return Ok(None) }; + let arr = v.as_array().ok_or_else(|| ToolError::InvalidArgs("endings must be an array of line-ending names".into()))?; + if arr.is_empty() || arr.len() > 2 { + return Err(ToolError::InvalidArgs("endings takes one name for a callout, or two for a line or polyline".into())); + } + arr.iter() + .map(|item| { + let name = item.as_str().ok_or_else(|| ToolError::InvalidArgs("endings must be line-ending names".into()))?; + LineEnding::parse(name).ok_or_else(|| ToolError::InvalidArgs(format!("unknown line ending {name:?} ({ENDING_NAMES})"))) + }) + .collect::>>() + .map(Some) +} + +/// `[start, end]` for a line, arrow or polyline. An arrow with no `endings` stays `[None, OpenArrow]`. +fn ending_pair(kind: &str, endings: Option<&[LineEnding]>) -> Result<[LineEnding; 2]> { + match endings { + None if kind == "arrow" => Ok([LineEnding::None, LineEnding::OpenArrow]), + None => Ok([LineEnding::None, LineEnding::None]), + Some(v) if v.len() == 2 => Ok([v.first().copied().unwrap_or(LineEnding::None), v.get(1).copied().unwrap_or(LineEnding::None)]), + Some(_) => Err(ToolError::InvalidArgs("a line or polyline needs two endings, the start and the end".into())), + } +} + fn hex(c: [f32; 3]) -> String { let b = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8; format!("#{:02X}{:02X}{:02X}", b(c[0]), b(c[1]), b(c[2])) @@ -86,6 +115,35 @@ impl Args<'_> { } impl Automation { + /// One of the read-only layers the GUI uses to drag/resize an embedded image signature. + pub(crate) fn comment_image_preview(&self, a: &Args) -> Result> { + let (page, index) = self.comment_target(a)?; + let dpi = a.opt_num("dpi")?.unwrap_or(DEFAULT_DPI); + if !(1.0..=MAX_DPI).contains(&dpi) { + return Err(ToolError::InvalidArgs(format!("dpi must be between 1 and {MAX_DPI}"))); + } + let doc = self.doc(a)?; + let preview = doc.image_signature_preview(page, index).map_err(failed)?.ok_or_else(|| failed("choose an image signature or initials"))?; + let annotation = doc.info.annotations.iter().find(|c| c.page == page && c.index == index).ok_or_else(|| failed("no such comment"))?; + let info = doc.info.pages.get(page).ok_or_else(|| failed("no such page"))?; + let [w, h] = preview.image.size(); + let layer = a.opt_str("layer")?.unwrap_or("background"); + let image = match layer { + "image" => Content::Png { data: preview.image.bytes().as_ref().clone(), width: w as u32, height: h as u32 }, + "background" => { + let out = preview.render_background((dpi / 72.0) as f32).map_err(failed)?; + Content::Png { data: encode_png(out.width, out.height, &out.rgba)?, width: out.width, height: out.height } + } + _ => return Err(ToolError::InvalidArgs("layer must be background or image".into())), + }; + Ok(vec![ + Content::Json(json!({ "page": page + 1, "index": index + 1, "rect": rect_to_view(info, annotation.rect), "rotation": info.rotation, + "image_rotation": (i64::from(info.rotation) - preview.turn).rem_euclid(360), + "layer": layer, "opacity": preview.opacity, "dpi": dpi })), + image, + ]) + } + /// Comments of a document, optionally only of one 0-based page. fn comments(&self, a: &Args) -> Result> { let doc = self.doc(a)?; @@ -152,6 +210,10 @@ impl Automation { let page = self.page(a)?; let info = self.doc(a)?.info.pages[page].clone(); let kind = a.str("type")?; + let endings = line_endings(a)?; + if endings.is_some() && !matches!(kind, "line" | "arrow" | "polyline" | "callout") { + return Err(ToolError::InvalidArgs("endings apply to a line, arrow, polyline or callout".into())); + } let markup = match kind { "highlight" => Some(Markup::Highlight), "underline" => Some(Markup::Underline), @@ -225,7 +287,8 @@ impl Automation { } "line" | "arrow" => { let (f, t) = (a.need::<2>("from", "a line")?, a.need::<2>("to", "a line")?); - Shape::Line { from: to_user(&info, f[0], f[1]), to: to_user(&info, t[0], t[1]), arrow: kind == "arrow" } + let [start, end] = ending_pair(kind, endings.as_deref())?; + Shape::Line { from: to_user(&info, f[0], f[1]), to: to_user(&info, t[0], t[1]), start, end } } "ink" => { let wrong = || ToolError::InvalidArgs("strokes must be an array of arrays of [x, y] points".into()); @@ -258,7 +321,10 @@ impl Automation { }) .collect::>>()?; match kind { - "polyline" => Shape::PolyLine { vertices }, + "polyline" => { + let [start, end] = ending_pair(kind, endings.as_deref())?; + Shape::PolyLine { vertices, start, end } + } _ => Shape::Polygon { vertices, cloud: kind == "cloud" }, } } @@ -281,7 +347,17 @@ impl Automation { [(point[0] + side) / 2.0, mid] } }; - Shape::Callout { rect, knee, point, font_size: a.opt_num("font_size")?.unwrap_or(10.0) } + Shape::Callout { + rect, + knee, + point, + font_size: a.opt_num("font_size")?.unwrap_or(10.0), + ending: match endings.as_deref() { + None => LineEnding::OpenArrow, + Some([ending]) => *ending, + Some(_) => return Err(ToolError::InvalidArgs("a callout needs one ending".into())), + }, + } } "attachment" => { let [x, y] = a.need::<2>("at", "an attachment (its icon's top-left)")?; @@ -350,13 +426,24 @@ impl Automation { let [x, y] = a.need::<2>("at", "Fill & Sign")?; let at = to_user(&info, x, y); let author = a.opt_str("author")?.unwrap_or(DEFAULT_AUTHOR).to_string(); + let kind = a.str("type")?; + if let Some(path) = a.opt_str("path")? { + if !matches!(kind, "signature" | "initials") || a.opt_str("text")?.is_some() { + return Err(ToolError::InvalidArgs("path is only for an image signature or initials; pass either path or text".into())); + } + let path = self.resolve(path, false)?; + let file = std::fs::File::open(&path).map_err(|e| failed(format!("{}: {e}", path.display())))?; + let image = pdfcraft_engine::SignatureImage::read(file).map_err(|e| ToolError::InvalidArgs(e.to_string()))?; + let edit = image.edit(page, &info, at, kind == "initials", &author).ok_or_else(|| ToolError::InvalidArgs("at must be finite".into()))?; + return self.apply(a, edit); + } let size = 10.0; let text_at = |t: &str| { let w = (pdfcraft_engine::annot_text::text_width(t, size) + 8.0).clamp(20.0, 600.0); let h = size * 1.2 + 6.0; Shape::Typewriter { rect: [at[0], at[1] - h, at[0] + w, at[1]], font_size: size } }; - let (shape, contents) = match a.str("type")? { + let (shape, contents) = match kind { "text" => { let t = a.str("text")?.to_string(); (text_at(&t), t) @@ -366,12 +453,12 @@ impl Automation { let t = format!("{m}/{d}/{yy}"); (text_at(&t), t) } - // A typed signature or initials in the script font, left edge at `at`. + // A typed signature or initials in the script font, left edge at `at`, upright as displayed. kind @ ("signature" | "initials") => { let t = a.str("text")?; let h = if kind == "initials" { 24.0 } else { 32.0 }; - let shape = - pdfcraft_engine::typed_signature_shape(at, t, h).ok_or_else(|| ToolError::InvalidArgs("text has nothing to draw".into()))?; + let shape = pdfcraft_engine::typed_signature_shape(at, t, h, i64::from(info.rotation)) + .ok_or_else(|| ToolError::InvalidArgs("text has nothing to draw".into()))?; (shape, String::new()) } kind => { @@ -444,7 +531,7 @@ impl Automation { } let (color, opacity, width) = (a.color("color")?, a.opt_num("opacity")?, a.opt_num("width")?); if color.is_some() || opacity.is_some() || width.is_some() { - edits.push(Edit::StyleAnnotation { page, index, color, opacity, width }); + edits.push(Edit::StyleAnnotation { page, index, color, opacity, width, endings: None }); } if let Some(r) = a.nums::<4>("rect")? { edits.push(Edit::ResizeAnnotation { page, index, rect: rect_to_user(&info, r) }); diff --git a/crates/automation/src/conventions.rs b/crates/automation/src/conventions.rs new file mode 100644 index 00000000..62153082 --- /dev/null +++ b/crates/automation/src/conventions.rs @@ -0,0 +1,124 @@ +//! Shared command/inspection/preview entry points, using the existing confined tool path. +use serde_json::{Value, json}; + +use crate::{Args, Automation, Content, Result, ToolError, encode_png, failed, info, tools}; +use pdfcraft_render::{RenderRequest, RequestKind}; + +/// The most steps one `command_batch` runs. +pub(crate) const MAX_BATCH_STEPS: usize = 1000; + +impl Automation { + pub(crate) fn command_run(&mut self, args: &Args) -> Result> { + let id = args.str("id")?; + let tool = tools::tool_for_command(id).ok_or_else(|| failed(format!("command {id:?} has no headless tool; see command_list")))?; + let def = tools::find(tool).ok_or_else(|| failed("command tool is unavailable"))?; + let mut params = match args.get("params") { + None | Some(Value::Null) => serde_json::Map::new(), + Some(Value::Object(params)) => params.clone(), + Some(_) => return Err(ToolError::InvalidArgs("params must be an object".into())), + }; + let props = def.input_schema.get("properties").and_then(Value::as_object).ok_or_else(|| failed("tool has no parameter schema"))?; + let accepted = props.keys().map(String::as_str).collect::>().join(", "); + let mut warnings = Vec::new(); + params.retain(|key, _| { + if props.contains_key(key) { + return true; + } + warnings.push(format!("unknown param {key:?} for {id} (accepted: {accepted})")); + false + }); + // This preserves the task tool's validation, atomic writes and --root confinement. + let mut content = self.call(tool, &Value::Object(params))?; + if !warnings.is_empty() { + if let Some(object) = content.iter_mut().find_map(|c| match c { + Content::Json(v) => v.as_object_mut(), + _ => None, + }) { + let mut all = match object.remove("warnings") { + Some(Value::Array(old)) => old, + Some(old) => vec![old], + None => Vec::new(), + }; + all.extend(warnings.into_iter().map(Value::String)); + object.insert("warnings".into(), Value::Array(all)); + } else { + content.push(Content::Json(json!({"warnings":warnings}))); + } + } + Ok(content) + } + + pub(crate) fn command_batch(&mut self, args: &Args) -> Result { + let steps = args.get("steps").and_then(Value::as_array).ok_or_else(|| ToolError::InvalidArgs("steps must be an array".into()))?; + if steps.len() > MAX_BATCH_STEPS { + return Err(ToolError::InvalidArgs(format!("a batch has at most {MAX_BATCH_STEPS} steps ({} given); split it", steps.len()))); + } + // Check every envelope before applying any edit. + for step in steps { + let a = Args(step); + a.str("id")?; + if a.get("params").is_some_and(|p| !p.is_object() && !p.is_null()) { + return Err(ToolError::InvalidArgs("step params must be an object".into())); + } + } + let stop = args.opt_bool("stop_on_error")?.unwrap_or(true); + let (mut completed, mut failed, mut results) = (0usize, 0usize, Vec::new()); + for step in steps { + match self.command_run(&Args(step)) { + Ok(content) => { + completed += 1; + let result = content.into_iter().find_map(|c| match c { + Content::Json(v) => Some(v), + _ => None, + }); + results.push(json!({"ok":true,"result":result})); + } + Err(e) => { + failed += 1; + results.push(json!({"ok":false,"error":e.to_string()})); + if stop { + break; + } + } + } + } + Ok(json!({"completed":completed,"failed":failed,"results":results})) + } + + pub(crate) fn doc_inspect(&self, args: &Args) -> Result { + if args.opt_int("doc")?.is_some() { + return Ok(info(self.doc(args)?)); + } + Ok(json!({"documents":self.session.docs().iter().map(info).collect::>()})) + } + + pub(crate) fn render_preview(&mut self, args: &Args) -> Result { + let id = if args.opt_int("doc")?.is_some() { + self.doc(args)?.id + } else { + match self.session.docs() { + [doc] => doc.id, + _ => return Err(failed("give doc from doc_list (a single open document is selected automatically)")), + } + }; + let page = args.opt_int("page")?.unwrap_or(1); + let max = args.opt_int("max_side")?.unwrap_or(1024); + if !(1..=4096).contains(&max) { + return Err(ToolError::InvalidArgs("max_side must be between 1 and 4096".into())); + } + let page = usize::try_from(page).ok().and_then(|p| p.checked_sub(1)).ok_or_else(|| failed("page must be at least 1"))?; + let doc = self.session.get(id).ok_or_else(|| failed("no such document"))?; + let size = doc.info.pages.get(page).ok_or_else(|| failed("page is out of range"))?; + let side = size.width.max(size.height); + if !side.is_finite() || side <= 0.0 { + return Err(failed("page has invalid dimensions")); + } + let scale = (max as f32 / side).min(1.0); + let out = self.renderer(id)?.render(RenderRequest { page, kind: RequestKind::Pixels, tile: None, scale, tag: 0 }); + if let Some(error) = out.error { + return Err(failed(error)); + } + let data = encode_png(out.width, out.height, &out.rgba)?; + Ok(Content::Png { data, width: out.width, height: out.height }) + } +} diff --git a/crates/automation/src/forms.rs b/crates/automation/src/forms.rs index 0203934b..eb3f1b38 100644 --- a/crates/automation/src/forms.rs +++ b/crates/automation/src/forms.rs @@ -189,10 +189,12 @@ impl Automation { "value": match f.kind { FormFieldKind::CheckBox => json!(!f.value.is_empty()), FormFieldKind::List if f.has(field_flags::MULTI_SELECT) => json!(f.value), + FormFieldKind::Radio => json!(f.value.first().map(|v| f.export_for_state(v))), _ => json!(f.value.first()), }, "page": w.and_then(|w| w.page).map(|p| p + 1), "rect": rect, + "rotation": w.map(|w| w.rotation).unwrap_or(0), "read_only": f.read_only(), "required": f.has(field_flags::REQUIRED), }); @@ -225,7 +227,7 @@ impl Automation { } match f.kind { FormFieldKind::Radio => { - o.insert("options".into(), json!(f.widgets.iter().filter_map(|w| w.on_state.clone()).collect::>())); + o.insert("options".into(), json!((0..f.widgets.len()).filter_map(|i| f.export_of(i)).collect::>())); } FormFieldKind::Combo | FormFieldKind::List => { o.insert("options".into(), f.options.iter().map(|(e, d)| json!({ "value": e, "label": d })).collect()); @@ -393,6 +395,11 @@ impl Automation { out } }, + rotation: match a.opt_int("rotation")? { + None => None, + Some(r @ (0 | 90 | 180 | 270)) => Some((0, r)), + Some(r) => return Err(ToolError::InvalidArgs(format!("rotation must be 0, 90, 180 or 270, not {r}"))), + }, actions: None, check_style: match a.opt_str("check_style")? { None => None, diff --git a/crates/automation/src/lib.rs b/crates/automation/src/lib.rs index 1e199d2c..684eebd2 100644 --- a/crates/automation/src/lib.rs +++ b/crates/automation/src/lib.rs @@ -14,6 +14,7 @@ mod a11y; mod comments; mod content; +mod conventions; mod forms; mod links; #[cfg(feature = "mcp")] @@ -29,6 +30,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use pdfcraft_engine::{DocId, Document, Edit, Session, commands}; +use pdfcraft_platform::staging::{StagingName, create_staging, staging_suffixes}; use pdfcraft_render::{PageRenderer, PageText, RenderConfig, RenderRequest, RequestKind}; use serde_json::{Value, json}; @@ -154,6 +156,7 @@ impl Automation { self.apply(&a, edit)? } "page_render" => return self.page_render(&a).map(|c| vec![c]), + "comment_image_preview" => return self.comment_image_preview(&a), "text_extract" => self.text_extract(&a)?, "text_find" => self.text_find(&a)?, "page_rotate" => { @@ -200,6 +203,7 @@ impl Automation { "page_insert_file" => self.insert_file(&a)?, "page_extract" => self.page_extract(&a)?, "doc_combine" => self.doc_combine(&a)?, + "doc_create_multiple" => self.doc_create_multiple(&a)?, "doc_split" => self.doc_split(&a)?, "edit_undo" => { let id = self.doc(&a)?.id; @@ -212,6 +216,10 @@ impl Automation { json!({ "redone": label, "document": summary(self.doc(&a)?) }) } "command_list" => self.command_list(&a)?, + "command_run" => return self.command_run(&a), + "command_batch" => self.command_batch(&a)?, + "doc_inspect" => self.doc_inspect(&a)?, + "render_preview" => return self.render_preview(&a).map(|c| vec![c]), "page_number" => { use pdfcraft_organize::LabelStyle as L; let n = self.doc(&a)?.info.pages.len(); @@ -329,7 +337,7 @@ impl Automation { .enumerate() .map(|(i, im)| { let (u, v) = (info.user_to_view(im.rect[0] as f32, im.rect[1] as f32), info.user_to_view(im.rect[2] as f32, im.rect[3] as f32)); - json!({ "image": i + 1, "rect": [r(u[0].min(v[0])), r(u[1].min(v[1])), r(u[0].max(v[0])), r(u[1].max(v[1]))], "pixels": [im.width, im.height], "name": im.name }) + json!({ "image": i + 1, "rect": [r(u[0].min(v[0])), r(u[1].min(v[1])), r(u[0].max(v[0])), r(u[1].max(v[1]))], "pixels": [im.width, im.height], "name": im.name, "kind": if im.is_form { "form" } else { "image" } }) }) .collect(); json!({ "page": page + 1, "count": list.len(), "images": list }) @@ -417,6 +425,7 @@ impl Automation { let block = self.doc(&a)?.text_blocks(page)[k as usize - 1].clone(); let text = a.opt_str("text")?.map(str::to_owned).unwrap_or(block.text); let mut style = pdfcraft_engine::BlockStyle { + bold: a.opt_bool("bold")?, size: a.opt_num("size")?, underline: a.opt_bool("underline")?, line_spacing: a.opt_num("line_spacing")?, @@ -1140,6 +1149,63 @@ impl Automation { Ok(result) } + fn doc_create_multiple(&mut self, a: &Args) -> Result { + let paths = a.strs("paths")?; + if paths.is_empty() || paths.len() > pdfcraft_engine::MAX_CREATE_FILES { + return Err(ToolError::InvalidArgs(format!("paths must list 1 to {} files", pdfcraft_engine::MAX_CREATE_FILES))); + } + let separate = match a.opt_str("mode")? { + None | Some("combine") => false, + Some("separate") => true, + Some(m) => return Err(ToolError::InvalidArgs(format!("mode must be \"combine\" or \"separate\", not {m:?}"))), + }; + if separate { + return self.create_separate(a, &paths); + } + let ranges: Vec> = match a.get("pages") { + None | Some(Value::Null) => vec![None; paths.len()], + Some(Value::Array(v)) if v.len() == paths.len() => v.iter().map(|x| x.as_str().map(str::to_owned)).collect(), + Some(_) => return Err(ToolError::InvalidArgs("pages must list a range (or null) for each path".into())), + }; + let mut sources = Vec::new(); + for (p, range) in paths.into_iter().zip(ranges) { + let path = self.resolve(p, false)?; + let bytes = std::fs::read(&path).map_err(|e| failed(format!("{}: {e}", path.display())))?; + let name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default(); + let (_, pdf) = self.session.convert_to_pdf(&name, &Arc::new(bytes)).map_err(failed)?; + let title = path.file_stem().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default(); + sources.push((title, pdf, range)); + } + let bytes = self.session.combine_ranges(&sources).map_err(failed)?; + self.deliver(a, "Combined", bytes) + } + + /// One PDF per file, written into `out_dir`; a file that fails doesn't stop the others. + fn create_separate(&mut self, a: &Args, paths: &[&str]) -> Result { + let dir = self.resolve(a.str("out_dir").map_err(|_| ToolError::InvalidArgs("mode \"separate\" needs out_dir".into()))?, true)?; + std::fs::create_dir_all(&dir).map_err(|e| failed(format!("{}: {e}", dir.display())))?; + let mut out = Vec::new(); + for &p in paths { + let converted = self.resolve(p, false).map_err(|e| e.to_string()).and_then(|src| { + let bytes = std::fs::read(&src).map_err(|e| format!("{}: {e}", src.display()))?; + let name = src.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default(); + let stem = src.file_stem().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| "document".into()); + let (kind, pdf) = self.session.convert_to_pdf(&name, &Arc::new(bytes)).map_err(|e| e.to_string())?; + Ok((kind, stem, pdf)) + }); + out.push(match converted { + Ok((pdfcraft_engine::SourceKind::Pdf, ..)) => json!({ "path": p, "skipped": "already a PDF" }), + Ok((_, stem, pdf)) => { + let target = unused(&dir, &stem); + write_atomic(&target, &pdf)?; + json!({ "path": p, "output": target.to_string_lossy(), "bytes": pdf.len() }) + } + Err(e) => json!({ "path": p, "error": e }), + }); + } + Ok(json!({ "files": out })) + } + fn page_extract(&mut self, a: &Args) -> Result { let doc = self.doc(a)?; let (id, name) = (doc.id, format!("{} (extract)", doc.name)); @@ -1175,9 +1241,24 @@ impl Automation { } let ranges: Vec> = match a.get("pages") { None | Some(Value::Null) => vec![None; paths.len()], - Some(Value::Array(v)) if v.len() == paths.len() => v.iter().map(|x| x.as_str().map(str::to_owned)).collect(), + Some(Value::Array(v)) if v.len() == paths.len() => v + .iter() + .enumerate() + .map(|(index, value)| match value { + Value::Null => Ok(None), + Value::String(range) => Ok(Some(range.clone())), + _ => Err(ToolError::InvalidArgs(format!("pages[{index}] must be a range string or null"))), + }) + .collect::>()?, Some(_) => return Err(ToolError::InvalidArgs("pages must list a range (or null) for each path".into())), }; + let passwords: Vec> = match a.get("passwords") { + None | Some(Value::Null) => vec![None; paths.len()], + Some(Value::Array(v)) if v.len() == paths.len() && v.iter().all(|x| x.is_string() || x.is_null()) => { + v.iter().map(|x| x.as_str().map(str::to_owned)).collect() + } + Some(_) => return Err(ToolError::InvalidArgs("passwords must list a password (or null) for each path".into())), + }; let mut sources = Vec::new(); for (p, range) in paths.into_iter().zip(ranges) { let path = self.resolve(p, false)?; @@ -1185,7 +1266,8 @@ impl Automation { let name = path.file_stem().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default(); sources.push((name, Arc::new(bytes), range)); } - let bytes = self.session.combine_ranges(&sources).map_err(failed)?; + let passwords: Vec> = passwords.iter().map(Option::as_deref).collect(); + let bytes = self.session.combine_unlocked(&sources, &passwords).map_err(failed)?; self.deliver(a, "Combined", bytes) } @@ -1267,10 +1349,10 @@ impl Automation { let doc = self.session.get(id).ok_or_else(|| failed("no such document"))?; let fresh = || { let config = RenderConfig { password: doc.password.as_deref().map(Arc::from), ..Default::default() }; - (doc.bytes.clone(), PageRenderer::new(doc.bytes.clone(), config)) + (doc.display.clone(), PageRenderer::new(doc.display.clone(), config)) }; let entry = self.renderers.entry(id).or_insert_with(fresh); - if !Arc::ptr_eq(&entry.0, &doc.bytes) { + if !Arc::ptr_eq(&entry.0, &doc.display) { *entry = fresh(); } Ok(&mut entry.1) @@ -1356,6 +1438,8 @@ impl Automation { Some(_) => Some(self.doc(a)?.id), None => None, }; + let filter = a.opt_str("filter")?.unwrap_or("").to_lowercase(); + let enabled_only = a.opt_bool("enabled_only")?.unwrap_or(false); let list: Vec = commands::COMMANDS .iter() .map(|c| { @@ -1366,9 +1450,18 @@ impl Automation { "shortcut": c.shortcut.map(|s| s.label(cfg!(target_os = "macos"))), "enabled": commands::is_enabled(c, &self.session, active), "tool": tools::tool_for_command(c.id), + "params": tools::tool_for_command(c.id).and_then(tools::find).map(|t| &t.input_schema), }) }) .collect(); + let list: Vec = list + .into_iter() + .filter(|c| { + (!enabled_only || c.get("enabled").and_then(Value::as_bool) == Some(true)) + && (filter.is_empty() + || ["id", "label", "menu"].iter().any(|k| c.get(k).is_some_and(|v| v.to_string().to_lowercase().contains(&filter)))) + }) + .collect(); Ok(json!({ "commands": list })) } @@ -1613,7 +1706,7 @@ fn info(d: &Document) -> Value { "links": i.links.iter().map(|l| json!({ "page": page1(l.page), "rect": view_rect(i, l.page, l.rect), "target": match &l.target { - pdfcraft_render::LinkTarget::Page(p) => json!({ "page": page1(*p) }), + pdfcraft_render::LinkTarget::Page(p, _) => json!({ "page": page1(*p) }), pdfcraft_render::LinkTarget::Uri(u) => json!({ "uri": u }), pdfcraft_render::LinkTarget::SetLayers { changes, preserve_rb } => json!({ "layers": changes.iter().map(|(op, ocg)| json!({ @@ -1718,8 +1811,23 @@ fn child(dir: &Path, name: &str) -> PathBuf { dir.join(safe) } +/// `.pdf` in `dir`, or ` (2).pdf` and so on when that name is taken. +fn unused(dir: &Path, stem: &str) -> PathBuf { + let first = child(dir, &format!("{stem}.pdf")); + if !first.exists() { + return first; + } + (2..10_000u32).map(|n| child(dir, &format!("{stem} ({n}).pdf"))).find(|p| !p.exists()).unwrap_or(first) +} + /// Write via a temporary file in the same directory, then rename over the target. fn write_atomic(path: &Path, bytes: &[u8]) -> Result<()> { + write_atomic_with(path, bytes, staging_suffixes()) +} + +/// [`write_atomic`], trying the staging names that `suffixes` give. +fn write_atomic_with(path: &Path, bytes: &[u8], suffixes: impl IntoIterator) -> Result<()> { + use std::io::Write; // A folder can't be replaced, and staging beside it could land outside the root: "." names // the root itself, whose parent isn't ours to write in. if path.is_dir() { @@ -1728,8 +1836,17 @@ fn write_atomic(path: &Path, bytes: &[u8]) -> Result<()> { let dir = path.parent().filter(|p| !p.as_os_str().is_empty()).unwrap_or(Path::new(".")); let name = path.file_name().ok_or_else(|| failed(format!("{}: not a file path", path.display())))?; std::fs::create_dir_all(dir).map_err(|e| failed(format!("{}: {e}", dir.display())))?; - let tmp = dir.join(format!(".{}.pdfcraft-tmp", name.to_string_lossy())); - std::fs::write(&tmp, bytes).map_err(|e| failed(format!("{}: {e}", tmp.display())))?; + let (tmp, file) = + create_staging(dir, &name.to_string_lossy(), StagingName::SuffixThenTag, suffixes).map_err(|e| failed(format!("{}: {e}", dir.display())))?; + // Closed at the end of the block, before the rename. + let written = { + let mut file = file; + file.write_all(bytes) + }; + if let Err(e) = written { + let _ = std::fs::remove_file(&tmp); + return Err(failed(format!("{}: {e}", tmp.display()))); + } std::fs::rename(&tmp, path).map_err(|e| { let _ = std::fs::remove_file(&tmp); failed(format!("{}: {e}", path.display())) @@ -1739,6 +1856,7 @@ fn write_atomic(path: &Path, bytes: &[u8]) -> Result<()> { #[cfg(test)] mod tests { use super::*; + use pdfcraft_platform::staging::STAGING_ATTEMPTS; #[test] fn dot_dot_resolves_by_name_and_never_climbs_above_the_start() { @@ -1790,4 +1908,158 @@ mod tests { assert!(foreign_share(Path::new(r"\\server\other\x.pdf"), shared)); assert!(foreign_share(Path::new(r"\\attacker\docs\x.pdf"), shared)); } + + /// A fresh, empty folder for one staging test. + fn staging_dir(test: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!("pdfcraft-staging-{}-{test}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + dir + } + + fn staged(dir: &Path, suffix: u64) -> PathBuf { + dir.join(format!(".out.pdf.{suffix:016x}.pdfcraft-tmp")) + } + + fn read(p: &Path) -> String { + std::fs::read_to_string(p).unwrap() + } + + /// A symbolic link to a file, where the system allows one (Windows needs Developer Mode or an + /// administrator for it). + fn file_symlink(target: &Path, link: &Path) -> std::io::Result<()> { + #[cfg(unix)] + { + std::os::unix::fs::symlink(target, link) + } + #[cfg(windows)] + { + std::os::windows::fs::symlink_file(target, link) + } + #[cfg(not(any(unix, windows)))] + { + Err(std::io::Error::other(format!("no symbolic links here: {} {}", target.display(), link.display()))) + } + } + + #[test] + fn staging_never_writes_through_a_file_planted_at_its_name() { + let dir = staging_dir("planted"); + // A file elsewhere that a planted link points at. + let outside = dir.join("outside.txt"); + std::fs::write(&outside, "PRECIOUS").unwrap(); + let target = dir.join("out.pdf"); + // A hard link needs no privileges on any system, and writing to it writes to `outside`. + std::fs::hard_link(&outside, staged(&dir, 1)).unwrap(); + std::fs::write(staged(&dir, 2), "PLANTED").unwrap(); + write_atomic_with(&target, b"NEW", [1, 2, 3]).unwrap(); + assert_eq!(read(&target), "NEW"); + assert_eq!(read(&outside), "PRECIOUS"); + assert_eq!(read(&staged(&dir, 1)), "PRECIOUS"); + assert_eq!(read(&staged(&dir, 2)), "PLANTED"); + assert!(!staged(&dir, 3).exists(), "the staging file was renamed into place"); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn staging_never_writes_through_a_symbolic_link_planted_at_its_name() { + let dir = staging_dir("symlink"); + let outside = dir.join("outside.txt"); + std::fs::write(&outside, "PRECIOUS").unwrap(); + let target = dir.join("out.pdf"); + if let Err(e) = file_symlink(&outside, &staged(&dir, 1)) { + eprintln!("symbolic links not checked: {e}"); + return; + } + write_atomic_with(&target, b"NEW", [1, 2]).unwrap(); + assert_eq!(read(&target), "NEW"); + assert_eq!(read(&outside), "PRECIOUS"); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn staging_never_creates_a_file_through_a_dangling_link_at_its_name() { + let dir = staging_dir("dangling"); + let unborn = dir.join("created-through-a-link.txt"); + let target = dir.join("out.pdf"); + if let Err(e) = file_symlink(&unborn, &staged(&dir, 1)) { + eprintln!("symbolic links not checked: {e}"); + return; + } + write_atomic_with(&target, b"NEW", [1, 2]).unwrap(); + assert_eq!(read(&target), "NEW"); + assert!(!unborn.exists(), "nothing was created through the dangling link"); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn staging_gives_up_rather_than_reuse_a_taken_name() { + let dir = staging_dir("taken"); + let target = dir.join("out.pdf"); + std::fs::write(&target, "OLD").unwrap(); + for s in 1..=STAGING_ATTEMPTS as u64 { + std::fs::write(staged(&dir, s), "PLANTED").unwrap(); + } + assert!(write_atomic_with(&target, b"NEW", 1..).is_err()); + assert_eq!(read(&target), "OLD"); + for s in 1..=STAGING_ATTEMPTS as u64 { + assert_eq!(read(&staged(&dir, s)), "PLANTED"); + } + let _ = std::fs::remove_dir_all(&dir); + } + + /// The names in a folder: what a test can see was left behind. + fn listing(dir: &Path) -> Vec { + let mut names: Vec<_> = std::fs::read_dir(dir).unwrap().map(|e| e.unwrap().file_name().to_string_lossy().into_owned()).collect(); + names.sort(); + names + } + + #[test] + fn staging_names_differ_between_saves_and_fit_long_names() { + let suffixes: std::collections::HashSet = staging_suffixes().take(64).collect(); + assert_eq!(suffixes.len(), 64); + assert_ne!(staging_suffixes().next(), staging_suffixes().next(), "each save draws new names"); + // 60 four-byte characters: a 244-byte name, within every system's limit. Its staging name + // must be too (on Linux the whole name in it would be 275 bytes). + let dir = staging_dir("long"); + let name = format!("{}.pdf", "\u{1F600}".repeat(60)); + write_atomic(&dir.join(&name), b"NEW").unwrap(); + assert_eq!(read(&dir.join(&name)), "NEW"); + assert_eq!(listing(&dir), [name], "no staging file is left behind"); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn a_folder_at_the_staging_name_is_left_alone() { + let dir = staging_dir("folder"); + std::fs::create_dir(staged(&dir, 1)).unwrap(); + std::fs::write(staged(&dir, 1).join("inside.txt"), "PLANTED").unwrap(); + let target = dir.join("out.pdf"); + write_atomic_with(&target, b"NEW", [1, 2]).unwrap(); + assert_eq!(read(&target), "NEW"); + assert_eq!(read(&staged(&dir, 1).join("inside.txt")), "PLANTED"); + let _ = std::fs::remove_dir_all(&dir); + } + + /// Windows refuses to replace a read-only file, so the rename fails: the staging file must not + /// be left behind. + #[cfg(windows)] + #[test] + fn a_failed_rename_removes_the_staging_file() { + let dir = staging_dir("readonly"); + let target = dir.join("out.pdf"); + std::fs::write(&target, "OLD").unwrap(); + let mut perms = std::fs::metadata(&target).unwrap().permissions(); + perms.set_readonly(true); + std::fs::set_permissions(&target, perms.clone()).unwrap(); + let e = write_atomic_with(&target, b"NEW", [7]).unwrap_err(); + assert!(e.to_string().contains(&target.display().to_string()), "the rename failed, not the staging: {e}"); + assert_eq!(read(&target), "OLD"); + assert_eq!(listing(&dir), ["out.pdf"], "the staging file was removed"); + #[allow(clippy::permissions_set_readonly_false)] + perms.set_readonly(false); + std::fs::set_permissions(&target, perms).unwrap(); + let _ = std::fs::remove_dir_all(&dir); + } } diff --git a/crates/automation/src/mcp.rs b/crates/automation/src/mcp.rs index e746cf53..d114ea6d 100644 --- a/crates/automation/src/mcp.rs +++ b/crates/automation/src/mcp.rs @@ -29,15 +29,53 @@ const INVALID_PARAMS: i64 = -32602; const INSTRUCTIONS: &str = "PdfCraft edits PDFs. Open a file with doc_open to get a document id, then inspect \ (doc_info, text_extract, text_find, page_render) or edit it (page_*, doc_set_info). Edits are undoable \ (edit_undo) and stay in memory until doc_save. Page numbers are 1-based. Open documents are also \ -resources: pdfcraft://doc/{doc}/info, /text, /page/{page}/text and /page/{page}/image."; +resources: pdfcraft://doc/{doc}/info, /text, /page/{page}/text and /page/{page}/image. \ +command_list discovers command ids and tool schemas; command_run executes one, command_batch runs several. \ +doc_inspect returns open document state; render_preview returns a bounded PNG."; + +/// Appended to the instructions in compact mode. +const COMPACT_INSTRUCTIONS: &str = " Only a core set of tools is listed. Every other tool is still available: find it with \ +tool_search (optional query and category, or name for its full input schema) and run it with tool_call."; + +/// The tools `tools/list` returns in compact mode, besides the two meta tools. +pub const COMPACT_CORE_TOOLS: &[&str] = &[ + "doc_open", + "doc_info", + "doc_save", + "doc_close", + "page_render", + "text_extract", + "text_find", + "doc_combine", + "doc_split", + "edit_undo", + "command_list", + "command_run", + "command_batch", + "doc_inspect", + "render_preview", +]; + +/// Meta tools that exist only in compact mode. +const TOOL_SEARCH: &str = "tool_search"; +const TOOL_CALL: &str = "tool_call"; pub struct McpServer { automation: Automation, + compact: bool, } impl McpServer { pub fn new(automation: Automation) -> Self { - Self { automation } + Self { automation, compact: false } + } + + /// In compact mode `tools/list` returns only [`COMPACT_CORE_TOOLS`] plus `tool_search` and + /// `tool_call`, which find and run any other tool. `tools/call` accepts every tool by name + /// either way. Off by default. + pub fn with_compact(mut self, on: bool) -> Self { + self.compact = on; + self } pub fn automation(&self) -> &Automation { @@ -78,7 +116,8 @@ impl McpServer { }; let params = obj.get("params").cloned().unwrap_or(Value::Null); let id = id?; // notifications/initialized, notifications/cancelled, …: nothing to answer - Some(match self.dispatch(method, ¶ms) { + let result = if method == "tools/call" { guarded_tool(|| self.dispatch(method, ¶ms)) } else { self.dispatch(method, ¶ms) }; + Some(match result { Ok(result) => json!({ "jsonrpc": "2.0", "id": id, "result": result }), Err((code, message)) => error(id, code, &message), }) @@ -93,18 +132,46 @@ impl McpServer { "protocolVersion": version, "capabilities": { "tools": { "listChanged": false }, "resources": { "listChanged": false, "subscribe": false } }, "serverInfo": { "name": "pdfcraft", "title": "PdfCraft", "version": env!("CARGO_PKG_VERSION"), "websiteUrl": pdfcraft_engine::links::APP_PAGE }, - "instructions": INSTRUCTIONS, + "instructions": if self.compact { format!("{INSTRUCTIONS}{COMPACT_INSTRUCTIONS}") } else { INSTRUCTIONS.to_string() }, })) } "ping" => Ok(json!({})), - "tools/list" => Ok(json!({ "tools": crate::tools().iter().map(tool_json).collect::>() })), + "tools/list" => Ok(json!({ "tools": self.tool_list() })), "tools/call" => { let name = params.get("name").and_then(Value::as_str).ok_or((INVALID_PARAMS, "tools/call needs a tool name".to_string()))?; let args = params.get("arguments").cloned().unwrap_or(Value::Null); + if self.compact && name == TOOL_SEARCH { + check_keys(name, &args, &["query", "category", "name"]).map_err(|e| (INVALID_PARAMS, e))?; + return Ok(match search_tools(&args) { + Ok(found) => call_result(vec![Content::Json(found)]), + Err(message) => tool_error(&message), + }); + } + if self.compact && name == TOOL_CALL { + check_keys(name, &args, &["name", "arguments"]).map_err(|e| (INVALID_PARAMS, e))?; + let (inner, inner_args) = match unwrap_tool_call(&args) { + Ok(pair) => pair, + Err(message) => return Ok(tool_error(&message)), + }; + return Ok(match self.automation.call(&inner, &inner_args) { + Ok(content) => tool_content(&inner, content), + Err(ToolError::UnknownTool(t)) => tool_error(&format!("unknown tool {t:?}; use tool_search to find the right name")), + Err(e) => tool_error(&e.to_string()), + }); + } + if let Some(def) = crate::tools::find(name) { + let keys: Vec<&str> = def + .input_schema + .get("properties") + .and_then(Value::as_object) + .map(|p| p.keys().map(String::as_str).collect()) + .unwrap_or_default(); + check_keys(name, &args, &keys).map_err(|e| (INVALID_PARAMS, e))?; + } match self.automation.call(name, &args) { - Ok(content) => Ok(call_result(content)), + Ok(content) => Ok(tool_content(name, content)), Err(ToolError::UnknownTool(t)) => Err((INVALID_PARAMS, format!("unknown tool {t:?}"))), - Err(e) => Ok(json!({ "content": [{ "type": "text", "text": e.to_string() }], "isError": true })), + Err(e) => Ok(tool_error(&e.to_string())), } } "resources/list" => Ok(json!({ "resources": self.resource_list() })), @@ -118,6 +185,167 @@ impl McpServer { } } +impl McpServer { + /// What `tools/list` returns: every tool, or in compact mode the core set and the meta tools. + fn tool_list(&self) -> Vec { + let all = crate::tools(); + if !self.compact { + return all.iter().map(tool_json).collect(); + } + let mut out: Vec = COMPACT_CORE_TOOLS.iter().filter_map(|n| all.iter().find(|t| t.name == *n)).map(tool_json).collect(); + out.extend(meta_tools()); + out + } +} + +/// The two tools only compact mode lists. +fn meta_tools() -> [Value; 2] { + [ + json!({ + "name": TOOL_SEARCH, + "title": "Find PdfCraft tools", + "description": "Search and list all PdfCraft automation tools, including the core tools, with a one-sentence description each. Filter by query \ + (words matched against name, title and description) and/or category (the name prefix: doc, page, text, form, …). \ + With name, return that one tool in full, including its input_schema. Run a tool with tool_call.", + "inputSchema": { + "type": "object", + "properties": { + "query": { "type": "string", "description": "Words to look for, all of which must match (case-insensitive)." }, + "category": { "type": "string", "description": "Tool name prefix, e.g. page, text, form, redact." }, + "name": { "type": "string", "description": "Exact tool name: return its description and input_schema." }, + }, + "required": [], + "additionalProperties": false, + }, + "annotations": { "title": "Find PdfCraft tools", "readOnlyHint": true, "destructiveHint": false, "idempotentHint": true, "openWorldHint": false }, + }), + json!({ + "name": TOOL_CALL, + "title": "Run any PdfCraft tool", + "description": "Run any PdfCraft tool by name with its arguments, exactly as if it were called directly. \ + Find names and argument schemas with tool_search. The result is the tool's own result.", + "inputSchema": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "The tool to run, e.g. page_rotate." }, + "arguments": { "type": "object", "description": "The tool's arguments (see its input_schema from tool_search)." }, + }, + "required": ["name"], + "additionalProperties": false, + }, + "annotations": { "title": "Run any PdfCraft tool", "readOnlyHint": false, "destructiveHint": true, "idempotentHint": false, "openWorldHint": false }, + }), + ] +} + +/// The category of a tool: its name up to the first underscore (`page_rotate` → `page`). +fn category_of(name: &str) -> &str { + name.split('_').next().unwrap_or(name) +} + +/// A description up to and including its first sentence. +fn first_sentence(text: &str) -> &str { + // ASCII lowercasing keeps byte offsets, so positions found in `lower` are valid in `text`. + let lower = text.to_ascii_lowercase(); + let mut from = 0; + while let Some(i) = lower.get(from..).and_then(|rest| rest.find(". ")).map(|i| i + from) { + // "e.g. " and "i.e. " are abbreviations, not the end of the sentence. + if !lower.get(..i).is_some_and(|before| before.ends_with("e.g") || before.ends_with("i.e")) { + return text.get(..=i).unwrap_or(text); + } + from = i + 2; + } + text +} + +/// Reject keys other than `allowed`, as the tool table does for a tool's own arguments. +fn check_keys(tool: &str, args: &Value, allowed: &[&str]) -> Result<(), String> { + match args.as_object().and_then(|o| o.keys().find(|k| !allowed.contains(&k.as_str()))) { + Some(k) => Err(format!("{tool}: unknown argument {k:?} (expected: {})", allowed.join(", "))), + None => Ok(()), + } +} + +/// `tool_search`: filter the tool table by query and category, or describe one tool in full. +fn search_tools(args: &Value) -> Result { + if !(args.is_object() || args.is_null()) { + return Err(format!("{TOOL_SEARCH} takes an object with optional query, category and name")); + } + check_keys(TOOL_SEARCH, args, &["query", "category", "name"])?; + let text = |key: &str| match args.get(key) { + None | Some(Value::Null) => Ok(None), + Some(Value::String(s)) => Ok(Some(s.trim())), + Some(_) => Err(format!("{TOOL_SEARCH}: {key} must be a string")), + }; + let (query, category, name) = (text("query")?, text("category")?, text("name")?); + let all = crate::tools(); + if let Some(name) = name { + let t = + all.iter().find(|t| t.name == name).ok_or_else(|| format!("unknown tool {name:?}; call {TOOL_SEARCH} without name to list the tools"))?; + return Ok(json!({ "tool": { + "name": t.name, "title": t.title, "category": category_of(t.name), "description": t.description, + "read_only": t.read_only, "destructive": t.destructive, "input_schema": t.input_schema, + } })); + } + let category = category.map(|c| c.trim_end_matches('_').to_lowercase()).filter(|c| !c.is_empty()); + let words: Vec = query.map(|q| q.to_lowercase().split_whitespace().map(str::to_owned).collect()).unwrap_or_default(); + let mut categories: Vec<&str> = all.iter().map(|t| category_of(t.name)).collect(); + categories.sort_unstable(); + categories.dedup(); + let found: Vec = all + .iter() + .filter(|t| category.as_deref().is_none_or(|c| category_of(t.name) == c)) + .filter(|t| { + let haystack = format!("{} {} {}", t.name, t.title, t.description).to_lowercase(); + words.iter().all(|w| haystack.contains(w.as_str())) + }) + .map(|t| json!({ "name": t.name, "category": category_of(t.name), "description": first_sentence(t.description), "read_only": t.read_only })) + .collect(); + Ok(json!({ "count": found.len(), "tools": found, "categories": categories })) +} + +/// `tool_call`: the tool name and its arguments from the meta tool's own arguments. +fn unwrap_tool_call(args: &Value) -> Result<(String, Value), String> { + let Some(obj) = args.as_object() else { return Err(format!("{TOOL_CALL} needs an object with name and arguments")) }; + check_keys(TOOL_CALL, args, &["name", "arguments"])?; + let name = obj + .get("name") + .and_then(Value::as_str) + .ok_or_else(|| format!("{TOOL_CALL} needs the name of the tool to run; find it with {TOOL_SEARCH}"))?; + if name == TOOL_CALL || name == TOOL_SEARCH { + return Err(format!("{TOOL_SEARCH} and {TOOL_CALL} are called directly, not through {TOOL_CALL}")); + } + let inner = match obj.get("arguments") { + None | Some(Value::Null) => Value::Null, + Some(v @ Value::Object(_)) => v.clone(), + Some(_) => return Err(format!("{TOOL_CALL}: arguments must be an object")), + }; + Ok((name.to_owned(), inner)) +} + +/// Last-resort boundary shared by every tools/call route, including compact dispatch. +fn guarded_tool(f: impl FnOnce() -> Result) -> Result { + pdfcraft_engine::guard(f).unwrap_or_else(|message| Ok(tool_error(&format!("internal error: {message}")))) +} + +fn tool_content(name: &str, content: Vec) -> Value { + let failed = name == "command_batch" + && content.iter().any(|c| match c { + Content::Json(v) => v.get("failed").and_then(Value::as_u64).is_some_and(|n| n > 0), + _ => false, + }); + let mut result = call_result(content); + if let Some(object) = result.as_object_mut() { + object.insert("isError".into(), json!(failed)); + } + result +} + +/// A failed tool call, as the agent should read it. +fn tool_error(message: &str) -> Value { + json!({ "content": [{ "type": "text", "text": message }], "isError": true }) +} + /// The resource behind a `pdfcraft://` URI. #[derive(Debug, PartialEq)] enum Resource { @@ -176,10 +404,29 @@ impl McpServer { } } } + out.extend([ + json!({"uri":"pdfcraft://document","name":"Open documents","mimeType":"application/json","description":"Session document state, as doc_inspect without arguments."}), + json!({"uri":"pdfcraft://commands","name":"Command catalog","mimeType":"application/json","description":"Commands and mapped tool schemas, as command_list without arguments."}), + ]); out } fn read_resource(&mut self, uri: &str) -> Result { + if let Some(tool) = match uri { + "pdfcraft://document" => Some("doc_inspect"), + "pdfcraft://commands" => Some("command_list"), + _ => None, + } { + let content = self.automation.call(tool, &json!({})).map_err(|e| (INVALID_PARAMS, e.to_string()))?; + let value = content + .into_iter() + .find_map(|c| match c { + Content::Json(v) => Some(v), + _ => None, + }) + .ok_or((INVALID_PARAMS, "resource returned no JSON".into()))?; + return Ok(json!({"uri":uri,"mimeType":"application/json","text":value.to_string()})); + } let r = parse_uri(uri).ok_or((INVALID_PARAMS, format!("unknown resource {uri:?}")))?; let call = |a: &mut Automation, tool: &str, args: Value| a.call(tool, &args).map_err(|e| (INVALID_PARAMS, e.to_string())); let json_of = |c: Vec| c.into_iter().find_map(|c| if let Content::Json(v) = c { Some(v) } else { None }).unwrap_or(Value::Null); @@ -232,7 +479,7 @@ fn tool_json(t: &crate::ToolDef) -> Value { "title": t.title, "description": t.description, "inputSchema": t.input_schema, - "annotations": { "title": t.title, "readOnlyHint": t.read_only, "destructiveHint": t.destructive, "openWorldHint": false }, + "annotations": { "title": t.title, "readOnlyHint": t.read_only, "destructiveHint": t.destructive, "idempotentHint": t.read_only || matches!(t.name, "doc_save" | "doc_optimize" | "doc_export_data" | "measure_export" | "doc_export_images" | "doc_export_office" | "doc_export_all_images" | "doc_export_text" | "image_save"), "openWorldHint": false }, }) } @@ -262,7 +509,28 @@ fn call_result(content: Vec) -> Value { #[cfg(test)] mod tests { - use super::{Resource, parse_uri}; + use super::{Resource, first_sentence, parse_uri}; + + #[test] + fn conventions_panic_is_a_tool_error() { + let r = super::guarded_tool(|| panic!("synthetic tool failure")).unwrap(); + assert_eq!(r["isError"], true); + assert!(r["content"][0]["text"].as_str().unwrap().contains("synthetic tool failure")); + let next = super::guarded_tool(|| Ok(serde_json::json!({"ok":true}))).unwrap(); + assert_eq!(next["ok"], true); + } + + #[test] + fn first_sentence_skips_abbreviations() { + assert_eq!(first_sentence("Rotate pages. Undoable."), "Rotate pages."); + assert_eq!(first_sentence("Label pages (i.e. roman numerals). Undoable."), "Label pages (i.e. roman numerals)."); + assert_eq!(first_sentence("Label pages (I.E. roman numerals, E.G. i, ii). Undoable."), "Label pages (I.E. roman numerals, E.G. i, ii)."); + assert_eq!(first_sentence("Use a value, e.g. en-US, or none. Then more."), "Use a value, e.g. en-US, or none."); + assert_eq!(first_sentence("No full stop here"), "No full stop here"); + assert_eq!(first_sentence("Only an abbreviation, e.g. this"), "Only an abbreviation, e.g. this"); + assert_eq!(first_sentence(""), ""); + assert_eq!(first_sentence("Zażółć gęślą, i.e. jaźń. Dalej."), "Zażółć gęślą, i.e. jaźń."); + } #[test] fn resource_uris_parse() { diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index 2588efaa..5e9e10ca 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -185,17 +185,31 @@ pub fn tools() -> Vec { json!({ "doc": doc(), "pages": pages("to extract"), "out": save_out.clone(), "open": open.clone(), "separate": { "type": "boolean" }, "out_dir": { "type": "string" }, "delete": { "type": "boolean" } }), &["doc", "pages"], )), - t("doc_combine", "Combine files", "Combine PDFs, in order, into one (bookmarks are kept under one entry per file). pages optionally chooses each file's pages, in step with paths: a range such as \"1-3, 6\" or null for all pages.") + t("doc_combine", "Combine files", "Combine PDFs, in order, into one (bookmarks are kept under one entry per file). pages optionally chooses each file's pages, in step with paths: a range such as \"1-3, 6\" or null for all pages. passwords, also in step with paths, opens encrypted files (the open password, or the permissions password where a file's security doesn't allow copying pages; null for none). The result is not encrypted. Passwords are never echoed back.") .cmd("page.combine") .with(schema( json!({ "paths": { "type": "array", "items": { "type": "string" }, "minItems": 2 }, "pages": { "type": "array", "items": { "type": ["string", "null"] } }, + "passwords": { "type": "array", "items": { "type": ["string", "null"] } }, "out": save_out, "open": open, }), &["paths"], )), + t("doc_create_multiple", "Create PDF from multiple files", "Convert several files (PDFs, PNG/JPEG/JPEG 2000/TIFF/GIF/BMP images, .txt files) to PDF in one run. mode \"combine\" (default) joins them, in order, into one PDF with a bookmark per file; pages optionally chooses each file's pages, in step with paths (a range such as \"1-3, 6\" or null). mode \"separate\" writes one PDF per file into out_dir (files that are already PDFs are skipped; existing files are never overwritten) and reports each file's result.") + .cmd("create.multiple") + .with(schema( + json!({ + "paths": { "type": "array", "items": { "type": "string" }, "minItems": 1, "maxItems": pdfcraft_engine::MAX_CREATE_FILES }, + "mode": { "type": "string", "enum": ["combine", "separate"] }, + "pages": { "type": "array", "items": { "type": ["string", "null"] } }, + "out": save_out.clone(), + "open": open.clone(), + "out_dir": { "type": "string", "description": "Folder for mode \"separate\"." }, + }), + &["paths"], + )), t("doc_split", "Split a document", "Split into several files written to out_dir: every N pages, before given pages, at top-level bookmarks (bookmarks: true; files named after them), or by file size (max_mb). Files are -partK.pdf.") .cmd("page.split") .with(schema( @@ -231,7 +245,7 @@ pub fn tools() -> Vec { }), &["doc", "from", "to"], )), - t("form_fields", "List form fields", "Every interactive form field: name, type (text, checkbox, radio, combo, list, button, signature), value, options, page and rect (top-left-origin points), read-only and required flags.") + t("form_fields", "List form fields", "Every interactive form field: name, type (text, checkbox, radio, combo, list, button, signature), value, options, page, rect (top-left-origin points) and rotation (0, 90, 180 or 270 degrees counterclockwise), read-only and required flags.") .ro() .with(schema(json!({ "doc": doc() }), &["doc"])), t( @@ -284,7 +298,7 @@ pub fn tools() -> Vec { t( "form_set_props", "Field properties", - "Change a field's properties (General and Options tabs): name (renames it), tooltip, read_only, required, multiline, max_length (0 = no limit), options (combo/list items), font_size (0 = auto), rect (position), format, validate, calculate (Acrobat's Format/Validate/Calculate tabs, run natively: values are checked, formatted and recalculated as in Acrobat). Options tab: align (left, center, right), default (the value Reset form restores; for check boxes and radio buttons their on state, \"\" for off), flags { scroll, rich_text, password, file_select, spell_check, comb (needs max_length), sort, editable, multi_select, commit_immediately }, check_style (check boxes and radio buttons: check, circle, cross, diamond, square, star). Only the given ones change. Undoable.", + "Change a field's properties (General and Options tabs): name (renames it), tooltip, read_only, required, multiline, max_length (0 = no limit), options (combo/list items), font_size (0 = auto), rect (position), rotation (0, 90, 180 or 270 degrees counterclockwise; changing between horizontal and vertical swaps width and height around the center), format, validate, calculate (Acrobat's Format/Validate/Calculate tabs, run natively: values are checked, formatted and recalculated as in Acrobat). Options tab: align (left, center, right), default (the value Reset form restores; for check boxes and radio buttons their on state, \"\" for off), flags { scroll, rich_text, password, file_select, spell_check, comb (needs max_length), sort, editable, multi_select, commit_immediately }, check_style (check boxes and radio buttons: check, circle, cross, diamond, square, star). Only the given ones change. Undoable.", ) .with(schema( json!({ @@ -304,6 +318,7 @@ pub fn tools() -> Vec { "flags": { "type": "object", "additionalProperties": { "type": "boolean" } }, "font_size": { "type": "number", "minimum": 0 }, "rect": { "type": "array", "items": { "type": "number" }, "minItems": 4, "maxItems": 4, "description": "Move/resize: points from the top-left of the displayed page." }, + "rotation": { "type": "integer", "enum": [0, 90, 180, 270], "description": "Widget rotation in degrees, counterclockwise. Changing between horizontal and vertical swaps the rectangle around its center." }, "appearance": { "type": "object", "description": "Appearance tab: border, fill, text_color (#RRGGBB, a name or \"none\"), width (1 thin, 2 medium, 3 thick), style (solid, dashed, beveled, inset, underline), font (helvetica, times, courier)." }, "format": { "description": "Format tab: {\"type\": \"number\", \"decimals\": 2, \"currency\": \"$\", \"separator\": 0-4, \"negative\": 0-3}, {\"type\": \"percent\"}, {\"type\": \"date\"|\"time\", \"pattern\": \"mm/dd/yyyy\"}, {\"type\": \"zip\"|\"zip4\"|\"phone\"|\"ssn\"}, {\"type\": \"mask\", \"mask\": \"AA-9999\"}, or \"none\"." }, "validate": { "description": "Validate tab: {\"min\": 0, \"max\": 100} (either may be left out) or \"none\"." }, @@ -513,7 +528,7 @@ pub fn tools() -> Vec { "Add a comment", "Add a comment as Acrobat's commenting tools do. Geometry is in points with the origin at the top-left of the displayed page, y down (as in page_render images at 72 dpi and text_find rects). \ note: `at` [x, y] (icon top-left). stamp: `at` [x, y] (its centre) and `stamp`: approved, completed, confidential, draft, final, for comment, for public release, information only, not approved, not for public release, preliminary results, void, accepted, initial here, rejected, sign here, witness; dynamic: true for the dynamic approved/confidential/received/reviewed/revised stamps with a By … at … line. highlight/underline/strikeout/squiggly: `find` (text on the page to mark; every match with all: true) or `quads`. \ - rectangle/oval/textbox: `rect` [x0, y0, x1, y1]. line/arrow: `from`, `to`. ink: `strokes` [[[x, y], …], …]. \ + rectangle/oval/textbox: `rect` [x0, y0, x1, y1]. line/arrow: `from`, `to`. Optional `endings` (two names: start, end) for a line, arrow or polyline, or one name for a callout: None, Square, Circle, Diamond, OpenArrow, ClosedArrow, Butt, ROpenArrow, RClosedArrow, Slash. An arrow without endings is None then OpenArrow. ink: `strokes` [[[x, y], …], …]. \ polygon/cloud/polyline (connected lines): `points` [[x, y], …]. callout: `rect` (its text box), `to` (the point the arrow touches), optional `knee`. caret (inserted text): `at`, the insertion point on the baseline. replace (Replace Text): `find` or `quads` like highlight, `contents` the replacement. attachment: `path` (the file), `at`, optional `icon` (PushPin, Paperclip, Graph, Tag). Undoable.", ) .with(schema( @@ -541,6 +556,7 @@ pub fn tools() -> Vec { "fill": color(), "opacity": { "type": "number", "minimum": 0, "maximum": 1 }, "width": { "type": "number", "minimum": 0, "description": "Line width in points." }, + "endings": { "type": "array", "items": { "type": "string", "enum": ["None", "Square", "Circle", "Diamond", "OpenArrow", "ClosedArrow", "Butt", "ROpenArrow", "RClosedArrow", "Slash"] }, "minItems": 1, "maxItems": 2, "description": "Line endings. Two names (start, end) for a line, arrow or polyline; one name for a callout." }, "font_size": { "type": "number", "exclusiveMinimum": 0 }, }), &["doc", "page", "type"], @@ -617,7 +633,7 @@ pub fn tools() -> Vec { json!({ "doc": doc(), "sort": { "type": "string", "enum": ["page", "author", "date", "type"] }, "out": save_out, "open": open }), &["doc"], )), - t("comment_edit", "Edit a comment", "Change a comment's text, colour, opacity, line width, rectangle (rectangle/oval/text box) or position (`move` [dx, dy] in points). One undo step.").with(schema( + t("comment_edit", "Edit a comment", "Change a comment's text, colour, opacity, line width, rectangle (rectangle/oval/text box/stamp) or position (`move` [dx, dy] in points). Stamps keep their original appearance when resized. One undo step.").with(schema( comment_ref(json!({ "contents": { "type": "string" }, "color": color(), @@ -628,6 +644,8 @@ pub fn tools() -> Vec { })), &["doc"], )), + t("comment_image_preview", "Preview image signature layers", "Return a PNG of the page without the selected image signature/initials (layer=background, default), or its embedded image with alpha (layer=image). Includes the displayed rectangle, document rotation, image_rotation (how far clockwise the image is shown turned) and annotation opacity. Cache these layers for live placement/resizing; commit once with comment_edit. Does not change the document or undo history.") + .ro().with(schema(comment_ref(json!({ "layer": { "type": "string", "enum": ["background", "image"], "default": "background" }, "dpi": { "type": "number", "minimum": 1, "maximum": 600, "default": 96 } })), &["doc"])), t("comment_delete", "Delete a comment", "Delete a comment with its pop-up and replies. Undoable.").destructive().with(schema(comment_ref(json!({})), &["doc"])), t( "doc_protect", @@ -892,7 +910,7 @@ pub fn tools() -> Vec { t( "fill_sign_add", "Fill & Sign: type text or place a mark", - "Fill in a form that has no fields, as Acrobat's Fill & Sign does: type text (`text`, 10 pt), place a check, cross, dot or line, today's date, or a typed signature or initials (`text` drawn in a script font as filled outlines; at is its left edge, centred vertically), at `at` [x, y] in points from the top-left of the page (the text's top-left; a mark's centre). Creates movable, undoable annotations.", + "Fill in a form that has no fields, as Acrobat's Fill & Sign does: type text (`text`, 10 pt), place a check, cross, dot or line, today's date, or a signature or initials (either `text` drawn in a script font, or `path` to a local PNG/JPEG image up to 4 MiB and 4 megapixels, preserving transparency). `at` [x, y] is in points from the top-left of the page: text's top-left, mark's centre, signature's left edge centred vertically. Image signatures fit within 150 pt wide and 32 pt tall (24 pt for initials). Creates movable, undoable annotations.", ) .cmd("sign.fill.text") .with(schema( @@ -902,6 +920,7 @@ pub fn tools() -> Vec { "type": { "type": "string", "enum": ["text", "check", "cross", "dot", "line", "date", "signature", "initials"] }, "at": point(), "text": { "type": "string", "minLength": 1 }, + "path": { "type": "string", "description": "PNG or JPEG for signature/initials; pass either path or text." }, "author": { "type": "string" }, }), &["doc", "page", "type", "at"], @@ -955,11 +974,11 @@ pub fn tools() -> Vec { .ro() .cmd("edit.edit_text") .with(schema(json!({ "doc": doc(), "page": { "type": "integer", "minimum": 1 } }), &["doc", "page"])), - t("page_images", "List page images", "The images a page draws (Edit a PDF): number, box (top-left-origin points), pixel size and resource name.") + t("page_images", "List page images", "The raster images and grouped Form artwork a page draws (Edit a PDF): number, box (top-left-origin points), kind (image/form), pixel size ([0,0] for forms) and resource name. Forms include nested graphics and are edited as a whole. Numbers count forms and images together in drawing order, so a page with forms numbers its images differently than before forms were listed: always take numbers from a fresh page_images call.") .ro() .cmd("edit.edit_text") .with(schema(json!({ "doc": doc(), "page": { "type": "integer", "minimum": 1 } }), &["doc", "page"])), - t("image_edit", "Edit an image", "Change one of a page's images (number from page_images): action move (rect: new box in top-left-origin points), rotate (quarters clockwise, default 1), flip_horizontal, flip_vertical, replace (path: an image file, drawn in the same place) or delete. Undoable.") + t("image_edit", "Edit an image", "Change one of a page's images (number from page_images): action move (rect: new box in top-left-origin points), rotate (quarters clockwise, default 1), flip_horizontal, flip_vertical, replace (path: an image file, drawn in the same place) or delete. Undoable. Form artwork supports move/resize/rotate/flip/delete as a group; replace and image_save only support raster images.") .cmd("edit.edit_text") .with(schema( json!({ @@ -973,7 +992,7 @@ pub fn tools() -> Vec { }), &["doc", "page", "image", "action"], )), - t("image_save", "Save image as", "Write one of a page's images to path: JPEG images unchanged, others as PNG (the extension is added when missing).") + t("image_save", "Save image as", "Write one of a page's raster images (number from page_images; forms are refused) to path: JPEG images unchanged, others as PNG (the extension is added when missing).") .destructive() .cmd("edit.edit_text") .with(schema(json!({ "doc": doc(), "page": { "type": "integer", "minimum": 1 }, "image": { "type": "integer", "minimum": 1 }, "path": { "type": "string" } }), &["doc", "page", "image", "path"])), @@ -981,7 +1000,7 @@ pub fn tools() -> Vec { .ro() .cmd("edit.edit_text") .with(schema(json!({ "doc": doc(), "page": { "type": "integer", "minimum": 1 } }), &["doc", "page"])), - t("text_edit", "Edit text", "Replace the text of one paragraph (paragraph, from text_paragraphs: rewrapped to the paragraph's width with its line spacing) or one line (line, from text_lines) in place, keeping position, size and colour. For a paragraph, also change its formatting: font (helvetica, times, courier) with bold/italic, size (points), color (#rrggbb), align (left, center, right, justify), underline, line_spacing (× size), char_spacing (points) and scale (horizontal, percent), or move it (dx, dy in points; up is +dy) and rewrap it to a new width (points); text may then be omitted. Its own font is reused when it can show every character; otherwise the line is set in Helvetica (the result shows the font used). Text that no available font can show is refused. Undoable.") + t("text_edit", "Edit text", "Replace the text of one paragraph (paragraph, from text_paragraphs: rewrapped to the paragraph's width with its line spacing) or one line (line, from text_lines) in place, keeping position, size and colour. For a paragraph, also change its formatting: font (helvetica, times, courier) with bold/italic, size (points), color (#rrggbb), align (left, center, right, justify), underline, line_spacing (× size), char_spacing (points) and scale (horizontal, percent), or move it (dx, dy in points; up is +dy) and rewrap it to a new width (points); text may then be omitted. Its own font is reused when it can show every character and no font/weight change is requested; otherwise it uses a standard font matching the source style or, for Japanese and other text no standard font can show (Cyrillic, Greek), real Mincho/Gothic fallback outlines matching the requested/source family and available weight, from a face that has every character (the result shows the font used). Missing Japanese weights use Regular, not synthetic bold; the small web build has only Gothic Regular. Paragraph bold may be set without font to keep the source family. Text that no available font can show is refused. Undoable.") .cmd("edit.edit_text") .with(schema( json!({ "doc": doc(), "page": { "type": "integer", "minimum": 1 }, "line": { "type": "integer", "minimum": 1 }, @@ -1032,7 +1051,15 @@ pub fn tools() -> Vec { t("edit_redo", "Redo", "Redo the last undone edit of a document.").cmd("edit.redo").with(schema(json!({ "doc": doc() }), &["doc"])), t("command_list", "List commands", "Every registered PdfCraft command with its menu, shortcut, whether it is enabled now, and the tool that automates it.") .ro() - .with(schema(json!({ "doc": doc() }), &[])), + .with(schema(json!({ "doc": doc(), "filter":{"type":"string"}, "enabled_only":{"type":"boolean"} }), &[])), + t("command_run", "Run a command", "Run a registry command through its headless tool. Pass that tool's arguments in params (see command_list). Unknown params are ignored with warnings; path confinement still applies.") + .destructive().with(schema(json!({"id":{"type":"string"},"params":{"type":"object"}}), &["id"])), + t("command_batch", "Run several commands", "Run steps in order (at most 1000). Returns completed/failed counts and per-step ok/result/error; stop_on_error defaults to true.") + .destructive().with(schema(json!({"steps":{"type":"array","items":schema(json!({"id":{"type":"string"},"params":{"type":"object"}}), &["id"])},"stop_on_error":{"type":"boolean"}}), &["steps"])), + t("doc_inspect", "Inspect open documents", "With doc, return doc_info; otherwise return information for every open document (empty documents array when none are open).") + .ro().with(schema(json!({"doc":doc()}), &[])), + t("render_preview", "Render a preview", "Render a PNG bounded by max_side (default 1024), without changing or saving the document. Page defaults to 1; doc may be omitted when exactly one document is open.") + .ro().with(schema(json!({"doc":doc(),"page":{"type":"integer","minimum":1},"max_side":{"type":"integer","minimum":1,"maximum":4096}}), &[])), ] } diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index 3083dce4..7f8b2853 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -405,12 +405,20 @@ fn root_refusals_do_not_reveal_what_exists_outside() { #[test] fn writing_to_a_folder_touches_nothing_beside_it() { // "." names the root itself. Saving there used to stage its temporary file next to the - // root, outside it, overwriting and then deleting any file of that name. + // root, outside it, overwriting and then deleting any file of that name. Staging names are + // random now and a failed rename removes the staging file, so the "is a folder" refusal is + // what this checks; the listings and the file at the old staging name are canaries. let (base, root) = sandbox("root-itself"); let mut a = auto(&root); let beside = base.join(".root.pdfcraft-tmp"); std::fs::write(&beside, "SENTINEL").unwrap(); std::fs::create_dir_all(root.join("folder")).unwrap(); + let listing = |dir: &Path| { + let mut names: Vec<_> = std::fs::read_dir(dir).unwrap().map(|e| e.unwrap().file_name()).collect(); + names.sort(); + names + }; + let (base_before, folder_before) = (listing(&base), listing(&root.join("folder"))); let doc = ok(&mut a, "doc_open", json!({ "path": "inside.pdf" }))["doc"].as_u64().unwrap(); for p in [".", "", "folder", "folder/"] { let e = a.call("doc_save", &json!({ "doc": doc, "path": p })).unwrap_err(); @@ -419,7 +427,8 @@ fn writing_to_a_folder_touches_nothing_beside_it() { let png = vec![1, 2, 3]; assert!(a.write_output(".", &png).is_err()); assert_eq!(std::fs::read_to_string(&beside).unwrap(), "SENTINEL"); - assert!(!root.join(".folder.pdfcraft-tmp").exists()); + assert_eq!(listing(&base), base_before, "nothing was left beside the root"); + assert_eq!(listing(&root.join("folder")), folder_before, "nothing was left in the folder"); // `image_save` adds an extension when the path has none, which turned "." into `root.png` // beside the root. ok(&mut a, "doc_export_images", json!({ "doc": doc, "folder": "src", "dpi": 18 })); @@ -561,11 +570,11 @@ fn mcp_resources_expose_open_documents() { let dir = workdir("mcp-resources"); let mut s = McpServer::new(auto(&dir)); assert!(rpc(&mut s, 1, "initialize", json!({}))["result"]["capabilities"]["resources"].is_object()); - assert_eq!(rpc(&mut s, 2, "resources/list", json!({}))["result"]["resources"], json!([])); + assert_eq!(rpc(&mut s, 2, "resources/list", json!({}))["result"]["resources"].as_array().unwrap().len(), 2); assert_eq!(rpc(&mut s, 3, "resources/templates/list", json!({}))["result"]["resourceTemplates"].as_array().unwrap().len(), 4); rpc(&mut s, 4, "tools/call", json!({ "name": "doc_open", "arguments": { "path": "a.pdf" } })); let list = rpc(&mut s, 5, "resources/list", json!({}))["result"]["resources"].as_array().cloned().unwrap(); - assert_eq!(list.len(), 2 + 3, "info, text and three page images"); + assert_eq!(list.len(), 2 + 3 + 2, "info, text, three page images and two session resources"); assert_eq!(list[0]["uri"], "pdfcraft://doc/1/info"); let read = |s: &mut McpServer, uri: &str| rpc(s, 6, "resources/read", json!({ "uri": uri }))["result"]["contents"][0].clone(); let text = read(&mut s, "pdfcraft://doc/1/text"); @@ -581,6 +590,272 @@ fn mcp_resources_expose_open_documents() { assert_eq!(rpc(&mut s, 7, "resources/read", json!({ "uri": "pdfcraft://doc/1/page/9/image" }))["error"]["code"], -32602); } +/// A server in compact mode with `dir` as its root. +#[cfg(feature = "mcp")] +fn compact_server(dir: &Path) -> McpServer { + McpServer::new(auto(dir)).with_compact(true) +} + +/// Run a tool through the compact server's `tool_call`. +#[cfg(feature = "mcp")] +fn via_tool_call(s: &mut McpServer, id: u64, tool: &str, arguments: Value) -> Value { + rpc(s, id, "tools/call", json!({ "name": "tool_call", "arguments": { "name": tool, "arguments": arguments } })) +} + +#[cfg(feature = "mcp")] +fn tool_names(reply: &Value) -> Vec { + reply["result"]["tools"].as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap().to_string()).collect() +} + +#[cfg(feature = "mcp")] +#[test] +fn mcp_default_lists_every_tool_and_has_no_meta_tools() { + let mut s = McpServer::new(Automation::new()); + let names = tool_names(&rpc(&mut s, 1, "tools/list", json!({}))); + assert_eq!(names.len(), tools().len()); + assert!(!names.iter().any(|n| n == "tool_search" || n == "tool_call")); + assert!(!rpc(&mut s, 2, "initialize", json!({}))["result"]["instructions"].as_str().unwrap().contains("tool_search")); + // Without --compact the meta tools do not exist. + assert_eq!(rpc(&mut s, 3, "tools/call", json!({ "name": "tool_search" }))["error"]["code"], -32602); +} + +#[cfg(feature = "mcp")] +#[test] +fn mcp_compact_lists_the_core_tools_and_two_meta_tools() { + let mut s = McpServer::new(Automation::new()).with_compact(true); + let list = rpc(&mut s, 1, "tools/list", json!({})); + let mut expected: Vec = pdfcraft_automation::mcp::COMPACT_CORE_TOOLS.iter().map(|n| n.to_string()).collect(); + expected.extend(["tool_search".to_string(), "tool_call".to_string()]); + assert_eq!(tool_names(&list), expected); + let all: Vec<&str> = tools().iter().map(|t| t.name).collect(); + for core in pdfcraft_automation::mcp::COMPACT_CORE_TOOLS { + assert!(all.contains(core), "core tool {core} is not in the tool table"); + } + // Core tools are the real definitions, and the list is far smaller than the full one. + let full = rpc(&mut McpServer::new(Automation::new()), 2, "tools/list", json!({})); + let core_open = list["result"]["tools"][0].clone(); + assert_eq!(Some(&core_open), full["result"]["tools"].as_array().unwrap().iter().find(|t| t["name"] == "doc_open")); + assert!(list.to_string().len() * 5 < full.to_string().len(), "compact {} vs full {}", list.to_string().len(), full.to_string().len()); + for meta in &list["result"]["tools"].as_array().unwrap()[pdfcraft_automation::mcp::COMPACT_CORE_TOOLS.len()..] { + assert_eq!(meta["inputSchema"]["type"], "object"); + } + let instructions = rpc(&mut s, 3, "initialize", json!({}))["result"]["instructions"].as_str().unwrap().to_string(); + assert!(instructions.contains("tool_search") && instructions.contains("tool_call"), "{instructions}"); +} + +#[cfg(feature = "mcp")] +#[test] +fn mcp_compact_tool_search_filters_by_query_and_category() { + let mut s = McpServer::new(Automation::new()).with_compact(true); + let mut search = |args: Value| rpc(&mut s, 1, "tools/call", json!({ "name": "tool_search", "arguments": args })); + + let everything = search(json!({})); + assert_eq!(everything["result"]["isError"], false); + assert_eq!(everything["result"]["structuredContent"]["count"], tools().len()); + let first = &everything["result"]["structuredContent"]["tools"][0]; + assert!(first["name"].is_string() && first["description"].is_string() && first["read_only"].is_boolean(), "{first}"); + + let rotate = search(json!({ "query": "ROTATE" }))["result"]["structuredContent"].clone(); + let names: Vec<&str> = rotate["tools"].as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap()).collect(); + assert!(names.contains(&"page_rotate"), "{names:?}"); + assert!(names.len() < tools().len()); + + let docs = search(json!({ "category": "doc" }))["result"]["structuredContent"].clone(); + let expected = tools().iter().filter(|t| t.name.starts_with("doc_")).count(); + assert!(expected > 0); + assert_eq!(docs["count"], expected); + assert!(docs["tools"].as_array().unwrap().iter().all(|t| t["name"].as_str().unwrap().starts_with("doc_") && t["category"] == "doc")); + + let both = search(json!({ "category": "page", "query": "rotate" }))["result"]["structuredContent"].clone(); + assert!(both["tools"].as_array().unwrap().iter().all(|t| t["name"].as_str().unwrap().starts_with("page_"))); + assert!(both["tools"].as_array().unwrap().iter().any(|t| t["name"] == "page_rotate")); + + // No match is an empty result that lists the categories, not an error. + let none = search(json!({ "category": "nonsense" })); + assert_eq!(none["result"]["isError"], false); + assert_eq!(none["result"]["structuredContent"]["count"], 0); + assert!(none["result"]["structuredContent"]["categories"].as_array().unwrap().iter().any(|c| c == "page")); + + // One tool in full. + let one = search(json!({ "name": "page_rotate" }))["result"]["structuredContent"]["tool"].clone(); + assert_eq!(one["name"], "page_rotate"); + assert_eq!(one["input_schema"]["type"], "object"); + let def = tools().into_iter().find(|t| t.name == "page_rotate").unwrap(); + assert_eq!(one["input_schema"], def.input_schema); + assert_eq!(one["description"], def.description); + + // Bad arguments are tool errors. + for bad in [json!({ "name": "no_such_tool" }), json!({ "query": 3 }), json!({ "category": ["doc"] }), json!("doc")] { + let r = search(bad.clone()); + assert_eq!(r["result"]["isError"], true, "{bad}"); + assert!(r["result"]["content"][0]["text"].as_str().is_some_and(|t| !t.is_empty())); + } +} + +#[cfg(feature = "mcp")] +#[test] +fn mcp_compact_tool_call_dispatches_to_any_tool() { + let dir = workdir("mcp-compact"); + let mut s = compact_server(&dir); + let opened = via_tool_call(&mut s, 1, "doc_open", json!({ "path": "a.pdf" })); + assert_eq!(opened["result"]["isError"], false); + assert_eq!(opened["result"]["structuredContent"]["pages"], 3); + + // A tool outside the core list, through the meta tool. + let deleted = via_tool_call(&mut s, 2, "page_delete", json!({ "doc": 1, "pages": [3] })); + assert_eq!(deleted["result"]["isError"], false, "{deleted}"); + assert_eq!(deleted["result"]["structuredContent"]["pages"], 2); + + // The same tool called by name directly still works, and sees the same session. + let listed = rpc(&mut s, 3, "tools/call", json!({ "name": "doc_list", "arguments": {} })); + assert_eq!(listed["result"]["isError"], false); + assert_eq!(listed["result"]["structuredContent"]["documents"][0]["pages"], 2); + + // Images come back as images. + let png = via_tool_call(&mut s, 4, "page_render", json!({ "doc": 1, "page": 1, "dpi": 36 })); + assert_eq!(png["result"]["content"][0]["type"], "image"); + + // `arguments` may be left out for tools that need none. + assert_eq!(via_tool_call(&mut s, 5, "doc_list", Value::Null)["result"]["isError"], false); + let no_args = rpc(&mut s, 6, "tools/call", json!({ "name": "tool_call", "arguments": { "name": "doc_list" } })); + assert_eq!(no_args["result"]["isError"], false); + + // The result equals what the direct call returns. + let direct = rpc(&mut s, 7, "tools/call", json!({ "name": "doc_info", "arguments": { "doc": 1 } })); + let wrapped = via_tool_call(&mut s, 8, "doc_info", json!({ "doc": 1 })); + assert_eq!(direct["result"], wrapped["result"]); +} + +#[cfg(feature = "mcp")] +#[test] +fn mcp_compact_tool_call_reports_errors_without_panicking() { + let dir = workdir("mcp-compact-errors"); + let mut s = compact_server(&dir); + let text = |r: &Value| r["result"]["content"][0]["text"].as_str().unwrap_or_default().to_string(); + + let unknown = via_tool_call(&mut s, 1, "no_such_tool", json!({})); + assert_eq!(unknown["result"]["isError"], true); + assert!(text(&unknown).contains("no_such_tool") && text(&unknown).contains("tool_search"), "{}", text(&unknown)); + + // The tool's own validation errors pass through. + let missing = via_tool_call(&mut s, 2, "doc_open", json!({})); + assert_eq!(missing["result"]["isError"], true); + let outside = via_tool_call(&mut s, 3, "doc_open", json!({ "path": "/definitely/not/here.pdf" })); + assert_eq!(outside["result"]["isError"], true); + let wrong_type = via_tool_call(&mut s, 4, "page_delete", json!({ "doc": "one", "pages": "all" })); + assert_eq!(wrong_type["result"]["isError"], true); + + // Malformed meta-tool arguments. + for args in [ + json!({}), + json!({ "name": 7 }), + json!({ "name": "doc_list", "arguments": [1] }), + json!("doc_list"), + Value::Null, + json!({ "name": "tool_call" }), + json!({ "name": "tool_search" }), + ] { + let r = rpc(&mut s, 5, "tools/call", json!({ "name": "tool_call", "arguments": args })); + assert_eq!(r["result"]["isError"], true, "{args}"); + assert!(!text(&r).is_empty()); + } + + // Direct calls keep the usual protocol error for an unknown tool, and the session survives all of it. + assert_eq!(rpc(&mut s, 6, "tools/call", json!({ "name": "nope" }))["error"]["code"], -32602); + assert_eq!(via_tool_call(&mut s, 7, "doc_open", json!({ "path": "a.pdf" }))["result"]["isError"], false); +} + +#[cfg(feature = "mcp")] +#[test] +fn mcp_compact_tool_search_summarizes_the_first_sentence() { + let mut s = McpServer::new(Automation::new()).with_compact(true); + let mut summary = |tool: &str| { + let found = rpc( + &mut s, + 1, + "tools/call", + json!({ "name": "tool_search", "arguments": { "query": tool, "category": tool.split('_').next().unwrap() } }), + ); + found["result"]["structuredContent"]["tools"].as_array().unwrap().iter().find(|t| t["name"] == tool).unwrap()["description"] + .as_str() + .unwrap() + .to_string() + }; + // Abbreviations such as "e.g." do not end the summary. + assert_eq!(summary("page_number"), "Label a range of pages (e.g. i, ii, iii for front matter, or A-1, A-2 for an appendix)."); + assert_eq!( + summary("accessibility_fix"), + "Apply the checker's automatic fix for a rule: primary-language (value: the language, e.g. en-US), title (value: the title; default the current title or file name; also shows it in the title bar) or tab-order (every page tabs in structure order)." + ); +} + +#[cfg(feature = "mcp")] +#[test] +fn mcp_compact_meta_tools_reject_unknown_arguments() { + let dir = workdir("mcp-compact-keys"); + let mut s = compact_server(&dir); + let list = rpc(&mut s, 1, "tools/list", json!({})); + let metas = &list["result"]["tools"].as_array().unwrap()[pdfcraft_automation::mcp::COMPACT_CORE_TOOLS.len()..]; + assert_eq!(metas.len(), 2); + for meta in metas { + assert_eq!(meta["inputSchema"]["additionalProperties"], false, "{}", meta["name"]); + } + let text = |r: &Value| r["error"]["message"].as_str().or_else(|| r["result"]["content"][0]["text"].as_str()).unwrap_or_default().to_string(); + + // A misspelled filter is an error, not an unfiltered listing. + let typo = rpc(&mut s, 2, "tools/call", json!({ "name": "tool_search", "arguments": { "qurey": "rotate" } })); + assert_eq!(typo["error"]["code"], -32602, "{typo}"); + assert!(text(&typo).contains("\"qurey\"") && text(&typo).contains("query"), "{}", text(&typo)); + let extra = rpc(&mut s, 3, "tools/call", json!({ "name": "tool_search", "arguments": { "query": "rotate", "extra": 1 } })); + assert_eq!(extra["error"]["code"], -32602, "{extra}"); + assert!(text(&extra).contains("\"extra\""), "{}", text(&extra)); + + // tool_call rejects extra outer keys before running anything. + let misspelled = + rpc(&mut s, 4, "tools/call", json!({ "name": "tool_call", "arguments": { "name": "doc_open", "argumentz": { "path": "a.pdf" } } })); + assert_eq!(misspelled["error"]["code"], -32602, "{misspelled}"); + assert!(text(&misspelled).contains("\"argumentz\""), "{}", text(&misspelled)); + let extra = rpc(&mut s, 5, "tools/call", json!({ "name": "tool_call", "arguments": { "name": "doc_list", "unexpected": "value" } })); + assert_eq!(extra["error"]["code"], -32602, "{extra}"); + assert!(text(&extra).contains("\"unexpected\""), "{}", text(&extra)); + assert_eq!(rpc(&mut s, 6, "tools/call", json!({ "name": "doc_list", "arguments": {} }))["result"]["structuredContent"]["documents"], json!([])); + + // Keys inside "arguments" are left to the target tool, which rejects its unknown ones itself. + let inner = via_tool_call(&mut s, 7, "doc_list", json!({ "unexpected": "value" })); + assert_eq!(inner["result"]["isError"], true, "{inner}"); + assert!(text(&inner).contains("doc_list: unknown argument \"unexpected\""), "{}", text(&inner)); +} + +#[cfg(feature = "mcp")] +#[test] +fn mcp_compact_tool_call_keeps_the_root_confinement() { + let root = workdir("mcp-compact-root"); + let outside_dir = workdir("mcp-compact-outside"); + let outside = outside_dir.join("a.pdf").to_string_lossy().into_owned(); + let text = |r: &Value| r["result"]["content"][0]["text"].as_str().unwrap_or_default().to_string(); + + // Direct call and tool_call both refuse a real PDF outside the root, with the same words. + let mut confined = compact_server(&root); + let direct = rpc(&mut confined, 1, "tools/call", json!({ "name": "doc_open", "arguments": { "path": outside } })); + assert_eq!(direct["result"]["isError"], true, "{direct}"); + assert!(text(&direct).contains("is outside the allowed directory"), "{}", text(&direct)); + let wrapped = via_tool_call(&mut confined, 2, "doc_open", json!({ "path": outside })); + assert_eq!(wrapped["result"]["isError"], true, "{wrapped}"); + assert_eq!(text(&wrapped), text(&direct)); + // Nothing was opened. + let listed = rpc(&mut confined, 3, "tools/call", json!({ "name": "doc_list", "arguments": {} })); + assert_eq!(listed["result"]["structuredContent"]["documents"], json!([])); + + // The same file opens through tool_call on a server without a root, so the refusal came from the root. + let mut open = McpServer::new(Automation::new()).with_compact(true); + let opened = via_tool_call(&mut open, 4, "doc_open", json!({ "path": outside })); + assert_eq!(opened["result"]["isError"], false, "{opened}"); + assert_eq!(opened["result"]["structuredContent"]["pages"], 3); + + // And a file inside the root still opens through the confined server. + assert_eq!(via_tool_call(&mut confined, 5, "doc_open", json!({ "path": "a.pdf" }))["result"]["isError"], false); +} + #[test] fn parallel_text_extraction_keeps_page_order_and_follows_edits() { let dir = workdir("parallel"); @@ -747,6 +1022,33 @@ fn protecting_through_tools() { ok(&mut c, "doc_open", json!({ "path": "open.pdf" })); } +#[test] +fn combine_opens_protected_files_with_their_passwords() { + let dir = workdir("combine-passwords"); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + // Opens with "open"; only "boss" may assemble pages. + ok(&mut a, "doc_protect", json!({ "doc": doc, "open_password": "open", "permissions_password": "boss", "changes": "none" })); + ok(&mut a, "doc_save", json!({ "doc": doc, "path": "locked.pdf" })); + let combine = |a: &mut Automation, passwords: Value| { + a.call("doc_combine", &json!({ "paths": ["locked.pdf", "b.pdf"], "passwords": passwords, "open": true })) + }; + let err = |r: Result<_, ToolError>| match r { + Err(ToolError::Failed(m)) => m, + Err(other) => panic!("expected a failure, got {other:?}"), + Ok(_) => panic!("expected a failure"), + }; + assert!(err(combine(&mut a, Value::Null)).contains("password-protected")); + assert!(err(combine(&mut a, json!(["wrong", null]))).contains("password is wrong")); + assert!(err(combine(&mut a, json!(["open", null]))).contains("don't allow copying pages")); + assert!(matches!(combine(&mut a, json!(["boss"])), Err(ToolError::InvalidArgs(_))), "one per path"); + let done = ok(&mut a, "doc_combine", json!({ "paths": ["locked.pdf", "b.pdf"], "passwords": ["boss", null], "open": true })); + assert!(!done.to_string().contains("boss"), "passwords are never echoed"); + let out = done["document"]["doc"].as_u64().unwrap(); + assert_eq!(page_text(&mut a, out), ["Page 1", "Page 2", "Page 3", "Page 1", "Page 2"]); + assert_eq!(ok(&mut a, "doc_info", json!({ "doc": out }))["security"]["protected"], false, "the result is not encrypted"); +} + /// Restrictions exist only behind a permissions password: open_password alone encrypts and /// restricts nothing, and asking for a restriction without one is refused rather than ignored (#134). #[test] @@ -986,6 +1288,20 @@ fn editing_existing_text_through_tools() { assert_eq!(p["lines"].as_array().map(Vec::len), Some(2), "{p}"); } +#[test] +fn paragraph_bold_without_font_keeps_the_source_family() { + let dir = workdir("paragraph-bold"); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + ok(&mut a, "text_edit", json!({ "doc": doc, "page": 2, "paragraph": 1, "font": "times" })); + let bold = ok(&mut a, "text_edit", json!({ "doc": doc, "page": 2, "paragraph": 1, "bold": true })); + assert_eq!(bold["paragraph"]["font"], "Times-Bold"); + let regular = ok(&mut a, "text_edit", json!({ "doc": doc, "page": 2, "paragraph": 1, "bold": false })); + assert_eq!(regular["paragraph"]["font"], "Times-Roman"); + assert_eq!(regular["paragraph"]["text"], "Page 2"); + assert!(matches!(a.call("text_edit", &json!({ "doc": doc, "page": 2, "paragraph": 1, "bold": "yes" })), Err(ToolError::InvalidArgs(_)))); +} + #[test] fn editing_page_images_through_tools() { let dir = workdir("page-images"); @@ -1011,6 +1327,51 @@ fn editing_page_images_through_tools() { assert!(matches!(a.call("image_edit", &json!({ "doc": made, "page": 1, "image": 1, "action": "delete" })), Err(ToolError::InvalidArgs(_)))); } +#[test] +fn editing_form_artwork_through_tools() { + use pdfcraft_cos::{Dict, Document, Object, SaveOptions, Stream, write_incremental}; + let dir = workdir("form-artwork"); + let mut cos = Document::open(std::sync::Arc::new(fixture(1))).unwrap(); + let page = pdfcraft_model::pages(&cos)[0].clone(); + let mut form = Dict::new(); + form.set(b"Subtype".to_vec(), Object::name("Form")); + form.set(b"BBox".to_vec(), Object::Array([0, 0, 80, 40].map(Object::Int).to_vec())); + let artwork = cos.add(Object::Stream(Stream::flate(form, b"0.2 0.5 0.9 rg 0 0 80 40 re f"))); + let contents = cos.add(Object::Stream(Stream::flate(Dict::new(), b"q 1 0 0 1 20 240 cm /Figure Do Q"))); + cos.update_dict(page.obj, |d| { + d.set(b"Contents".to_vec(), Object::Ref(contents)); + let mut xo = Dict::new(); + xo.set(b"Figure".to_vec(), Object::Ref(artwork)); + let mut resources = Dict::new(); + resources.set(b"XObject".to_vec(), Object::Dict(xo)); + d.set(b"Resources".to_vec(), Object::Dict(resources)); + }) + .unwrap(); + std::fs::write(dir.join("figure.pdf"), write_incremental(&cos, &SaveOptions::default()).unwrap()).unwrap(); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({"path": "figure.pdf"}))["doc"].as_u64().unwrap(); + let before = ok(&mut a, "page_images", json!({"doc": doc, "page": 1}))["images"][0].clone(); + assert_eq!(before["kind"], "form"); + assert_eq!(before["pixels"], json!([0, 0])); + assert_eq!(before["rect"], json!([20.0, 20.0, 100.0, 60.0])); + let rendered_before = a.call("page_render", &json!({"doc": doc, "page": 1, "dpi": 72})).unwrap(); + assert!(a.call("image_save", &json!({"doc": doc, "page": 1, "image": 1, "path": "out/figure"})).is_err()); + ok(&mut a, "image_edit", json!({"doc": doc, "page": 1, "image": 1, "action": "move", "rect": [40, 50, 120, 90]})); + assert_eq!(ok(&mut a, "page_images", json!({"doc": doc, "page": 1}))["images"][0]["rect"], json!([40.0, 50.0, 120.0, 90.0])); + let rendered_moved = a.call("page_render", &json!({"doc": doc, "page": 1, "dpi": 72})).unwrap(); + assert_ne!(rendered_before, rendered_moved, "the rendered artwork actually moves"); + ok(&mut a, "doc_save", json!({"doc": doc, "path": "out/moved.pdf"})); + let re = ok(&mut a, "doc_open", json!({"path": "out/moved.pdf"}))["doc"].as_u64().unwrap(); + assert_eq!(ok(&mut a, "page_images", json!({"doc": re, "page": 1}))["images"][0]["rect"], json!([40.0, 50.0, 120.0, 90.0])); + ok(&mut a, "image_edit", json!({"doc": re, "page": 1, "image": 1, "action": "delete"})); + assert_eq!(ok(&mut a, "page_images", json!({"doc": re, "page": 1}))["count"], 0); + ok(&mut a, "edit_undo", json!({"doc": re})); + assert_eq!(ok(&mut a, "page_images", json!({"doc": re, "page": 1}))["count"], 1); + ok(&mut a, "edit_undo", json!({"doc": doc})); + assert_eq!(ok(&mut a, "page_images", json!({"doc": doc, "page": 1}))["images"][0], before); + assert_eq!(a.call("page_render", &json!({"doc": doc, "page": 1, "dpi": 72})).unwrap(), rendered_before); +} + #[test] fn auditing_space_through_tools() { let dir = workdir("audit"); @@ -1064,6 +1425,136 @@ fn fill_and_sign_through_tools() { assert!(matches!(a.call("fill_sign_add", &json!({ "doc": doc, "page": 1, "type": "text", "at": [1, 1] })), Err(ToolError::InvalidArgs(_)))); } +#[test] +fn image_signatures_through_tools_preserve_transparency_and_survive_save() { + let dir = workdir("image-signatures"); + let mut png = Vec::new(); + { + let mut encoder = png::Encoder::new(&mut png, 120, 40); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + let rgba: Vec = (0..40) + .flat_map(|y| (0..120).flat_map(move |x| if (40..80).contains(&x) && (10..30).contains(&y) { [0, 0, 0, 255] } else { [0, 0, 0, 0] })) + .collect(); + encoder.write_header().unwrap().write_image_data(&rgba).unwrap(); + } + std::fs::write(dir.join("signature.png"), png).unwrap(); + std::fs::write(dir.join("broken.png"), b"broken").unwrap(); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_create", json!({ "from": "blank", "width": 200, "height": 300 }))["doc"].as_u64().unwrap(); + for (kind, y) in [("signature", 60), ("initials", 120)] { + ok(&mut a, "fill_sign_add", json!({ "doc": doc, "page": 1, "type": kind, "at": [20, y], "path": "signature.png" })); + } + let render = |a: &mut Automation, doc| { + let output = a.call("page_render", &json!({ "doc": doc, "page": 1, "dpi": 72 })).unwrap(); + let Content::Png { data, .. } = &output[0] else { panic!() }; + image::load_from_memory(data).unwrap().to_rgba8() + }; + let before = render(&mut a, doc); + assert_eq!(before.get_pixel(25, 60).0, [255, 255, 255, 255], "transparent margin exposes the page"); + assert_eq!(before.get_pixel(65, 60).0, [0, 0, 0, 255], "signature ink is embedded"); + assert_eq!(ok(&mut a, "edit_undo", json!({ "doc": doc }))["undone"], "Add initials"); + assert_eq!(ok(&mut a, "comment_list", json!({ "doc": doc }))["count"], 1); + ok(&mut a, "edit_redo", json!({ "doc": doc })); + // The same resize edit the selection handles use must keep the imported appearance. + for rect in [json!([20, 40, 20, 88]), json!([20, 40, 100000000, 88])] { + assert!(a.call("comment_edit", &json!({ "doc": doc, "page": 1, "index": 1, "rect": rect })).is_err()); + } + ok(&mut a, "comment_lock", json!({ "doc": doc, "page": 1, "index": 1 })); + assert!(a.call("comment_edit", &json!({ "doc": doc, "page": 1, "index": 1, "rect": [20, 40, 164, 88] })).is_err()); + ok(&mut a, "comment_lock", json!({ "doc": doc, "page": 1, "index": 1, "locked": false })); + assert_eq!(render(&mut a, doc), before, "invalid and locked resizes leave the image intact"); + ok(&mut a, "comment_edit", json!({ "doc": doc, "page": 1, "index": 1, "rect": [20, 40, 164, 88] })); + let resized = render(&mut a, doc); + assert_eq!(resized.get_pixel(110, 64).0, [0, 0, 0, 255], "resizing scales the original ink"); + assert_eq!(resized.get_pixel(25, 64).0, [255, 255, 255, 255], "resizing retains alpha"); + assert_eq!(ok(&mut a, "edit_undo", json!({ "doc": doc }))["undone"], "Resize comment"); + assert_eq!(render(&mut a, doc), before); + ok(&mut a, "edit_redo", json!({ "doc": doc })); + assert_eq!(render(&mut a, doc), resized); + ok(&mut a, "doc_save", json!({ "doc": doc, "path": "signed.pdf" })); + let reopened = ok(&mut a, "doc_open", json!({ "path": "signed.pdf" }))["doc"].as_u64().unwrap(); + assert_eq!(ok(&mut a, "comment_list", json!({ "doc": reopened }))["count"], 2); + assert_eq!(render(&mut a, reopened), resized, "resized appearances retain alpha and geometry after save"); + for args in [ + json!({ "type": "signature", "path": "broken.png" }), + json!({ "type": "signature", "path": "signature.png", "text": "Ada" }), + json!({ "type": "check", "path": "signature.png" }), + json!({ "type": "signature", "path": "../outside.png" }), + ] { + let mut args = args; + args["doc"] = json!(doc); + args["page"] = json!(1); + args["at"] = json!([20, 80]); + assert!(a.call("fill_sign_add", &args).is_err(), "{args}"); + } + assert_eq!(ok(&mut a, "comment_list", json!({ "doc": doc }))["count"], 2, "errors leave the PDF intact"); +} + +#[test] +fn image_signature_preview_layers_are_read_only_and_survive_encrypted_save() { + let dir = workdir("signature-preview"); + let mut png = Vec::new(); + { + let mut encoder = png::Encoder::new(&mut png, 120, 40); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + let rgba: Vec = (0..40) + .flat_map(|y| (0..120).flat_map(move |x| if (40..80).contains(&x) && (10..30).contains(&y) { [20, 40, 60, 128] } else { [0, 0, 0, 0] })) + .collect(); + encoder.write_header().unwrap().write_image_data(&rgba).unwrap(); + } + std::fs::write(dir.join("signature.png"), png).unwrap(); + std::fs::write(dir.join("form.pdf"), fixture(1)).unwrap(); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "form.pdf" }))["doc"].as_u64().unwrap(); + ok(&mut a, "page_rotate", json!({ "doc": doc, "pages": [1], "degrees": 90 })); + ok(&mut a, "fill_sign_add", json!({ "doc": doc, "page": 1, "type": "initials", "at": [20, 40], "path": "signature.png" })); + let background = a.call("page_render", &json!({ "doc": doc, "page": 1, "dpi": 72 })).unwrap(); + let Content::Png { data: expected, .. } = &background[0] else { panic!() }; + let expected = image::load_from_memory(expected).unwrap().to_rgba8(); + ok(&mut a, "fill_sign_add", json!({ "doc": doc, "page": 1, "type": "signature", "at": [120, 100], "path": "signature.png" })); + ok(&mut a, "comment_edit", json!({ "doc": doc, "page": 1, "index": 2, "opacity": 0.5 })); + ok(&mut a, "doc_protect", json!({ "doc": doc, "open_password": "preview-test" })); + ok(&mut a, "doc_save", json!({ "doc": doc, "path": "signed.pdf" })); + let reopened = ok(&mut a, "doc_open", json!({ "path": "signed.pdf", "password": "preview-test" }))["doc"].as_u64().unwrap(); + for doc in [doc, reopened] { + let before = ok(&mut a, "doc_info", json!({ "doc": doc })); + let d = a.session().docs().iter().find(|d| d.id.0 == doc).unwrap(); + let generation = d.edit_generation(); + let bytes = d.bytes.clone(); + let output = a.call("comment_image_preview", &json!({ "doc": doc, "page": 1, "index": 2, "dpi": 72 })).unwrap(); + let [Content::Json(meta), Content::Png { data: background, .. }] = output.as_slice() else { panic!("labeled background layer") }; + let image = a.call("comment_image_preview", &json!({ "doc": doc, "page": 1, "index": 2, "layer": "image" })).unwrap(); + let [Content::Json(image_meta), Content::Png { data: signature, width, height }] = image.as_slice() else { panic!("labeled image layer") }; + assert_eq!(meta["layer"], "background"); + assert_eq!(image_meta["layer"], "image"); + assert_eq!(meta["rotation"], 90); + assert_eq!(meta["image_rotation"], 0, "placed upright as displayed on the turned page"); + assert_eq!(meta["opacity"], 0.5); + assert_eq!((*width, *height), (120, 40)); + assert_eq!(image::load_from_memory(background).unwrap().to_rgba8(), expected, "page text and the other signature remain"); + let signature = image::load_from_memory(signature).unwrap().to_rgba8(); + assert_eq!(signature.get_pixel(5, 20).0[3], 0); + assert_eq!(signature.get_pixel(60, 20).0, [20, 40, 60, 128], "embedded alpha and colour survive reopen"); + assert_eq!(ok(&mut a, "doc_info", json!({ "doc": doc })), before); + let d = a.session().docs().iter().find(|d| d.id.0 == doc).unwrap(); + assert_eq!(d.edit_generation(), generation); + assert!(std::sync::Arc::ptr_eq(&bytes, &d.bytes)); + assert!(d.image_signature_preview(0, 1).unwrap().unwrap().render_background(f32::NAN).is_err()); + assert!(d.image_signature_preview(0, 999).is_err()); + } + assert!(tools().iter().find(|t| t.name == "comment_image_preview").unwrap().read_only); + for dpi in [0, 601] { + assert!(matches!( + a.call("comment_image_preview", &json!({ "doc": reopened, "page": 1, "index": 2, "dpi": dpi })), + Err(ToolError::InvalidArgs(_)) + )); + } + ok(&mut a, "fill_sign_add", json!({ "doc": reopened, "page": 1, "type": "check", "at": [10, 10] })); + assert!(a.call("comment_image_preview", &json!({ "doc": reopened, "page": 1, "index": 3 })).is_err()); +} + #[test] fn creating_and_reducing_through_tools() { let dir = workdir("create"); @@ -1079,6 +1570,62 @@ fn creating_and_reducing_through_tools() { assert!(matches!(a.call("doc_create", &json!({ "from": "images", "paths": ["notes.txt"] })), Err(ToolError::Failed(_)))); } +#[test] +fn creating_from_multiple_files_through_tools() { + let dir = workdir("create-multiple"); + let mut png = Vec::new(); + { + let mut enc = png::Encoder::new(&mut png, 4, 2); + enc.set_color(png::ColorType::Rgb); + enc.set_depth(png::BitDepth::Eight); + enc.write_header().unwrap().write_image_data(&[100; 4 * 2 * 3]).unwrap(); + } + std::fs::write(dir.join("scan.png"), png).unwrap(); + std::fs::write(dir.join("notes.txt"), "hello").unwrap(); + std::fs::write(dir.join("report.docx"), b"PK\x03\x04").unwrap(); + let mut a = auto(&dir); + + // Combine: every file converted, in the order given, with a bookmark per file. + let made = ok( + &mut a, + "doc_create_multiple", + json!({ "paths": ["notes.txt", "a.pdf", "scan.png"], "pages": [null, "3", null], "out": "all.pdf", "open": true }), + ); + let doc = made["document"]["doc"].as_u64().unwrap(); + assert_eq!(page_text(&mut a, doc), ["hello", "Page 3", ""]); + let titles: Vec = ok(&mut a, "bookmark_list", json!({ "doc": doc }))["bookmarks"] + .as_array() + .unwrap() + .iter() + .map(|b| b["title"].as_str().unwrap().to_string()) + .collect(); + assert_eq!(titles, ["notes", "a", "scan"]); + assert!(dir.join("all.pdf").is_file()); + // A file that can't be converted fails the whole combine, naming it. + let err = a.call("doc_create_multiple", &json!({ "paths": ["a.pdf", "report.docx"] })).unwrap_err(); + assert!(matches!(&err, ToolError::Failed(m) if m.contains("report.docx") && m.contains("can't be converted")), "{err}"); + assert!(matches!(a.call("doc_create_multiple", &json!({ "paths": [] })), Err(ToolError::InvalidArgs(_)))); + assert!(matches!(a.call("doc_create_multiple", &json!({ "paths": ["a.pdf"], "mode": "zip" })), Err(ToolError::InvalidArgs(_)))); + assert!(matches!(a.call("doc_create_multiple", &json!({ "paths": ["a.pdf"], "mode": "separate" })), Err(ToolError::InvalidArgs(_)))); + + // Separate: one PDF per file; PDFs are skipped, a bad file doesn't stop the rest, and an + // existing file is never overwritten. + std::fs::create_dir_all(dir.join("out")).unwrap(); + std::fs::write(dir.join("out/notes.pdf"), b"mine").unwrap(); + let args = json!({ "paths": ["notes.txt", "report.docx", "a.pdf", "scan.png", "missing.txt"], "mode": "separate", "out_dir": "out" }); + let made = ok(&mut a, "doc_create_multiple", args); + let files = made["files"].as_array().unwrap(); + assert!(files[0]["output"].as_str().unwrap().ends_with("notes (2).pdf"), "{files:?}"); + assert!(files[1]["error"].as_str().unwrap().contains("can't be converted")); + assert_eq!(files[2]["skipped"], "already a PDF"); + assert!(files[3]["output"].as_str().unwrap().ends_with("scan.pdf")); + assert!(files[4]["error"].is_string()); + assert_eq!(std::fs::read(dir.join("out/notes.pdf")).unwrap(), b"mine"); + let reopened = ok(&mut a, "doc_open", json!({ "path": "out/notes (2).pdf" }))["doc"].as_u64().unwrap(); + assert_eq!(page_text(&mut a, reopened), ["hello"]); + assert!(!dir.join("out/a.pdf").exists()); +} + #[test] fn creating_images_with_dpi_through_tools() { let dir = workdir("image-dpi"); @@ -1196,6 +1743,34 @@ fn preparing_a_form_through_tools() { assert!(matches!(a.call("form_set_image", &json!({ "doc": doc, "field": "size", "path": "pics/a_page_1.png" })), Err(ToolError::Failed(_)))); } +#[test] +fn rotating_a_field_through_tools() { + let dir = workdir("rotate-field"); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + ok(&mut a, "form_add_field", json!({ "doc": doc, "page": 1, "type": "text", "rect": [20, 20, 180, 42], "name": "City" })); + let rect_of = |a: &mut Automation| { + let f = &ok(a, "form_fields", json!({ "doc": doc }))["fields"][0]; + (f["rotation"].as_i64().unwrap(), f["rect"].as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect::>()) + }; + let (rot, before) = rect_of(&mut a); + assert_eq!(rot, 0); + assert!(matches!(a.call("form_set_props", &json!({ "doc": doc, "field": "City", "rotation": 45 })), Err(ToolError::InvalidArgs(_)))); + assert_eq!(rect_of(&mut a).1, before, "a rejected rotation changes nothing"); + ok(&mut a, "form_set_props", json!({ "doc": doc, "field": "City", "rotation": 90 })); + let (rot, turned) = rect_of(&mut a); + assert_eq!(rot, 90); + let (bw, bh) = (before[2] - before[0], before[3] - before[1]); + let (tw, th) = (turned[2] - turned[0], turned[3] - turned[1]); + assert!((tw - bh).abs() < 0.2 && (th - bw).abs() < 0.2, "swapped {before:?} -> {turned:?}"); + let center = |r: &[f64]| ((r[0] + r[2]) / 2.0, (r[1] + r[3]) / 2.0); + let (bc, tc) = (center(&before), center(&turned)); + assert!((bc.0 - tc.0).abs() < 0.2 && (bc.1 - tc.1).abs() < 0.2, "center moved {bc:?} -> {tc:?}"); + assert_eq!(ok(&mut a, "edit_undo", json!({ "doc": doc }))["undone"], "Change field properties"); + let (rot, back) = rect_of(&mut a); + assert_eq!((rot, back), (0, before)); +} + #[test] fn redacting_through_tools() { let dir = workdir("redact"); @@ -1381,6 +1956,47 @@ fn comments_and_form_data_travel_as_xfdf_fdf_and_text() { assert_eq!(r["undo"], "Import all.xfdf"); } +#[test] +fn natural_image_stamps_through_tools_on_rotated_pages() { + for degrees in [0, 90, 180, 270] { + let dir = workdir(&format!("natural-stamps-{degrees}")); + let mut bytes = Vec::new(); + { + let mut encoder = png::Encoder::new(&mut bytes, 80, 40); + encoder.set_color(png::ColorType::Rgb); + encoder.set_depth(png::BitDepth::Eight); + let pixels: Vec = (0..40) + .flat_map(|y| { + (0..80).flat_map(move |x| { + [[240, 20, 20], [20, 180, 20], [20, 20, 240], [230, 180, 20]][usize::from(y >= 20) * 2 + usize::from(x >= 40)] + }) + }) + .collect(); + encoder.write_header().unwrap().write_image_data(&pixels).unwrap(); + } + std::fs::write(dir.join("quadrants.png"), bytes).unwrap(); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({"path":"a.pdf"}))["doc"].as_u64().unwrap(); + ok(&mut a, "page_rotate", json!({"doc":doc,"pages":[1],"degrees":degrees})); + ok(&mut a, "stamp_custom", json!({"doc":doc,"page":1,"path":"quadrants.png","at":[100,100]})); + let rendered = a.call("page_render", &json!({"doc":doc,"page":1,"dpi":72})).unwrap(); + let Content::Png { data, .. } = &rendered[0] else { panic!("expected PNG") }; + let mut reader = png::Decoder::new(std::io::Cursor::new(data)).read_info().unwrap(); + let mut pixels = vec![0; reader.output_buffer_size().unwrap()]; + let info = reader.next_frame(&mut pixels).unwrap(); + assert_eq!(info.color_type, png::ColorType::Rgba); + for ((x, y), colour) in [(80, 90), (120, 90), (80, 110), (120, 110)].into_iter().zip([ + [240, 20, 20, 255], + [20, 180, 20, 255], + [20, 20, 240, 255], + [230, 180, 20, 255], + ]) { + let offset = ((y * info.width + x) * 4) as usize; + assert_eq!(&pixels[offset..offset + 4], &colour, "page rotation {degrees}"); + } + } +} + #[test] fn stamps_through_tools() { let dir = workdir("stamps"); @@ -1539,6 +2155,112 @@ fn note_icons_anchor_at_the_requested_corner_on_rotated_pages() { } } +/// An image signature is drawn upright as displayed, and anchored at the displayed point asked +/// for, on every `/Rotate`: its picture is placed in user space, which the page turns, so the +/// appearance is counter-rotated and its box is found in user space. +#[test] +fn image_signatures_stay_upright_on_rotated_pages() { + let dir = workdir("image-upright"); + // 80 x 40 px, one colour per quadrant: red and green above, blue and yellow below. + let quadrants = image::RgbaImage::from_fn(80, 40, |x, y| match (x < 40, y < 20) { + (true, true) => image::Rgba([255, 0, 0, 255]), + (false, true) => image::Rgba([0, 255, 0, 255]), + (true, false) => image::Rgba([0, 0, 255, 255]), + (false, false) => image::Rgba([255, 255, 0, 255]), + }); + quadrants.save(dir.join("quadrants.png")).unwrap(); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_create", json!({ "from": "blank", "width": 200, "height": 300, "pages": 4 }))["doc"].as_u64().unwrap(); + for (page, degrees) in [(2, 90), (3, 180), (4, 270)] { + ok(&mut a, "page_rotate", json!({ "doc": doc, "pages": [page], "degrees": degrees })); + } + for page in 1..=4 { + // 64 x 32 pt from x = 60 as displayed, centred on y = 140. + ok(&mut a, "fill_sign_add", json!({ "doc": doc, "page": page, "type": "signature", "at": [60, 140], "path": "quadrants.png" })); + } + let (white, red, green, blue, yellow) = ([255, 255, 255], [255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 0]); + let check = |a: &mut Automation, doc: u64| { + for page in 1..=4 { + let output = a.call("page_render", &json!({ "doc": doc, "page": page, "dpi": 72 })).unwrap(); + let Content::Png { data, .. } = &output[0] else { panic!() }; + let pixels = image::load_from_memory(data).unwrap().to_rgba8(); + for (x, y, want, what) in [ + (76, 132, red, "signature top-left"), + (108, 132, green, "signature top-right"), + (76, 148, blue, "signature bottom-left"), + (108, 148, yellow, "signature bottom-right"), + (56, 140, white, "left of the signature"), + (128, 140, white, "right of the signature"), + (92, 120, white, "above the signature"), + (92, 160, white, "below the signature"), + ] { + let got = pixels.get_pixel(x, y).0; + assert!(got[..3].iter().zip(want).all(|(g, w)| g.abs_diff(w) < 12), "page {page}, {what} at ({x}, {y}): {got:?}, want {want:?}"); + } + } + }; + check(&mut a, doc); + ok(&mut a, "doc_save", json!({ "doc": doc, "path": "placed.pdf" })); + let reopened = ok(&mut a, "doc_open", json!({ "path": "placed.pdf" }))["doc"].as_u64().unwrap(); + check(&mut a, reopened); +} + +/// Issue #299: a typed signature and initials read across, as displayed, on every `/Rotate`, +/// anchored at the displayed point, and look the same as on an unturned page. +#[test] +fn typed_signatures_stay_upright_on_rotated_pages() { + let dir = workdir("typed-upright"); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_create", json!({ "from": "blank", "width": 200, "height": 300, "pages": 4 }))["doc"].as_u64().unwrap(); + for (page, degrees) in [(2, 90), (3, 180), (4, 270)] { + ok(&mut a, "page_rotate", json!({ "doc": doc, "pages": [page], "degrees": degrees })); + } + for page in 1..=4 { + ok(&mut a, "fill_sign_add", json!({ "doc": doc, "page": page, "type": "signature", "at": [30, 60], "text": "Ada Lovelace" })); + ok(&mut a, "fill_sign_add", json!({ "doc": doc, "page": page, "type": "initials", "at": [30, 140], "text": "AL" })); + } + let check = |a: &mut Automation, doc: u64| { + let mut upright = Vec::new(); + for page in 1..=4 { + let output = a.call("page_render", &json!({ "doc": doc, "page": page, "dpi": 72 })).unwrap(); + let Content::Png { data, .. } = &output[0] else { panic!() }; + let pixels = image::load_from_memory(data).unwrap().to_rgba8(); + assert_eq!(pixels.dimensions(), if page % 2 == 1 { (200, 300) } else { (300, 200) }, "page {page}"); + let dark: Vec = pixels.pixels().map(|p| p.0[..3].iter().all(|v| *v < 128)).collect(); + for y in [60, 140] { + let mut b = [u32::MAX, u32::MAX, 0, 0]; + for (x, py, p) in pixels.enumerate_pixels() { + if py.abs_diff(y) < 40 && p.0[..3].iter().all(|v| *v < 128) { + b = [b[0].min(x), b[1].min(py), b[2].max(x), b[3].max(py)]; + } + } + assert!(b[0] <= b[2], "page {page}: no ink near y = {y}"); + assert!(b[2] - b[0] > b[3] - b[1], "page {page}: the name reads across: {b:?}"); + assert!(b[0].abs_diff(30) <= 2 && b[1] < y && b[3] > y, "page {page}: anchored at (30, {y}): {b:?}"); + } + if page == 1 { + upright = dark; + continue; + } + // Same pixels as the unturned page where both pages are (pages 2 and 4 are wider). + let width = pixels.width(); + let (mut differ, mut ink) = (0, 0); + for y in 0..200 { + for x in 0..200 { + let (want, got) = (upright[(y * 200 + x) as usize], dark[(y * width + x) as usize]); + ink += usize::from(want); + differ += usize::from(want != got); + } + } + assert!(differ * 10 < ink, "page {page}: {differ} of {ink} ink pixels differ from the unturned page"); + } + }; + check(&mut a, doc); + ok(&mut a, "doc_save", json!({ "doc": doc, "path": "placed.pdf" })); + let reopened = ok(&mut a, "doc_open", json!({ "path": "placed.pdf" }))["doc"].as_u64().unwrap(); + check(&mut a, reopened); +} + #[test] fn comment_checkmarks_locks_hiding_and_summaries_through_tools() { let dir = workdir("comment-polish"); @@ -1570,6 +2292,72 @@ fn comment_checkmarks_locks_hiding_and_summaries_through_tools() { assert!(matches!(a.call("comments_summarize", &json!({ "doc": doc, "sort": "colour" })), Err(ToolError::InvalidArgs(_)))); } +#[test] +fn line_endings_through_the_tool() { + let dir = workdir("endings"); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + ok(&mut a, "comment_add", json!({ "doc": doc, "page": 1, "type": "line", "from": [20, 40], "to": [120, 80], "endings": ["None", "Diamond"] })); + ok( + &mut a, + "comment_add", + json!({ "doc": doc, "page": 1, "type": "polyline", "points": [[20, 120], [60, 100], [100, 140]], "endings": ["Circle", "Slash"] }), + ); + ok( + &mut a, + "comment_add", + json!({ "doc": doc, "page": 1, "type": "callout", "rect": [110, 200, 190, 240], "to": [40, 160], "contents": "Look", "endings": ["Square"] }), + ); + assert!(matches!( + a.call("comment_add", &json!({ "doc": doc, "page": 1, "type": "line", "from": [20, 40], "to": [120, 80], "endings": ["Sparkle", "None"] })), + Err(ToolError::InvalidArgs(_)) + )); + assert!(matches!( + a.call( + "comment_add", + &json!({ "doc": doc, "page": 1, "type": "callout", "rect": [10, 10, 80, 40], "to": [90, 80], "endings": ["None", "None"] }) + ), + Err(ToolError::InvalidArgs(_)) + )); + assert!(matches!( + a.call("comment_add", &json!({ "doc": doc, "page": 1, "type": "rectangle", "rect": [10, 10, 40, 40], "endings": ["Circle"] })), + Err(ToolError::InvalidArgs(_)) + )); + ok(&mut a, "doc_save", json!({ "doc": doc, "path": "ended.pdf" })); + let saved = pdfcraft_cos::Document::open(std::sync::Arc::new(std::fs::read(dir.join("ended.pdf")).unwrap())).unwrap(); + for name in [b"Diamond".as_slice(), b"Circle", b"Slash", b"Square"] { + assert!( + saved.object_numbers().iter().any(|n| saved.try_get(*n).ok().is_some_and(|o| object_has_name(&o, name))), + "missing /{}", + String::from_utf8_lossy(name) + ); + } + let drawn: Vec = saved + .object_numbers() + .into_iter() + .filter_map(|n| { + let obj = saved.try_get(n).ok()?; + let pdfcraft_cos::Object::Stream(s) = &*obj else { return None }; + if s.dict.name(b"Subtype") != Some(b"Form") { + return None; + } + s.decoded().ok().map(|b| String::from_utf8_lossy(&b).into_owned()) + }) + .collect(); + assert!(drawn.iter().any(|s| s.contains("h B")), "a filled ending was drawn: {drawn:?}"); + assert!(drawn.iter().any(|s| s.contains(" l S")), "an open ending was drawn: {drawn:?}"); +} + +fn object_has_name(obj: &pdfcraft_cos::Object, name: &[u8]) -> bool { + match obj { + pdfcraft_cos::Object::Name(n) => n.as_slice() == name, + pdfcraft_cos::Object::Array(items) => items.iter().any(|o| object_has_name(o, name)), + pdfcraft_cos::Object::Dict(d) => d.iter().any(|(_, o)| object_has_name(o, name)), + pdfcraft_cos::Object::Stream(s) => s.dict.iter().any(|(_, o)| object_has_name(o, name)), + _ => false, + } +} + #[test] fn drawing_comments_through_tools() { let dir = workdir("drawing"); @@ -2313,3 +3101,211 @@ mod close_argument_tests { assert_eq!(std::fs::read(dir.0.join("a.pdf")).unwrap(), source); } } + +mod combine_argument_tests { + use super::*; + use std::sync::atomic::{AtomicU64, Ordering}; + + // Own only a newly created directory; the bounded runner provides project-local TMPDIR. + struct CombineDir(PathBuf); + + impl CombineDir { + fn new() -> Self { + static NEXT: AtomicU64 = AtomicU64::new(0); + let parent = std::env::temp_dir(); + for _ in 0..128 { + let serial = NEXT.fetch_add(1, Ordering::Relaxed); + let path = parent.join(format!("pdfcraft-combine-{}-{serial}", std::process::id())); + match std::fs::create_dir(&path) { + Ok(()) => { + let dir = Self(path); + std::fs::write(dir.0.join("a.pdf"), fixture(3)).unwrap(); + std::fs::write(dir.0.join("b.pdf"), fixture(2)).unwrap(); + return dir; + } + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(error) => panic!("creating combine test directory: {error}"), + } + } + panic!("no unused combine test directory after 128 attempts"); + } + } + + impl Drop for CombineDir { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + + #[test] + fn invalid_combine_selectors_preserve_outputs_inputs_and_open_documents() { + let dir = CombineDir::new(); + let first = std::fs::read(dir.0.join("a.pdf")).unwrap(); + let second = std::fs::read(dir.0.join("b.pdf")).unwrap(); + let sentinel = b"existing destination"; + std::fs::write(dir.0.join("combined.pdf"), sentinel).unwrap(); + let mut a = auto(&dir.0); + let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + ok(&mut a, "doc_set_info", json!({ "doc": doc, "key": "Title", "value": "Unsaved title" })); + ok(&mut a, "doc_open", json!({ "path": "b.pdf" })); + let list = ok(&mut a, "doc_list", json!({})); + let info = ok(&mut a, "doc_info", json!({ "doc": doc })); + let text = page_text(&mut a, doc); + let bytes = a.session().get(pdfcraft_engine::DocId(doc)).unwrap().bytes.clone(); + for invalid in [json!(1), json!(true), json!({ "page": 1 }), json!(["1"])] { + for (index, pages) in [json!([invalid, null]), json!([null, invalid])].into_iter().enumerate() { + for out in ["combined.pdf", "not-created.pdf"] { + let error = a.call("doc_combine", &json!({ "paths": ["a.pdf", "b.pdf"], "pages": pages, "out": out, "open": true })).unwrap_err(); + assert!( + matches!(error, ToolError::InvalidArgs(ref message) if message == &format!("pages[{index}] must be a range string or null")) + ); + assert_eq!(std::fs::read(dir.0.join("combined.pdf")).unwrap(), sentinel); + assert!(!dir.0.join("not-created.pdf").exists()); + assert_eq!(ok(&mut a, "doc_list", json!({})), list); + assert_eq!(ok(&mut a, "doc_info", json!({ "doc": doc })), info); + assert_eq!(page_text(&mut a, doc), text); + assert_eq!(a.session().get(pdfcraft_engine::DocId(doc)).unwrap().bytes, bytes); + assert_eq!(std::fs::read(dir.0.join("a.pdf")).unwrap(), first); + assert_eq!(std::fs::read(dir.0.join("b.pdf")).unwrap(), second); + } + } + } + let error = a + .call("doc_combine", &json!({ "paths": ["missing-a.pdf", "missing-b.pdf"], "pages": [null, false], "out": "not-created.pdf" })) + .unwrap_err(); + assert!(matches!(error, ToolError::InvalidArgs(ref message) if message == "pages[1] must be a range string or null")); + assert_eq!(ok(&mut a, "doc_list", json!({})), list); + assert_eq!(std::fs::read_dir(&dir.0).unwrap().count(), 3, "no output or staging files are created"); + } + + #[test] + fn valid_combine_selectors_keep_all_pages_and_selected_order_after_reopen() { + let dir = CombineDir::new(); + let first = std::fs::read(dir.0.join("a.pdf")).unwrap(); + let second = std::fs::read(dir.0.join("b.pdf")).unwrap(); + let mut a = auto(&dir.0); + let all = vec!["Page 1", "Page 2", "Page 3", "Page 1", "Page 2"]; + for (pages, expected) in [ + (None, all.clone()), + (Some(Value::Null), all.clone()), + (Some(json!([null, null])), all.clone()), + (Some(json!(["", null])), all), + (Some(json!(["3, 1", null])), vec!["Page 3", "Page 1", "Page 1", "Page 2"]), + (Some(json!([null, "2"])), vec!["Page 1", "Page 2", "Page 3", "Page 2"]), + ] { + let mut args = json!({ "paths": ["a.pdf", "b.pdf"], "out": "combined.pdf", "open": false }); + if let Some(pages) = pages { + args["pages"] = pages; + } + let result = ok(&mut a, "doc_combine", args); + assert!(result["bytes"].as_u64().unwrap() > 0); + assert!(result.get("document").is_none()); + assert!(a.session().docs().is_empty()); + let mut fresh = auto(&dir.0); + let opened = ok(&mut fresh, "doc_open", json!({ "path": "combined.pdf" })); + assert_eq!(opened["pages"].as_u64().unwrap(), expected.len() as u64); + assert_eq!(page_text(&mut fresh, opened["doc"].as_u64().unwrap()), expected); + assert_eq!(std::fs::read(dir.0.join("a.pdf")).unwrap(), first); + assert_eq!(std::fs::read(dir.0.join("b.pdf")).unwrap(), second); + } + } +} + +#[cfg(feature = "mcp")] +#[test] +fn conventions_core_tools_preserve_root_and_batch_errors() { + let dir = workdir("conventions-core"); + let mut s = McpServer::new(auto(&dir)); + let run = |s: &mut McpServer, name: &str, args: Value| rpc(s, 1, "tools/call", json!({"name":name,"arguments":args})); + assert_eq!(run(&mut s, "doc_inspect", json!({}))["result"]["structuredContent"]["documents"], json!([])); + let opened = run(&mut s, "command_run", json!({"id":"file.open","params":{"path":"a.pdf"}})); + assert_eq!(opened["result"]["isError"], false, "{opened}"); + let doc = opened["result"]["structuredContent"]["doc"].as_u64().unwrap(); + let filtered = run(&mut s, "command_list", json!({"doc":doc,"filter":"rotate","enabled_only":true})); + let commands = filtered["result"]["structuredContent"]["commands"].as_array().unwrap(); + assert!(!commands.is_empty()); + assert!(commands.iter().all(|c| c["enabled"] == true)); + assert!(commands.iter().find(|c| c["id"] == "page.rotate").unwrap()["params"].is_object()); + for stop in [true, false] { + let r = run( + &mut s, + "command_batch", + json!({"steps":[{"id":"page.rotate","params":{"doc":doc,"degrees":90}},{"id":"no.such.command"},{"id":"page.rotate","params":{"doc":doc,"degrees":90}}],"stop_on_error":stop}), + ); + assert_eq!(r["result"]["isError"], true, "{r}"); + assert_eq!(r["result"]["structuredContent"]["completed"], if stop { 1 } else { 2 }); + assert_eq!(r["result"]["structuredContent"]["failed"], 1); + } + let r = run(&mut s, "command_run", json!({"id":"file.save","params":{"doc":doc,"path":"../escaped.pdf"}})); + assert_eq!(r["result"]["isError"], true); + assert!(r["result"]["content"][0]["text"].as_str().unwrap().contains("outside the allowed directory")); + let r = run(&mut s, "command_run", json!({"id":"page.rotate","params":{"doc":doc,"degrees":90,"typo":1}})); + assert_eq!(r["result"]["isError"], false); + assert!(r["result"]["structuredContent"]["warnings"][0].as_str().unwrap().contains("typo")); + let before = run(&mut s, "doc_info", json!({"doc":doc})); + let r = run(&mut s, "render_preview", json!({"doc":doc,"page":1,"max_side":64})); + assert_eq!(r["result"]["isError"], false, "{r}"); + use base64::Engine as _; + let png = base64::engine::general_purpose::STANDARD.decode(r["result"]["content"][0]["data"].as_str().unwrap()).unwrap(); + let image = image::load_from_memory(&png).unwrap(); + assert!(image.width().max(image.height()) <= 64); + assert_eq!(before["result"], run(&mut s, "doc_info", json!({"doc":doc}))["result"]); +} + +#[cfg(feature = "mcp")] +#[test] +fn conventions_annotations_and_strict_keys_in_both_modes() { + for compact in [false, true] { + let mut s = McpServer::new(Automation::new()).with_compact(compact); + let r = rpc(&mut s, 1, "tools/list", json!({})); + let tools = r["result"]["tools"].as_array().unwrap(); + for name in ["command_list", "command_run", "command_batch", "doc_inspect", "render_preview"] { + assert!(tools.iter().any(|t| t["name"] == name), "missing {name}"); + } + for tool in tools { + for hint in ["readOnlyHint", "destructiveHint", "idempotentHint", "openWorldHint"] { + assert!(tool["annotations"][hint].is_boolean(), "{}: {hint}", tool["name"]); + } + let r = rpc(&mut s, 2, "tools/call", json!({"name":tool["name"],"arguments":{"bogus_arg":1}})); + assert_eq!(r["error"]["code"], -32602, "{}", tool["name"]); + let message = r["error"]["message"].as_str().unwrap(); + assert!(message.contains("bogus_arg") && message.contains("expected:")); + } + } +} + +#[cfg(feature = "mcp")] +#[test] +fn conventions_resources() { + let dir = workdir("conventions-resources"); + let mut s = McpServer::new(auto(&dir)); + let read = |s: &mut McpServer, uri: &str| { + let r = rpc(s, 1, "resources/read", json!({"uri":uri})); + serde_json::from_str::(r["result"]["contents"][0]["text"].as_str().expect("JSON resource")).unwrap() + }; + assert_eq!(read(&mut s, "pdfcraft://document"), json!({"documents":[]})); + rpc(&mut s, 2, "tools/call", json!({"name":"doc_open","arguments":{"path":"a.pdf"}})); + let doc = rpc(&mut s, 3, "tools/call", json!({"name":"doc_inspect","arguments":{}})); + assert_eq!(read(&mut s, "pdfcraft://document"), doc["result"]["structuredContent"]); + let commands = rpc(&mut s, 4, "tools/call", json!({"name":"command_list","arguments":{}})); + assert_eq!(read(&mut s, "pdfcraft://commands"), commands["result"]["structuredContent"]); + // Responses keep the shape every supported protocol revision expects: no cache hints. + for (method, params) in [("tools/list", json!({})), ("resources/list", json!({})), ("resources/read", json!({"uri":"pdfcraft://document"}))] { + assert!(rpc(&mut s, 5, method, params)["result"].get("resultType").is_none()); + } + assert_eq!(rpc(&mut s, 7, "initialize", json!({"protocolVersion":"2026-07-28"}))["result"]["protocolVersion"], "2025-06-18"); + assert!(s.handle_line(r#"{"jsonrpc":"2.0","method":"notifications/cancelled","params":{"requestId":999}}"#).is_none()); + assert_eq!( + rpc(&mut s, 9, "tools/call", json!({"name":"doc_inspect","arguments":{},"_meta":{"progressToken":"quick"}}))["result"]["isError"], + false + ); +} + +#[test] +fn command_batch_refuses_more_than_a_thousand_steps() { + let dir = workdir("batch-cap"); + let mut a = auto(&dir); + let steps: Vec = (0..1001).map(|_| json!({"id":"no.such.command"})).collect(); + let err = a.call("command_batch", &json!({"steps": steps})).unwrap_err(); + assert!(err.to_string().contains("at most 1000 steps"), "{err}"); +} diff --git a/crates/cos/README.md b/crates/cos/README.md new file mode 100644 index 00000000..b3ac964d --- /dev/null +++ b/crates/cos/README.md @@ -0,0 +1,91 @@ +# pdfcraft-cos + +The PDF object layer (ISO 32000-2 §7): the COS object graph, parsed lazily and tolerantly, edited +through a copy-on-write overlay, and written back incrementally or as a full rewrite. + +- **Layer:** L1, standalone. The layering table lets it use only `pdfcraft-filters`, + `pdfcraft-crypt` and `pdfcraft-geom` (`xtask/src/layers.rs::STANDALONE_DEPS`); it currently uses + the first two. That keeps the parser and the crypto independently auditable and reusable. +- `#![forbid(unsafe_code)]`, and the crate denies `unwrap`/`expect`/`panic!`/`todo!`. Builds for + `wasm32-unknown-unknown`. + +**The object graph is the document model.** Nothing is lifted into a tidier representation and +written back out, because that is how data gets silently dropped. Typed views live above, in +`pdfcraft-model`. + +## API + +```rust +let doc = Document::open(bytes)?; // Arc> +let doc = Document::open_with_password(bytes, Some("owner"))?; // encrypted files + +let root = doc.root(); // Option — the catalog +let obj = doc.get(ObjRef { num: 12, gen: 0 }); // load (and cache): Arc +let real = doc.resolve(&obj); // follow one indirect reference +let d = doc.dict(&obj); // Option, resolving first +let data = stream.decoded()?; // filters applied on demand + +doc.set(r, Object::Dict(d)); // edit: lands in the overlay +let r = doc.add(obj); // append a new object +doc.free(r); + +let bytes = write_incremental(&doc, &SaveOptions::default())?; // append a revision +let bytes = write_full(&doc, &SaveOptions::default())?; // garbage-collected rewrite +``` + +Both writers **return the bytes**; the caller decides where they go, which is what lets the engine +produce a working file in memory after every edit and keep saving atomic. + +Useful alongside: `doc.repair_log()` (what was reconstructed), `doc.revisions()` and +`doc.revision_ends()` (the incremental history), `doc.is_edited(num)` and `doc.modified_objects()`, +`doc.permissions()` and `doc.security()`, `doc.set_encryption()` / `doc.remove_encryption()`, and +`doc.require_full_save()` for edits that cannot be expressed incrementally. + +`Document` is **cheap to clone**: the original bytes, the xref index and the parse caches are +shared, so a clone is a snapshot. Undo/redo and background jobs hold clones while the UI edits +another. Edits never touch the original bytes. + +## Reading + +- **Every cross-reference form:** classic tables with their quirky subsections, cross-reference + streams, hybrid-reference files (`/XRefStm`), and object streams. +- **Repair.** A file whose xref is wrong or missing is reconstructed by scanning for `obj` + headers. Repairs are *recorded*, never silent, so the UI can say the file was damaged. +- **Tolerant, never fatal.** The lexer works on a byte slice and a position; it allocates nothing + for skipped data, bounds-checks every index, and caps nesting depth. Malformed input yields + `CosError`, never a panic — this crate is the first thing a hostile file meets. +- **Lazy.** Objects are parsed on first use and cached. (Whole-file buffering is still the case; + `ByteSource` streaming is `core.lazy-loading`, not yet done.) +- **Encryption** is handled on load and save through `pdfcraft-crypt`: every standard-security + revision R2–R6 (RC4, AES-128/256), crypt filters, `/EFF` embedded-file encryption, and + `/EncryptMetadata`. `security.rs` knows what the spec exempts — the `/Encrypt` dictionary, xref + streams, the trailer `/ID`, and objects inside an object stream (the stream is encrypted whole). + +## Writing + +`write_incremental` appends only the edited objects, a cross-reference section **in the same +style as the file's last one**, and a trailer with `/Prev`. The original bytes are never modified, +which is what keeps a signature over an earlier revision valid. + +`write_full` writes only what the trailer can reach, renumbered densely — the Save As and +garbage-collecting path. By default (`SaveOptions::object_streams`) it packs non-stream objects +into compressed object streams with a cross-reference stream (PDF 1.5+); turning that off writes a +classic table any reader accepts. + +Dictionaries keep their key order and streams keep their *encoded* bytes, so rewritten objects stay +recognisable in a diff and byte-stable when re-serialized. + +## Errors + +`CosError`: `NotPdf`, `Syntax { offset, detail }`, `MissingObject`, `NotADictionary`, `Filter`, +`NeedsPassword`, `WrongPassword`, `Security`, `Poisoned`. + +## Not done here + +- Streaming very large files within a memory budget (`ByteSource`, `core.lazy-loading`); files are + read whole today. +- Linearized output, Fast Web View (`core.linearization`, M11). +- Structural validation against the Arlington model (`core.arlington-validation`), which will live + in the reserved `arlington` crate. +- Image codecs. `pdfcraft-filters` carries the non-image filters; DCT, JPX, JBIG2 and CCITT are + still decoded through the bootstrap renderer (`core.image-filters`, M2.6). diff --git a/crates/cos/src/bytes.rs b/crates/cos/src/bytes.rs new file mode 100644 index 00000000..d2dcf238 --- /dev/null +++ b/crates/cos/src/bytes.rs @@ -0,0 +1,93 @@ +//! Stream data that can share a larger buffer instead of copying out of it. + +use std::ops::{Deref, Range}; +use std::sync::Arc; + +/// A stream's bytes: a buffer of their own, or a range of a larger shared buffer. Streams read +/// from a file point into the file's bytes, which the document keeps anyway, so caching a +/// parsed stream costs its dictionary and not a second copy of its data. +#[derive(Clone, Default)] +pub struct Bytes { + buf: Arc>, + range: Range, +} + +impl Bytes { + /// `range` of `buf`, without copying; `None` when the range is not within `buf`. + pub(crate) fn view(buf: &Arc>, range: Range) -> Option { + (range.start <= range.end && range.end <= buf.len()).then(|| Self { buf: buf.clone(), range }) + } + + /// Whether these bytes keep alive a larger buffer than themselves that isn't `owner`. + pub(crate) fn borrows_other_than(&self, owner: &Arc>) -> bool { + self.range.len() < self.buf.len() && !Arc::ptr_eq(&self.buf, owner) + } + + /// The same bytes, in a buffer of their own. + pub(crate) fn detached(&self) -> Self { + Self::from(self.to_vec()) + } +} + +impl From> for Bytes { + fn from(v: Vec) -> Self { + let range = 0..v.len(); + Self { buf: Arc::new(v), range } + } +} + +impl From>> for Bytes { + fn from(buf: Arc>) -> Self { + let range = 0..buf.len(); + Self { buf, range } + } +} + +impl Deref for Bytes { + type Target = [u8]; + + fn deref(&self) -> &[u8] { + // `view` and `From` only build in-bounds ranges; an empty slice keeps this panic-free. + self.buf.get(self.range.clone()).unwrap_or_default() + } +} + +impl AsRef<[u8]> for Bytes { + fn as_ref(&self) -> &[u8] { + self + } +} + +impl PartialEq for Bytes { + fn eq(&self, other: &Self) -> bool { + (Arc::ptr_eq(&self.buf, &other.buf) && self.range == other.range) || **self == **other + } +} + +impl Eq for Bytes {} + +impl std::fmt::Debug for Bytes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + (**self).fmt(f) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn views_share_their_buffer_and_compare_by_content() { + let file = Arc::new(b"0123456789".to_vec()); + let view = Bytes::view(&file, 2..5).expect("in range"); + assert_eq!(&*view, b"234"); + assert_eq!(view.as_ptr(), file[2..].as_ptr(), "no copy"); + assert_eq!(view, Bytes::from(b"234".to_vec())); + assert!(Bytes::view(&file, 5..11).is_none() && Bytes::view(&file, Range { start: 6, end: 5 }).is_none()); + let other = Arc::new(Vec::new()); + assert!(view.borrows_other_than(&other) && !view.borrows_other_than(&file)); + let own = view.detached(); + assert!(!own.borrows_other_than(&other) && own == view && own.as_ptr() != view.as_ptr()); + assert!(!Bytes::from(file.clone()).borrows_other_than(&other), "a whole buffer is its own"); + } +} diff --git a/crates/cos/src/document.rs b/crates/cos/src/document.rs index f44d2d24..665bebe5 100644 --- a/crates/cos/src/document.rs +++ b/crates/cos/src/document.rs @@ -10,7 +10,7 @@ use std::sync::{Arc, Mutex}; use crate::CosError; use crate::object::{Dict, ObjRef, Object}; -use crate::parser::{Lexer, is_whitespace, parse_indirect}; +use crate::parser::{Lexer, is_whitespace, parse_indirect, parse_indirect_shared}; thread_local! { /// The objects being loaded on this thread, innermost last (see `Document::try_get`). @@ -509,7 +509,7 @@ impl Document { length }; let off = *offset as usize; - let at = |off: usize| match parse_indirect(&self.data, off, &resolve) { + let at = |off: usize| match parse_indirect_shared(&self.data, off, &resolve) { Ok((id, o)) if id.num == num => Some(self.decrypted(id, o)), _ => None, }; @@ -564,7 +564,7 @@ impl Document { if p > 0 && !is_whitespace(data[p - 1]) { continue; } - if let Ok((id, o)) = parse_indirect(data, p, &|_| None) + if let Ok((id, o)) = parse_indirect_shared(data, p, &|_| None) && id.num == num { found = Some((id, o)); // keep the last (newest) definition @@ -575,9 +575,22 @@ impl Document { // ── editing ───────────────────────────────────────────────────────────────────────────── + /// Stream data still pointing into another document's file would keep that whole file + /// alive for as long as this edit exists: give it a buffer of its own (just its bytes). + fn adopt(&self, obj: Object) -> Object { + match obj { + Object::Stream(mut s) if s.raw.borrows_other_than(&self.data) => { + s.raw = s.raw.detached(); + Object::Stream(s) + } + other => other, + } + } + /// Replace an object (keeping its generation). pub fn set(&mut self, r: ObjRef, obj: impl Into) { - self.overlay.insert(r.num, Slot::Set(r.generation, Arc::new(obj.into()))); + let obj = self.adopt(obj.into()); + self.overlay.insert(r.num, Slot::Set(r.generation, Arc::new(obj))); if r.num >= self.next_num { self.next_num = r.num + 1; } @@ -587,7 +600,8 @@ impl Document { pub fn add(&mut self, obj: impl Into) -> ObjRef { let r = ObjRef::new(self.next_num, 0); self.next_num += 1; - self.overlay.insert(r.num, Slot::Set(0, Arc::new(obj.into()))); + let obj = self.adopt(obj.into()); + self.overlay.insert(r.num, Slot::Set(0, Arc::new(obj))); r } @@ -1010,7 +1024,7 @@ mod tests { // Either recovered via the endstream search or reported — but never a stack overflow. let o = doc.get(ObjRef::new(3, 0)); if let Object::Stream(s) = o.as_ref() { - assert_eq!(s.raw.as_slice(), b"abc"); + assert_eq!(&s.raw[..], b"abc"); } } diff --git a/crates/cos/src/lib.rs b/crates/cos/src/lib.rs index 4cab44d7..f2cdc1a5 100644 --- a/crates/cos/src/lib.rs +++ b/crates/cos/src/lib.rs @@ -5,18 +5,22 @@ //! untouched) or as a full, garbage-collected rewrite. The PDF object graph *is* PdfCraft's //! document model (plan/architecture.md §5, ADR-0010). //! -//! Status (M1 in progress): parsing of all xref forms, object streams, repair by scanning, -//! incremental and full writing. Not yet: encryption (M1.6), object-stream / xref-stream output -//! for full saves, linearization (M11). +//! Done: every cross-reference form (tables, streams, hybrid `/XRefStm`), object streams, repair +//! by scanning, encryption on load and save (R2–R6, crypt filters, `/EFF`), incremental writing, +//! and full saves that pack objects into compressed object streams with a cross-reference stream. +//! Not yet: streaming very large files within a memory budget (`ByteSource`, +//! `core.lazy-loading`), and linearized output (`core.linearization`, M11). See the README. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] +mod bytes; mod document; mod object; mod parser; mod security; mod writer; +pub use bytes::Bytes; pub use document::{Document, Revision, XrefEntry}; pub use object::{Dict, MAX_DECODED, Name, ObjRef, Object, PdfString, Stream}; pub use parser::{Lexer, parse_indirect}; diff --git a/crates/cos/src/object.rs b/crates/cos/src/object.rs index 983b5373..42659f9f 100644 --- a/crates/cos/src/object.rs +++ b/crates/cos/src/object.rs @@ -3,11 +3,9 @@ //! Dictionaries keep their key order so rewritten objects stay recognisable in diffs and //! byte-stable when re-serialized. Streams keep their *encoded* bytes; decoding is on demand. -use std::sync::Arc; - use pdfcraft_filters::{Filter, Params}; -use crate::CosError; +use crate::{Bytes, CosError}; /// An indirect reference: object number and generation. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -160,7 +158,7 @@ impl FromIterator<(Name, Object)> for Dict { #[derive(Clone, Debug, PartialEq)] pub struct Stream { pub dict: Dict, - pub raw: Arc>, + pub raw: Bytes, } /// Upper bound for a single decoded stream (decompression-bomb defence). @@ -169,14 +167,14 @@ pub const MAX_DECODED: usize = 1 << 30; impl Stream { /// A new stream from already-encoded bytes (`/Length` is set at write time). pub fn from_raw(dict: Dict, raw: Vec) -> Self { - Self { dict, raw: Arc::new(raw) } + Self { dict, raw: raw.into() } } /// A new stream holding `data` compressed with Flate. pub fn flate(mut dict: Dict, data: &[u8]) -> Self { dict.set(b"Filter".to_vec(), Object::Name(b"FlateDecode".to_vec())); dict.remove(b"DecodeParms"); - Self { dict, raw: Arc::new(pdfcraft_filters::encode_flate(data)) } + Self { dict, raw: pdfcraft_filters::encode_flate(data).into() } } /// The filter chain declared in the dictionary. @@ -223,19 +221,29 @@ impl Stream { pub fn decoded_within(&self, max: usize) -> Result, CosError> { let chain = self.filters(); if chain.is_empty() { - return Ok(self.raw.as_ref().clone()); + return self.raw_within(max); } pdfcraft_filters::decode_tolerant(&chain, &self.raw, max.min(MAX_DECODED)).map(|(v, _)| v).map_err(|e| CosError::Filter(e.to_string())) } - /// Decoded data; any corruption is an error. + /// Decoded data, bounded by [`MAX_DECODED`]; any corruption is an error. pub fn decoded_strict(&self) -> Result, CosError> { let chain = self.filters(); if chain.is_empty() { - return Ok(self.raw.as_ref().clone()); + return self.raw_within(MAX_DECODED); } pdfcraft_filters::decode(&chain, &self.raw, MAX_DECODED).map_err(|e| CosError::Filter(e.to_string())) } + + fn raw_within(&self, max: usize) -> Result, CosError> { + let max = max.min(MAX_DECODED); + // An empty decoder chain still produces an owned output buffer: check its limit + // before cloning, including streams whose /Crypt filter was applied on load. + if self.raw.len() > max { + return Err(CosError::Filter(pdfcraft_filters::FilterError::LimitExceeded(max).to_string())); + } + Ok(self.raw.to_vec()) + } } #[derive(Clone, Debug, PartialEq)] @@ -391,4 +399,55 @@ mod tests { assert_eq!(keys, vec![b"B".to_vec(), b"A".to_vec()]); assert_eq!(d.int(b"B"), Some(3)); } + + #[test] + fn raw_streams_refuse_decoding_past_a_caller_limit() { + let stream = Stream::from_raw(Dict::new(), b"abc".to_vec()); + for max in [0, 2] { + assert!(matches!(stream.decoded_within(max), Err(CosError::Filter(_)))); + } + } + + #[test] + fn empty_filter_arrays_refuse_decoding_past_a_caller_limit() { + let mut dict = Dict::new(); + dict.set(b"Filter".to_vec(), Object::Array(Vec::new())); + let stream = Stream::from_raw(dict, b"abc".to_vec()); + assert!(matches!(stream.decoded_within(2), Err(CosError::Filter(_)))); + } + + #[test] + fn crypt_only_streams_refuse_decoding_past_a_caller_limit() { + let mut dict = Dict::new(); + dict.set(b"Filter".to_vec(), Object::name("Crypt")); + let mut parms = Dict::new(); + parms.set(b"Name".to_vec(), Object::name("Identity")); + dict.set(b"DecodeParms".to_vec(), Object::Dict(parms)); + let stream = Stream::from_raw(dict, b"abc".to_vec()); + assert!(matches!(stream.decoded_within(2), Err(CosError::Filter(_)))); + } + + #[test] + fn identity_streams_preserve_bytes_within_a_caller_limit() { + let mut array = Dict::new(); + array.set(b"Filter".to_vec(), Object::Array(Vec::new())); + let mut crypt = Dict::new(); + crypt.set(b"Filter".to_vec(), Object::name("Crypt")); + for dict in [Dict::new(), array, crypt] { + let stream = Stream::from_raw(dict.clone(), b"abc".to_vec()); + assert_eq!(stream.decoded_within(3).unwrap(), b"abc"); + assert_eq!(stream.decoded_within(usize::MAX).unwrap(), b"abc"); + assert_eq!(stream.decoded().unwrap(), b"abc"); + assert_eq!(stream.decoded_strict().unwrap(), b"abc"); + assert!(Stream::from_raw(dict, Vec::new()).decoded_within(0).unwrap().is_empty()); + } + } + + #[test] + fn filtered_streams_preserve_the_caller_limit() { + let stream = Stream::flate(Dict::new(), b"abc"); + assert!(matches!(stream.decoded_within(2), Err(CosError::Filter(_)))); + assert_eq!(stream.decoded_within(3).unwrap(), b"abc"); + assert_eq!(stream.decoded_strict().unwrap(), b"abc"); + } } diff --git a/crates/cos/src/parser.rs b/crates/cos/src/parser.rs index 8bb71398..dcb1c58a 100644 --- a/crates/cos/src/parser.rs +++ b/crates/cos/src/parser.rs @@ -4,8 +4,10 @@ //! never panics on malformed input (every index is bounds-checked). Depth is limited to defend //! against pathological nesting. -use crate::CosError; +use std::sync::Arc; + use crate::object::{Dict, ObjRef, Object, PdfString, Stream}; +use crate::{Bytes, CosError}; const MAX_DEPTH: usize = 128; @@ -356,6 +358,25 @@ fn hexval(b: u8) -> Option { /// Parse an indirect object at `offset`: `N G obj [stream…endstream] endobj`. /// `resolve_length` resolves an indirect `/Length`. Returns the object and its (num, gen). pub fn parse_indirect(data: &[u8], offset: usize, resolve_length: &dyn Fn(ObjRef) -> Option) -> Result<(ObjRef, Object), CosError> { + parse_indirect_in(data, None, offset, resolve_length) +} + +/// [`parse_indirect`] for a document's own bytes: stream data points into `data` instead of +/// being copied out of it. +pub(crate) fn parse_indirect_shared( + data: &Arc>, + offset: usize, + resolve_length: &dyn Fn(ObjRef) -> Option, +) -> Result<(ObjRef, Object), CosError> { + parse_indirect_in(data, Some(data), offset, resolve_length) +} + +fn parse_indirect_in( + data: &[u8], + shared: Option<&Arc>>, + offset: usize, + resolve_length: &dyn Fn(ObjRef) -> Option, +) -> Result<(ObjRef, Object), CosError> { let mut lx = Lexer::new(data, offset); lx.skip_ws(); let num_tok = lx.token(); @@ -407,10 +428,15 @@ pub fn parse_indirect(data: &[u8], offset: usize, resolve_length: &dyn Fn(ObjRef // Wrong or missing /Length: search for "endstream" (§7.3.8.1 recovery). _ => find_endstream(data, start).ok_or_else(|| CosError::Syntax { offset: start, detail: "stream without endstream".into() })? - start, }; - let raw = data[start..start + len].to_vec(); + let range = start..start + len; + let raw = match shared { + Some(buf) => Bytes::view(buf, range), + None => data.get(range).map(|r| Bytes::from(r.to_vec())), + } + .ok_or_else(|| CosError::Syntax { offset: start, detail: "stream data past the end of the file".into() })?; let mut dict = dict.clone(); dict.set(b"Length".to_vec(), Object::Int(raw.len() as i64)); - return Ok((id, Object::Stream(Stream::from_raw(dict, raw)))); + return Ok((id, Object::Stream(Stream { dict, raw }))); } Ok((id, obj)) } @@ -506,7 +532,7 @@ mod tests { let (id, o) = parse_indirect(data, 0, &|_| None).unwrap(); assert_eq!(id, ObjRef::new(7, 0)); let Object::Stream(s) = o else { panic!("stream expected") }; - assert_eq!(s.raw.as_slice(), b"HELLO"); + assert_eq!(&s.raw[..], b"HELLO"); assert_eq!(s.dict.int(b"Length"), Some(5)); } @@ -515,7 +541,7 @@ mod tests { let data = b"7 0 obj\n<< /Length 8 0 R >>\nstream\r\nAB\r\nendstream\nendobj\n"; let (_, o) = parse_indirect(data, 0, &|r| (r.num == 8).then_some(2)).unwrap(); let Object::Stream(s) = o else { panic!() }; - assert_eq!(s.raw.as_slice(), b"AB"); + assert_eq!(&s.raw[..], b"AB"); } #[test] diff --git a/crates/cos/src/security.rs b/crates/cos/src/security.rs index 709176b3..252b2b4a 100644 --- a/crates/cos/src/security.rs +++ b/crates/cos/src/security.rs @@ -108,7 +108,7 @@ fn walk(o: &Object, strings: &dyn Fn(&PdfString) -> PdfString, h: &SecurityHandl _ => StreamKind::Normal, }; let raw = if decrypt { h.decrypt_stream(num, generation, &s.raw, kind) } else { h.encrypt_stream(num, generation, &s.raw, kind) }; - Object::Stream(Stream { dict, raw: std::sync::Arc::new(raw) }) + Object::Stream(Stream { dict, raw: raw.into() }) } other => other.clone(), } diff --git a/crates/cos/src/writer.rs b/crates/cos/src/writer.rs index 858a3067..418891b3 100644 --- a/crates/cos/src/writer.rs +++ b/crates/cos/src/writer.rs @@ -181,16 +181,17 @@ pub fn write_incremental(doc: &Document, opts: &SaveOptions) -> Result, let mut doc = doc.clone(); stamp_mod_date(&mut doc, opts); let original = doc.bytes().clone(); - let mut out = Vec::with_capacity(original.len() + 4096); - out.extend_from_slice(&original); - if !matches!(out.last(), Some(b'\n' | b'\r')) { - out.push(b'\n'); - } + // The revision is written on its own and joined to the original at the end, so the result is + // allocated once at its exact size: growing a buffer that holds a large original past a + // guessed capacity would copy the whole file again and keep up to as much again unused. + let newline = !matches!(original.last(), Some(b'\n' | b'\r')); + let base = (original.len() + usize::from(newline)) as u64; + let mut out = Vec::new(); let mut offsets: BTreeMap = BTreeMap::new(); // num → (offset, gen, in use) for (num, generation, obj) in doc.overlay_entries().collect::>() { match obj { Some(o) => { - offsets.insert(num, (out.len() as u64, generation, true)); + offsets.insert(num, (base + out.len() as u64, generation, true)); write_indirect(num, generation, &prepared(&doc, num, num, generation, &o), &mut out); } None => { @@ -210,16 +211,22 @@ pub fn write_incremental(doc: &Document, opts: &SaveOptions) -> Result, let size = doc.next_num().max(trailer.int(b"Size").unwrap_or(0) as u32); if as_stream { let rows = offsets.iter().map(|(n, (o, g, used))| (*n, if *used { Row::InFile(*o, *g) } else { Row::Free(*g) })).collect(); - write_xref_stream(&mut out, &mut trailer, rows, size); + write_xref_stream(&mut out, base, &mut trailer, rows, size); } else { trailer.set(b"Size".to_vec(), Object::Int(size as i64)); - let xref_at = out.len(); + let xref_at = base + out.len() as u64; write_xref_table(&mut out, &offsets, prev.is_none()); out.extend_from_slice(b"trailer\n"); write_dict(&trailer, &mut out); let _ = write!(out, "\nstartxref\n{xref_at}\n%%EOF\n"); } - Ok(out) + let mut file = Vec::with_capacity(original.len() + usize::from(newline) + out.len()); + file.extend_from_slice(&original); + if newline { + file.push(b'\n'); + } + file.extend_from_slice(&out); + Ok(file) } /// Write only reachable objects, renumbered from 1, with a classic cross-reference table. @@ -317,7 +324,7 @@ pub fn write_full(doc: &Document, opts: &SaveOptions) -> Result, CosErro ensure_id(&mut trailer, opts, doc.bytes()); } if opts.object_streams { - write_xref_stream(&mut out, &mut trailer, rows, next); + write_xref_stream(&mut out, 0, &mut trailer, rows, next); } else { trailer.set(b"Size".to_vec(), Object::Int(next as i64)); let offsets: BTreeMap = @@ -328,6 +335,9 @@ pub fn write_full(doc: &Document, opts: &SaveOptions) -> Result, CosErro write_dict(&trailer, &mut out); let _ = write!(out, "\nstartxref\n{xref_at}\n%%EOF\n"); } + // The size isn't known until the end, and the buffer grew by doubling. The result is often + // kept as a document's working bytes for as long as it's open: drop the unused capacity. + out.shrink_to_fit(); Ok(out) } @@ -374,9 +384,10 @@ fn write_xref_table(out: &mut Vec, offsets: &BTreeMap } /// A cross-reference stream section (§7.5.8) that takes object number `num`, ending the file. -/// Rows are Flate-compressed with the PNG Up predictor, and field widths fit the largest value. -fn write_xref_stream(out: &mut Vec, trailer: &mut Dict, mut rows: BTreeMap, num: u32) { - let xref_at = out.len() as u64; +/// `out` holds the file from byte offset `base` on. Rows are Flate-compressed with the PNG Up +/// predictor, and field widths fit the largest value. +fn write_xref_stream(out: &mut Vec, base: u64, trailer: &mut Dict, mut rows: BTreeMap, num: u32) { + let xref_at = base + out.len() as u64; rows.insert(num, Row::InFile(xref_at, 0)); if trailer.get(b"Prev").is_none() { rows.entry(0).or_insert(Row::Free(65535)); diff --git a/crates/cos/tests/shared_streams.rs b/crates/cos/tests/shared_streams.rs new file mode 100644 index 00000000..477da65b --- /dev/null +++ b/crates/cos/tests/shared_streams.rs @@ -0,0 +1,78 @@ +//! Stream data read from a file points into the file's bytes instead of being copied: the +//! document's object cache holds parsed dictionaries, not a second copy of every stream. + +use std::sync::Arc; + +use pdfcraft_cos::{Document, ObjRef, Object, SaveOptions, write_full, write_incremental}; + +const CONTENT: &[u8] = b"0 0 1 rg 10 10 30 20 re f"; + +fn fixture() -> Vec { + let objs = [ + "<< /Type /Catalog /Pages 2 0 R >>".to_string(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 100 50] >>".to_string(), + "<< /Type /Page /Parent 2 0 R /Contents 4 0 R >>".to_string(), + format!("<< /Length {} >>\nstream\n{}\nendstream", CONTENT.len(), String::from_utf8_lossy(CONTENT)), + ]; + let mut out = b"%PDF-1.4\n".to_vec(); + let mut offsets = Vec::new(); + for (i, o) in objs.iter().enumerate() { + offsets.push(out.len()); + out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes()); + } + let xref = out.len(); + out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes()); + for o in offsets { + out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes()); + } + out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes()); + out +} + +const CONTENTS: ObjRef = ObjRef { num: 4, generation: 0 }; + +fn stream_bytes(doc: &Document, r: ObjRef) -> (Vec, *const u8) { + match &*doc.get(r) { + Object::Stream(s) => (s.raw.to_vec(), s.raw.as_ptr()), + other => panic!("{other:?}"), + } +} + +#[test] +fn cached_streams_are_the_files_own_bytes() { + let file = Arc::new(fixture()); + let doc = Document::open(file.clone()).unwrap(); + let (data, at) = stream_bytes(&doc, CONTENTS); + assert_eq!(data, CONTENT); + assert!(file.as_ptr_range().contains(&at), "not a copy"); + // A full save reads and caches every object; the cache still holds no copy of stream data. + let full = write_full(&doc, &SaveOptions::default()).unwrap(); + assert!(file.as_ptr_range().contains(&stream_bytes(&doc, CONTENTS).1)); + // Saves are unchanged: the stream is written as it was read. + let back = Document::open(Arc::new(full)).unwrap(); + assert_eq!(stream_bytes(&back, ObjRef::new(4, 0)).0, CONTENT); +} + +#[test] +fn edits_within_a_document_keep_pointing_into_its_file() { + let file = Arc::new(fixture()); + let mut doc = Document::open(file.clone()).unwrap(); + doc.update_dict(CONTENTS, |d| d.set(b"Marked".to_vec(), Object::Bool(true))).unwrap(); + assert!(file.as_ptr_range().contains(&stream_bytes(&doc, CONTENTS).1)); + let updated = write_incremental(&doc, &SaveOptions::default()).unwrap(); + let back = Document::open(Arc::new(updated)).unwrap(); + assert_eq!(stream_bytes(&back, CONTENTS).0, CONTENT); +} + +#[test] +fn streams_moved_to_another_document_get_their_own_bytes() { + let file = Arc::new(fixture()); + let source = Document::open(file.clone()).unwrap(); + let moved = (*source.get(CONTENTS)).clone(); + let mut target = Document::new_empty(); + let r = target.add(moved); + drop(source); + // The target keeps just the stream's bytes, not the whole source file. + assert_eq!(Arc::strong_count(&file), 1); + assert_eq!(stream_bytes(&target, r).0, CONTENT); +} diff --git a/crates/cos/tests/writer.rs b/crates/cos/tests/writer.rs index bfe85882..4be398bf 100644 --- a/crates/cos/tests/writer.rs +++ b/crates/cos/tests/writer.rs @@ -3,7 +3,7 @@ use std::sync::Arc; -use pdfcraft_cos::{Dict, Document, ObjRef, Object, SaveOptions, write_full, write_incremental}; +use pdfcraft_cos::{Dict, Document, ObjRef, Object, SaveOptions, Stream, write_full, write_incremental}; /// Four pages, a shared font, document info with a non-ASCII title. fn fixture() -> Vec { @@ -87,6 +87,54 @@ fn incremental_saves_stack_on_an_xref_stream_file() { assert_eq!(hayro_pages(&updated), 4); } +/// The revision is joined to the original once, at its exact size. A revision of more than a few +/// kilobytes used to outgrow the buffer: the whole original was copied a second time, and the +/// result kept up to as much again unused for as long as it was the document's working bytes. +#[test] +fn incremental_saves_are_allocated_at_their_exact_size() { + let table = Arc::new(fixture()); + let stream = Arc::new(write_full(&Document::open(table.clone()).unwrap(), &SaveOptions::default()).unwrap()); + let no_newline = Arc::new(fixture().trim_ascii_end().to_vec()); + for original in [table, stream, no_newline] { + let mut doc = Document::open(original.clone()).unwrap(); + let data: Vec = (0..100_000u32).map(|i| (i % 251) as u8).collect(); + let added = doc.add(Object::Stream(Stream { dict: Dict::new(), raw: data.clone().into() })); + let updated = write_incremental(&doc, &SaveOptions::default()).unwrap(); + assert_eq!(updated.capacity(), updated.len()); + assert!(updated.starts_with(&original), "an incremental save only appends"); + let back = Document::open(Arc::new(updated.clone())).unwrap(); + assert!(back.repair_log().is_empty(), "{:?}", back.repair_log()); + match &*back.get(added) { + Object::Stream(s) => assert_eq!(*s.raw, data), + other => panic!("{other:?}"), + } + assert_eq!(hayro_pages(&updated), 4); + } +} + +/// A full save grows its buffer as it writes, so it used to end with up to as much again unused, +/// kept alive with the document's working bytes. It now ends at its exact size. +#[test] +fn full_saves_are_allocated_at_their_exact_size() { + let mut doc = Document::open(Arc::new(fixture())).unwrap(); + let data: Vec = (0..100_000u32).map(|i| (i % 251) as u8).collect(); + let added = doc.add(Object::Stream(Stream { dict: Dict::new(), raw: Arc::new(data.clone()).into() })); + let root = doc.root().unwrap(); + doc.update_dict(root, |d| d.set(b"Extra".to_vec(), Object::Ref(added))).unwrap(); + for object_streams in [true, false] { + let full = write_full(&doc, &SaveOptions { object_streams, ..SaveOptions::default() }).unwrap(); + assert_eq!(full.capacity(), full.len(), "object streams: {object_streams}"); + let back = Document::open(Arc::new(full.clone())).unwrap(); + assert!(back.repair_log().is_empty(), "{:?}", back.repair_log()); + let extra = back.dict(&Object::Ref(back.root().unwrap())).and_then(|d| d.get(b"Extra").cloned()).unwrap(); + match &*back.resolve(&extra) { + Object::Stream(s) => assert_eq!(*s.raw, data), + other => panic!("{other:?}"), + } + assert_eq!(hayro_pages(&full), 4); + } +} + #[test] fn qpdf_accepts_object_stream_output() { let Ok(out) = std::process::Command::new("qpdf").arg("--version").output() else { diff --git a/crates/create/src/lib.rs b/crates/create/src/lib.rs index 267e7af2..ad213c08 100644 --- a/crates/create/src/lib.rs +++ b/crates/create/src/lib.rs @@ -374,6 +374,45 @@ fn tiff_pages(name: &str, bytes: &[u8]) -> Result, CreateError> { Ok(out) } +/// What a file picked for Create is, by its bytes (and, for text, its name). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SourceKind { + Pdf, + Image, + Text, +} + +/// File extensions Create converts to PDF (images and plain text). +pub const CONVERTIBLE: [&str; 12] = ["png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx", "txt", "text"]; + +fn is_image(bytes: &[u8]) -> bool { + bytes.starts_with(&[0xFF, 0xD8]) + || bytes.starts_with(b"\x89PNG\r\n\x1a\n") + || bytes.starts_with(b"II*\0") + || bytes.starts_with(b"MM\0*") + || bytes.starts_with(b"GIF8") + // JPEG 2000: a JP2 file or a raw codestream. + || bytes.starts_with(JP2_SIGNATURE) + || bytes.starts_with(&[0xFF, 0x4F, 0xFF, 0x51]) + // BMP: "BM" and a known header size (so text starting with "BM" stays text). + || (bytes.starts_with(b"BM") + && bytes.get(14..18).and_then(|h| <[u8; 4]>::try_from(h).ok()).is_some_and(|h| matches!(u32::from_le_bytes(h), 12 | 40 | 52 | 56 | 108 | 124))) +} + +/// Whether `bytes` named `name` is a PDF, an image Create can embed, or plain text (a `.txt` or +/// `.text` file). `None` for anything else. +pub fn source_kind(name: &str, bytes: &[u8]) -> Option { + let head = bytes.get(..bytes.len().min(1024)).unwrap_or_default(); + if head.windows(5).any(|w| w == b"%PDF-") { + return Some(SourceKind::Pdf); + } + if is_image(bytes) { + return Some(SourceKind::Image); + } + let lower = name.to_ascii_lowercase(); + (lower.ends_with(".txt") || lower.ends_with(".text")).then_some(SourceKind::Text) +} + /// Detect the image format from its bytes; a TIFF may hold several pages. fn embed(name: &str, bytes: &[u8]) -> Result, CreateError> { if bytes.starts_with(&[0xFF, 0xD8]) { diff --git a/crates/create/src/tests.rs b/crates/create/src/tests.rs index f4dd33b9..9edc3fac 100644 --- a/crates/create/src/tests.rs +++ b/crates/create/src/tests.rs @@ -258,3 +258,18 @@ fn jpeg_2000_images_are_embedded_as_is() { let doc = reopen(&from_images(&[("raw.j2k".into(), siz)]).unwrap()); assert_eq!(media(&pages(&doc)[0])[2], 64.0); } + +#[test] +fn source_kind_tells_pdfs_images_and_text_apart() { + assert_eq!(source_kind("a.bin", b"%PDF-1.7\n"), Some(SourceKind::Pdf)); + assert_eq!(source_kind("a.txt", b"junk before\n%PDF-1.4"), Some(SourceKind::Pdf), "a header after leading junk"); + assert_eq!(source_kind("a", &png_bytes(false)), Some(SourceKind::Image)); + assert_eq!(source_kind("a.txt", &jpeg_bytes()), Some(SourceKind::Image), "bytes win over the name"); + assert_eq!(source_kind("notes.TXT", b"hello"), Some(SourceKind::Text)); + assert_eq!(source_kind("notes.text", b""), Some(SourceKind::Text)); + // Text that merely starts like a BMP stays text; a truncated BMP header is not an image. + assert_eq!(source_kind("b.txt", b"BMW drivers"), Some(SourceKind::Text)); + assert_eq!(source_kind("b.bmp", b"BM"), None); + assert_eq!(source_kind("a.docx", b"PK\x03\x04"), None); + assert_eq!(source_kind("", b""), None); +} diff --git a/crates/edit/Cargo.toml b/crates/edit/Cargo.toml index 1aadd633..42733780 100644 --- a/crates/edit/Cargo.toml +++ b/crates/edit/Cargo.toml @@ -14,6 +14,8 @@ pdfcraft-content = { workspace = true } pdfcraft-fonts = { workspace = true } pdfcraft-model = { workspace = true } thiserror = { workspace = true } +# Display order of right-to-left lines in added text; already in the tree via the interface. +unicode-bidi = "0.3" [dev-dependencies] hayro-syntax = { workspace = true } diff --git a/crates/edit/src/added.rs b/crates/edit/src/added.rs index d189dedf..40a2770e 100644 --- a/crates/edit/src/added.rs +++ b/crates/edit/src/added.rs @@ -7,8 +7,11 @@ //! content. Boxes are in display space (origin at the bottom-left of the page as shown, after //! `/Rotate`), so items stay upright on rotated pages. +use std::collections::HashMap; + use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream}; -use pdfcraft_fonts::{helvetica_width, literal, win_ansi}; +use pdfcraft_fonts::{GlyphError, GlyphOutline, ShapedCluster, arabic_glyph, helvetica_width, literal, shape_arabic, win_ansi}; +use unicode_bidi::{Level, ParagraphBidiInfo}; use crate::{EditError, check, contents, n, page_list, place_tagged}; @@ -195,24 +198,167 @@ fn norm(r: [f64; 4]) -> [f64; 4] { /// The lines of a text item after wrapping to its box width. pub fn lines(t: &AddedText) -> Vec { + wrapped(t).into_iter().map(|(line, _)| line).collect() +} + +/// [`lines`], each with whether its paragraph runs right to left (its first strong character +/// does). +fn wrapped(t: &AddedText) -> Vec<(String, bool)> { let width = (t.rect[2] - t.rect[0]).max(t.size); let mut out = Vec::new(); for para in t.text.split('\n') { - let mut line = String::new(); + let arabic = para.chars().any(is_arabic); + let rtl = arabic && unicode_bidi::get_base_direction(para) == unicode_bidi::Direction::Rtl; + // Arabic words are shaped one by one (joining stops at spaces), so a line's width is the + // sum of its words' and shaping stays linear in the paragraph's length. The space is the + // paragraph's; a space between Latin words is drawn in the item's font, a few hundredths + // of an em apart. + let measure = |s: &str| if arabic { arabic_width(t, s, rtl) } else { t.family.width(s, t.size, t.bold) }; + let space = measure(" "); + let (mut line, mut line_w) = (String::new(), 0.0); for word in para.split(' ') { let candidate = if line.is_empty() { word.to_string() } else { format!("{line} {word}") }; - if !line.is_empty() && t.family.width(&candidate, t.size, t.bold) > width { - out.push(std::mem::take(&mut line)); + let candidate_w = if arabic { if line.is_empty() { measure(word) } else { line_w + space + measure(word) } } else { measure(&candidate) }; + if !line.is_empty() && candidate_w > width { + out.push((std::mem::take(&mut line), rtl)); line = word.to_string(); + line_w = measure(word); } else { line = candidate; + line_w = candidate_w; } } - out.push(line); + out.push((line, rtl)); } out } +/// Arabic letters, marks, digits and punctuation: an item with any of them is shaped with the +/// craft-fonts Arabic face. +fn is_arabic(c: char) -> bool { + matches!(u32::from(c), 0x0600..=0x06FF | 0x0750..=0x077F | 0x0870..=0x08FF | 0xFB50..=0xFDFF | 0xFE70..=0xFEFC) +} + +/// The first character of `t` that would be drawn as `?`: outside WinAnsiEncoding and not drawn +/// with the craft-fonts Arabic face (#125), or Arabic that the face can't draw (or that has no +/// face to draw it). Page-content tools refuse such text; the Add-text editor keeps it open. +pub fn first_undrawable(t: &AddedText) -> Option { + // Arabic that nothing can draw: there's no face, or the face lacks it. + if let Some(c) = t.text.chars().find(|c| shaped_arabic(*c) && !pdfcraft_fonts::arabic_has(*c)) { + return Some(c); + } + if pdfcraft_fonts::document_arabic_font().is_none() || !t.text.chars().any(is_arabic) { + // Drawn line by line in the item's standard font (`\n` splits lines). Without the face + // that is also how `draw` redraws an existing item, so it's checked that way. + return t.text.split('\n').find_map(pdfcraft_fonts::first_non_win_ansi); + } + // What `arabic_layout` leaves to the standard font, exactly as `draw_arabic` splits it. + wrapped(t).into_iter().find_map(|(line, rtl)| { + let (pieces, _) = arabic_layout(t, &line, rtl).ok()?; + pieces.iter().find_map(|p| match p { + Piece::Latin(s) => pdfcraft_fonts::first_non_win_ansi(s), + Piece::Arabic(_) => None, + }) + }) +} + +/// An Arabic character the face shapes and draws (U+061C, a direction mark, is dropped). +fn shaped_arabic(c: char) -> bool { + is_arabic(c) && !is_bidi_control(c) +} + +/// Direction marks, embeddings, overrides and isolates: they steer the order and are not drawn. +fn is_bidi_control(c: char) -> bool { + matches!(c, '\u{061C}' | '\u{200E}' | '\u{200F}' | '\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}') +} + +fn mirrored(c: char) -> char { + match c { + '(' => ')', + ')' => '(', + '[' => ']', + ']' => '[', + '{' => '}', + '}' => '{', + '<' => '>', + '>' => '<', + '«' => '»', + '»' => '«', + other => other, + } +} + +/// A piece of a line with Arabic text, in drawing order. +enum Piece { + /// Shown in the item's own standard font. + Latin(String), + Arabic(Vec), +} + +/// A line of an Arabic item in drawing order (the Unicode bidirectional algorithm, UAX #9), and +/// its width at the item's size. `rtl` is its paragraph's direction. +fn arabic_layout(t: &AddedText, line: &str, rtl: bool) -> Result<(Vec, f64), EditError> { + // unicode-bidi indexes the first level of a line, so an empty one would panic. + if line.is_empty() { + return Ok((Vec::new(), 0.0)); + } + let base = if rtl { Level::rtl() } else { Level::ltr() }; + let info = ParagraphBidiInfo::new(line, Some(base)); + let (levels, runs) = info.visual_runs(0..line.len()); + let mut pieces = Vec::new(); + for run in runs { + let run_rtl = levels.get(run.start).is_some_and(Level::is_rtl); + // Runs lie on character boundaries; a missing slice is skipped, never a panic. + let Some(text) = line.get(run) else { continue }; + // Arabic letters take the Arabic face; so do spaces and punctuation between them, which the + // shaper then mirrors. Latin text (and what the face lacks) keeps the item's font. + let mut segments: Vec<(bool, String)> = Vec::new(); + for c in text.chars().filter(|c| !is_bidi_control(*c)) { + let arabic = is_arabic(c) || (run_rtl && !c.is_alphanumeric() && pdfcraft_fonts::arabic_has(c)); + match segments.last_mut() { + Some((kind, s)) if *kind == arabic => s.push(c), + _ => segments.push((arabic, c.to_string())), + } + } + if run_rtl { + segments.reverse(); + } + for (arabic, s) in segments { + pieces.push(if arabic { + Piece::Arabic(shape_arabic(&s, run_rtl).map_err(|e| arabic_error(e, &s))?) + } else if run_rtl { + Piece::Latin(s.chars().rev().map(mirrored).collect()) + } else { + Piece::Latin(s) + }); + } + } + let width = pieces + .iter() + .map(|p| match p { + Piece::Latin(s) => t.family.width(s, t.size, t.bold), + Piece::Arabic(clusters) => clusters.iter().map(|c| c.advance).sum::() * t.size, + }) + .sum(); + Ok((pieces, width)) +} + +/// The width of `s` in an Arabic item; the item's font's estimate if it can't be shaped (drawing +/// then reports why). +fn arabic_width(t: &AddedText, s: &str, rtl: bool) -> f64 { + arabic_layout(t, s, rtl).map(|(_, w)| w).unwrap_or_else(|_| t.family.width(s, t.size, t.bold)) +} + +fn arabic_error(e: GlyphError, text: &str) -> EditError { + EditError::Invalid(match e { + GlyphError::NoFont => "Arabic text needs PdfCraft's Arabic font, which this build doesn't include \ + (to build it in, set CRAFT_FONTS_DIR to a craft-fonts checkout that has an Arab face)" + .into(), + GlyphError::Missing => format!("the Arabic font can't show \"{text}\""), + GlyphError::TooComplex => format!("the Arabic font's glyphs for \"{text}\" are too complex"), + }) +} + /// The box a text item occupies (height from its lines). pub fn text_rect(t: &AddedText) -> [f64; 4] { let r = norm(t.rect); @@ -224,8 +370,196 @@ fn font_name(base: &str) -> String { format!("PCF{}", base.replace('-', "")) } -/// Content and resources for an item; `view` maps display space to user space. -fn draw(doc: &Document, c: &Content, view: [f64; 6]) -> Result<(Vec, Dict), EditError> { +/// Most glyphs in one Type 3 font: its codes are single bytes, 1–240. +const GLYPHS_PER_FONT: usize = 240; +/// Most Type 3 fonts one item may use, which bounds the objects it adds (ordinary text needs one +/// or two). +const MAX_ARABIC_FONTS: usize = 16; + +/// Draw a text item that has Arabic in it. Each line is laid out in display order; Arabic glyphs +/// come from Type 3 fonts drawn from the craft-fonts face's outlines (no font file is embedded), +/// with a ToUnicode map for copy and search. Other text uses the item's standard font `latin`. +fn draw_arabic(doc: &mut Document, t: &AddedText, latin: &str, taken: &Dict, fonts: &mut Dict, out: &mut Vec) -> Result<(), EditError> { + let r = text_rect(t); + let lines = wrapped(t); + let laid = lines.iter().map(|(line, rtl)| arabic_layout(t, line, *rtl)).collect::, _>>()?; + // Every distinct cluster (a letter with its dots and the text it stands for) is one glyph of + // one of the fonts, so copy and search get each character exactly once. + let mut keys: Vec<&ShapedCluster> = Vec::new(); + let mut index: HashMap = HashMap::new(); + for c in laid.iter().flat_map(|(pieces, _)| pieces).flat_map(|p| match p { + Piece::Arabic(clusters) => clusters.as_slice(), + Piece::Latin(_) => &[], + }) { + index.entry(cluster_key(c)).or_insert_with(|| { + keys.push(c); + keys.len() - 1 + }); + } + if keys.len() > GLYPHS_PER_FONT * MAX_ARABIC_FONTS { + return Err(EditError::Invalid("the text uses too many different Arabic glyphs; split it into several boxes".into())); + } + // Every outline is read before anything is written, so a failure leaves the document as it was. + let mut outlines = HashMap::new(); + for c in &keys { + for (id, _) in &c.glyphs { + if !outlines.contains_key(id) { + outlines.insert(*id, arabic_glyph(*id).map_err(|e| arabic_error(e, &c.text))?); + } + } + } + let mut names = Vec::new(); + for chunk in keys.chunks(GLYPHS_PER_FONT) { + let font = type3_arabic(doc, chunk, &outlines); + // A name no font on the page has, so items never replace each other's fonts (object + // numbers alone don't do: a full save renumbers objects but keeps resource names). + let mut name = format!("PCAr{}", font.num); + let mut suffix = 0usize; + while taken.contains(name.as_bytes()) || fonts.contains(name.as_bytes()) { + suffix = suffix.saturating_add(1); + name = format!("PCAr{}_{suffix}", font.num); + } + fonts.set(name.clone().into_bytes(), Object::Ref(font)); + names.push(name); + } + let [cr, cg, cb] = t.color.map(|v| v.clamp(0.0, 1.0)); + out.extend(format!("BT {} {} {} rg\n", n(cr), n(cg), n(cb)).bytes()); + for (i, ((line, rtl), (pieces, w))) in lines.iter().zip(&laid).enumerate() { + let spaces = line.matches(' ').count(); + // Justified: the spaces of every line but the last stretch to fill the box. + let tw = if t.align == Align::Justify && i + 1 < lines.len() && spaces > 0 { ((r[2] - r[0]) - w).max(0.0) / spaces as f64 } else { 0.0 }; + let x = match t.align { + // A right-to-left paragraph's unstretched last line ends at the right edge. + Align::Justify if *rtl => r[2] - w - tw * spaces as f64, + Align::Left | Align::Justify => r[0], + Align::Center => r[0] + ((r[2] - r[0]) - w) / 2.0, + Align::Right => r[2] - w, + }; + let y = r[3] - (i as f64 * 1.2 + 0.95) * t.size; + let mut pen = x; + let mut current: Option<&str> = None; + for piece in pieces { + match piece { + Piece::Latin(s) => { + out.extend(format!("/{latin} {} Tf {} Tw 1 0 0 1 {} {} Tm ", n(t.size), n(tw), n(pen), n(y)).bytes()); + out.extend(literal(&win_ansi(s))); + out.extend_from_slice(b" Tj\n"); + current = Some(latin); + pen += t.family.width(s, t.size, t.bold) + tw * s.matches(' ').count() as f64; + } + Piece::Arabic(clusters) => { + for c in clusters { + let Some(&k) = index.get(&cluster_key(c)) else { continue }; + let Some(name) = names.get(k / GLYPHS_PER_FONT) else { continue }; + // Codes start at 1; k % 240 + 1 is at most 240. + let code = u8::try_from(k % GLYPHS_PER_FONT + 1).unwrap_or(1); + if current != Some(name.as_str()) { + out.extend(format!("/{name} {} Tf ", n(t.size)).bytes()); + current = Some(name.as_str()); + } + out.extend(format!("1 0 0 1 {} {} Tm ", n(pen), n(y)).bytes()); + out.extend(literal(&[code])); + out.extend_from_slice(b" Tj\n"); + pen += c.advance * t.size + if c.text == " " { tw } else { 0.0 }; + } + } + } + } + } + out.extend_from_slice(b"ET\n"); + Ok(()) +} + +/// What makes two clusters the same Type 3 glyph: text, glyphs, offsets and advance (bit for bit; +/// equal clusters come from the same font units). +type ClusterKey<'a> = (&'a str, Vec<(u32, u64, u64)>, u64); + +fn cluster_key(c: &ShapedCluster) -> ClusterKey<'_> { + (c.text.as_str(), c.glyphs.iter().map(|(id, [x, y])| (*id, x.to_bits(), y.to_bits())).collect(), c.advance.to_bits()) +} + +/// A Type 3 font with `clusters` as codes 1…, each drawn from the Arabic face's glyph `outlines`. +fn type3_arabic(doc: &mut Document, clusters: &[&ShapedCluster], outlines: &HashMap) -> ObjRef { + let mut charprocs = Dict::new(); + let mut differences = vec![Object::Int(1)]; + let mut widths = Vec::with_capacity(clusters.len()); + let mut bbox = [0.0f64; 4]; + let mut to_unicode = Vec::new(); + for (i, c) in clusters.iter().enumerate() { + let code = i + 1; + // Glyph space is 1000 units per em. + let mut paths = Vec::new(); + let mut ink: Option<[f64; 4]> = None; + for (id, [dx, dy]) in &c.glyphs { + let Some(g) = outlines.get(id) else { continue }; + for contour in &g.contours { + for (j, p) in contour.iter().enumerate() { + paths.extend(format!("{} {} {}\n", n((p[0] + dx) * 1000.0), n((p[1] + dy) * 1000.0), if j == 0 { "m" } else { "l" }).bytes()); + } + paths.extend_from_slice(b"h\n"); + let b = [g.bbox[0] + dx, g.bbox[1] + dy, g.bbox[2] + dx, g.bbox[3] + dy]; + ink = Some(ink.map_or(b, |a| [a[0].min(b[0]), a[1].min(b[1]), a[2].max(b[2]), a[3].max(b[3])])); + } + } + // Rounded outwards so no ink is clipped. + let [x0, y0, x1, y1] = + ink.map_or([0.0; 4], |b| [(b[0] * 1000.0).floor(), (b[1] * 1000.0).floor(), (b[2] * 1000.0).ceil(), (b[3] * 1000.0).ceil()]); + bbox = [bbox[0].min(x0), bbox[1].min(y0), bbox[2].max(x1), bbox[3].max(y1)]; + let mut proc = format!("{} 0 {} {} {} {} d1\n", n(c.advance * 1000.0), n(x0), n(y0), n(x1), n(y1)).into_bytes(); + if !paths.is_empty() { + proc.extend(paths); + proc.extend_from_slice(b"f\n"); + } + let glyph = format!("g{code:02X}"); + charprocs.set(glyph.clone().into_bytes(), Object::Ref(doc.add(Object::Stream(Stream::flate(Dict::new(), &proc))))); + differences.push(Object::name(&glyph)); + widths.push(Object::Real((c.advance * 1000.0).round())); + if !c.text.is_empty() { + let hex: String = c.text.encode_utf16().map(|u| format!("{u:04X}")).collect(); + to_unicode.push(format!("<{code:02X}> <{hex}>\n")); + } + } + let mut cmap = String::from( + "/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n/CMapType 2 def\n1 begincodespacerange\n<01> \nendcodespacerange\n", + ); + // A CMap block holds at most 100 entries. + for block in to_unicode.chunks(100) { + cmap.push_str(&format!("{} beginbfchar\n{}endbfchar\n", block.len(), block.concat())); + } + cmap.push_str("endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n"); + let cmap = doc.add(Object::Stream(Stream::flate(Dict::new(), cmap.as_bytes()))); + let family = pdfcraft_fonts::document_arabic_font().map_or("Arabic", |f| f.family); + let mut descriptor = Dict::new(); + descriptor.set(b"Type".to_vec(), Object::name("FontDescriptor")); + descriptor.set(b"FontName".to_vec(), Object::name(&family.replace(' ', ""))); + descriptor.set(b"FontFamily".to_vec(), Object::String(PdfString::literal(family.as_bytes().to_vec()))); + descriptor.set(b"Flags".to_vec(), Object::Int(4)); + descriptor.set(b"ItalicAngle".to_vec(), Object::Int(0)); + let mut encoding = Dict::new(); + encoding.set(b"Type".to_vec(), Object::name("Encoding")); + encoding.set(b"Differences".to_vec(), Object::Array(differences)); + let mut font = Dict::new(); + font.set(b"Type".to_vec(), Object::name("Font")); + font.set(b"Subtype".to_vec(), Object::name("Type3")); + font.set(b"FontDescriptor".to_vec(), Object::Ref(doc.add(Object::Dict(descriptor)))); + font.set(b"FontBBox".to_vec(), Object::Array(bbox.iter().map(|v| Object::Real(*v)).collect())); + font.set( + b"FontMatrix".to_vec(), + Object::Array(vec![Object::Real(0.001), Object::Int(0), Object::Int(0), Object::Real(0.001), Object::Int(0), Object::Int(0)]), + ); + font.set(b"FirstChar".to_vec(), Object::Int(1)); + font.set(b"LastChar".to_vec(), Object::Int(clusters.len() as i64)); + font.set(b"Widths".to_vec(), Object::Array(widths)); + font.set(b"Encoding".to_vec(), Object::Dict(encoding)); + font.set(b"CharProcs".to_vec(), Object::Dict(charprocs)); + font.set(b"ToUnicode".to_vec(), Object::Ref(cmap)); + doc.add(Object::Dict(font)) +} + +/// Content and resources for an item; `view` maps display space to user space. `taken` holds the +/// page's font names. `existing`: the item is being replaced, so a build without the Arabic face +/// keeps drawing its Arabic as `?`, as before Arabic was supported, rather than refusing to move it. +fn draw(doc: &mut Document, c: &Content, view: [f64; 6], taken: &Dict, existing: bool) -> Result<(Vec, Dict), EditError> { let mut res = Dict::new(); let mut out = format!("q {} {} {} {} {} {} cm\n", n(view[0]), n(view[1]), n(view[2]), n(view[3]), n(view[4]), n(view[5])).into_bytes(); match c { @@ -241,6 +575,12 @@ fn draw(doc: &Document, c: &Content, view: [f64; 6]) -> Result<(Vec, Dict), } let mut fonts = Dict::new(); fonts.set(name.clone().into_bytes(), Object::Dict(font)); + if t.text.chars().any(is_arabic) && (pdfcraft_fonts::document_arabic_font().is_some() || !existing) { + draw_arabic(doc, t, &name, taken, &mut fonts, &mut out)?; + res.set(b"Font".to_vec(), Object::Dict(fonts)); + out.extend_from_slice(b"Q\n"); + return Ok((out, res)); + } res.set(b"Font".to_vec(), Object::Dict(fonts)); let r = text_rect(t); let [cr, cg, cb] = t.color.map(|v| v.clamp(0.0, 1.0)); @@ -394,6 +734,17 @@ fn validate(c: &Content) -> Result<(), EditError> { if (r[2] - r[0]).abs() < 1.0 { return Err(EditError::Invalid("the text box is too narrow".into())); } + // Without the Arabic face, `draw` decides for the Arabic letters themselves (#403): a new + // item is refused with what's missing, and one that already holds Arabic stays movable. + // Everything else in it is drawn in the standard font, so it still has to fit. + let undrawable = if t.text.chars().any(shaped_arabic) && pdfcraft_fonts::document_arabic_font().is_none() { + t.text.split('\n').find_map(|line| line.chars().find(|c| !shaped_arabic(*c) && !pdfcraft_fonts::win_ansi_encodable(*c))) + } else { + first_undrawable(t) + }; + if let Some(c) = undrawable { + return Err(crate::undrawable(c)); + } } Content::Image(_) => { if (r[2] - r[0]).abs() < 1.0 || (r[3] - r[1]).abs() < 1.0 { @@ -410,9 +761,15 @@ fn write(doc: &mut Document, page: usize, c: &Content, obj: Option) -> R let all = page_list(doc); check(&[page], all.len())?; let p = &all[page]; - let (content, res) = draw(doc, c, p.view_matrix(doc))?; - // The page gets its own copy of its resources with the item's fonts and images added. + let view = p.view_matrix(doc); + // The page gets its own copy of its resources with the item's fonts and images added, and + // without the Arabic fonts of the version it replaces. let mut pres = p.dict.get(b"Resources").map(|r| doc.resolve(r)).and_then(|r| r.as_dict().cloned()).unwrap_or_default(); + if let Some(r) = obj { + drop_fonts(doc, &mut pres, &own_fonts(doc, r)); + } + let taken = pres.get(b"Font").map(|f| doc.resolve(f)).and_then(|f| f.as_dict().cloned()).unwrap_or_default(); + let (content, res) = draw(doc, c, view, &taken, obj.is_some())?; for (k, v) in res.iter() { let mut sub = pres.get(k).map(|s| doc.resolve(s)).and_then(|s| s.as_dict().cloned()).unwrap_or_default(); for (n2, o) in v.as_dict().into_iter().flat_map(|d| d.iter()) { @@ -497,6 +854,40 @@ pub fn delete_content(doc: &mut Document, page: usize, index: usize) -> Result<( let a = find(doc, page, index)?; let p = page_list(doc).swap_remove(page); let list: Vec = contents(doc, &p)?.into_iter().filter(|o| o.as_ref() != Some(a.obj)).collect(); - doc.update_dict(p.obj, |d| d.set(b"Contents".to_vec(), Object::Array(list)))?; + let fonts = own_fonts(doc, a.obj); + // Only the page's own resources carry an item's fonts (`write` gives the page a copy). + let res = match p.dict.get(b"Resources") { + Some(Object::Dict(d)) if !fonts.is_empty() => Some(d.clone()), + _ => None, + }; + let res = res.map(|mut r| { + drop_fonts(doc, &mut r, &fonts); + r + }); + doc.update_dict(p.obj, |d| { + d.set(b"Contents".to_vec(), Object::Array(list)); + if let Some(r) = res { + d.set(b"Resources".to_vec(), Object::Dict(r)); + } + })?; Ok(()) } + +/// The Type 3 fonts an Arabic item's stream shows text with. Each item has its own (`draw_arabic` +/// names them so), so they go when the item is replaced or deleted. +fn own_fonts(doc: &Document, obj: ObjRef) -> Vec> { + let Object::Stream(s) = &*doc.get(obj) else { return Vec::new() }; + let Ok(data) = s.decoded() else { return Vec::new() }; + data.split(u8::is_ascii_whitespace).filter_map(|w| w.strip_prefix(b"/")).filter(|w| w.starts_with(b"PCAr")).map(<[u8]>::to_vec).collect() +} + +fn drop_fonts(doc: &Document, res: &mut Dict, names: &[Vec]) { + if names.is_empty() { + return; + } + let Some(mut fonts) = res.get(b"Font").map(|f| doc.resolve(f)).and_then(|f| f.as_dict().cloned()) else { return }; + for name in names { + fonts.remove(name); + } + res.set(b"Font".to_vec(), Object::Dict(fonts)); +} diff --git a/crates/edit/src/images.rs b/crates/edit/src/images.rs index ff6f5494..7f3a2a69 100644 --- a/crates/edit/src/images.rs +++ b/crates/edit/src/images.rs @@ -1,4 +1,4 @@ -//! Edit a PDF ▸ edit existing images: the images a page draws (image XObjects painted by `Do` +//! Edit a PDF ▸ edit existing images and grouped artwork (Image/Form XObjects painted by `Do` //! in its own content streams), and changing one: move/resize/rotate/flip (the `Do` is wrapped in //! `q … cm … Q` with the extra transform), replace (it draws another XObject in the same place), //! or delete. Only the stream that draws it is rewritten, as a new object. @@ -13,16 +13,20 @@ use crate::EditError; pub struct PageImage { /// Its box in user space. pub rect: [f64; 4], - /// The placement: the unit square → user space. + /// The placement: image unit square or Form coordinates → user space. pub matrix: [f64; 6], /// The XObject resource name and object. pub name: String, pub object: Option, - /// Pixel size. + /// Whether this is grouped Form artwork, edited as a whole (including its nested content). + pub is_form: bool, + /// Pixel size; zero for vector/grouped Form artwork. pub width: u32, pub height: u32, stream: usize, op: usize, + /// CTM before `Do`, excluding a Form's own Matrix. + draw_matrix: [f64; 6], } fn streams(doc: &Document, page: &Dict) -> Vec<(Object, Vec)> { @@ -55,7 +59,8 @@ fn xobjects(doc: &Document, page: &Dict) -> Dict { .unwrap_or_default() } -/// The images page `page` (0-based) draws, in drawing order. +/// The images and Form artwork page `page` (0-based) draws, in drawing order. +/// Forms stay intact: no traversal or rewriting of their internal objects is needed. pub fn page_images(doc: &Document, page: usize) -> Result, EditError> { let p = page_of(doc, page)?; let xo = xobjects(doc, &p.dict); @@ -75,21 +80,59 @@ pub fn page_images(doc: &Document, page: usize) -> Result, EditEr } b"Do" => { let Some(name) = op.name(0) else { continue }; - let Some(r) = xo.get(name).and_then(Object::as_ref) else { continue }; - let obj = doc.get(r); + let Some(resource) = xo.get(name) else { continue }; + let obj = doc.resolve(resource); let Object::Stream(s) = &*obj else { continue }; - if s.dict.name(b"Subtype") != Some(b"Image") { + let is_form = s.dict.name(b"Subtype") == Some(b"Form"); + let (matrix, bounds) = if is_form { + let Some(bounds) = s.dict.get(b"BBox").map(|v| doc.resolve(v)).and_then(|v| { + v.as_array().and_then(|a| { + if a.len() != 4 { + return None; + } + let mut b = [0.0; 4]; + for (n, v) in b.iter_mut().zip(a) { + *n = v.as_f64()?; + } + Some(b) + }) + }) else { + continue; + }; + let matrix = match s.dict.get(b"Matrix") { + Some(v) => { + let v = doc.resolve(v); + let Some(m) = v.as_array().and_then(|a| Matrix::from_operands(a)) else { continue }; + m + } + None => Matrix::IDENTITY, + }; + (matrix.then(&ctm), bounds) + } else if s.dict.name(b"Subtype") == Some(b"Image") { + (ctm, [0.0, 0.0, 1.0, 1.0]) + } else { + continue; + }; + let rect = matrix.bbox(bounds); + if !bounds.iter().chain(matrix.0.iter()).chain(rect.iter()).all(|v| v.is_finite()) + || bounds[2] <= bounds[0] + || bounds[3] <= bounds[1] + || rect[2] <= rect[0] + || rect[3] <= rect[1] + { continue; } out.push(PageImage { - rect: ctm.bbox([0.0, 0.0, 1.0, 1.0]), - matrix: ctm.0, + rect, + matrix: matrix.0, name: String::from_utf8_lossy(name).into_owned(), - object: Some(r), - width: s.dict.int(b"Width").unwrap_or(0).max(0) as u32, - height: s.dict.int(b"Height").unwrap_or(0).max(0) as u32, + object: resource.as_ref(), + is_form, + width: if is_form { 0 } else { s.dict.int(b"Width").unwrap_or(0).clamp(0, u32::MAX as i64) as u32 }, + height: if is_form { 0 } else { s.dict.int(b"Height").unwrap_or(0).clamp(0, u32::MAX as i64) as u32 }, stream: si, op: i, + draw_matrix: ctm.0, }); } _ => {} @@ -137,24 +180,31 @@ pub fn change_image(doc: &mut Document, page: usize, index: usize, change: &Imag let img = images.get(index).cloned().ok_or_else(|| EditError::Invalid(format!("page {} has no image {}", page + 1, index + 1)))?; let p = page_of(doc, page)?; let all = streams(doc, &p.dict); - let (obj, data) = all[img.stream].clone(); + let (obj, data) = all.get(img.stream).cloned().ok_or_else(|| EditError::Invalid("the image stream is missing".into()))?; let mut ops = parse(&data).ops; + let original = ops.get(img.op).cloned().ok_or_else(|| EditError::Invalid("the image operation is missing".into()))?; let mut resources = None; let replacement: Vec = match change { ImageChange::Delete => Vec::new(), ImageChange::Transform(t) => { + if !t.iter().all(|v| v.is_finite()) { + return Err(EditError::Invalid("the image transform must be finite".into())); + } // New CTM = placement × T; as a `cm` before the Do: M = P · T · P⁻¹. - let pm = Matrix(img.matrix); + let pm = Matrix(img.draw_matrix); let inv = pm.invert().ok_or_else(|| EditError::Invalid("the image has no area".into()))?; let m = pm.then(&Matrix(*t)).then(&inv); - vec![ - Op::new("q", vec![]), - Op::new("cm", m.0.iter().map(|v| pdfcraft_content::num(*v)).collect()), - ops[img.op].clone(), - Op::new("Q", vec![]), - ] + if !m.0.iter().all(|v| v.is_finite()) { + return Err(EditError::Invalid("the image transform is too large".into())); + } + vec![Op::new("q", vec![]), Op::new("cm", m.0.iter().map(|v| pdfcraft_content::num(*v)).collect()), original, Op::new("Q", vec![])] } ImageChange::Replace(r) => { + if img.is_form { + return Err(EditError::Invalid( + "grouped Form artwork can be moved, resized, rotated, flipped or deleted; replacing it with a bitmap is not supported".into(), + )); + } let name = format!("PCImg{}", r.num); let mut res = p.dict.get(b"Resources").map(|x| doc.resolve(x)).and_then(|x| x.as_dict().cloned()).unwrap_or_default(); let mut xo = res.get(b"XObject").map(|x| doc.resolve(x)).and_then(|x| x.as_dict().cloned()).unwrap_or_default(); @@ -173,7 +223,7 @@ pub fn change_image(doc: &mut Document, page: usize, index: usize, change: &Imag let new = doc.add(Object::Stream(Stream::flate(dict, &serialize_ops(&ops)))); let contents: Vec = all.iter().enumerate().map(|(i, (o, _))| if i == img.stream { Object::Ref(new) } else { o.clone() }).collect(); doc.update_dict(p.obj, |d| { - d.set(b"Contents".to_vec(), if contents.len() == 1 { contents[0].clone() } else { Object::Array(contents) }); + d.set(b"Contents".to_vec(), if let [one] = contents.as_slice() { one.clone() } else { Object::Array(contents) }); if let Some(res) = resources { d.set(b"Resources".to_vec(), Object::Dict(res)); } diff --git a/crates/edit/src/lib.rs b/crates/edit/src/lib.rs index 7950184e..93df6829 100644 --- a/crates/edit/src/lib.rs +++ b/crates/edit/src/lib.rs @@ -375,6 +375,24 @@ fn begin(kind: MarkKind, subtype: &str, matrix: [f64; 6]) -> String { const END: &str = "EMC\nQ\n"; +/// Refuses page text the standard-14 fonts (WinAnsiEncoding) can't draw: written anyway, each +/// such character would become a `?` on the page (#125). `lines` must split the text the way the +/// caller draws it, so a `\r` that would be drawn is refused too. +pub(crate) fn drawable<'a>(lines: impl IntoIterator) -> Result<(), EditError> { + match lines.into_iter().find_map(pdfcraft_fonts::first_non_win_ansi) { + Some(c) => Err(undrawable(c)), + None => Ok(()), + } +} + +/// The refusal for page text with `c`, which nothing here can draw. +pub(crate) fn undrawable(c: char) -> EditError { + EditError::Invalid(format!( + "the standard fonts can't draw \"{c}\" (U+{:04X}); only Western European characters can be added as text for now", + u32::from(c) + )) +} + fn text_op(x: f64, y: f64, text: &str) -> Vec { let mut v = format!("1 0 0 1 {} {} Tm ", n(x), n(y)).into_bytes(); v.extend(literal(&win_ansi(text))); @@ -390,6 +408,9 @@ pub fn add_header_footer(doc: &mut Document, pages: &[usize], hf: &HeaderFooter, if hf.text.iter().all(|t| t.trim().is_empty()) { return Err(EditError::Invalid("type the header or footer text first".into())); } + // Tokens only add ASCII (Bates prefixes and suffixes are part of the template), so checking + // the templates, split as `expand`'s output is, covers every page. + hf.text.iter().try_for_each(|t| drawable(t.lines()))?; if !(hf.font_size.is_finite() && hf.font_size > 0.0 && hf.font_size <= 200.0 && hf.margins.iter().all(|m| m.is_finite() && *m >= 0.0)) { return Err(EditError::Invalid("invalid font size or margins".into())); } @@ -452,6 +473,9 @@ pub fn add_watermark(doc: &mut Document, pages: &[usize], wm: &Watermark, replac if text.is_empty() && wm.source.is_none() { return Err(EditError::Invalid("type the watermark text first".into())); } + if wm.source.is_none() { + drawable(text.lines())?; + } if !(wm.opacity.is_finite() && wm.rotation.is_finite() && wm.font_size.is_finite() && wm.font_size >= 0.0) { return Err(EditError::Invalid("invalid watermark settings".into())); } @@ -594,7 +618,7 @@ pub use images::{ImageChange, PageImage, change_image, page_images, rect_to_rect pub mod text; pub use text::{BlockStyle, LineEdit, TextBlock, TextLine, replace_block, replace_line, rewrite_block, text_blocks, text_lines}; pub mod added; -pub use added::{Added, AddedImage, AddedText, Align, Content, Family, add_content, delete_content, list_added, update_content}; +pub use added::{Added, AddedImage, AddedText, Align, Content, Family, add_content, delete_content, first_undrawable, list_added, update_content}; #[cfg(test)] mod tests; diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index a48e9647..2d7cfd1d 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -227,6 +227,142 @@ fn added_text_and_images_are_page_content_that_stays_editable() { assert!(marks_present(&doc).is_empty()); } +#[test] +fn added_arabic_text_is_shaped_in_display_order_and_stays_searchable() { + let mut doc = fixture(); + let text = + AddedText { rect: [72.0, 600.0, 400.0, 700.0], text: "مرحبا World".into(), size: 14.0, align: Align::Right, ..AddedText::default() }; + if pdfcraft_fonts::document_arabic_font().is_none() { + eprintln!("skipping the Arabic drawing checks: built without a craft-fonts Arab face (set CRAFT_FONTS_DIR)"); + let err = add_content(&mut doc, 0, &Content::Text(text)).unwrap_err(); + assert!(matches!(&err, EditError::Invalid(m) if m.contains("CRAFT_FONTS_DIR")), "{err:?}"); + assert!(!doc.is_modified(), "nothing is written when the font is missing"); + return; + } + add_content(&mut doc, 0, &Content::Text(text.clone())).unwrap(); + // A second Arabic item on the same page keeps its own fonts. + add_content(&mut doc, 0, &Content::Text(AddedText { text: "شكرا".into(), rect: [72.0, 500.0, 400.0, 520.0], ..text.clone() })).unwrap(); + let doc = reopen(&doc); + let added = list_added(&doc); + let Content::Text(t) = &added[0].content else { panic!() }; + assert_eq!(t.text, "مرحبا World", "the logical text is kept for editing"); + let s = streams(&doc, 0); + let item = s.iter().find(|s| s.contains("(World) Tj")).unwrap(); + // A right-to-left paragraph: "World" is drawn left of the Arabic word. + let x_before = |s: &str, at: usize| -> f64 { + let tm = s[..at].rfind("1 0 0 1 ").unwrap() + 8; + s[tm..].split(' ').next().unwrap().parse().unwrap() + }; + let latin_x = x_before(item, item.find("(World) Tj").unwrap()); + let arabic_at = item.find("/PCAr").unwrap(); + let arabic_x = x_before(item, item[arabic_at..].find(" Tj").unwrap() + arabic_at); + assert!(latin_x < arabic_x, "{item}"); + // The Arabic glyphs come from Type 3 fonts whose ToUnicode maps give back each letter. + let p = &pdfcraft_model::pages(&doc)[0]; + let res = doc.resolve(p.dict.get(b"Resources").unwrap()); + let fonts = doc.resolve(res.as_dict().unwrap().get(b"Font").unwrap()).as_dict().unwrap().clone(); + let arabic: Vec<_> = fonts.iter().filter(|(k, _)| k.starts_with(b"PCAr")).map(|(_, v)| doc.resolve(v).as_dict().unwrap().clone()).collect(); + assert_eq!(arabic.len(), 2, "one font per item"); + let mut mapped = String::new(); + for f in &arabic { + assert_eq!(f.name(b"Subtype"), Some(&b"Type3"[..])); + mapped += &String::from_utf8_lossy(&stream_bytes(&doc, f.get(b"ToUnicode").unwrap()).unwrap()); + } + for letter in ["0645", "0631", "062D", "0628", "0627", "0634", "0643"] { + assert!(mapped.contains(&format!("<{letter}>")), "{letter} in {mapped}"); + } +} + +/// The `PCAr` font names in page `page`'s resources. +fn arabic_font_names(doc: &Document, page: usize) -> Vec { + let p = &pdfcraft_model::pages(doc)[page]; + let Some(res) = p.dict.get(b"Resources").map(|r| doc.resolve(r)) else { return vec![] }; + let Some(fonts) = res.as_dict().and_then(|r| r.get(b"Font")).map(|f| doc.resolve(f)) else { return vec![] }; + fonts.as_dict().unwrap().iter().map(|(k, _)| String::from_utf8_lossy(k).into_owned()).filter(|k| k.starts_with("PCAr")).collect() +} + +#[test] +fn arabic_items_keep_their_own_fonts_through_saves_updates_and_deletes() { + let mut doc = fixture(); + let text = AddedText { rect: [72.0, 600.0, 400.0, 700.0], text: "مرحبا".into(), size: 14.0, ..AddedText::default() }; + if pdfcraft_fonts::document_arabic_font().is_none() { + // An item that already holds Arabic (stored by an older version, drawn as "?") can still + // be moved and retyped without the face. + add_content(&mut doc, 0, &Content::Text(AddedText { text: "x".into(), ..text.clone() })).unwrap(); + update_content(&mut doc, 0, 0, &Content::Text(text)).unwrap(); + assert!(streams(&doc, 0).join("").contains("(?????) Tj")); + return; + } + // A full save renumbers objects but keeps resource names, so a font named after its object + // number may meet a name already on the page: here, every name it could take. + let mut taken = Dict::new(); + for num in 1..2_000 { + taken.set(format!("PCAr{num}").into_bytes(), Object::name("Taken")); + } + let mut res = Dict::new(); + res.set(b"Font".to_vec(), Object::Dict(taken)); + let page = pdfcraft_model::pages(&doc)[0].obj; + doc.update_dict(page, |d| d.set(b"Resources".to_vec(), Object::Dict(res))).unwrap(); + add_content(&mut doc, 0, &Content::Text(text.clone())).unwrap(); + add_content(&mut doc, 0, &Content::Text(AddedText { text: "شكرا".into(), rect: [72.0, 500.0, 400.0, 520.0], ..text.clone() })).unwrap(); + let names = arabic_font_names(&doc, 0); + assert_eq!(names.len(), 1_999 + 2, "nothing replaced"); + let p = &pdfcraft_model::pages(&doc)[0]; + let fonts = doc.resolve(p.dict.get(b"Resources").unwrap()).as_dict().unwrap().get(b"Font").cloned().unwrap(); + let fonts = doc.resolve(&fonts).as_dict().unwrap().clone(); + assert_eq!(fonts.iter().filter(|(_, v)| v.as_name() == Some(&b"Taken"[..])).count(), 1_999); + let first = own(&doc, 0); + assert!(first.len() == 1 && first[0].contains('_'), "a free name, not a taken one: {first:?}"); + // Updating an item swaps its fonts; deleting it removes them. + update_content(&mut doc, 0, 0, &Content::Text(AddedText { text: "أهلا".into(), ..text })).unwrap(); + let names = arabic_font_names(&doc, 0); + assert!(names.len() == 2_001 && !names.contains(&first[0]), "the replaced item's font is gone"); + delete_content(&mut doc, 0, 0).unwrap(); + delete_content(&mut doc, 0, 0).unwrap(); + assert_eq!(arabic_font_names(&doc, 0).len(), 1_999, "only the page's own names are left"); + reopen(&doc); +} + +/// The `PCAr` fonts the stream of added item `index` on page 0 shows text with. +fn own(doc: &Document, index: usize) -> Vec { + let s = String::from_utf8_lossy(&stream_bytes(doc, &Object::Ref(list_added(doc)[index].obj)).unwrap()).into_owned(); + let mut names: Vec = + s.split_whitespace().filter_map(|w| w.strip_prefix('/')).filter(|w| w.starts_with("PCAr")).map(String::from).collect(); + names.dedup(); + names +} + +#[test] +fn odd_arabic_text_never_panics() { + let mut doc = fixture(); + let long = "بسم الله ".repeat(2_000); + for s in ["\u{202E}ب\u{064B}\u{064B} (]", "\u{064B}", "ا\u{200F}\u{2067}b\u{2069}", "ا\n\nب\tc", "ﷺ ١٢٣ 456", long.as_str()] { + let r = add_content(&mut doc, 0, &Content::Text(AddedText { rect: [0.0, 800.0, 300.0, 780.0], text: s.into(), ..AddedText::default() })); + assert_eq!(r.is_ok(), pdfcraft_fonts::document_arabic_font().is_some(), "{:?}: {r:?}", s.chars().take(12).collect::()); + } + // U+2029 and U+0085 (paragraph and line separators) have no WinAnsi code, so the standard font + // would draw them as `?`: refused, by name, rather than written (#125). Laying it out still + // mustn't panic. + let r = add_content( + &mut doc, + 0, + &Content::Text(AddedText { rect: [0.0, 800.0, 300.0, 780.0], text: "ا\u{2029}ب\rc\u{85}د\n".into(), ..AddedText::default() }), + ); + assert!(r.is_err(), "{r:?}"); + if pdfcraft_fonts::document_arabic_font().is_some() { + assert!(matches!(&r, Err(EditError::Invalid(m)) if m.contains("U+2029") || m.contains("U+0085")), "{r:?}"); + } + // A character the face lacks is an error that names it, not a box or a crash. + if pdfcraft_fonts::document_arabic_font().is_some() && !pdfcraft_fonts::arabic_has('\u{FDFD}') { + let r = add_content( + &mut doc, + 0, + &Content::Text(AddedText { rect: [0.0, 800.0, 300.0, 780.0], text: "ب \u{FDFD}".into(), ..AddedText::default() }), + ); + assert!(matches!(&r, Err(EditError::Invalid(m)) if m.contains('\u{FDFD}')), "{r:?}"); + } +} + /// One page with Helvetica (WinAnsi) and a subset font that has only the glyphs it uses. fn text_page(content: &str) -> Document { let objs: Vec = vec![ @@ -296,7 +432,7 @@ fn japanese_line_uses_unicode_type3_fallback() { let mut doc = text_page("BT /F2 12 Tf 72 700 Td (ab) Tj ET"); let replacement = "25362738こんにちは、お元気ですか? 2.3444"; let result = text::replace_line(&mut doc, 0, 0, replacement).unwrap(); - assert_eq!(result.substituted.as_deref(), Some("Shippori Mincho Type3")); + assert_eq!(result.substituted.as_deref(), Some("BIZ UDPGothic Type3")); let bytes = page_content_bytes(&doc, 0); let content = String::from_utf8_lossy(&bytes); assert!(content.contains("/PCJp"), "{content}"); @@ -304,6 +440,138 @@ fn japanese_line_uses_unicode_type3_fallback() { assert_eq!(text::text_lines(&reopened, 0).unwrap()[0].text, replacement); } +fn styled_text_page(base: &str) -> Document { + let mut doc = text_page("BT /F1 12 Tf 72 700 Td (Original) Tj ET"); + let r = pdfcraft_cos::ObjRef::new(5, 0); + let mut font = doc.get(r).as_dict().unwrap().clone(); + font.set(b"BaseFont".to_vec(), pdfcraft_cos::Object::name(base)); + doc.set(r, pdfcraft_cos::Object::Dict(font)); + doc +} + +fn fallback_paths(doc: &Document) -> Vec> { + let p = pdfcraft_model::pages(doc).swap_remove(0); + let res = doc.resolve(p.dict.get(b"Resources").unwrap()); + let fonts = doc.resolve(res.as_dict().unwrap().get(b"Font").unwrap()); + let font = doc.resolve(fonts.as_dict().unwrap().get(b"PCJp").unwrap()); + let procs = doc.resolve(font.as_dict().unwrap().get(b"CharProcs").unwrap()); + procs + .as_dict() + .unwrap() + .iter() + .map(|(_, object)| match &*doc.resolve(object) { + pdfcraft_cos::Object::Stream(s) => s.decoded().unwrap(), + other => panic!("expected a glyph stream, got {other:?}"), + }) + .collect() +} + +#[test] +fn japanese_line_fallback_uses_real_source_family_and_weight() { + if without_craft_fonts("japanese_line_fallback_uses_real_source_family_and_weight") { + return; + } + let mut sans = styled_text_page("Helvetica"); + let mut bold = descriptor_text_page(); + let mut serif = styled_text_page("ABCDEF+ShipporiMincho-Regular"); + let replacement = "見本商会 御中"; + text::replace_line(&mut sans, 0, 0, replacement).unwrap(); + text::replace_line(&mut bold, 0, 0, replacement).unwrap(); + text::replace_line(&mut serif, 0, 0, replacement).unwrap(); + assert_ne!(fallback_paths(&sans), fallback_paths(&serif), "sans source must not use Mincho outlines"); + if pdfcraft_fonts::CRAFT_FONTS.iter().any(|f| f.family == "BIZ UDPGothic" && f.style == "Bold") { + assert_ne!(fallback_paths(&sans), fallback_paths(&bold), "bold must change the actual glyph paths"); + } + for doc in [&sans, &bold, &serif] { + assert_eq!(text::text_lines(&reopen(doc), 0).unwrap()[0].text, replacement); + } +} + +#[test] +fn japanese_paragraph_fallback_honors_requested_family_and_weight() { + if without_craft_fonts("japanese_paragraph_fallback_honors_requested_family_and_weight") { + return; + } + let replacement = "見本商会 御中"; + let mut regular = styled_text_page("Helvetica"); + let mut bold = styled_text_page("Helvetica"); + let mut serif = styled_text_page("Helvetica"); + text::replace_block(&mut regular, 0, 0, replacement).unwrap(); + text::rewrite_block( + &mut bold, + 0, + 0, + Some(replacement), + &text::BlockStyle { family: Some((added::Family::Helvetica, true, false)), ..Default::default() }, + ) + .unwrap(); + text::rewrite_block( + &mut serif, + 0, + 0, + Some(replacement), + &text::BlockStyle { family: Some((added::Family::Times, false, false)), ..Default::default() }, + ) + .unwrap(); + assert_ne!(fallback_paths(®ular), fallback_paths(&serif), "requested serif must change actual outlines"); + if pdfcraft_fonts::CRAFT_FONTS.iter().any(|f| f.family == "BIZ UDPGothic" && f.style == "Bold") { + assert_ne!(fallback_paths(®ular), fallback_paths(&bold), "requested bold must change actual outlines"); + let mut source_bold = styled_text_page("Helvetica-Bold"); + text::replace_block(&mut source_bold, 0, 0, replacement).unwrap(); + assert_eq!(fallback_paths(&bold), fallback_paths(&source_bold)); + } + for doc in [®ular, &bold, &serif] { + assert_eq!(text::text_blocks(&reopen(doc), 0).unwrap()[0].text, replacement); + } +} + +#[test] +fn japanese_fallback_reports_the_face_after_save_and_reopen() { + if without_craft_fonts("japanese_fallback_reports_the_face_after_save_and_reopen") { + return; + } + // F2 is (a subset of) Arial: a regular sans face, so the fallback is the regular Gothic. + let face = pdfcraft_fonts::document_japanese_font_for_style(false, false).unwrap(); + let expected = format!("{}-{}", face.family, face.style).replace(' ', ""); + for paragraph in [false, true] { + let mut doc = text_page("BT /F2 12 Tf 72 700 Td (ab) Tj ET"); + let replacement = "見本商会 御中"; + if paragraph { + text::replace_block(&mut doc, 0, 0, replacement).unwrap(); + } else { + text::replace_line(&mut doc, 0, 0, replacement).unwrap(); + } + for d in [&doc, &reopen(&doc)] { + let lines = text::text_lines(d, 0).unwrap(); + assert_eq!(lines[0].text, replacement); + assert_eq!(lines[0].base_font, expected, "report the real fallback face, not an empty BaseFont"); + assert_eq!(text::text_blocks(d, 0).unwrap()[0].base_font, expected); + } + } +} + +#[test] +fn cyrillic_in_a_serif_line_falls_back_to_a_face_that_has_it() { + if without_craft_fonts("cyrillic_in_a_serif_line_falls_back_to_a_face_that_has_it") { + return; + } + let replacement = "Привет, world"; + let has_all = |f: &pdfcraft_fonts::CraftFont| replacement.chars().all(|ch| pdfcraft_fonts::japanese_glyph_from(f, ch).is_ok()); + if !pdfcraft_fonts::CRAFT_FONTS.iter().any(|f| f.covers("Jpan") && has_all(f)) { + eprintln!("skipping: no Japanese fallback face has Cyrillic"); + return; + } + for paragraph in [false, true] { + let mut doc = styled_text_page("Times-Roman"); + if paragraph { + text::replace_block(&mut doc, 0, 0, replacement).unwrap(); + } else { + text::replace_line(&mut doc, 0, 0, replacement).unwrap(); + } + assert_eq!(text::text_lines(&reopen(&doc), 0).unwrap()[0].text, replacement); + } +} + #[test] fn japanese_paragraph_uses_unicode_type3_fallback() { if without_craft_fonts("japanese_paragraph_uses_unicode_type3_fallback") { @@ -316,6 +584,74 @@ fn japanese_paragraph_uses_unicode_type3_fallback() { assert_eq!(text::text_blocks(&reopened, 0).unwrap()[0].text, replacement); } +/// Every Type 3 fallback glyph starts with a well-formed `d1`: six operands, the second 0, and a +/// box that encloses every point of the glyph. Acrobat shows a bullet for a glyph whose `d1` +/// has the wrong operand count. +#[test] +fn type3_fallback_glyphs_declare_a_well_formed_d1() { + if without_craft_fonts("type3_fallback_glyphs_declare_a_well_formed_d1") { + return; + } + // Cyrillic is respaced (shifted by its side bearing), and its box must move with it. + let mut glyphs = Vec::new(); + for replacement in ["見本商会 御中", "София 2027"] { + let mut doc = text_page("BT /F2 12 Tf 72 700 Td (ab) Tj ET"); + text::replace_line(&mut doc, 0, 0, replacement).unwrap(); + glyphs.extend(fallback_paths(&reopen(&doc))); + } + assert!(!glyphs.is_empty()); + for glyph in glyphs { + let text = String::from_utf8(glyph).unwrap(); + let mut lines = text.lines(); + let d1: Vec<&str> = lines.next().unwrap().split_whitespace().collect(); + assert_eq!(d1.len(), 7, "six operands and d1: {d1:?}"); + assert_eq!(d1[6], "d1"); + let n: Vec = d1[..6].iter().map(|v| v.parse().unwrap()).collect(); + assert_eq!(n[1], 0.0, "wy"); + assert!(n[2] <= n[4] && n[3] <= n[5], "box: {n:?}"); + for line in lines.filter(|l| l.ends_with(" m") || l.ends_with(" l")) { + let p: Vec = line.split_whitespace().take(2).map(|v| v.parse().unwrap()).collect(); + assert!(p[0] >= n[2] && p[0] <= n[4] && p[1] >= n[3] && p[1] <= n[5], "{line} outside {n:?}"); + } + } +} + +/// Shippori Mincho, the serif fallback, has no Cyrillic: Bulgarian replacement text in a serif +/// line takes a fallback face that has every letter instead of failing on the first one. +#[test] +fn cyrillic_replacement_in_a_serif_line_uses_a_face_with_the_letters() { + if without_craft_fonts("cyrillic_replacement_in_a_serif_line_uses_a_face_with_the_letters") { + return; + } + let replacement = "София 2027"; + for paragraph in [false, true] { + let mut doc = styled_text_page("ABCDEF+Garamond"); + if paragraph { + text::replace_block(&mut doc, 0, 0, replacement).unwrap(); + } else { + text::replace_line(&mut doc, 0, 0, replacement).unwrap(); + } + let lines = text::text_lines(&reopen(&doc), 0).unwrap(); + assert_eq!(lines[0].text, replacement); + // Letters are spaced by their shape, not one em each (the faces' full-width Cyrillic). + let widths = fallback_widths(&doc); + assert!(widths.iter().all(|w| *w > 300.0 && *w < 900.0), "{widths:?}"); + assert!(widths.windows(2).any(|w| w[0] != w[1]), "proportional: {widths:?}"); + } +} + +/// The `/Widths` of the page's Type 3 fallback font, for its Cyrillic letters. +fn fallback_widths(doc: &Document) -> Vec { + let p = pdfcraft_model::pages(doc).swap_remove(0); + let res = doc.resolve(p.dict.get(b"Resources").unwrap()); + let fonts = doc.resolve(res.as_dict().unwrap().get(b"Font").unwrap()); + let font = doc.resolve(fonts.as_dict().unwrap().get(b"PCJp").unwrap()); + let font = font.as_dict().unwrap(); + let widths = doc.resolve(font.get(b"Widths").unwrap()).as_array().unwrap().iter().filter_map(|w| w.as_f64()).collect::>(); + // "София 2027": the first five codes are the letters. + widths[..5].to_vec() +} + #[test] fn japanese_paragraph_keeps_ideographic_spaces() { if without_craft_fonts("japanese_paragraph_keeps_ideographic_spaces") { @@ -460,6 +796,22 @@ fn editing_text_keeps_the_tokens_a_stream_shares_with_its_neighbours() { assert_split_tokens_kept(&reopen(&doc), "Rewrapped"); } +#[test] +fn win_ansi_high_glyphs_use_helvetica_not_the_fallback_font() { + // š ž Š Ž Œ Ÿ ƒ † ‰ (WinAnsi 0x80–0x9F) are standard-font glyphs (#347): no Type3 fallback. + let text = "Šumava žaba œuvre Ÿ ƒ † ‰ €"; + let mut doc = text_page("BT /F2 10 Tf 72 600 Td (ab) Tj ET"); + let r = text::replace_line(&mut doc, 0, 0, text).unwrap(); + assert_eq!(r.substituted.as_deref(), Some("Helvetica")); + let lines = text::text_lines(&reopen(&doc), 0).unwrap(); + assert_eq!((lines[0].text.as_str(), lines[0].base_font.as_str()), (text, "Helvetica")); + let mut doc = text_page("BT /F2 10 Tf 72 600 Td (ab) Tj ET"); + let r = text::replace_block(&mut doc, 0, 0, text).unwrap(); + assert!(r.substituted.as_deref().is_none_or(|s| !s.contains("Type3")), "{:?}", r.substituted); + let lines = text::text_lines(&reopen(&doc), 0).unwrap(); + assert_eq!(lines.iter().map(|l| l.text.as_str()).collect::>().join(" "), text); +} + #[test] fn missing_glyphs_substitute_helvetica_and_impossible_text_is_refused() { let mut doc = text_page("BT /F2 10 Tf 72 600 Td (ab) Tj ET"); @@ -469,9 +821,9 @@ fn missing_glyphs_substitute_helvetica_and_impossible_text_is_refused() { let doc2 = reopen(&doc); let lines = text::text_lines(&doc2, 0).unwrap(); assert_eq!((lines[0].text.as_str(), lines[0].base_font.as_str()), ("abc", "Helvetica")); - // Neither font can show Greek. + // Neither the standard font nor the craft-fonts Japanese faces have this emoji. let mut doc = text_page("BT /F1 12 Tf 72 700 Td (Hello) Tj ET"); - assert!(text::replace_line(&mut doc, 0, 0, "Ωmega").is_err()); + assert!(text::replace_line(&mut doc, 0, 0, "\u{1f4a9}").is_err()); assert!(text::replace_line(&mut doc, 0, 5, "x").is_err(), "no such line"); } @@ -601,6 +953,56 @@ fn page_images_move_turn_replace_and_delete() { assert!(images::change_image(&mut doc, 0, 0, &images::ImageChange::Delete).is_err()); } +#[test] +fn form_artwork_edits_only_the_selected_placement() { + let mut doc = fixture(); // three pages share resources; pages 0 and 1 also share contents + let mut form = Dict::new(); + form.set(b"Subtype".to_vec(), Object::name("Form")); + form.set(b"BBox".to_vec(), Object::Array([10, 20, 110, 70].map(Object::Int).to_vec())); + form.set(b"Matrix".to_vec(), Object::Array([2, 0, 0, 3, -20, -60].map(Object::Int).to_vec())); + let artwork = doc.add(Object::Stream(Stream::flate(form, b"1 0 0 rg 10 20 100 50 re f"))); + doc.update_dict(pdfcraft_cos::ObjRef::new(6, 0), |d| { + let mut xo = Dict::new(); + xo.set(b"Figure".to_vec(), Object::Ref(artwork)); + d.set(b"XObject".to_vec(), Object::Dict(xo)); + }) + .unwrap(); + let content = b"q 1 0 0 1 40 80 cm /Figure Do Q q 1 0 0 1 300 400 cm /Figure Do Q BT /F1 9 Tf (Caption) Tj ET"; + doc.set(pdfcraft_cos::ObjRef::new(7, 0), Object::Stream(Stream::flate(Dict::new(), content))); + let mut doc = reopen(&doc); + let original = doc.get(artwork); + let imgs = images::page_images(&doc, 0).unwrap(); + assert_eq!(imgs.len(), 2); + assert!(imgs[0].is_form); + assert_eq!((imgs[0].width, imgs[0].height), (0, 0)); + assert!(close(imgs[0].rect, [40.0, 80.0, 240.0, 230.0])); + let target = [60.0, 100.0, 160.0, 175.0]; + images::change_image(&mut doc, 0, 0, &images::ImageChange::Transform(images::rect_to_rect(imgs[0].rect, target))).unwrap(); + let mut doc = reopen(&doc); + let moved = images::page_images(&doc, 0).unwrap(); + assert!(close(moved[0].rect, target)); + assert_eq!(moved[1].rect, imgs[1].rect); + assert_eq!(images::page_images(&doc, 1).unwrap()[0].rect, imgs[0].rect); + assert_eq!(*doc.get(artwork), *original, "the shared Form and its unknown data are untouched"); + assert_eq!(text::text_lines(&doc, 0).unwrap()[0].text, "Caption"); + let t = images::turn_about_centre(target, 1, false, false); + images::change_image(&mut doc, 0, 0, &images::ImageChange::Transform(t)).unwrap(); + assert!(close(images::page_images(&doc, 0).unwrap()[0].rect, [72.5, 87.5, 147.5, 187.5])); + assert!(images::change_image(&mut doc, 0, 0, &images::ImageChange::Replace(artwork)).is_err()); + images::change_image(&mut doc, 0, 0, &images::ImageChange::Delete).unwrap(); + let doc = reopen(&doc); + assert_eq!(images::page_images(&doc, 0).unwrap().len(), 1); + assert_eq!(images::page_images(&doc, 1).unwrap().len(), 2); + // Invalid bounds are skipped without interpreting the Form stream. + let mut doc = doc; + let mut broken = (*doc.get(artwork)).clone(); + if let Object::Stream(s) = &mut broken { + s.dict.set(b"BBox".to_vec(), Object::Array(vec![Object::Int(0)])); + } + doc.set(artwork, broken); + assert!(images::page_images(&doc, 0).unwrap().is_empty()); +} + #[test] fn paragraphs_take_new_formatting() { let mut doc = text_page("BT /F1 10 Tf 12 TL 100 700 Td (One two three four five six seven) Tj T* (eight nine ten eleven twelve) Tj ET"); @@ -852,3 +1254,67 @@ fn the_graphics_state_carries_over_between_content_streams() { let l = &text::text_lines(&doc, 0).unwrap()[0]; assert!(close(l.rect[0], 100.0) && close(l.size, 10.0), "{l:?}"); } + +/// #125: page-content tools refuse text the standard fonts can't draw instead of writing `?`, +/// and leave the document untouched. +#[test] +fn page_text_the_standard_fonts_cant_draw_is_refused() { + let mut doc = fixture(); + let before: Vec> = (0..3).map(|p| streams(&reopen(&doc), p)).collect(); + let text = AddedText { rect: [72.0, 600.0, 300.0, 700.0], text: "日本語のテキスト".into(), ..AddedText::default() }; + let err = add_content(&mut doc, 0, &Content::Text(text)).unwrap_err().to_string(); + assert!(err.contains("\"日\" (U+65E5)"), "{err}"); + let hf = HeaderFooter { + text: ["Ελληνικά".into(), String::new(), String::new(), String::new(), String::new(), String::new()], + ..HeaderFooter::default() + }; + assert!(add_header_footer(&mut doc, &[0, 1], &hf, true, &cx()).unwrap_err().to_string().contains("U+0395")); + let wm = Watermark { text: "机密".into(), ..Watermark::default() }; + assert!(add_watermark(&mut doc, &[0], &wm, true).unwrap_err().to_string().contains("U+673A")); + let after: Vec> = (0..3).map(|p| streams(&reopen(&doc), p)).collect(); + assert_eq!(before, after, "a refused edit changes nothing"); + // Western European text, including what only WinAnsi (not Latin-1) has, still works. + let text = AddedText { rect: [72.0, 600.0, 300.0, 700.0], text: "Café — 5€ ™".into(), ..AddedText::default() }; + add_content(&mut doc, 0, &Content::Text(text.clone())).unwrap(); + // Retyping existing text is refused the same way, and the whole document is left as it was. + let saved = write_incremental(&doc, &SaveOptions::default()).unwrap(); + let retyped = AddedText { text: "Café 中".into(), ..text.clone() }; + assert!(update_content(&mut doc, 0, 0, &Content::Text(retyped)).unwrap_err().to_string().contains("U+4E2D")); + assert_eq!(write_incremental(&doc, &SaveOptions::default()).unwrap(), saved, "a refused update changes nothing"); + // A carriage return is checked where it would be drawn: added text splits on LF only, so + // CRLF would leave a `?`; headers and watermarks split CRLF into lines, but not a lone CR. + let crlf = AddedText { text: "A\r\nB".into(), ..text }; + assert!(add_content(&mut doc, 0, &Content::Text(crlf)).unwrap_err().to_string().contains("U+000D")); + let hf = |t: &str| HeaderFooter { + text: [t.into(), String::new(), String::new(), String::new(), String::new(), String::new()], + ..HeaderFooter::default() + }; + add_header_footer(&mut doc, &[0], &hf("Left\r\n<>"), true, &cx()).unwrap(); + assert!(add_header_footer(&mut doc, &[0], &hf("A\rB"), true, &cx()).unwrap_err().to_string().contains("U+000D")); + // Arabic is shaped with the craft-fonts Arabic face (#403): drawable when that face is built + // in, refused (never written as `?`) when it isn't. Text left to the standard font still counts. + let arabic = |text: &str| AddedText { rect: [72.0, 600.0, 300.0, 700.0], text: text.into(), ..AddedText::default() }; + // U+061C is in the Arabic block but is a direction mark that's dropped, not shaped: it never + // makes text drawable, nor (with the face) undrawable. + assert!(add_content(&mut doc, 0, &Content::Text(arabic("\u{061C}中"))).is_err()); + if pdfcraft_fonts::arabic_has('م') { + add_content(&mut doc, 0, &Content::Text(arabic("مرحبا Hello"))).unwrap(); + add_content(&mut doc, 0, &Content::Text(arabic("\u{061C}Hello"))).unwrap(); + for text in ["مرحبا 中", "\u{061C}中"] { + let err = add_content(&mut doc, 0, &Content::Text(arabic(text))).unwrap_err().to_string(); + assert!(err.contains("U+4E2D"), "{text:?}: {err}"); + } + } else { + // #403's own refusal says what's missing; the Add-text editor still keeps the draft. + let err = add_content(&mut doc, 0, &Content::Text(arabic("مرحبا Hello"))).unwrap_err().to_string(); + assert!(err.contains("CRAFT_FONTS_DIR"), "{err}"); + assert_eq!(first_undrawable(&arabic("مرحبا Hello")), Some('م')); + // An existing item may still take Arabic (#403 keeps it movable), but nothing else that + // the standard font would write as `?`. + let n = list_added(&doc).iter().filter(|a| a.page == 0).count(); + add_content(&mut doc, 0, &Content::Text(arabic("x"))).unwrap(); + let err = update_content(&mut doc, 0, n, &Content::Text(arabic("ب中"))).unwrap_err().to_string(); + assert!(err.contains("U+4E2D"), "{err}"); + update_content(&mut doc, 0, n, &Content::Text(arabic("ب"))).unwrap(); + } +} diff --git a/crates/edit/src/text.rs b/crates/edit/src/text.rs index ff559cf2..2fe05ef6 100644 --- a/crates/edit/src/text.rs +++ b/crates/edit/src/text.rs @@ -16,7 +16,7 @@ use std::rc::Rc; use pdfcraft_content::{Matrix, Op, parse, serialize_ops}; use pdfcraft_cos::{Dict, Document, Object, PdfString, Stream}; use pdfcraft_fonts::pdf::Metrics; -use pdfcraft_fonts::{GlyphError, japanese_glyph}; +use pdfcraft_fonts::{CraftFont, GlyphError, japanese_glyph_from}; use crate::EditError; @@ -27,7 +27,7 @@ pub struct TextLine { pub text: String, /// Its box in user space. pub rect: [f64; 4], - /// The font's resource name and `/BaseFont`, and its size in text space. + /// The font's resource name and `/BaseFont` (or Type 3 descriptor's `/FontName`), and its size in text space. pub font: String, pub base_font: String, pub size: f64, @@ -499,7 +499,7 @@ pub fn text_lines(doc: &Document, page: usize) -> Result, EditErro type Encoder = Box Option>>; fn is_win_ansi_char(c: char) -> bool { - matches!(c, '\u{20}'..='\u{7e}' | '\u{a0}'..='\u{ff}' | '€' | '‚' | '„' | '…' | '‘' | '’' | '“' | '”' | '•' | '–' | '—' | '™' | '\t') + c == '\t' || pdfcraft_fonts::win_ansi_byte(c).is_some() } fn needs_type3(text: &str) -> bool { @@ -510,7 +510,9 @@ fn source_family(base_font: &str) -> crate::added::Family { let name = base_font.to_ascii_lowercase(); if ["courier", "mono", "consolas", "menlo", "monaco", "lucida console"].iter().any(|s| name.contains(s)) { crate::added::Family::Courier - } else if !name.contains("sans") && ["times", "serif", "roman", "cambria", "georgia", "palatino", "garamond"].iter().any(|s| name.contains(s)) { + } else if !name.contains("sans") + && ["times", "serif", "roman", "mincho", "cambria", "georgia", "palatino", "garamond"].iter().any(|s| name.contains(s)) + { crate::added::Family::Times } else { crate::added::Family::Helvetica @@ -533,24 +535,52 @@ fn pdf_num(v: f64) -> String { if v.fract() == 0.0 { format!("{v:.0}") } else { format!("{v:.4}").trim_end_matches('0').trim_end_matches('.').to_string() } } -fn type3_path(ch: char) -> Result<(Vec, f64), EditError> { - let glyph = japanese_glyph(ch).map_err(|e| match e { +fn type3_path(face: &CraftFont, ch: char) -> Result<(Vec, f64), EditError> { + let glyph = japanese_glyph_from(face, ch).map_err(|e| match e { GlyphError::NoFont => no_japanese_font(), - GlyphError::Missing => EditError::Invalid(format!("Japanese fallback font has no glyph for U+{:04X}", ch as u32)), + GlyphError::Missing => EditError::Invalid(format!("no fallback font has a glyph for U+{:04X}", ch as u32)), GlyphError::TooComplex => EditError::Invalid(format!("Japanese fallback glyph U+{:04X} is too complex", ch as u32)), })?; + let (dx, width) = proportional(ch, &glyph); let scale = 1000.0; - let mut out = format!("{} 0 0 0 0 1000 1000 d1\n", pdf_num(glyph.width * scale)).into_bytes(); + // d1 is `wx wy llx lly urx ury` (ISO 32000-2 §9.6.4) with a box enclosing the glyph; + // Acrobat draws a bullet in place of a glyph whose d1 is malformed. + let b = if glyph.contours.is_empty() { [0.0; 4] } else { glyph.bbox }; + let mut out = format!( + "{} 0 {} {} {} {} d1\n", + pdf_num(width * scale), + pdf_num(((b[0] + dx) * scale).floor()), + pdf_num((b[1] * scale).floor()), + pdf_num(((b[2] + dx) * scale).ceil()), + pdf_num((b[3] * scale).ceil()) + ) + .into_bytes(); for contour in glyph.contours { let Some(first) = contour.first() else { continue }; - out.extend_from_slice(format!("{} {} m\n", pdf_num(first[0] * scale), pdf_num(first[1] * scale)).as_bytes()); + out.extend_from_slice(format!("{} {} m\n", pdf_num((first[0] + dx) * scale), pdf_num(first[1] * scale)).as_bytes()); for p in contour.iter().skip(1) { - out.extend_from_slice(format!("{} {} l\n", pdf_num(p[0] * scale), pdf_num(p[1] * scale)).as_bytes()); + out.extend_from_slice(format!("{} {} l\n", pdf_num((p[0] + dx) * scale), pdf_num(p[1] * scale)).as_bytes()); } out.extend_from_slice(b"h\n"); } out.extend_from_slice(b"f\n"); - Ok((out, glyph.width)) + Ok((out, width)) +} + +/// Side bearing (em) given to a respaced glyph on each side. +const SIDE_BEARING: f64 = 0.05; + +/// Shift and advance for a fallback glyph. Japanese faces draw Cyrillic and Greek full-width +/// (one em each, as in JIS X 0208) and have no proportional (`palt`) metrics for them, so a +/// word set that way reads as letter-spaced. Those letters get their ink width plus a side +/// bearing instead; everything else keeps the face's own advance. +fn proportional(ch: char, glyph: &pdfcraft_fonts::GlyphOutline) -> (f64, f64) { + let respace = matches!(ch, '\u{0370}'..='\u{03FF}' | '\u{0400}'..='\u{052F}' | '\u{1F00}'..='\u{1FFF}'); + let ink = glyph.bbox[2] - glyph.bbox[0]; + if !respace || glyph.contours.is_empty() || !(ink > 0.0 && ink < glyph.width) { + return (0.0, glyph.width); + } + (SIDE_BEARING - glyph.bbox[0], ink + 2.0 * SIDE_BEARING) } fn unicode_hex(ch: char) -> String { @@ -568,8 +598,13 @@ fn no_japanese_font() -> EditError { ) } -fn type3_font(doc: &mut Document, fonts_res: &mut Dict, text: &str) -> Result { - let family = pdfcraft_fonts::document_japanese_font().ok_or_else(no_japanese_font)?.family; +/// The first of `faces` (best match first) that has a glyph for every one of `chars`, else the first. +fn fallback_face<'a>(faces: &[&'a CraftFont], chars: &[char], has_glyph: impl Fn(&CraftFont, char) -> bool) -> Option<&'a CraftFont> { + faces.iter().copied().find(|face| chars.iter().all(|ch| has_glyph(face, *ch))).or_else(|| faces.first().copied()) +} + +fn type3_font(doc: &mut Document, fonts_res: &mut Dict, text: &str, family: crate::added::Family, bold: bool) -> Result { + let faces = pdfcraft_fonts::document_japanese_fonts_for_style(family == crate::added::Family::Times, bold); let mut chars = Vec::new(); for ch in text.chars() { if !chars.contains(&ch) { @@ -582,6 +617,10 @@ fn type3_font(doc: &mut Document, fonts_res: &mut Dict, text: &str) -> Result Result Result Some(bytes) => replacement.push(Op::new("Tj", vec![Object::String(PdfString::literal(bytes))])), None => { if needs_type3(&text) { - let fallback = type3_font(doc, &mut fonts_res, &text)?; + let fallback = type3_font(doc, &mut fonts_res, &text, source_family(&target.base_font), target.bold)?; let bytes = type3_encode(&fallback, &text) .ok_or_else(|| EditError::Invalid(format!("\"{text}\" can't be shown by the Japanese fallback")))?; let size = font_size_before(&ops, first).unwrap_or(target.size); @@ -857,6 +904,8 @@ pub fn replace_block(doc: &mut Document, page: usize, block: usize, text: &str) pub struct BlockStyle { /// A standard font family, bold, italic. pub family: Option<(crate::added::Family, bool, bool)>, + /// Override weight without choosing a different family (`None` keeps the source weight). + pub bold: Option, /// Font size in points (user space). pub size: Option, /// Fill colour (RGB 0–1). @@ -904,10 +953,11 @@ pub fn rewrite_block(doc: &mut Document, page: usize, block: usize, text: Option } let o_state = ts_state.clone(); let metrics = fonts_res.get(font_name.as_bytes()).and_then(|f| doc.resolve(f).as_dict().cloned()).map(|d| Metrics::from_dict(doc, &d)); - let reuse = style.family.is_none() && metrics.as_ref().is_some_and(|m| m.encode(&text).is_some()); - let type3 = if !reuse && needs_type3(&text) { Some(type3_font(doc, &mut fonts_res, &text)?) } else { None }; - // The standard font used when the paragraph's own can't be (chosen, or substituted). + let reuse = style.family.is_none() && style.bold.is_none() && metrics.as_ref().is_some_and(|m| m.encode(&text).is_some()); + // The chosen style also determines the real Japanese fallback outlines and advances. let (family, bold, italic) = style.family.unwrap_or((source_family(&b.base_font), b.bold, b.italic)); + let bold = style.bold.unwrap_or(bold); + let type3 = if !reuse && needs_type3(&text) { Some(type3_font(doc, &mut fonts_res, &text, family, bold)?) } else { None }; let std_width = move |s: &str, size: f64| -> f64 { match family { crate::added::Family::Courier => s.chars().count() as f64 * 0.6 * size, @@ -1124,4 +1174,20 @@ mod tests { assert_eq!(wrap("A\u{2028}B C", 3.0, width), ["A B", "C"]); assert_eq!(wrap(" \tA B\r\nC ", 3.0, width), ["A B", "C"]); } + + #[test] + fn fallback_face_moves_on_when_the_best_face_lacks_a_character() { + static JPAN: &[&str] = &["Jpan"]; + let mincho = CraftFont { family: "Mincho", style: "Regular", scripts: JPAN, bytes: b"" }; + let gothic = CraftFont { family: "Gothic", style: "Regular", scripts: JPAN, bytes: b"" }; + let faces = [&mincho, &gothic]; + // Only the Gothic face has the Cyrillic letter. + let has = |face: &CraftFont, ch: char| ch.is_ascii() || face.family == "Gothic"; + assert_eq!(fallback_face(&faces, &['a', 'ф'], has).map(|f| f.family), Some("Gothic")); + // Characters the best face has keep it. + assert_eq!(fallback_face(&faces, &['a', 'b'], has).map(|f| f.family), Some("Mincho")); + // No face has them all: the best face stays, and names the missing glyph. + assert_eq!(fallback_face(&faces, &['a', 'ф'], |_, ch| ch.is_ascii()).map(|f| f.family), Some("Mincho")); + assert!(fallback_face(&[], &['a'], has).is_none()); + } } diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 272698ea..bcf762fc 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -10,7 +10,7 @@ workspace = true [dependencies] pdfcraft-measure = { workspace = true } -image = { version = "0.25", default-features = false, features = ["jpeg", "tiff"] } +image = { version = "0.25", default-features = false, features = ["png", "jpeg", "tiff"] } pdfcraft-render = { workspace = true } pdfcraft-cos = { workspace = true } pdfcraft-organize = { workspace = true } @@ -34,6 +34,11 @@ pdfcraft-create = { workspace = true } pdfcraft-model = { workspace = true } thiserror = { workspace = true } png = "0.18" +# Already in the graph (url and idna through webbrowser, icu_properties through boa): they show +# the host of a document's link the way the browser reads it (links::display_host). +url = "2.5" +idna = "1.1" +icu_properties = "2.3" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Already in the graph through lopdf; used only for the local UTC offset ("today"). diff --git a/crates/engine/README.md b/crates/engine/README.md new file mode 100644 index 00000000..c0553769 --- /dev/null +++ b/crates/engine/README.md @@ -0,0 +1,80 @@ +# pdfcraft-engine + +The façade every frontend talks to. It holds the open documents, their edit history and the tool +catalogue; frontends never reach past it into the parsing, rendering or editing crates. + +- **Layer:** L6. It depends on most of L1–L4 and on nothing above itself. **No UI toolkit** — + `egui`, `winit`, `eframe` and `rfd` are forbidden below L7, and `xtask layers` enforces it. +- The crate denies `unwrap`/`expect`/`panic!`/`todo!`. Entry points that run untrusted-document + code keep a last-resort `catch_unwind` guard so an escaped panic becomes an error and the user + keeps their document. + +Two frontends exist on top of it today: `pdfcraft-ui-egui` (desktop and web) and +`pdfcraft-automation` (headless tools, the CLI and MCP). Anything one can do, the other must be +able to do — see AGENTS.md §3. + +## The editing model + +Each open document holds a `pdfcraft_cos::Document`: the object graph plus a copy-on-write overlay. + +1. An edit runs **on a clone**. On success the previous state is pushed onto the undo stack. Clones + share all unchanged data, so a snapshot is cheap — this is what makes 100 levels of undo + affordable on a 200 MB file. +2. After every edit the **working file** is produced by an incremental write: the original bytes + plus one appended revision. +3. The view is refreshed from those bytes, so **what you see is exactly what Save will write**. +4. Saving rebases onto the written bytes, so the next save appends only new edits. + +Consequences worth preserving: the original bytes of an opened file are never rewritten in place; a +full rewrite happens only where it must (redaction, PDF/A conversion, print imposition, a changed +password); and unknown data survives a round trip because nothing is reconstructed that wasn't +understood. + +## API + +```rust +let mut s = Session::new(); +let id = s.open("report.pdf", Some(path), bytes, None)?; // -> DocId + +s.apply(id, Edit::RotatePages { pages: vec![0], degrees: 90 })?; +let label = s.undo(id)?; // "Rotate pages" +s.redo(id)?; + +let doc = s.get(id).ok_or("gone")?; // &Document: queries, never edits +doc.can_undo(); doc.editable(); doc.allows_printing(); +doc.text_blocks(page); doc.page_boxes(page); doc.security_summary(); + +let bytes = s.save_bytes(id)?; // incremental: keeps signatures over earlier revisions valid +let bytes = s.save_full_bytes(id)?; // full rewrite, garbage-collected +s.mark_saved(id, bytes.clone(), path)?; +``` + +`Edit` is one enum of 75 variants — every undoable change in the product, from +`RotatePages` to `AddAnnotation`, `SetFieldValue`, `ApplyRedactions` and `AddWatermark`. Adding a +feature means adding a variant and handling it in `apply`, which is what keeps undo, the dirty +flag, autosave and the automation tools consistent for free. + +`Session` also owns the operations that produce a *new* document rather than editing one: +`create_blank`, `create_from_images`, `create_from_text`, `combine`, `extract`, `split`, +`split_by_size`, `reduced_bytes`, `optimized_bytes`, `print_pdf`, `export_data`, and the signing +family (`sign`, `sign_with_timestamp`, `timestamp_document`, `embed_ltv`). + +## Modules + +| Module | What it holds | +|---|---| +| `commands` | The command registry: id, label, menu, shortcut, and a `Needs` precondition encoding document security. Menus, the ⌘K palette, shortcuts and the control channel all read it, so every surface agrees on what exists and when it is enabled | +| `catalog` | Acrobat's "All tools" information architecture as data — groups, sections, icons, hues, and an `Availability` of `Ready`, `Planned(milestone)` or `Provider`. The UI renders what is *not* built yet, honestly | +| `actions` | PDF action dispatch (go-to, URI, named, set-layer-state, JavaScript) | +| `links` | Link creation, properties, and finding URLs in page text | +| `js` | The bridge to `pdfcraft-js`. `forms` never depends on `js` directly; the engine injects it, so a build without JavaScript is possible | +| `xfa`, `ocr`, `compare`, `export` | Façades over the L3/L4 crates of the same names | + +## Not done here + +- **Rendering is still borrowed.** `pdfcraft-render` wraps `hayro` and inspects through `lopdf` + (ADR-0004). The engine only sees `inspect() -> DocInfo`, `RenderPool` and `text::PageText`, and + that API stays frozen while M2 replaces what is behind it. +- Performance budgets (`misc.performance-budgets`) are not set or enforced. +- Some shipped features are still reachable only from the UI, not through an `automation` tool; + `cargo xtask parity` lists them; closing that gap is tracked in the execution plan (D4). diff --git a/crates/engine/src/catalog.rs b/crates/engine/src/catalog.rs index 9e774127..c553675c 100644 --- a/crates/engine/src/catalog.rs +++ b/crates/engine/src/catalog.rs @@ -149,7 +149,7 @@ pub static TOOL_GROUPS: &[ToolGroup] = &[ title: "Create from", items: &[ item("Single file", "file-input", "create.file", Ready), - item("Multiple files", "files", "create.multiple", Planned("M10")), + item("Multiple files", "files", "create.multiple", Ready), item("Images", "image", "create.images", Ready), item("Clipboard", "copy-plus", "create.clipboard", Ready), item("Blank page", "file-plus-2", "create.blank", Ready), diff --git a/crates/engine/src/commands.rs b/crates/engine/src/commands.rs index ae68cecc..35576137 100644 --- a/crates/engine/src/commands.rs +++ b/crates/engine/src/commands.rs @@ -149,6 +149,10 @@ const HELP: Option<&str> = Some("Help"); /// Every command, in menu order. pub const COMMANDS: &[CommandSpec] = &[ c("file.open", "Open…", FILE, Some(Shortcut::cmd("O")), Nothing, "folder-open"), + c("file.open_recent", "Open Recent", FILE, None, Nothing, "clock"), + // Shown at the foot of File ▸ Open Recent and on Home, not as its own File menu item. + c("file.clear_recent", "Clear Recent Files", None, None, Nothing, "trash-2"), + c("file.pin_folder", "Pin folder to Home…", FILE, None, Nothing, "folder-plus"), c("create.blank", "New blank PDF", FILE, None, Nothing, "file-plus-2"), c("measure.distance", "Measure distance", None, None, Annotate, "ruler"), c("measure.perimeter", "Measure perimeter", None, None, Annotate, "ruler"), @@ -161,6 +165,7 @@ pub const COMMANDS: &[CommandSpec] = &[ c("page.cut", "Cut pages", None, None, Assembly, "scissors"), c("page.paste", "Paste pages", None, None, Assembly, "clipboard-paste"), c("create.file", "Create PDF from file…", FILE, None, Nothing, "file-input"), + c("create.multiple", "Create PDF from multiple files…", FILE, None, Nothing, "files"), c("create.images", "Create PDF from images…", FILE, None, Nothing, "image"), c("create.clipboard", "Create PDF from clipboard", FILE, None, Nothing, "copy-plus"), c("page.combine", "Combine files…", FILE, None, Nothing, "files"), @@ -190,6 +195,7 @@ pub const COMMANDS: &[CommandSpec] = &[ c("edit.snapshot", "Take a snapshot", EDIT, None, Document, "camera"), c("view.full_screen", "Full screen mode", VIEW, Some(Shortcut::cmd("L")), Document, "maximize"), c("view.read_mode", "Read mode", VIEW, Some(Shortcut { command: true, shift: false, mac_ctrl: true, key: "H" }), Document, "book-open"), + c("view.focus_page_input", "Go to page…", VIEW, Some(Shortcut::cmd_shift("N")), Document, "text-cursor-input"), c("view.theme", "Switch light / dark theme", None, None, Nothing, "moon"), c("view.theme.system", "Use system setting", None, None, Nothing, "settings"), c("view.theme.light", "Light gray", None, None, Nothing, "sun"), diff --git a/crates/engine/src/compare.rs b/crates/engine/src/compare.rs index bfcf578b..7da14c82 100644 --- a/crates/engine/src/compare.rs +++ b/crates/engine/src/compare.rs @@ -170,6 +170,7 @@ impl crate::Document { size: b.size, bold: f.contains("bold") || f.contains("black") || f.contains("heavy"), italic: f.contains("italic") || f.contains("oblique"), + color: b.color, } }) .collect(); diff --git a/crates/engine/src/export.rs b/crates/engine/src/export.rs index ca90c009..5360531a 100644 --- a/crates/engine/src/export.rs +++ b/crates/engine/src/export.rs @@ -25,7 +25,7 @@ pub struct ExportSource { impl Document { /// The current state, for exporting on another thread. pub fn export_source(&self) -> ExportSource { - ExportSource { bytes: self.bytes.clone(), config: self.config.clone(), pages: self.info.pages.len() } + ExportSource { bytes: self.display.clone(), config: self.config.clone(), pages: self.info.pages.len() } } } diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index c7ce0650..31d66e63 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -20,8 +20,13 @@ pub mod export; pub mod js; pub mod links; pub mod ocr; +pub mod optimizer; +pub mod signature_image; +mod upright_image; pub mod xfa; +pub use signature_image::{ImageSignaturePreview, SignatureImage}; + pub use pdfcraft_organize::{BoxSpec, PageBox, SplitBy, split_ranges}; /// One file produced by a split: (1-based first page, last page, PDF bytes). @@ -29,7 +34,7 @@ pub use pdfcraft_organize::LabelStyle; pub use pdfcraft_organize::view::{InitialView, Layout as InitialLayout, Magnification, Navigation}; pub use pdfcraft_cos::Algorithm; -pub use pdfcraft_create::ImageResolution; +pub use pdfcraft_create::{CONVERTIBLE, ImageResolution, SourceKind, source_kind}; pub use pdfcraft_edit::{ Added, AddedImage, AddedText, Align as TextAlign, Background, Content as AddedContent, Family as FontFamily, HeaderFooter, MarkKind, Watermark, }; @@ -39,7 +44,7 @@ pub use pdfcraft_forms::{ }; pub use pdfcraft_a11y as a11y; -pub use pdfcraft_edit::{BlockStyle, PageImage, TextBlock, TextLine}; +pub use pdfcraft_edit::{BlockStyle, PageImage, TextBlock, TextLine, first_undrawable}; pub use pdfcraft_measure as measure; pub use pdfcraft_xfa::Report as XfaLayout; @@ -64,25 +69,33 @@ pub use pdfcraft_fonts::{MAX_SIGNATURE_CHARS, ScriptOutline, script_outline}; /// Fill & Sign: `text` in the script font as a typed signature, its left edge at `at` (user /// space, vertically centred) and `height` points tall. `None` for text with no outlines. -pub fn typed_signature_shape(at: [f64; 2], text: &str, height: f64) -> Option { +/// +/// Left, centred and upright are as displayed on a page turned by `rotation` (its `/Rotate`): +/// the box runs along the displayed axes, like an image signature's +/// ([`SignatureImage::rect`]), and the outlines are turned back so the name reads across. +pub fn typed_signature_shape(at: [f64; 2], text: &str, height: f64, rotation: i64) -> Option { let o = script_outline(text); let [left, bottom, right, top] = o.bounds(); let span = (top - bottom).max(0.1); let width = right - left; - if o.contours.is_empty() || o.width <= 0.0 { + if o.contours.is_empty() || o.width <= 0.0 || !(width > 0.0 && height > 0.0) { return None; } let k = height / span; - let rect = [at[0], at[1] - height / 2.0, at[0] + width * k, at[1] + height / 2.0]; - let contours = o.contours.iter().map(|c| c.iter().map(|p| [(p[0] - left) / width, (p[1] - bottom) / span]).collect()).collect(); + let rect = signature_image::upright_box(rotation, at, width * k, height)?; + // Displayed right and up as user-space unit vectors: a point at (nx, ny) of the displayed box + // is at this fraction of the user-space box. Exact for an unturned page. + let [a, b, c, d, ..] = pdfcraft_model::view_matrix_for(rotation, [0.0; 4]); + let to_user = |nx: f64, ny: f64| [a * nx + c * ny + (-a).max(0.0) + (-c).max(0.0), b * nx + d * ny + (-b).max(0.0) + (-d).max(0.0)]; + let contours = o.contours.iter().map(|c| c.iter().map(|p| to_user((p[0] - left) / width, (p[1] - bottom) / span)).collect()).collect(); Some(Shape::TypedSignature { rect, contours }) } /// Comment geometry helpers (text-box line breaking) for frontends. pub use pdfcraft_annot::appearance as annot_text; pub use pdfcraft_annot::links::{Highlight as LinkHighlight, LinkAction, LinkItem, LinkStyle}; pub use pdfcraft_annot::{ - AttachIcon, FillMark, Markup, NOTE_SIZE, NewAnnotation, NoteIcon, OverlayFont, OverlayLook, Props as CommentProps, ReviewState, Rgb, Shape, - StampGroup, StampKind, Style, rect_quad, + AttachIcon, FillMark, LineEnding, Markup, NOTE_SIZE, NewAnnotation, NoteIcon, OverlayFont, OverlayLook, Props as CommentProps, ReviewState, Rgb, + Shape, StampGroup, StampKind, Style, rect_quad, }; pub use pdfcraft_forms::detect; pub use pdfcraft_optimize as optimize; @@ -190,8 +203,11 @@ pub struct Document { pub id: DocId, pub name: String, pub path: Option, - /// The working file: what Save writes and what is displayed. + /// The working file: what Save writes. pub bytes: Arc>, + /// What is displayed (the page view, `page_render`, page images): `bytes`, plus appearances + /// drawn for display only for comments that have none ([`display_bytes`]). Never saved. + pub display: Arc>, pub info: DocInfo, pub renderer: RenderPool, /// The password the document was opened with (needed to read attachments, etc.). @@ -252,6 +268,9 @@ impl Document { pub fn page_image_file(&self, page: usize, index: usize) -> Result<(&'static str, Vec), String> { let editor = self.editor.as_ref().ok_or("the document can't be read")?; let img = self.page_images(page).into_iter().nth(index).ok_or_else(|| format!("page {} has no image {}", page + 1, index + 1))?; + if img.is_form { + return Err("grouped Form artwork is not a bitmap; Save Image As is only available for raster images".into()); + } pdfcraft_create::image_file(&editor.cos, img.object.ok_or("the image has no object")?) } @@ -470,6 +489,15 @@ impl Document { } } +/// What is displayed: the working file, plus (in memory only, never saved) the appearances +/// PdfCraft draws for comments that have none ([`pdfcraft_annot::with_missing_appearances`]). +/// The working file is unchanged, and so is everything that reads it (saving, signatures, …). +fn display_bytes(editor: Option<&Editor>, bytes: &Arc>) -> Arc> { + let Some(display) = editor.and_then(|e| pdfcraft_annot::with_missing_appearances(&e.cos)) else { return bytes.clone() }; + // Writing fails only as saving the document would; the page then shows as it always did. + write_incremental(&display, &SaveOptions::default()).map(Arc::new).unwrap_or_else(|_| bytes.clone()) +} + fn render_threads() -> usize { std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4).clamp(2, 8) - 1 } @@ -478,7 +506,7 @@ fn render_threads() -> usize { fn use_layer_choices(doc: &mut Document) { let overrides: Vec<(i32, i32, bool)> = doc.info.layers.iter().map(|l| (l.id.0 as i32, l.id.1 as i32, l.visible)).collect(); doc.config.layers = Arc::new(overrides); - doc.renderer = RenderPool::new(doc.bytes.clone(), render_threads(), doc.config.clone()); + doc.renderer = RenderPool::new(doc.display.clone(), render_threads(), doc.config.clone()); } /// Apply a set-layer-visibility action to `layers`, one change at a time, so a toggle flips the @@ -827,6 +855,8 @@ pub enum Edit { color: Option, opacity: Option, width: Option, + /// Line or polyline: two endings. Callout: one. `None` leaves `/LE` unchanged. + endings: Option>, }, /// Comment properties ▸ General / note icon. SetAnnotationInfo { @@ -1172,7 +1202,7 @@ fn annotation_noun(s: &Shape) -> &'static str { Shape::TextMarkup { kind: Markup::Squiggly, .. } => "squiggly underline", Shape::Rectangle { .. } => "rectangle", Shape::Oval { .. } => "oval", - Shape::Line { arrow: true, .. } => "arrow", + Shape::Line { start: LineEnding::None, end: LineEnding::OpenArrow, .. } => "arrow", Shape::Line { .. } => "line", Shape::Ink { .. } => "drawing", Shape::TextBox { .. } => "text box", @@ -1434,9 +1464,11 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> Edit::AddCustomStamp { page, rect, name, file, author } => { let src = mark_source(doc, file)?; let (sw, sh) = (src.size.0.max(1.0), src.size.1.max(1.0)); - let rect = if (rect[2] - rect[0]).abs() < 1.0 || (rect[3] - rect[1]).abs() < 1.0 { + let natural = rect[2] == rect[0] && rect[3] == rect[1]; + let rotation = if natural && src.image { pdfcraft_organize::page_rotation(doc, *page)? } else { 0 }; + let rect = if natural { let k = (200.0 / sw.max(sh)).min(1.0); - let (w, h) = (sw * k, sh * k); + let (w, h) = if matches!(rotation, 90 | 270) { (sh * k, sw * k) } else { (sw * k, sh * k) }; [rect[0] - w / 2.0, rect[1] - h / 2.0, rect[0] + w / 2.0, rect[1] + h / 2.0] } else { *rect @@ -1444,7 +1476,8 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> let shape = Shape::CustomStamp { rect, name: name.clone(), picture: src.xobject, image: src.image, size: (sw, sh) }; let style = pdfcraft_annot::Style::default_for(&shape); let new = NewAnnotation { page: *page, shape, style, contents: name.clone(), author: author.clone() }; - pdfcraft_annot::add_annotation(doc, &new, &cx.meta())?; + let index = pdfcraft_annot::add_annotation(doc, &new, &cx.meta())?; + pdfcraft_annot::orient_image_stamp(doc, *page, index, rotation)?; } Edit::DeleteAnnotation { page, index } => pdfcraft_annot::delete_annotation(doc, *page, *index)?, Edit::SetAnnotationContents { page, index, text } => pdfcraft_annot::set_contents(doc, *page, *index, text, &cx.meta())?, @@ -1466,8 +1499,8 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> Edit::LockAnnotation { page, index, locked } => pdfcraft_annot::set_locked(doc, *page, *index, *locked)?, Edit::MoveAnnotation { page, index, dx, dy } => pdfcraft_annot::move_annotation(doc, *page, *index, *dx, *dy, &cx.meta())?, Edit::ResizeAnnotation { page, index, rect } => pdfcraft_annot::set_rect(doc, *page, *index, *rect, &cx.meta())?, - Edit::StyleAnnotation { page, index, color, opacity, width } => { - pdfcraft_annot::set_style(doc, *page, *index, *color, *opacity, *width, &cx.meta())?; + Edit::StyleAnnotation { page, index, color, opacity, width, endings } => { + pdfcraft_annot::set_style(doc, *page, *index, *color, *opacity, *width, endings.as_deref(), &cx.meta())?; } Edit::SetAnnotationInfo { page, index, author, subject, icon } => { pdfcraft_annot::set_info(doc, *page, *index, author.as_deref(), subject.as_deref(), *icon, &cx.meta())?; @@ -1489,7 +1522,7 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> ran.map_err(EditError::Invalid)?; } Edit::SetFieldImage { name, image } => { - let (img, _) = pdfcraft_create::image_xobject(doc, name, image)?; + let (img, _) = embed_image(doc, name, image)?; let px = match &*doc.get(img) { pdfcraft_cos::Object::Stream(s) => (s.dict.int(b"Width").unwrap_or(1) as u32, s.dict.int(b"Height").unwrap_or(1) as u32), _ => (1, 1), @@ -1546,7 +1579,14 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> ImageEdit::Flip { horizontal } => { pdfcraft_edit::ImageChange::Transform(pdfcraft_edit::turn_about_centre(img.rect, 0, *horizontal, !*horizontal)) } - ImageEdit::Replace { name, bytes } => pdfcraft_edit::ImageChange::Replace(pdfcraft_create::image_xobject(doc, name, bytes)?.0), + ImageEdit::Replace { name, bytes } => { + if img.is_form { + return Err(EditError::Edit(pdfcraft_edit::EditError::Invalid( + "grouped Form artwork cannot be replaced with a bitmap".into(), + ))); + } + pdfcraft_edit::ImageChange::Replace(embed_image(doc, name, bytes)?.0) + } ImageEdit::Delete => pdfcraft_edit::ImageChange::Delete, }; pdfcraft_edit::change_image(doc, *page, *index, &c)?; @@ -1586,7 +1626,7 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> pdfcraft_edit::add_content(doc, *page, &AddedContent::Text(text.clone()))?; } Edit::AddImage { page, rect, name, bytes } => { - let (image, natural) = pdfcraft_create::image_xobject(doc, name, bytes)?; + let (image, natural) = embed_image(doc, name, bytes)?; let rect = match rect { Some(r) => *r, None => { @@ -1606,7 +1646,7 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> let Some(AddedContent::Image(old)) = item.map(|a| a.content) else { return Err(EditError::Edit(pdfcraft_edit::EditError::Invalid("that item is not an image".into()))); }; - let (image, _) = pdfcraft_create::image_xobject(doc, name, bytes)?; + let (image, _) = embed_image(doc, name, bytes)?; pdfcraft_edit::update_content(doc, *page, *index, &AddedContent::Image(pdfcraft_edit::AddedImage { image, ..old }))?; } Edit::ApplyRedactions { pages } => { @@ -1732,19 +1772,57 @@ pub fn guard(f: impl FnOnce() -> T) -> Result { }) } +/// The most files Create ▸ Multiple files takes in one run. +pub const MAX_CREATE_FILES: usize = 1000; + /// Parse another PDF to copy pages from. fn open_source(name: &str, bytes: &Arc>) -> Result { - match std::panic::catch_unwind(|| pdfcraft_cos::Document::open(bytes.clone())) { - Ok(Ok(d)) if d.permissions().is_some_and(|p| !p.assemble()) => { - Err(EditError::Source(format!("{name}: its security settings don't allow copying pages"))) - } + open_source_with(name, bytes, None) +} + +/// [`open_source`], authenticating with `password` (user or owner) for an encrypted file. +fn open_source_with(name: &str, bytes: &Arc>, password: Option<&str>) -> Result { + source_document(bytes, password).map_err(|p| EditError::Source(format!("{name}: {p}"))) +} + +/// Why a file can't be a source for Combine Files (or Insert / Replace pages). +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum SourceProblem { + #[error("it is password-protected")] + Password, + #[error("the password is wrong")] + WrongPassword, + #[error("its security settings don't allow copying pages")] + NotPermitted, + #[error("{0}")] + Unreadable(String), +} + +fn source_document(bytes: &Arc>, password: Option<&str>) -> Result { + match std::panic::catch_unwind(|| pdfcraft_cos::Document::open_with_password(bytes.clone(), password)) { + Ok(Ok(d)) if d.permissions().is_some_and(|p| !p.assemble()) => Err(SourceProblem::NotPermitted), Ok(Ok(d)) => Ok(d), - Ok(Err(pdfcraft_cos::CosError::NeedsPassword)) => Err(EditError::Source(format!("{name}: it is password-protected"))), - Ok(Err(e)) => Err(EditError::Source(format!("{name}: {e}"))), - Err(_) => Err(EditError::Source(format!("{name}: the file could not be read"))), + Ok(Err(pdfcraft_cos::CosError::NeedsPassword)) => Err(SourceProblem::Password), + Ok(Err(pdfcraft_cos::CosError::WrongPassword)) => Err(SourceProblem::WrongPassword), + Ok(Err(e)) => Err(SourceProblem::Unreadable(e.to_string())), + Err(_) => Err(SourceProblem::Unreadable("the file could not be read".into())), } } +/// Whether `bytes` can be combined, opened with `password` (user or owner) if given, and if not +/// why: what Combine would refuse it for, so the Combine files list can say so before Combine +/// is pressed. The owner (permissions) password lifts the restriction on copying pages. +pub fn combine_source_check(bytes: &Arc>, password: Option<&str>) -> Result<(), SourceProblem> { + source_document(bytes, password).map(|_| ()) +} + +/// The number of pages of `bytes`, opened with `password` (user or owner) if given, whatever +/// its permissions allow; `None` when it can't be read. +pub fn source_page_count(bytes: &Arc>, password: Option<&str>) -> Option { + let doc = std::panic::catch_unwind(|| pdfcraft_cos::Document::open_with_password(bytes.clone(), password)).ok()?.ok()?; + pdfcraft_organize::page_count(&doc).ok() +} + fn plural(s: &str, n: usize) -> String { if n == 1 { s.to_string() } else { format!("{s}s") } } @@ -1795,6 +1873,8 @@ pub enum EditError { Sign(String), #[error("{0}")] Optimize(String), + #[error("cancelled")] + Cancelled, #[error("{0} isn't possible in a signed document: it would rewrite the file and invalidate the signatures")] SignedRewrite(String), #[error("this document is signed: rewriting it would invalidate its signatures (save it incrementally instead)")] @@ -1885,6 +1965,17 @@ fn xfa_values_from_datasets(doc: &mut pdfcraft_cos::Document) -> Result { + // Saved values, one per line for a multi-select list; shown text stands for + // its saved value (an older viewer may have written it). + let saved = |v: &str| f.options.iter().find(|(e, s)| e == v || s == v).map_or(v.to_string(), |(e, _)| e.clone()); + let picked: Vec = if f.has(pdfcraft_forms::flags::MULTI_SELECT) { + t.lines().map(str::trim).filter(|l| !l.is_empty()).map(saved).collect() + } else { + vec![saved(t.trim())] + }; + (f.value != picked).then_some(FieldValue::Choice(picked)) + } pdfcraft_xfa::FieldData::Text(t) => (f.value.join("\n") != t).then_some(FieldValue::Text(t)), pdfcraft_xfa::FieldData::Check(on) => (f.value.is_empty() == on).then_some(FieldValue::Check(on)), pdfcraft_xfa::FieldData::Radio(sel) => (f.value.first() != sel.as_ref()).then_some(FieldValue::Radio(sel)), @@ -2127,7 +2218,6 @@ impl Session { xfa: Option, ) -> Result { let config = RenderConfig { password: render_password.as_deref().map(Arc::from), ..Default::default() }; - let renderer = RenderPool::new(bytes.clone(), render_threads(), config.clone()); let (editor, read_only_reason) = match cos { Ok(Ok(cos)) => { let keys = Keys { render: render_password.clone(), reopen: password.map(str::to_owned) }; @@ -2136,6 +2226,8 @@ impl Session { Ok(Err(e)) => (None, Some(e.to_string())), Err(_) => (None, Some("the document structure could not be read for editing".into())), }; + let display = display_bytes(editor.as_ref(), &bytes); + let renderer = RenderPool::new(display.clone(), render_threads(), config.clone()); let mut form = editor.as_ref().map(|e| pdfcraft_forms::fields(&e.cos)).unwrap_or_default(); if let Some(e) = editor.as_ref() { xfa::mark_script_buttons(&e.cos, &mut form); @@ -2152,6 +2244,7 @@ impl Session { name, path, bytes, + display, info, renderer, password: render_password, @@ -2370,8 +2463,9 @@ impl Session { if !doc.signatures.is_empty() { doc.signatures = signatures_of(&editor.cos, &bytes, &doc.trust, &doc.sig_cache); } - doc.bytes = bytes.clone(); - doc.renderer = RenderPool::new(bytes, render_threads(), doc.config.clone()); + doc.display = display_bytes(Some(editor), &bytes); + doc.renderer = RenderPool::new(doc.display.clone(), render_threads(), doc.config.clone()); + doc.bytes = bytes; Ok(()) } @@ -2406,8 +2500,9 @@ impl Session { doc.added = pdfcraft_edit::list_added(&editor.cos); doc.links = pdfcraft_annot::links::list(&editor.cos); doc.signatures = signatures_of(&editor.cos, &bytes, &doc.trust, &doc.sig_cache); - doc.bytes = bytes.clone(); - doc.renderer = RenderPool::new(bytes, render_threads(), doc.config.clone()); + doc.display = display_bytes(Some(editor), &bytes); + doc.renderer = RenderPool::new(doc.display.clone(), render_threads(), doc.config.clone()); + doc.bytes = bytes; Ok(()) } @@ -2503,6 +2598,23 @@ impl Session { self.write_new(&pdfcraft_create::from_text(title, text, pdfcraft_create::LETTER, 11.0)?) } + /// Convert a file Create understands (an image or plain text) to PDF bytes; a PDF is checked + /// (it must open and allow copying pages) and returned as it is. + pub fn convert_to_pdf(&self, name: &str, bytes: &Arc>) -> Result<(SourceKind, Arc>), EditError> { + let Some(kind) = source_kind(name, bytes) else { + return Err(EditError::Source(format!("{name}: this file type can't be converted; use a PDF, an image or a .txt file"))); + }; + let title = name.rsplit_once('.').map_or(name, |(s, _)| s); + // Image decoders read untrusted bytes: a panic in one must not take the app down. + let created = guard(|| match kind { + SourceKind::Pdf => open_source(name, bytes).map(|_| bytes.clone()), + SourceKind::Image => self.create_from_images(&[(name.to_string(), bytes.to_vec())]), + SourceKind::Text => self.create_from_text(title, &String::from_utf8_lossy(bytes)), + }) + .map_err(|_| EditError::Source(format!("{name}: the file could not be read")))?; + Ok((kind, created?)) + } + /// Reduce File Size: Acrobat's defaults (images above 225 ppi to 150 ppi, JPEG medium /// quality; thumbnails dropped), identical resources merged, unused objects dropped, objects /// packed into compressed object streams. Returns the bytes and how many objects were @@ -2514,21 +2626,10 @@ impl Session { /// Optimize PDF ▸ Advanced optimization: `settings` for images and objects, plus Remove /// Hidden Information's `discard` categories (user data). A full rewrite: signed documents - /// are refused. The open document is not changed. + /// are refused. The open document is not changed. This is [`Self::optimize_job`] run in + /// place, without progress. pub fn optimized_bytes(&self, id: DocId, settings: &optimize::Settings, discard: &[Hidden]) -> Result<(Arc>, OptimizeReport), EditError> { - let doc = self.get(id).ok_or(EditError::NoDocument)?; - if doc.is_signed() { - return Err(EditError::Signed); - } - let editor = doc.editor.as_ref().ok_or_else(|| EditError::ReadOnly(doc.read_only_reason.clone().unwrap_or_default()))?; - let mut cos = editor.cos.clone(); - let discarded = if discard.is_empty() { Vec::new() } else { pdfcraft_redact::sanitize::remove_hidden(&mut cos, discard)? }; - let report = optimize::optimize(&mut cos, settings).map_err(|e| EditError::Optimize(e.to_string()))?; - let all: Vec = cos.object_numbers().into_iter().map(|n| pdfcraft_cos::ObjRef::new(n, cos.generation(n))).collect(); - let merged = pdfcraft_organize::dedupe_resources(&mut cos, &all, false); - let opts = SaveOptions { mod_date: self.now().map(pdfcraft_cos::pdf_date), ..SaveOptions::default() }; - let bytes = write_full(&cos, &opts).map_err(|e| EditError::Write(e.to_string()))?; - Ok((Arc::new(bytes), OptimizeReport { optimize: report, merged, discarded })) + self.optimize_job(id, settings, discard)?.run(|_| true) } /// Export comments and/or form data: XFDF and FDF carry either or both; XML, CSV and text @@ -2574,7 +2675,17 @@ impl Session { /// Combine Files with a page range per file ("1-3, 6"; `None` or empty for all pages). pub fn combine_ranges(&self, sources: &[CombineSource]) -> Result>, EditError> { - let docs = sources.iter().map(|(n, b, _)| open_source(n, b)).collect::, _>>()?; + self.combine_unlocked(sources, &[]) + } + + /// [`Self::combine_ranges`] with the password each encrypted source is opened with, by + /// position (missing or `None`: no password). The result is not encrypted. + pub fn combine_unlocked(&self, sources: &[CombineSource], passwords: &[Option<&str>]) -> Result>, EditError> { + let docs = sources + .iter() + .enumerate() + .map(|(i, (n, b, _))| open_source_with(n, b, passwords.get(i).copied().flatten())) + .collect::, _>>()?; let mut pages = Vec::with_capacity(docs.len()); for ((name, _, range), d) in sources.iter().zip(&docs) { let range = range.as_deref().map(str::trim).filter(|r| !r.is_empty()); @@ -2779,7 +2890,7 @@ impl Session { return false; } doc.config.hide_comments = hide; - doc.renderer = RenderPool::new(doc.bytes.clone(), render_threads(), doc.config.clone()); + doc.renderer = RenderPool::new(doc.display.clone(), render_threads(), doc.config.clone()); doc.generation += 1; true } @@ -2901,6 +3012,15 @@ pub struct MarkFile { pub page: usize, } +/// Embed an image that is placed on a page, turned the way its EXIF Orientation says. +fn embed_image( + doc: &mut pdfcraft_cos::Document, + name: &str, + bytes: &[u8], +) -> Result<(pdfcraft_cos::ObjRef, (f64, f64)), pdfcraft_create::CreateError> { + pdfcraft_create::image_xobject(doc, name, &upright_image::upright(bytes)) +} + /// Bring a mark's picture into `doc`: a PDF page as a form XObject, or an image. fn mark_source(doc: &mut pdfcraft_cos::Document, f: &MarkFile) -> Result { let head = &f.bytes[..f.bytes.len().min(1024)]; @@ -2909,7 +3029,7 @@ fn mark_source(doc: &mut pdfcraft_cos::Document, f: &MarkFile) -> Result write!(f, "the address contains hidden or invisible characters"), Self::Malformed => write!(f, "the address isn't a complete web or email address"), Self::Scheme(s) => write!(f, "it is a “{s}:” address"), + Self::MailFile => write!(f, "it asks your email app to attach or insert a file from your computer"), + Self::MailEncodedWord => write!(f, "it contains encoded text that some email apps turn into other instructions"), } } } @@ -103,14 +114,106 @@ pub fn document_url(raw: &str) -> Result { } let complete = match scheme.as_str() { "mailto" => !rest.is_empty(), - _ => authority(url).is_some_and(|a| !a.contains(' ')) && host(url).is_some(), + _ => authority(url).is_some_and(|a| !a.contains(' ')) && display_host(url).is_some(), }; - if !complete { + // The desktop app opens addresses through the webbrowser crate, which treats one the WHATWG URL + // parser rejects as a local file path and opens that instead (`mailto://a^b/` would become + // `file:///…/mailto:/a^b/`). So only an address the parser accepts, as the same scheme, goes on. + if !complete || !url::Url::parse(url).is_ok_and(|u| u.scheme() == scheme) { return Err(BlockedLink::Malformed); } + if scheme == "mailto" { + check_mail_parameters(rest)?; + } Ok(url.to_string()) } +/// Email-address parameters that make an email app read a file from the user's computer: Evolution, +/// KMail and Claws Mail attach the file named by `attach` (Evolution and KMail also by +/// `attachment`), and Claws Mail reads an `insert` file into the message body. Names are matched as +/// prefixes, so `attachments` is refused too. +const MAIL_FILE_PARAMETERS: &[&str] = &["attach", "insert"]; + +/// How many layers of percent-encoding are looked through. Email apps decode once at most. +const MAIL_DECODE_LAYERS: usize = 8; + +/// Refuse an email address (what follows `mailto:`) that asks for a local file. A document can't +/// need this, and a drafted message carrying the user's files is one click from being sent. +/// +/// Email apps disagree on how they read the address, so this doesn't try to parse it the way any +/// one of them does. Every email app reads a parameter as `name=value`, so it refuses any word +/// that starts with one of [`MAIL_FILE_PARAMETERS`] (in any case) and is followed by `=`, wherever +/// it stands: after `?`, `&`, `#` or `;` (KMail looks for the first `?` anywhere and reads `&` +/// as `&`), with no `?` at all (xdg-email reads `mailto:a@example.org&attach=…` as a parameter) or +/// after a comma (xdg-email copies the subject into Thunderbird's comma-separated `-compose` +/// argument). The check is repeated after each layer of percent-decoding (KMail decodes names, so +/// `%61ttach` is `attach`). A word without `=` (`Please attach the invoice`) is fine. +/// +/// The address is refused, not stripped of the parameter: PdfCraft never rewrites what a document +/// asks to open, and a stripped address could still be read differently by some email app. +fn check_mail_parameters(rest: &str) -> Result<(), BlockedLink> { + let mut text = rest.to_string(); + for _ in 0..MAIL_DECODE_LAYERS { + check_mail_layer(&text)?; + let decoded = String::from_utf8_lossy(&percent_decode(text.as_bytes())).into_owned(); + if decoded == text { + return Ok(()); + } + text = decoded; + } + check_mail_layer(&text) +} + +/// One layer of [`check_mail_parameters`]. +fn check_mail_layer(text: &str) -> Result<(), BlockedLink> { + // KMail decodes RFC 2047 encoded words (`=?utf-8?q?attach?=`) across the whole address before + // reading it, and its decoder is lenient, so any `=?` followed later by `?=` is refused. + if text.find("=?").is_some_and(|i| text.get(i + 2..).is_some_and(|after| after.contains("?="))) { + return Err(BlockedLink::MailEncodedWord); + } + // xdg-email passes the address through `echo`, which turns `\0141` (octal) or `\x61` into `a`. + if text.split('\\').skip(1).any(|after| after.starts_with(|c: char| c.is_ascii_digit() || matches!(c, 'x' | 'X'))) { + return Err(BlockedLink::MailEncodedWord); + } + let lower = text.to_ascii_lowercase(); + let is_word = |c: char| c.is_ascii_alphanumeric() || matches!(c, '_' | '-'); + let mut previous = None; + for (i, c) in lower.char_indices() { + let starts_word = is_word(c) && !previous.is_some_and(is_word); + previous = Some(c); + if !starts_word { + continue; + } + let Some(word) = lower.get(i..) else { continue }; + let name_end = word.find(|c: char| !is_word(c)).unwrap_or(word.len()); + let (name, after) = word.split_at(name_end); + let asks = MAIL_FILE_PARAMETERS.iter().any(|p| name.starts_with(p)); + if asks && after.trim_start_matches([' ', '\t', '+', '\'', '"']).starts_with('=') { + return Err(BlockedLink::MailFile); + } + } + Ok(()) +} + +/// Decode `%XX` escapes. A `%` not followed by two hex digits is kept as it is. +fn percent_decode(s: &[u8]) -> Vec { + let hex = |b: Option<&u8>| b.and_then(|b| char::from(*b).to_digit(16)).and_then(|d| u8::try_from(d).ok()); + let mut out = Vec::with_capacity(s.len()); + let mut i = 0; + while let Some(&b) = s.get(i) { + if b == b'%' + && let (Some(high), Some(low)) = (hex(s.get(i + 1)), hex(s.get(i + 2))) + { + out.push((high << 4) | low); + i += 3; + } else { + out.push(b); + i += 1; + } + } + out +} + /// The authority of a web address: what follows `//`, up to the first `/`, `\`, `?` or `#`. /// Browsers read `\` as `/` in web addresses (the WHATWG URL standard), so it ends the authority /// too; otherwise `https://other.example\@trusted.example/` would seem to go to @@ -138,6 +241,102 @@ pub fn host(url: &str) -> Option<&str> { (!host.is_empty()).then_some(host) } +/// How to show a web address's host so that a lookalike can't pass for a familiar site. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HostDisplay { + /// The host the browser connects to: lowercase ASCII, with international labels in punycode + /// (`xn--…`) and numeric addresses in their usual form (`192.168.1.1`, `[::1]`). Show this one, + /// not the document's spelling. + pub ascii: String, + /// The host has a label in another alphabet (`xn--…`), whose letters can look like familiar + /// ones. + pub international: bool, + /// A label mixes writing systems, such as the Cyrillic `а` in `pаypal.com`: a common way to + /// imitate another site's name. + pub mixed_scripts: bool, +} + +/// The host of a web address as the browser will see it (see [`HostDisplay`]). On the desktop the +/// address is handed to the operating system as the `url` crate (the WHATWG URL standard) +/// serialises it, and in the browser build the browser parses it by the same standard, so this +/// reads the host with that crate: percent-decoded, IDNA-mapped (case, fullwidth letters, `。`) and +/// with numeric hosts such as `0x7f.1` resolved. `None` for a non-web address, one without a host +/// (see [`host`]), or one the browser would reject. +pub fn display_host(url: &str) -> Option { + host(url)?; + let parsed = url::Url::parse(url).ok()?; + let ascii = match parsed.host()? { + url::Host::Domain(d) => d.to_string(), + url::Host::Ipv4(a) => a.to_string(), + url::Host::Ipv6(a) => format!("[{a}]"), + }; + if !ascii.split('.').any(|label| label.starts_with("xn--")) { + return Some(HostDisplay { ascii, international: false, mixed_scripts: false }); + } + let (unicode, valid) = idna::domain_to_unicode(&ascii); + let mixed_scripts = valid.is_err() || unicode.split('.').any(mixes_scripts); + Some(HostDisplay { ascii, international: true, mixed_scripts }) +} + +/// A writing system for comparing the characters of a label. Han characters are written with +/// Japanese, Korean and Chinese ones, so those three combinations count as one system each +/// (Unicode TR 39's augmented script sets). +#[derive(Clone, Copy, PartialEq, Eq)] +enum Writing { + Script(Script), + Japanese, + Korean, + Chinese, +} + +/// The writing systems a character belongs to, or `None` for one shared by all (digits, `-`, +/// combining marks). +fn writing_systems(c: char) -> Option> { + let scripts = icu_properties::script::ScriptWithExtensions::new().get_script_extensions_val(c); + let mut out = Vec::new(); + for s in scripts.iter() { + if s == Script::Common || s == Script::Inherited { + return None; + } + out.push(Writing::Script(s)); + match s { + Script::Han => out.extend([Writing::Japanese, Writing::Korean, Writing::Chinese]), + Script::Hiragana | Script::Katakana => out.push(Writing::Japanese), + Script::Hangul => out.push(Writing::Korean), + Script::Bopomofo => out.push(Writing::Chinese), + _ => {} + } + } + Some(out) +} + +/// The writing systems every character of `label` belongs to (Unicode TR 39's resolved script +/// set), skipping characters that `skip` picks. `None` when no character narrows it down. +fn shared_writing(label: &str, skip: impl Fn(&[Writing]) -> bool) -> Option> { + let mut shared: Option> = None; + for systems in label.chars().filter_map(writing_systems) { + if skip(&systems) { + continue; + } + shared = Some(match shared { + None => systems, + Some(s) => s.into_iter().filter(|w| systems.contains(w)).collect(), + }); + } + shared +} + +/// Whether a label mixes writing systems. Latin alongside Japanese, Korean or Chinese is allowed, as +/// in Unicode TR 39's "highly restrictive" level (`ソニーstore`). +fn mixes_scripts(label: &str) -> bool { + if shared_writing(label, |_| false).is_none_or(|s| !s.is_empty()) { + return false; + } + let latin = Writing::Script(Script::Latin); + let rest = shared_writing(label, |s| s.contains(&latin)).unwrap_or_default(); + !rest.iter().any(|w| matches!(w, Writing::Japanese | Writing::Korean | Writing::Chinese)) +} + #[cfg(test)] mod tests { #[test] @@ -223,4 +422,136 @@ mod tests { assert_eq!(host("mailto:a@example.org"), None); assert_eq!(host("https:///nohost"), None); } + + #[test] + fn addresses_the_url_parser_rejects_are_refused() { + use super::{BlockedLink as B, document_url}; + // The webbrowser crate would open each of these as a local file path. + for bad in ["mailto://a^b/x", "mailto://a:99999/", "mailto://[x]/", "mailto://a\\b/", "mailto://a b/", "https://a^b/", "http://a f32 { + match self { + OptimizeStage::Discarding => 0.0, + OptimizeStage::Images { done, total } => 0.05 + 0.75 * if total == 0 { 1.0 } else { done.min(total) as f32 / total as f32 }, + OptimizeStage::CleaningUp => 0.8, + OptimizeStage::Merging => 0.85, + OptimizeStage::Writing => 0.9, + } + } +} + +/// The optimized copy's bytes and what was done. +pub type Optimized = (Arc>, OptimizeReport); + +/// What an optimization needs, independent of the session. +pub struct OptimizeJob { + cos: pdfcraft_cos::Document, + settings: optimize::Settings, + discard: Vec, + opts: SaveOptions, +} + +impl Session { + /// Capture what optimizing document `id` with `settings` and the Remove Hidden Information + /// `discard` categories needs. A full rewrite: signed documents are refused. + pub fn optimize_job(&self, id: DocId, settings: &optimize::Settings, discard: &[Hidden]) -> Result { + let doc = self.get(id).ok_or(EditError::NoDocument)?; + if doc.is_signed() { + return Err(EditError::Signed); + } + let editor = doc.editor.as_ref().ok_or_else(|| EditError::ReadOnly(doc.read_only_reason.clone().unwrap_or_default()))?; + Ok(OptimizeJob { + cos: editor.cos.clone(), + settings: settings.clone(), + discard: discard.to_vec(), + opts: SaveOptions { mod_date: self.now().map(pdfcraft_cos::pdf_date), ..SaveOptions::default() }, + }) + } +} + +impl OptimizeJob { + /// Optimize and write the copy. `progress` is called as each stage starts and before each + /// image; returning `false` stops with [`EditError::Cancelled`]. A panic is returned as an + /// error, so a worker thread always reports back. + pub fn run(self, mut progress: impl FnMut(OptimizeStage) -> bool) -> Result { + guard(move || self.work(&mut progress)).map_err(EditError::Optimize)? + } + + fn work(self, progress: &mut dyn FnMut(OptimizeStage) -> bool) -> Result { + let OptimizeJob { mut cos, settings, discard, opts } = self; + let mut step = |stage| if progress(stage) { Ok(()) } else { Err(EditError::Cancelled) }; + step(OptimizeStage::Discarding)?; + let discarded = if discard.is_empty() { Vec::new() } else { pdfcraft_redact::sanitize::remove_hidden(&mut cos, &discard)? }; + let report = optimize::optimize_with_progress(&mut cos, &settings, &mut |s| { + step(match s { + optimize::Stage::Images { done, total } => OptimizeStage::Images { done, total }, + optimize::Stage::CleanUp => OptimizeStage::CleaningUp, + }) + .is_ok() + }) + .map_err(|e| match e { + optimize::OptimizeError::Cancelled => EditError::Cancelled, + e => EditError::Optimize(e.to_string()), + })?; + step(OptimizeStage::Merging)?; + let all: Vec = cos.object_numbers().into_iter().map(|n| pdfcraft_cos::ObjRef::new(n, cos.generation(n))).collect(); + let merged = pdfcraft_organize::dedupe_resources(&mut cos, &all, false); + step(OptimizeStage::Writing)?; + let bytes = write_full(&cos, &opts).map_err(|e| EditError::Write(e.to_string()))?; + Ok((Arc::new(bytes), OptimizeReport { optimize: report, merged, discarded })) + } +} diff --git a/crates/engine/src/signature_image.rs b/crates/engine/src/signature_image.rs new file mode 100644 index 00000000..d1e3ad5b --- /dev/null +++ b/crates/engine/src/signature_image.rs @@ -0,0 +1,327 @@ +//! Local image signatures, shared by Fill & Sign and its automation tool. + +use std::io::{Cursor, Read}; +use std::sync::Arc; + +use image::{DynamicImage, ImageDecoder, ImageFormat, ImageReader, Limits}; +use pdfcraft_render::PageInfo; + +use crate::{Document, Edit, MarkFile, guard}; + +/// Read-only layers for interactive movement: the page without this signature, and its image. +/// Preparing them never changes the working bytes, dirty flag or undo history. +pub struct ImageSignaturePreview { + pub image: SignatureImage, + pub opacity: f32, + /// The page `/Rotate` the picture is drawn turned back by (see [`pdfcraft_annot::picture_rotation`]). + pub turn: i64, + page: usize, + bytes: Arc>, + config: pdfcraft_render::RenderConfig, +} + +impl ImageSignaturePreview { + /// One background worker; the UI reuses its page texture throughout a gesture. + pub fn renderer(&self) -> pdfcraft_render::RenderPool { + pdfcraft_render::RenderPool::new(self.bytes.clone(), 1, self.config.clone()) + } + + /// Synchronous counterpart for automation, with the renderer's normal pixel limits. + pub fn render_background(&self, scale: f32) -> Result { + if !scale.is_finite() || scale <= 0.0 { + return Err("preview scale must be finite and positive".into()); + } + guard(|| { + let mut renderer = pdfcraft_render::PageRenderer::new(self.bytes.clone(), self.config.clone()); + let out = renderer.render(pdfcraft_render::RenderRequest { page: self.page, scale, ..Default::default() }); + if let Some(e) = &out.error { + return Err(e.clone()); + } + Ok(out) + })? + } +} + +impl Document { + /// Prepare the actual embedded signature, including alpha, and an independent background. + /// A cheap COS clone excludes only this annotation; other page content remains visible. + pub fn image_signature_preview(&self, page: usize, index: usize) -> Result, String> { + guard(|| { + let editor = self.editor.as_ref().ok_or("the document can't be read")?; + let Some(picture) = pdfcraft_annot::signature_image(&editor.cos, page, index).map_err(|e| e.to_string())? else { return Ok(None) }; + let obj = editor.cos.get(picture); + let pdfcraft_cos::Object::Stream(s) = &*obj else { return Err("the signature image is damaged".into()) }; + let (w, h) = (s.dict.int(b"Width").unwrap_or(0), s.dict.int(b"Height").unwrap_or(0)); + if w <= 0 || h <= 0 || w > 4096 || h > 4096 || w.saturating_mul(h) > MAX_PIXELS as i64 { + return Err(SignatureImageError::Size.to_string()); + } + let (_, bytes) = pdfcraft_create::image_file(&editor.cos, picture)?; + let image = SignatureImage::from_bytes(&bytes).map_err(|e| e.to_string())?; + let opacity = + pdfcraft_annot::props(&editor.cos, page, index).map(|p| p.opacity).filter(|v| v.is_finite()).unwrap_or(1.0).clamp(0.0, 1.0) as f32; + let turn = pdfcraft_annot::picture_rotation(&editor.cos, page, index).map_err(|e| e.to_string())?; + let mut cos = editor.cos.clone(); + pdfcraft_annot::delete_annotation(&mut cos, page, index).map_err(|e| e.to_string())?; + let bytes = Arc::new(pdfcraft_cos::write_incremental(&cos, &pdfcraft_cos::SaveOptions::default()).map_err(|e| e.to_string())?); + Ok(Some(ImageSignaturePreview { image, opacity, turn, page, bytes, config: self.config.clone() })) + })? + } +} + +/// The user-space box `width` x `height` points as displayed on a page turned by `rotation` (its +/// `/Rotate`), with its left edge at `at` (a user-space point) and vertically centred on it. On a +/// quarter-turned page it is `height` wide and `width` tall in user space. `None` for a non-finite +/// point. +pub fn upright_box(rotation: i64, at: [f64; 2], width: f64, height: f64) -> Option<[f64; 4]> { + if !at.iter().chain(&[width, height]).all(|v| v.is_finite()) { + return None; + } + // Right and up as displayed, as user-space vectors: the page's view matrix without its offset. + let [a, b, c, d, ..] = pdfcraft_model::view_matrix_for(rotation, [0.0; 4]); + let (right, up) = ([a * width, b * width], [c * height / 2.0, d * height / 2.0]); + Some([ + at[0] + right[0].min(0.0) - up[0].abs(), + at[1] + right[1].min(0.0) - up[1].abs(), + at[0] + right[0].max(0.0) + up[0].abs(), + at[1] + right[1].max(0.0) + up[1].abs(), + ]) +} + +/// Saved signatures live in settings, so both source and normalized PNG are bounded. +pub const MAX_SIGNATURE_IMAGE_BYTES: usize = 4 << 20; +const MAX_PIXELS: u64 = 4 << 20; + +#[derive(Debug, thiserror::Error)] +pub enum SignatureImageError { + #[error("Choose a PNG or JPEG image.")] + Format, + #[error("Choose an image smaller than 4 MiB and 4 megapixels (at most 4096 pixels per side).")] + Size, + #[error("Couldn't read the signature image: {0}")] + Read(#[from] std::io::Error), + #[error("Couldn't decode the signature image: {0}")] + Decode(#[from] image::ImageError), +} + +/// A validated image, normalized to PNG without source paths or metadata. Alpha is preserved. +#[derive(Clone, Debug, PartialEq)] +pub struct SignatureImage { + bytes: Arc>, + rgba: Arc>, + size: [usize; 2], +} + +impl SignatureImage { + /// Read at most the file limit plus one byte, even if the file grows while being read. + pub fn read(reader: impl Read) -> Result { + let mut bytes = Vec::new(); + reader.take(MAX_SIGNATURE_IMAGE_BYTES as u64 + 1).read_to_end(&mut bytes)?; + Self::from_bytes(&bytes) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() > MAX_SIGNATURE_IMAGE_BYTES { + return Err(SignatureImageError::Size); + } + let format = image::guess_format(bytes).map_err(|_| SignatureImageError::Format)?; + if !matches!(format, ImageFormat::Png | ImageFormat::Jpeg) { + return Err(SignatureImageError::Format); + } + let mut reader = ImageReader::with_format(Cursor::new(bytes), format); + let mut limits = Limits::default(); + limits.max_image_width = Some(4096); + limits.max_image_height = Some(4096); + limits.max_alloc = Some(32 << 20); + reader.limits(limits); + let mut decoder = reader.into_decoder()?; + let (w, h) = decoder.dimensions(); + if w == 0 || h == 0 || u64::from(w) * u64::from(h) > MAX_PIXELS { + return Err(SignatureImageError::Size); + } + let orientation = decoder.orientation()?; + let mut image = DynamicImage::from_decoder(decoder)?; + image.apply_orientation(orientation); + let rgba = image.into_rgba8(); + let size = [rgba.width() as usize, rgba.height() as usize]; + let mut png = Cursor::new(Vec::new()); + DynamicImage::ImageRgba8(rgba.clone()).write_to(&mut png, ImageFormat::Png)?; + let bytes = png.into_inner(); + if bytes.len() > MAX_SIGNATURE_IMAGE_BYTES { + return Err(SignatureImageError::Size); + } + Ok(Self { bytes: Arc::new(bytes), rgba: Arc::new(rgba.into_raw()), size }) + } + + pub fn bytes(&self) -> &Arc> { + &self.bytes + } + + pub fn rgba(&self) -> &[u8] { + &self.rgba + } + + pub fn size(&self) -> [usize; 2] { + self.size + } + + /// The user-space box for the image with its left edge at `at` (a user-space point on `page`), + /// vertically centered, with the same 150 pt width cap as typed names. Left and centered are + /// as displayed: on a page with `/Rotate` the box runs along the turned page's axes. + pub fn rect(&self, page: &PageInfo, at: [f64; 2], initials: bool) -> Option<[f64; 4]> { + if !at.iter().all(|v| v.is_finite()) { + return None; + } + let [w, h] = self.size; + let scale = (150.0 / w as f64).min(if initials { 24.0 } else { 32.0 } / h as f64); + upright_box(i64::from(page.rotation), at, w as f64 * scale, h as f64 * scale) + } + + pub fn edit(&self, page: usize, info: &PageInfo, at: [f64; 2], initials: bool, author: &str) -> Option { + let rect = self.rect(info, at, initials)?; + let label = if initials { "Add initials" } else { "Add signature" }; + Some(Edit::Batch { + label: label.into(), + edits: vec![Edit::AddCustomStamp { + page, + rect, + name: if initials { "Initials" } else { "Signature" }.into(), + file: MarkFile { name: "signature.png".into(), bytes: self.bytes.clone(), page: 0 }, + author: author.into(), + }], + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A 300 x 400 page shown turned `rotation` degrees clockwise. + fn page(rotation: u16) -> PageInfo { + let (width, height) = if rotation.is_multiple_of(180) { (300.0, 400.0) } else { (400.0, 300.0) }; + PageInfo { width, height, label: String::new(), crop: [0.0, 0.0, 300.0, 400.0], rotation } + } + + fn png(w: u32, h: u32) -> Vec { + let image = image::RgbaImage::from_pixel(w, h, image::Rgba([40, 60, 80, 128])); + let mut out = Cursor::new(Vec::new()); + image.write_to(&mut out, ImageFormat::Png).unwrap(); + out.into_inner() + } + + #[test] + fn image_signatures_preserve_alpha_and_fit_the_signature_box() { + let image = SignatureImage::from_bytes(&png(400, 40)).unwrap(); + assert_eq!(image.size(), [400, 40]); + assert_eq!(&image.rgba()[..4], &[40, 60, 80, 128]); + let Edit::Batch { edits, .. } = image.edit(0, &page(0), [20.0, 100.0], false, "Ada").unwrap() else { panic!() }; + let Edit::AddCustomStamp { rect, .. } = &edits[0] else { panic!() }; + assert_eq!(*rect, [20.0, 92.5, 170.0, 107.5]); + assert!(image.edit(0, &page(0), [f64::NAN, 0.0], false, "").is_none()); + assert_eq!(SignatureImage::from_bytes(image.bytes()).unwrap(), image); + } + + #[test] + fn invalid_and_oversized_signature_images_are_rejected() { + assert!(SignatureImage::from_bytes(b"not an image").is_err()); + assert!(SignatureImage::from_bytes(b"\x89PNG\r\n\x1a\ntruncated").is_err()); + assert!(SignatureImage::from_bytes(&vec![0; MAX_SIGNATURE_IMAGE_BYTES + 1]).is_err()); + assert!(SignatureImage::from_bytes(&png(4097, 1)).is_err()); + assert!(SignatureImage::from_bytes(&png(4096, 1025)).is_err()); + assert!(SignatureImage::read(std::io::repeat(0)).is_err()); + assert!(matches!(SignatureImage::from_bytes(b"GIF89a\x02\0\x02\0"), Err(SignatureImageError::Format))); + } + + #[test] + fn jpeg_signatures_are_normalized_to_png() { + let mut jpeg = Cursor::new(Vec::new()); + image::RgbImage::from_pixel(60, 20, image::Rgb([20, 20, 20])).write_to(&mut jpeg, ImageFormat::Jpeg).unwrap(); + let image = SignatureImage::from_bytes(&jpeg.into_inner()).unwrap(); + assert_eq!(image.size(), [60, 20]); + assert!(image.bytes().starts_with(b"\x89PNG\r\n\x1a\n")); + assert!(image.rgba().as_chunks::<4>().0.iter().all(|p| p[3] == 255)); + } + + #[test] + fn jpeg_signature_orientation_is_applied_before_preview_and_embedding() { + let mut jpeg = Cursor::new(Vec::new()); + image::RgbImage::from_pixel(60, 20, image::Rgb([20, 20, 20])).write_to(&mut jpeg, ImageFormat::Jpeg).unwrap(); + let jpeg = jpeg.into_inner(); + // Synthetic EXIF IFD with Orientation=6 (90 degrees clockwise). + let exif = b"Exif\0\0II\x2a\0\x08\0\0\0\x01\0\x12\x01\x03\0\x01\0\0\0\x06\0\0\0\0\0\0\0"; + let mut rotated = jpeg[..2].to_vec(); + rotated.extend_from_slice(&[0xff, 0xe1]); + rotated.extend_from_slice(&((exif.len() + 2) as u16).to_be_bytes()); + rotated.extend_from_slice(exif); + rotated.extend_from_slice(&jpeg[2..]); + let image = SignatureImage::from_bytes(&rotated).unwrap(); + assert_eq!(image.size(), [20, 60]); + assert_eq!(SignatureImage::from_bytes(image.bytes()).unwrap().size(), [20, 60]); + assert!(!image.bytes().windows(4).any(|w| w == b"Exif")); + } + + #[test] + fn very_wide_image_signatures_keep_the_requested_rectangle() { + let image = SignatureImage::from_bytes(&png(3000, 10)).unwrap(); + let mut session = crate::Session::new(); + let blank = session.create_blank(300.0, 400.0, 1).unwrap(); + let id = session.open_new("form.pdf", blank).unwrap(); + session.apply(id, image.edit(0, &page(0), [20.0, 100.0], false, "Ada").unwrap()).unwrap(); + let a = &session.get(id).unwrap().info.annotations[0]; + assert_eq!(a.rect, [20.0, 99.75, 170.0, 100.25]); + } + + #[test] + fn embedded_signature_preview_preserves_alpha_without_editing_the_document() { + let image = SignatureImage::from_bytes(&png(120, 40)).unwrap(); + let mut session = crate::Session::new(); + let blank = session.create_blank(300.0, 400.0, 1).unwrap(); + let id = session.open_new("form.pdf", blank).unwrap(); + session.apply(id, image.edit(0, &page(0), [20.0, 100.0], false, "Ada").unwrap()).unwrap(); + let doc = session.get(id).unwrap(); + let bytes = doc.bytes.clone(); + let generation = doc.edit_generation(); + let preview = doc.image_signature_preview(0, 0).unwrap().unwrap(); + assert_eq!(preview.image.rgba(), image.rgba()); + let background = preview.render_background(1.0).unwrap(); + assert!(background.rgba.as_chunks::<4>().0.iter().all(|p| *p == [255, 255, 255, 255])); + assert!(Arc::ptr_eq(&bytes, &doc.bytes)); + assert_eq!(doc.edit_generation(), generation); + assert_eq!(doc.can_undo(), Some("Add signature")); + assert!(doc.image_signature_preview(0, 1).is_err()); + assert!(preview.render_background(0.0).is_err()); + } + + #[test] + fn oversized_embedded_signature_preview_is_rejected_before_decoding() { + let image = SignatureImage::from_bytes(&png(120, 40)).unwrap(); + let mut session = crate::Session::new(); + let blank = session.create_blank(300.0, 400.0, 1).unwrap(); + let id = session.open_new("form.pdf", blank).unwrap(); + session.apply(id, image.edit(0, &page(0), [20.0, 100.0], false, "Ada").unwrap()).unwrap(); + let doc = session.doc_mut(id).unwrap(); + let cos = &mut doc.editor.as_mut().unwrap().cos; + let picture = pdfcraft_annot::signature_image(cos, 0, 0).unwrap().unwrap(); + let pdfcraft_cos::Object::Stream(mut stream) = (*cos.get(picture)).clone() else { panic!() }; + stream.dict.set(b"Width".to_vec(), pdfcraft_cos::Object::Int(i64::MAX)); + cos.set(picture, pdfcraft_cos::Object::Stream(stream)); + let error = doc.image_signature_preview(0, 0).err().unwrap(); + assert!(error.contains("4 megapixels"), "{error}"); + } + + /// "Left edge at the point, centered vertically" is as displayed: on a turned page the box + /// is another one in user space. The image is 64 x 32 pt; the displayed point is (60, 100). + #[test] + fn the_signature_box_is_laid_out_as_displayed_on_turned_pages() { + let image = SignatureImage::from_bytes(&png(80, 40)).unwrap(); + for (rotation, at, want) in [ + (0, [60.0, 300.0], [60.0, 284.0, 124.0, 316.0]), + (90, [100.0, 60.0], [84.0, 60.0, 116.0, 124.0]), + (180, [240.0, 100.0], [176.0, 84.0, 240.0, 116.0]), + (270, [200.0, 340.0], [184.0, 276.0, 216.0, 340.0]), + ] { + let rect = image.rect(&page(rotation), at, false).unwrap(); + assert!(rect.iter().zip(want).all(|(a, b)| (a - b).abs() < 0.01), "{rotation}: {rect:?}, want {want:?}"); + } + } +} diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 6689667c..7912e9c6 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -36,6 +36,70 @@ fn session_with(n: usize) -> (Session, DocId) { (s, id) } +#[test] +fn custom_image_stamp_keeps_displayed_orientation_on_rotated_pages() { + let colours = [[240, 20, 20, 255], [20, 180, 20, 255], [20, 20, 240, 255], [230, 180, 20, 255]]; + let image = image::RgbaImage::from_fn(80, 40, |x, y| image::Rgba(colours[usize::from(y >= 20) * 2 + usize::from(x >= 40)])); + let mut png = std::io::Cursor::new(Vec::new()); + image.write_to(&mut png, image::ImageFormat::Png).unwrap(); + let bytes = Arc::new(png.into_inner()); + for degrees in [0, 90, 180, 270] { + let (mut session, id) = session_with(1); + session.apply(id, Edit::RotatePages { pages: vec![0], degrees }).unwrap(); + let at = session.get(id).unwrap().info.pages[0].view_to_user(100.0, 100.0).map(f64::from); + session + .apply( + id, + Edit::AddCustomStamp { + page: 0, + rect: [at[0], at[1], at[0], at[1]], + name: "Original quadrants".into(), + file: MarkFile { name: "quadrants.png".into(), bytes: bytes.clone(), page: 0 }, + author: "Test".into(), + }, + ) + .unwrap(); + let check = |session: &Session, points: [(u32, u32); 4]| { + let doc = session.get(id).unwrap(); + let mut renderer = pdfcraft_render::PageRenderer::new(doc.bytes.clone(), doc.config.clone()); + let rendered = renderer.render(pdfcraft_render::RenderRequest { page: 0, scale: 1.0, ..Default::default() }); + assert!(rendered.error.is_none(), "{:?}", rendered.error); + for ((x, y), colour) in points.into_iter().zip(colours) { + let offset = ((y * rendered.width + x) * 4) as usize; + assert_eq!(&rendered.rgba[offset..offset + 4], &colour, "page rotation {degrees}; displayed point ({x}, {y})"); + } + }; + let points = [(80, 90), (120, 90), (80, 110), (120, 110)]; + check(&session, points); + let info = session.get(id).unwrap().info.pages[0].clone(); + let rect = session.get(id).unwrap().info.annotations[0].rect; + let a = info.user_to_view(rect[0], rect[1]); + let b = info.user_to_view(rect[2], rect[3]); + let bounds = [a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])]; + for (actual, expected) in bounds.into_iter().zip([60.0, 80.0, 140.0, 120.0]) { + assert!((actual - expected).abs() < 0.0001, "displayed bounds {bounds:?}"); + } + session.apply(id, Edit::StyleAnnotation { page: 0, index: 0, color: None, opacity: Some(1.0), width: None, endings: None }).unwrap(); + check(&session, points); + let resized = info.view_rect_to_user([40.0, 70.0, 160.0, 130.0]).map(f64::from); + session.apply(id, Edit::ResizeAnnotation { page: 0, index: 0, rect: resized }).unwrap(); + let resized_points = [(70, 85), (130, 85), (70, 115), (130, 115)]; + check(&session, resized_points); + session.undo(id).unwrap(); + check(&session, points); + session.redo(id).unwrap(); + check(&session, resized_points); + let from = info.view_to_user(100.0, 100.0); + let to = info.view_to_user(110.0, 115.0); + session.apply(id, Edit::MoveAnnotation { page: 0, index: 0, dx: f64::from(to[0] - from[0]), dy: f64::from(to[1] - from[1]) }).unwrap(); + check(&session, [(80, 100), (140, 100), (80, 130), (140, 130)]); + session.undo(id).unwrap(); + check(&session, resized_points); + session.redo(id).unwrap(); + check(&session, [(80, 100), (140, 100), (80, 130), (140, 130)]); + } +} + fn page_texts(s: &Session, id: DocId) -> Vec { let doc = s.get(id).unwrap(); let config = pdfcraft_render::RenderConfig { password: doc.password.as_deref().map(Arc::from), ..Default::default() }; @@ -297,6 +361,46 @@ fn combine_extract_split_and_insert_from_file() { assert!(matches!(bad, Err(EditError::Source(_)))); } +/// A minimal baseline JPEG (headers only; the data is embedded as is). +fn jpeg_bytes() -> Vec { + let mut v = vec![0xFF, 0xD8]; + v.extend_from_slice(&[0xFF, 0xE0, 0, 16, b'J', b'F', b'I', b'F', 0, 1, 1, 1, 0x01, 0x2C, 0x01, 0x2C, 0, 0]); + v.extend_from_slice(&[0xFF, 0xC0, 0, 17, 8, 0, 2, 0, 3, 3, 1, 0x11, 0, 2, 0x11, 1, 3, 0x11, 1]); + v.extend_from_slice(&[0xFF, 0xD9]); + v +} + +#[test] +fn mixed_files_convert_and_combine_in_order() { + let mut s = Session::new(); + let pdf = Arc::new(fixture(2)); + let (kind, same) = s.convert_to_pdf("a.pdf", &pdf).unwrap(); + assert_eq!(kind, SourceKind::Pdf); + assert!(Arc::ptr_eq(&same, &pdf), "a PDF is passed through untouched"); + let (kind, image) = s.convert_to_pdf("scan.jpg", &Arc::new(jpeg_bytes())).unwrap(); + assert_eq!(kind, SourceKind::Image); + let (kind, text) = s.convert_to_pdf("notes.txt", &Arc::new(b"hello".to_vec())).unwrap(); + assert_eq!(kind, SourceKind::Text); + let combined = s.combine_ranges(&[("notes".into(), text, None), ("a".into(), pdf, Some("2".into())), ("scan".into(), image, None)]).unwrap(); + let id = s.open_new("Combined.pdf", combined).unwrap(); + assert_eq!(page_texts(&s, id), ["hello", "Page 2", ""]); + assert_eq!(outline_titles(&s.get(id).unwrap().info.outline), ["notes→1", "a→2", "scan→3"]); +} + +#[test] +fn files_that_cannot_be_converted_are_refused_clearly() { + let s = Session::new(); + let err = s.convert_to_pdf("report.docx", &Arc::new(b"PK\x03\x04".to_vec())).unwrap_err(); + assert_eq!(err, EditError::Source("report.docx: this file type can't be converted; use a PDF, an image or a .txt file".into())); + assert!(matches!(s.convert_to_pdf("empty", &Arc::new(Vec::new())), Err(EditError::Source(_)))); + // Damaged inputs of a known type fail with an error, not a panic. + assert!(s.convert_to_pdf("bad.png", &Arc::new(b"\x89PNG\r\n\x1a\nnope".to_vec())).is_err()); + assert!(s.convert_to_pdf("bad.jpg", &Arc::new(vec![0xFF, 0xD8, 0xFF])).is_err()); + assert!(s.convert_to_pdf("bad.pdf", &Arc::new(b"%PDF-1.7 nope".to_vec())).is_err()); + let err = s.convert_to_pdf("locked.pdf", &protected("pw", "o", -1)).unwrap_err(); + assert_eq!(err, EditError::Source("locked.pdf: it is password-protected".into())); +} + fn protected(user: &str, owner: &str, permissions: i32) -> Arc> { let mut doc = pdfcraft_cos::Document::open(Arc::new(fixture(2))).unwrap(); doc.set_encryption(&pdfcraft_cos::NewEncryption { @@ -631,6 +735,38 @@ fn permissions_password_restricts_others_but_not_this_session() { assert!(s2.get(id3).unwrap().allows_security_change()); } +#[test] +fn combine_source_check_says_why_a_file_cannot_be_combined() { + let (mut s, id) = session_with(1); + let plain = s.get(id).unwrap().bytes.clone(); + assert_eq!(crate::combine_source_check(&plain, None), Ok(())); + // Page assembly withheld (Changes::None) without a password to open it. + let p = Protection { changes: Changes::None, ..protection(None, Some("boss")) }; + s.apply(id, Edit::Protect(p)).unwrap(); + let locked = s.save_bytes(id).unwrap(); + assert_eq!(crate::combine_source_check(&locked, None), Err(crate::SourceProblem::NotPermitted)); + let (mut s, id) = session_with(1); + s.apply(id, Edit::Protect(protection(Some("pw"), Some("owner")))).unwrap(); + let secret = s.save_bytes(id).unwrap(); + assert_eq!(crate::combine_source_check(&secret, None), Err(crate::SourceProblem::Password)); + assert_eq!(crate::combine_source_check(&secret, Some("nope")), Err(crate::SourceProblem::WrongPassword)); + // Its open password reads it, but its permissions withhold assembling pages: the owner's + // password allows it. + assert_eq!(crate::combine_source_check(&secret, Some("pw")), Err(crate::SourceProblem::NotPermitted)); + assert_eq!(crate::combine_source_check(&secret, Some("owner")), Ok(())); + // The permissions password lifts the restriction on copying pages. + assert_eq!(crate::combine_source_check(&locked, Some("boss")), Ok(())); + // Combining with the passwords: the result opens without any. + let s = Session::new(); + let sources = vec![("secret".to_string(), secret, None), ("locked".to_string(), locked, None)]; + assert!(s.combine_ranges(&sources).is_err(), "no passwords, no combining"); + let out = s.combine_unlocked(&sources, &[Some("owner"), Some("boss")]).unwrap(); + let combined = pdfcraft_cos::Document::open(out).unwrap(); + assert!(combined.permissions().is_none(), "the combined file is not encrypted"); + assert_eq!(pdfcraft_organize::page_count(&combined).unwrap(), 2); + assert!(matches!(crate::combine_source_check(&Arc::new(b"not a pdf".to_vec()), None), Err(crate::SourceProblem::Unreadable(_)))); +} + #[test] fn protection_is_validated_undoable_and_never_logged() { let (mut s, id) = session_with(1); @@ -915,6 +1051,39 @@ fn create_and_reduce() { assert_eq!(page_texts(&s3, r), ["Page 1", "Page 1"]); } +#[test] +fn an_optimize_job_reports_its_stages_and_can_be_cancelled() { + use crate::optimizer::OptimizeStage; + let (s, id) = session_with(2); + let settings = optimize::Settings::default(); + let mut stages = Vec::new(); + let (bytes, _) = s + .optimize_job(id, &settings, &[]) + .unwrap() + .run(|st| { + stages.push(st); + true + }) + .unwrap(); + assert!(bytes.starts_with(b"%PDF-")); + assert_eq!(stages.first(), Some(&OptimizeStage::Discarding)); + assert_eq!(&stages[stages.len() - 3..], [OptimizeStage::CleaningUp, OptimizeStage::Merging, OptimizeStage::Writing]); + let fractions: Vec = stages.iter().map(|st| st.fraction()).collect(); + assert!(fractions.windows(2).all(|w| w[0] <= w[1]), "the bar never goes back: {fractions:?}"); + assert!(fractions.iter().all(|f| (0.0..=1.0).contains(f))); + assert_eq!( + OptimizeStage::Images { done: 0, total: 0 }.fraction(), + OptimizeStage::Images { done: 5, total: 5 }.fraction(), + "no images: no division by zero" + ); + + // Cancelled at the merge: nothing is written, the open document is untouched. + let before = s.get(id).unwrap().bytes.clone(); + let r = s.optimize_job(id, &settings, &[]).unwrap().run(|st| st != OptimizeStage::Merging); + assert!(matches!(r, Err(EditError::Cancelled)), "{r:?}"); + assert_eq!(s.get(id).unwrap().bytes, before); +} + #[test] fn flattening_keeps_the_look_and_drops_the_objects() { let mut s = Session::new().with_clock(|| 1_700_000_000); @@ -1482,6 +1651,39 @@ fn exporting_office_files_keeps_images() { assert!(String::from_utf8(d.export_office(compare::OfficeFormat::Html)).unwrap().contains("data:image/png;base64,")); } +#[test] +fn exporting_office_files_keeps_text_colour() { + // #526: a dark green heading came out black in Word. + let content = "BT /F1 24 Tf 0.05 0.23 0.18 rg 72 700 Td (Annual Report) Tj ET \ +BT /F1 11 Tf 0 g 72 650 Td (Black body text that is long enough to be the body size.) Tj ET"; + let objs = [ + "<< /Type /Catalog /Pages 2 0 R >>".to_string(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(), + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>".into(), + format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()), + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >>".into(), + ]; + let mut pdf = b"%PDF-1.7\n".to_vec(); + let mut offsets = Vec::new(); + for (i, o) in objs.iter().enumerate() { + offsets.push(pdf.len()); + pdf.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes()); + } + let xref = pdf.len(); + pdf.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes()); + for o in offsets { + pdf.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes()); + } + pdf.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes()); + let mut s = Session::new(); + let id = s.open("c.pdf", None, Arc::new(pdf), None).unwrap(); + let d = s.get(id).unwrap(); + let html = String::from_utf8(d.export_office(compare::OfficeFormat::Html)).unwrap(); + assert!(html.contains("style=\"color:#0D3B2E\">Annual Report"), "{html}"); + let blocks = &d.export_pages()[0].blocks; + assert!(blocks.iter().any(|b| b.text == "Annual Report" && b.color == [0.05, 0.23, 0.18]), "{blocks:?}"); +} + #[test] fn guard_turns_a_panic_into_an_error() { assert_eq!(guard(|| 7), Ok(7)); @@ -1652,6 +1854,68 @@ fn xfa_data_no_field_binds_to_survives_an_edit_and_save() { assert!(doc.xfa_warnings.iter().any(|w| w.contains("structured content"))); } +#[test] +fn xfa_choice_lists_signature_and_picture_fields_open_fill_script_and_save() { + let data = "
JapanEnglish\nSpanish"; + let bytes = Arc::new(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::fields_template(data))); + let mut s = Session::new().with_clock(|| 1_700_000_000); + let id = s.open("fields.pdf", None, bytes, None).expect("opens"); + let field = |s: &Session, n: &str| s.get(id).unwrap().form.iter().find(|f| f.name == n).cloned().unwrap_or_else(|| panic!("no field {n}")); + use pdfcraft_forms::FieldKind as K; + // The forms layer sees a combo box with its options and the data's saved value, a + // multi-select list with both picks, an editable combo, a password field and a signature. + let country = field(&s, "country"); + assert_eq!((country.kind, country.value.clone()), (K::Combo, vec!["JP".to_string()])); + assert_eq!(country.options, vec![("CA".into(), "Canada".into()), ("FR".into(), "France".into()), ("JP".into(), "Japan".into())]); + let langs = field(&s, "langs"); + assert_eq!(langs.kind, K::List); + assert!(langs.has(pdfcraft_forms::flags::MULTI_SELECT)); + assert_eq!(langs.value, vec!["English".to_string(), "Spanish".to_string()]); + assert!(field(&s, "other").has(pdfcraft_forms::flags::EDIT)); + assert!(field(&s, "pin").has(pdfcraft_forms::flags::PASSWORD)); + assert_eq!(field(&s, "sign").kind, K::Signature); + // Scripts read a choice list's saved value. + assert_eq!(field(&s, "summary").value, vec!["Country: JP".to_string()], "{:?}", s.take_js_output(id)); + // Choosing another item recalculates; the list takes several values; a typed value is kept + // in the editable list and refused in the fixed one. + s.apply(id, Edit::SetFieldValue { name: "country".into(), value: FieldValue::Choice(vec!["CA".into()]) }).unwrap(); + assert_eq!(field(&s, "summary").value, vec!["Country: CA".to_string()]); + s.apply(id, Edit::SetFieldValue { name: "langs".into(), value: FieldValue::Choice(vec!["French".into(), "Spanish".into()]) }).unwrap(); + s.apply(id, Edit::SetFieldValue { name: "other".into(), value: FieldValue::Text("typed".into()) }).unwrap(); + assert!(s.apply(id, Edit::SetFieldValue { name: "country".into(), value: FieldValue::Choice(vec!["Mars".into()]) }).is_err()); + // Saved: the datasets hold the saved values, several on separate lines; reopened, the + // fields and the calculation are what they were. + let saved = s.save_bytes(id).unwrap(); + let cos = pdfcraft_cos::Document::open(saved.clone()).unwrap(); + let d = pdfcraft_xfa::data_of(&cos).unwrap(); + assert_eq!(d.text_at(&pdfcraft_xfa::som_to_path("form[0].page1[0].country[0]")), Some("CA")); + assert_eq!(d.text_at(&pdfcraft_xfa::som_to_path("form[0].page1[0].langs[0]")), Some("French\nSpanish")); + assert_eq!(d.text_at(&pdfcraft_xfa::som_to_path("form[0].page1[0].other[0]")), Some("typed")); + let id2 = s.open("again.pdf", None, saved.clone(), None).unwrap(); + let d2 = s.get(id2).unwrap(); + let f2 = |n: &str| d2.form.iter().find(|f| f.name == n).unwrap().value.clone(); + assert_eq!(f2("country"), vec!["CA".to_string()]); + assert_eq!(f2("langs"), vec!["French".to_string(), "Spanish".to_string()]); + assert_eq!(f2("summary"), vec!["Country: CA".to_string()]); + // The PNG of the image field and the GIF of the draw are pictures on the page. + assert_eq!(s.get(id).unwrap().page_images(0).len(), 2); + // Filled in by another viewer since: saved values, several on separate lines, and shown + // text (an older viewer's), are taken on reopen; shown text that means the current value + // is not a change. + let mut cos = pdfcraft_cos::Document::open(saved).unwrap(); + pdfcraft_xfa::write_data_value(&mut cos, &pdfcraft_xfa::som_to_path("form[0].page1[0].langs[0]"), "English\nSpanish").unwrap(); + pdfcraft_xfa::write_data_value(&mut cos, &pdfcraft_xfa::som_to_path("form[0].page1[0].country[0]"), "Canada").unwrap(); + let edited = Arc::new(pdfcraft_cos::write_incremental(&cos, &pdfcraft_cos::SaveOptions::default()).unwrap()); + let id3 = s.open("elsewhere.pdf", None, edited, None).unwrap(); + let d3 = s.get(id3).unwrap(); + let f3 = |n: &str| d3.form.iter().find(|f| f.name == n).unwrap().value.clone(); + assert_eq!(f3("langs"), vec!["English".to_string(), "Spanish".to_string()]); + assert_eq!(f3("country"), vec!["CA".to_string()]); + let taken: Vec<&String> = d3.info.warnings.iter().filter(|w| w.contains("another viewer")).collect(); + assert_eq!(taken.len(), 1, "{:?}", d3.info.warnings); + assert!(taken[0].contains("langs") && !taken[0].contains("country"), "{}", taken[0]); +} + #[test] fn xfa_scripts_initialize_calculate_validate_toggle_and_add_rows() { let bytes = Arc::new(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::scripted_template())); @@ -1937,3 +2201,69 @@ fn runaway_xfa_calculations_at_open_end_quickly_with_a_report() { assert!(started.elapsed() < std::time::Duration::from_secs(2)); assert!(s.take_js_output(id).errors.is_empty()); } + +/// The page view's raster of `page` at 1 px/pt (the document's own render pool). +fn shown(s: &Session, id: DocId, page: usize) -> pdfcraft_render::RenderedPage { + let doc = s.get(id).unwrap(); + doc.renderer.set_queue(vec![pdfcraft_render::RenderRequest { page, scale: 1.0, ..Default::default() }]); + let start = std::time::Instant::now(); + loop { + if let Some(p) = doc.renderer.try_recv() { + assert!(p.error.is_none(), "{:?}", p.error); + return p; + } + assert!(start.elapsed() < std::time::Duration::from_secs(20), "no render"); + std::thread::sleep(std::time::Duration::from_millis(5)); + } +} + +/// #260 (page 7): comments without an appearance stream, here a FreeText box (`/C` background, +/// blue `/DA` text) and an Ink stroke, weren't drawn. The page view now draws them from +/// appearances made for display only; the working file and what Save writes stay as they were. +#[test] +fn comments_without_appearances_are_drawn_but_not_saved() { + let objs: Vec<&[u8]> = vec![ + b"<< /Type /Catalog /Pages 2 0 R >>", + b"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 300] >>", + b"<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R] >>", + b"<< /Type /Annot /Subtype /FreeText /Rect [20 200 180 260] /Contents (Box) /DA (/Helv 10 Tf 0 0 1 rg) /Border [0 0 1] /C [.85 .47 .02] >>", + b"<< /Type /Annot /Subtype /Ink /Rect [20 20 180 120] /InkList [[30 30 100 110 170 30]] /Border [0 0 4] /C [0 .6 0] >>", + ]; + let mut bytes = b"%PDF-1.7\n".to_vec(); + let mut offsets = Vec::new(); + for (i, o) in objs.iter().enumerate() { + offsets.push(bytes.len()); + bytes.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes()); + bytes.extend_from_slice(o); + bytes.extend_from_slice(b"\nendobj\n"); + } + let xref = bytes.len(); + bytes.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes()); + for o in offsets { + bytes.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes()); + } + bytes.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes()); + let original = Arc::new(bytes); + let mut s = Session::new().with_clock(|| 1_700_000_000); + let id = s.open("comments.pdf", None, original.clone(), None).expect("opens"); + let check = |s: &Session| { + let p = shown(s, id, 0); + let px = |x: u32, y: u32| p.rgba[((y * p.width + x) * 4) as usize..][..4].to_vec(); + // y down: the FreeText box spans rows 40..100, the Ink apex is at (100, 190). + assert_eq!(px(100, 90), vec![217, 120, 5, 255], "the FreeText background"); + // On the apex row: Ink is drawn as a curve through its points (#374), level at the apex. + assert_eq!(px(100, 190), vec![0, 153, 0, 255], "the Ink stroke"); + }; + check(&s); + let doc = s.get(id).unwrap(); + assert!(Arc::ptr_eq(&doc.bytes, &original), "the working file is the file as opened"); + assert!(!Arc::ptr_eq(&doc.display, &doc.bytes) && Arc::ptr_eq(&doc.export_source().bytes, &doc.display), "page images show it too"); + // After an edit (a scoped refresh) they are still drawn, and Save writes no appearances. + s.apply(id, rect_comment(0, [150.0, 270.0, 190.0, 290.0])).unwrap(); + check(&s); + let saved = s.save_bytes(id).unwrap(); + let reopened = pdfcraft_cos::Document::open(saved).unwrap(); + for r in [pdfcraft_cos::ObjRef::new(4, 0), pdfcraft_cos::ObjRef::new(5, 0)] { + assert!(reopened.get(r).as_dict().is_some_and(|d| !d.contains(b"AP")), "{r:?} is saved without /AP"); + } +} diff --git a/crates/engine/src/upright_image.rs b/crates/engine/src/upright_image.rs new file mode 100644 index 00000000..dc2b9cdb --- /dev/null +++ b/crates/engine/src/upright_image.rs @@ -0,0 +1,133 @@ +//! Photos from phones are stored sideways with an EXIF Orientation tag. PDF image XObjects have +//! no such tag, so a JPEG embedded as it is would show up turned. Images placed into a page +//! (stamps, added images, replacements) go through here first. + +use std::borrow::Cow; +use std::io::Cursor; + +use image::codecs::jpeg::{JpegEncoder, PixelDensity, PixelDensityUnit}; +use image::metadata::Orientation; +use image::{DynamicImage, ImageDecoder, ImageFormat, ImageReader}; + +/// Quality of a re-encoded JPEG (the camera's own is rarely higher). +const QUALITY: u8 = 92; +/// Larger photos are embedded as they are: turning them would take hundreds of MB. +const MAX_PIXELS: u64 = 64 << 20; + +/// A JPEG whose EXIF Orientation is not "as stored" has its pixels turned (and re-encoded); +/// everything else, and any JPEG this can't read, is returned unchanged. +pub(crate) fn upright(bytes: &[u8]) -> Cow<'_, [u8]> { + match turned(bytes) { + Some(out) => Cow::Owned(out), + None => Cow::Borrowed(bytes), + } +} + +fn turned(bytes: &[u8]) -> Option> { + if !bytes.starts_with(&[0xFF, 0xD8]) { + return None; + } + let mut decoder = ImageReader::with_format(Cursor::new(bytes), ImageFormat::Jpeg).into_decoder().ok()?; + let orientation = decoder.orientation().ok()?; + let (w, h) = decoder.dimensions(); + if orientation == Orientation::NoTransforms || u64::from(w) * u64::from(h) > MAX_PIXELS { + return None; + } + let mut image = DynamicImage::from_decoder(decoder).ok()?; + image.apply_orientation(orientation); + let mut out = Vec::new(); + { + let mut encoder = JpegEncoder::new_with_quality(&mut out, QUALITY); + // Keep the resolution, which sets the image's natural size on the page. + if let Some(mut density) = jfif_density(bytes) { + if matches!(orientation, Orientation::Rotate90 | Orientation::Rotate270 | Orientation::Rotate90FlipH | Orientation::Rotate270FlipH) { + density.density = (density.density.1, density.density.0); + } + encoder.set_pixel_density(density); + } + match &image { + DynamicImage::ImageLuma8(gray) => encoder.encode_image(gray).ok()?, + _ => encoder.encode_image(&image.to_rgb8()).ok()?, + } + } + Some(out) +} + +/// The resolution in a JFIF header, if the file has one in inches or centimetres. +fn jfif_density(bytes: &[u8]) -> Option { + let mut i = 2usize; + while let Some(&[0xFF, marker, hi, lo]) = bytes.get(i..i.saturating_add(4)) { + let len = usize::from(u16::from_be_bytes([hi, lo])); + let seg = bytes.get(i + 4..i + 2 + len)?; + if marker == 0xE0 && seg.starts_with(b"JFIF\0") { + let &[unit, x0, x1, y0, y1] = seg.get(7..12)? else { return None }; + let unit = match unit { + 1 => PixelDensityUnit::Inches, + 2 => PixelDensityUnit::Centimeters, + _ => return None, + }; + return Some(PixelDensity { density: (u16::from_be_bytes([x0, x1]), u16::from_be_bytes([y0, y1])), unit }); + } + if marker == 0xDA { + return None; + } + i += 2 + len; + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Edit, MarkFile}; + + /// A 60 x 20 JPEG at `dpi` x 72 dpi, tagged with EXIF `orientation` (6: shown turned 90 + /// degrees clockwise). + fn phone_jpeg(orientation: u8, dpi: u16) -> Vec { + let mut jpeg = Vec::new(); + let mut encoder = JpegEncoder::new(&mut jpeg); + encoder.set_pixel_density(PixelDensity { density: (dpi, 72), unit: PixelDensityUnit::Inches }); + encoder.encode_image(&image::RgbImage::from_pixel(60, 20, image::Rgb([20, 20, 20]))).unwrap(); + let mut exif = b"Exif\0\0II\x2a\0\x08\0\0\0\x01\0\x12\x01\x03\0\x01\0\0\0".to_vec(); + exif.extend_from_slice(&[orientation, 0, 0, 0, 0, 0, 0, 0]); + let mut out = jpeg[..2].to_vec(); + out.extend_from_slice(&[0xff, 0xe1]); + out.extend_from_slice(&((exif.len() + 2) as u16).to_be_bytes()); + out.extend_from_slice(&exif); + out.extend_from_slice(&jpeg[2..]); + out + } + + #[test] + fn orientation_is_applied_and_plain_images_are_left_alone() { + let photo = phone_jpeg(6, 144); + let turned = upright(&photo); + assert!(matches!(turned, Cow::Owned(_))); + let decoded = image::load_from_memory(&turned).unwrap(); + assert_eq!((decoded.width(), decoded.height()), (20, 60)); + assert_eq!(jfif_density(&turned), Some(PixelDensity { density: (72, 144), unit: PixelDensityUnit::Inches })); + let stored = phone_jpeg(1, 72); + assert!(matches!(upright(&stored), Cow::Borrowed(_))); + assert!(matches!(upright(b"\x89PNG\r\n\x1a\n"), Cow::Borrowed(_))); + assert!(matches!(upright(b"\xff\xd8 not a jpeg"), Cow::Borrowed(_))); + } + + #[test] + fn a_phone_photo_used_as_a_custom_stamp_is_embedded_upright() { + let mut session = crate::Session::new(); + let blank = session.create_blank(300.0, 400.0, 1).unwrap(); + let id = session.open_new("form.pdf", blank).unwrap(); + // A click places the stamp at its natural size: 30 x 20 pt as stored, 20 x 30 pt upright. + // Named "Signature" so `signature_image` finds its picture. + let file = MarkFile { name: "photo.jpg".into(), bytes: std::sync::Arc::new(phone_jpeg(6, 144)), page: 0 }; + let stamp = Edit::AddCustomStamp { page: 0, rect: [100.0, 100.0, 100.0, 100.0], name: "Signature".into(), file, author: "Ada".into() }; + session.apply(id, stamp).unwrap(); + let cos = &session.get(id).unwrap().editor.as_ref().unwrap().cos; + let picture = pdfcraft_annot::signature_image(cos, 0, 0).unwrap().unwrap(); + let obj = cos.get(picture); + let pdfcraft_cos::Object::Stream(s) = &*obj else { panic!() }; + assert_eq!((s.dict.int(b"Width"), s.dict.int(b"Height")), (Some(20), Some(60))); + let rect = pdfcraft_annot::summaries(cos)[0].rect; + assert!(((rect[2] - rect[0]) - 20.0).abs() < 0.01 && ((rect[3] - rect[1]) - 30.0).abs() < 0.01, "{rect:?}"); + } +} diff --git a/crates/engine/src/xfa.rs b/crates/engine/src/xfa.rs index 50fdec2a..f8ef3fd2 100644 --- a/crates/engine/src/xfa.rs +++ b/crates/engine/src/xfa.rs @@ -208,6 +208,8 @@ struct FieldInfo { kind: pdfcraft_forms::FieldKind, value: Vec, on_state: String, + /// List boxes that take several values (one per line in the data). + multi: bool, } fn field_infos(doc: &pdfcraft_cos::Document) -> HashMap { @@ -215,7 +217,8 @@ fn field_infos(doc: &pdfcraft_cos::Document) -> HashMap { .into_iter() .map(|f| { let on_state = f.widgets.first().and_then(|w| w.on_state.clone()).unwrap_or_else(|| "1".into()); - (f.name, FieldInfo { kind: f.kind, value: f.value, on_state }) + let multi = f.kind == pdfcraft_forms::FieldKind::List && f.has(pdfcraft_forms::flags::MULTI_SELECT); + (f.name, FieldInfo { kind: f.kind, value: f.value, on_state, multi }) }) .collect() } @@ -380,11 +383,13 @@ impl<'a, 'b> Runner<'a, 'b> { } // Read-only fields still take values from scripts (calculated totals are read-only). K::PushButton | K::Signature => return false, + K::List if info.multi => value.lines().map(str::trim).filter(|l| !l.is_empty()).map(str::to_string).collect(), _ => vec![value.to_string()], }; let same = match info.kind { K::CheckBox => info.value.is_empty() == new_value.is_empty(), K::Radio => info.value.first() == new_value.first(), + K::List if info.multi => info.value == new_value, _ => info.value.first().map_or(value.is_empty(), |v| v == value), }; if same { diff --git a/crates/export/src/lib.rs b/crates/export/src/lib.rs index e92d09ec..5e78640a 100644 --- a/crates/export/src/lib.rs +++ b/crates/export/src/lib.rs @@ -21,6 +21,8 @@ pub struct Block { pub size: f64, pub bold: bool, pub italic: bool, + /// Text fill colour as sRGB components in 0..=1 (black when unknown). + pub color: [f64; 3], } /// An image of a page. @@ -48,6 +50,8 @@ pub struct Cell { pub size: f64, pub bold: bool, pub italic: bool, + /// Text fill colour as sRGB components in 0..=1. + pub color: [f64; 3], pub span: usize, } @@ -96,10 +100,11 @@ fn level(b: &Block, body: f64) -> u8 { /// How far two cell left edges may drift (points) and still be the same grid column. const COL_TOL: f64 = 4.0; -/// More grid columns than this isn't a table. It also bounds a table's size: every row is -/// materialized to all its columns, so a hostile page laid out as a staircase of blocks would -/// otherwise make (blocks / 2)² cells. -const MAX_COLS: usize = 64; +/// More grid columns than this isn't a table. It is Word's limit: a .docx whose table has 64 or +/// more columns doesn't open. It also bounds a table's size: every row is materialized to all +/// its columns, so a hostile page laid out as a staircase of blocks would otherwise make +/// (blocks / 2)² cells. +const MAX_COLS: usize = 63; /// A cell is short: taller blocks are body text (or a multi-column layout), not table cells. fn is_cell_like(b: &Block) -> bool { (b.rect[3] - b.rect[1]) <= b.size * 5.0 && !b.text.trim().is_empty() @@ -198,7 +203,7 @@ fn tables(blocks: &[Block]) -> (Vec
, Vec) { let b = &blocks[bi]; let Some(slot) = edges.iter().position(|e| (*e - b.rect[0]).abs() <= COL_TOL) else { continue }; let span = edges[slot + 1..].iter().filter(|e| **e > b.rect[0] + COL_TOL && **e < b.rect[2] - COL_TOL).count() + 1; - let cell = Cell { text: b.text.trim().to_string(), size: b.size, bold: b.bold, italic: b.italic, span }; + let cell = Cell { text: b.text.trim().to_string(), size: b.size, bold: b.bold, italic: b.italic, color: b.color, span }; match &mut slots[slot] { Some(c) => { c.text.push(' '); @@ -219,7 +224,7 @@ fn tables(blocks: &[Block]) -> (Vec
, Vec) { } None => { column += 1; - materialized.push(Cell { text: String::new(), size: 11.0, bold: false, italic: false, span: 1 }); + materialized.push(Cell { text: String::new(), size: 11.0, bold: false, italic: false, color: [0.0; 3], span: 1 }); } } } @@ -360,9 +365,10 @@ table{{border-collapse:collapse;margin:1em 0}}td,th{{border:1px solid #999;paddi if b.bold && lvl == 0 { t = format!("{t}"); } + let style = hex(b.color).map_or(String::new(), |h| format!(" style=\"color:#{h}\"")); match lvl { - 0 => s.push_str(&format!("

{t}

\n")), - l => s.push_str(&format!("{t}\n")), + 0 => s.push_str(&format!("{t}

\n")), + l => s.push_str(&format!("{t}\n")), } } Item::Table(t) => { @@ -378,7 +384,8 @@ table{{border-collapse:collapse;margin:1em 0}}td,th{{border:1px solid #999;paddi body = format!("{body}"); } let span = if c.span > 1 { format!(" colspan=\"{}\"", c.span) } else { String::new() }; - s.push_str(&format!("{body}")); + let style = hex(c.color).map_or(String::new(), |h| format!(" style=\"color:#{h}\"")); + s.push_str(&format!("{body}")); } s.push_str("\n"); } @@ -395,8 +402,20 @@ table{{border-collapse:collapse;margin:1em 0}}td,th{{border:1px solid #999;paddi s } +/// A colour's 8-bit sRGB components (NaN and out-of-range values clamp; NaN reads as 0). +fn rgb8(c: [f64; 3]) -> [u8; 3] { + // The value is clamped to 0..=255 before the cast, so it cannot truncate. + c.map(|v| if v.is_finite() { (v.clamp(0.0, 1.0) * 255.0).round() as u8 } else { 0 }) +} + +/// The colour as `RRGGBB`, or `None` for black (the default every format already uses). +fn hex(c: [f64; 3]) -> Option { + let [r, g, b] = rgb8(c); + (r | g | b != 0).then(|| format!("{r:02X}{g:02X}{b:02X}")) +} + /// A formatted text run (shared by paragraphs and table cells). -fn run_xml(text: &str, size: f64, bold: bool, italic: bool) -> String { +fn run_xml(text: &str, size: f64, bold: bool, italic: bool, color: [f64; 3]) -> String { let mut rpr = String::new(); if bold { rpr.push_str(""); @@ -404,6 +423,9 @@ fn run_xml(text: &str, size: f64, bold: bool, italic: bool) -> String { if italic { rpr.push_str(""); } + if let Some(h) = hex(color) { + rpr.push_str(&format!("")); + } let half_points = if size.is_finite() { (size * 2.0).round().clamp(2.0, 3276.0) as i64 } else { 24 }; rpr.push_str(&format!("")); format!("{rpr}{}", esc(text)) @@ -439,7 +461,7 @@ fn docx_table(t: &Table) -> String { let w: i64 = widths.iter().take(c.span.min(widths.len())).sum(); s.push_str(&format!( "{span}{}", - run_xml(&c.text, c.size, c.bold, c.italic) + run_xml(&c.text, c.size, c.bold, c.italic, c.color) )); } // Pad the grid so every row covers all columns (Word rejects short rows). @@ -467,7 +489,7 @@ pub fn docx(pages: &[Page], title: &str) -> Vec { let mut media: Vec<(String, &Image)> = Vec::new(); // Word needs a paragraph between adjacent tables and after the last one in the body. let mut after_table = false; - let run = |b: &Block| run_xml(&b.text, b.size, b.bold, b.italic); + let run = |b: &Block| run_xml(&b.text, b.size, b.bold, b.italic, b.color); for it in items(pages) { match &it { Item::Para(b, lvl) => { @@ -585,8 +607,36 @@ fn rtf_text(s: &str) -> String { /// Rich Text Format: paragraphs with their sizes and bold/italic, real table rows, page breaks /// between pages (images are left out). pub fn rtf(pages: &[Page]) -> String { + let items = items(pages); + // The colour table: entry 0 is the default (auto) colour, then each non-black colour once. + let mut colors: Vec<[u8; 3]> = Vec::new(); + let mut note = |c: [f64; 3]| { + let c = rgb8(c); + if c != [0, 0, 0] && !colors.contains(&c) { + colors.push(c); + } + }; + for it in &items { + match it { + Item::Para(b, _) => note(b.color), + Item::Table(t) => t.rows.iter().flatten().for_each(|c| note(c.color)), + Item::Img(_) | Item::PageBreak => {} + } + } + // `\cfN` for a colour (nothing for black, which keeps the default). + let cf = |c: [f64; 3]| { + let c = rgb8(c); + colors.iter().position(|e| *e == c).map_or(String::new(), |i| format!("\\cf{}", i + 1)) + }; let mut s = String::from("{\\rtf1\\ansi\\deff0{\\fonttbl{\\f0 Helvetica;}}\n"); - for it in items(pages) { + if !colors.is_empty() { + s.push_str("{\\colortbl;"); + for [r, g, b] in &colors { + s.push_str(&format!("\\red{r}\\green{g}\\blue{b};")); + } + s.push_str("}\n"); + } + for it in items { match it { Item::Para(b, _) => { let mut fmt = format!("\\fs{}", (b.size * 2.0).round() as i64); @@ -596,6 +646,7 @@ pub fn rtf(pages: &[Page]) -> String { if b.italic { fmt.push_str("\\i"); } + fmt.push_str(&cf(b.color)); s.push_str(&format!("{{\\pard{fmt} {}\\par}}\n", rtf_text(&b.text))); } Item::Table(t) => { @@ -621,6 +672,7 @@ pub fn rtf(pages: &[Page]) -> String { if c.italic { fmt.push_str("\\i"); } + fmt.push_str(&cf(c.color)); s.push_str(&format!("{{{fmt} {}}}\\cell", rtf_text(&c.text))); } s.push_str("\\row\n"); @@ -639,7 +691,14 @@ mod tests { use super::*; fn page() -> Page { - let b = |t: &str, y: f64, size: f64, bold: bool| Block { text: t.into(), rect: [72.0, y, 500.0, y + size], size, bold, italic: false }; + let b = |t: &str, y: f64, size: f64, bold: bool| Block { + text: t.into(), + rect: [72.0, y, 500.0, y + size], + size, + bold, + italic: false, + color: [0.0; 3], + }; Page { width: 612.0, height: 792.0, @@ -711,6 +770,7 @@ mod tests { size: f64::NAN, bold: false, italic: false, + color: [0.0; 3], }); let xml = part(&docx(&[p.clone()], "Receipt\u{1}"), "word/document.xml"); assert!(xml.contains(">Total 4.50\tGBP"), "{xml}"); @@ -724,10 +784,35 @@ mod tests { assert!(xml.contains(""), "{xml}"); + assert!(xml.contains(""), "out-of-range values clamp: {xml}"); + // Black text keeps Word's default colour. + assert_eq!(xml.matches("Annual Report"), "{h}"); + assert!(h.contains("

"), "{h}"); + let r = rtf(&[p]); + assert!(r.contains("{\\colortbl;\\red13\\green59\\blue46;\\red0\\green255\\blue0;}"), "{r}"); + assert!(r.contains("\\cf1 Annual Report"), "{r}"); + // A table cell keeps its colour too. + let mut t = table_page(); + t.blocks[0].color = [1.0, 0.0, 0.0]; + let xml = part(&docx(&[t.clone()], "Grid"), "word/document.xml"); + assert!(xml.contains(""), "{xml}"); + assert!(html(&[t], "Grid").contains("style=\"color:#FF0000\"")); + } + #[test] fn rtf_escapes_and_sizes() { let mut p = page(); - p.blocks.push(Block { text: "Café {x}".into(), rect: [72.0, 100.0, 200.0, 110.0], size: 10.0, bold: false, italic: true }); + p.blocks.push(Block { text: "Café {x}".into(), rect: [72.0, 100.0, 200.0, 110.0], size: 10.0, bold: false, italic: true, color: [0.0; 3] }); let r = rtf(&[p]); assert!(r.starts_with("{\\rtf1") && r.ends_with('}')); assert!(r.contains("\\fs48\\b Annual Report")); @@ -736,7 +821,14 @@ mod tests { /// A 3-column table with a spanning header row and a hole in the last row. fn table_page() -> Page { - let cell = |t: &str, x: f64, y: f64, w: f64| Block { text: t.into(), rect: [x, y, x + w, y + 12.0], size: 11.0, bold: false, italic: false }; + let cell = |t: &str, x: f64, y: f64, w: f64| Block { + text: t.into(), + rect: [x, y, x + w, y + 12.0], + size: 11.0, + bold: false, + italic: false, + color: [0.0; 3], + }; Page { width: 612.0, height: 792.0, @@ -757,6 +849,7 @@ mod tests { size: 11.0, bold: false, italic: false, + color: [0.0; 3], }, ], images: Vec::new(), @@ -800,7 +893,7 @@ mod tests { .map(|i| { let (row, col) = (i / 2, i / 2 + i % 2); let (x, y) = (10.0 + col as f64 * 20.0, 10_000.0 - row as f64 * 14.0); - Block { text: "x".into(), rect: [x, y, x + 8.0, y + 12.0], size: 11.0, bold: false, italic: false } + Block { text: "x".into(), rect: [x, y, x + 8.0, y + 12.0], size: 11.0, bold: false, italic: false, color: [0.0; 3] } }) .collect(); let started = std::time::Instant::now(); @@ -809,6 +902,28 @@ mod tests { assert!(started.elapsed() < std::time::Duration::from_secs(5), "took {:?}", started.elapsed()); } + #[test] + fn word_opens_tables_up_to_its_column_limit() { + // Rows of short numbers on a regular grid, n columns wide. + let grid = |n: usize| Page { + width: 1500.0, + height: 792.0, + blocks: (0..3) + .flat_map(|row| { + (0..n).map(move |col| { + let (x, y) = (20.0 + col as f64 * 22.0, 700.0 - row as f64 * 14.0); + Block { text: (col + 1).to_string(), rect: [x, y, x + 8.0, y + 8.0], size: 8.0, bold: false, italic: false, color: [0.0; 3] } + }) + }) + .collect(), + images: Vec::new(), + }; + let grid_cols = |p: &Page| part(&docx(std::slice::from_ref(p), "Grid"), "word/document.xml").matches(" Result { }; // Arabic interface faces are small, so the web build keeps them too. let arabic = scripts.split(',').any(|s| s.trim() == "Arab"); - if wasm && !(*family == "BIZ UDPGothic" && *style == "Regular") && !arabic { + // Telugu faces too, for the Telugu interface. + let telugu = scripts.split(',').any(|s| s.trim() == "Telu"); + let web_face = (*family == "BIZ UDPGothic" && *style == "Regular") || arabic || telugu; + if wasm && !web_face { continue; } let path = dir.join(file).canonicalize().map_err(|e| format!("{file}: {e}"))?; diff --git a/crates/fonts/src/arabic.rs b/crates/fonts/src/arabic.rs new file mode 100644 index 00000000..6a4a53a6 --- /dev/null +++ b/crates/fonts/src/arabic.rs @@ -0,0 +1,171 @@ +//! Arabic text written into PDFs: shaping (joining forms, lam-alef ligatures, mark placement) with +//! the craft-fonts `Arab` face, and that face's glyph outlines. + +use std::sync::OnceLock; + +use harfrust::{BufferClusterLevel, Direction, ShapeOptions, ShaperData, UnicodeBuffer, script}; +use skrifa::instance::{LocationRef, Size}; +use skrifa::raw::TableProvider; +use skrifa::{FontRef, GlyphId, MetadataProvider}; + +use crate::{GlyphError, GlyphOutline}; + +/// One cluster of shaped Arabic text: the glyphs that show one character (a letter's body and +/// dots, say), or several characters for a ligature. Em units. +#[derive(Clone, Debug, PartialEq)] +pub struct ShapedCluster { + /// Glyph ids, each with its offset from the cluster's pen position (marks sit above or below). + pub glyphs: Vec<(u32, [f64; 2])>, + pub advance: f64, + /// The characters it shows, in logical order. + pub text: String, +} + +fn face() -> Option<&'static (FontRef<'static>, ShaperData)> { + static FACE: OnceLock, ShaperData)>> = OnceLock::new(); + FACE.get_or_init(|| { + let font = FontRef::new(crate::document_arabic_font()?.bytes).ok()?; + let data = ShaperData::new(&font); + Some((font, data)) + }) + .as_ref() +} + +/// Whether the Arabic document face has a glyph for `c` (false without the face). +pub fn arabic_has(c: char) -> bool { + face().is_some_and(|(font, _)| font.charmap().map(c).is_some()) +} + +/// Shape one directional run of `text` with the Arabic document face. The clusters come in +/// drawing order, left to right. [`GlyphError::NoFont`] without the face; +/// [`GlyphError::Missing`] when it lacks a character. +pub fn shape_arabic(text: &str, rtl: bool) -> Result, GlyphError> { + let (font, data) = face().ok_or(GlyphError::NoFont)?; + let shaper = data.shaper(font).build(); + let scale = 1.0 / f64::from(shaper.units_per_em().max(1)); + let mut buffer = UnicodeBuffer::new(); + buffer.push_str(text); + buffer.set_direction(if rtl { Direction::RightToLeft } else { Direction::LeftToRight }); + buffer.set_script(script::ARABIC); + // One cluster per character, so marks keep their own text for copy and search. + buffer.set_cluster_level(BufferClusterLevel::Characters); + let shaped = shaper.shape(buffer, ShapeOptions::new()); + // Clusters are byte offsets into `text`; a cluster shows the characters up to the next one. + let mut starts: Vec = shaped.glyph_infos().iter().map(|g| g.cluster as usize).collect(); + starts.sort_unstable(); + starts.dedup(); + let mut out: Vec<(usize, ShapedCluster)> = Vec::new(); + for (info, pos) in shaped.glyph_infos().iter().zip(shaped.glyph_positions()) { + if info.glyph_id == 0 { + return Err(GlyphError::Missing); + } + let start = info.cluster as usize; + // A cluster's glyphs are adjacent in the shaped run. + let cluster = match out.last_mut() { + Some((s, c)) if *s == start => c, + _ => { + let end = starts.get(starts.partition_point(|s| *s <= start)).copied().unwrap_or(text.len()); + let text = text.get(start..end).unwrap_or_default().to_string(); + out.push((start, ShapedCluster { glyphs: Vec::new(), advance: 0.0, text })); + // Just pushed. + let Some((_, c)) = out.last_mut() else { continue }; + c + } + }; + let at = [cluster.advance + f64::from(pos.x_offset) * scale, f64::from(pos.y_offset) * scale]; + cluster.glyphs.push((info.glyph_id, at)); + cluster.advance += f64::from(pos.x_advance) * scale; + } + Ok(out.into_iter().map(|(_, c)| c).collect()) +} + +/// Glyph `id` of the Arabic document face, bounded like [`crate::japanese_glyph`]. Glyphs without +/// outlines (spaces) come back empty. +pub fn arabic_glyph(id: u32) -> Result { + let (font, _) = face().ok_or(GlyphError::NoFont)?; + if font.maxp().map_or(true, |m| id >= u32::from(m.num_glyphs())) { + return Err(GlyphError::Missing); + } + let gid = GlyphId::new(id); + let loc = LocationRef::default(); + let scale = 1.0 / font.metrics(Size::unscaled(), loc).units_per_em.max(1) as f64; + let width = font.glyph_metrics(Size::unscaled(), loc).advance_width(gid).unwrap_or(0.0) as f64 * scale; + // Marks have no advance; the widest ligatures (U+FDFD) span a few em. + if !width.is_finite() || !(0.0..=8.0).contains(&width) { + return Err(GlyphError::Missing); + } + if font.outline_glyphs().get(gid).is_none() { + return Ok(GlyphOutline { contours: Vec::new(), width, bbox: [0.0, 0.0, width, 0.0] }); + } + crate::script::bounded_outline(font, gid, width) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn face_or_skip() -> bool { + if crate::document_arabic_font().is_none() { + eprintln!("skipping the Arabic shaping checks: built without a craft-fonts Arab face (set CRAFT_FONTS_DIR)"); + assert_eq!(shape_arabic("ب", true), Err(GlyphError::NoFont)); + assert_eq!(arabic_glyph(1).map(|_| ()), Err(GlyphError::NoFont)); + assert!(!arabic_has('ب')); + return false; + } + true + } + + #[test] + fn letters_take_their_joining_forms() { + if !face_or_skip() { + return; + } + let ids = + |s: &str| shape_arabic(s, true).unwrap().iter().flat_map(|c| c.glyphs.iter().map(|g| g.0)).collect::>(); + let (isolated, joined) = (ids("ب"), ids("ببب")); + // Initial, medial and final forms are glyphs the isolated letter doesn't use. + assert!(joined.difference(&isolated).count() >= 2, "{isolated:?} {joined:?}"); + // One cluster per letter, however many glyphs the face draws it with (Noto Sans Arabic: + // body and dots). + let c = shape_arabic("ببب", true).unwrap(); + assert!(c.len() == 3 && c.iter().all(|c| c.text == "ب" && c.advance > 0.0 && !c.glyphs.is_empty()), "{c:?}"); + } + + #[test] + fn right_to_left_runs_are_drawn_from_their_last_letter() { + if !face_or_skip() { + return; + } + let g = shape_arabic("سلام", true).unwrap(); + let drawn: String = g.iter().map(|g| g.text.as_str()).collect(); + assert_eq!(drawn.chars().rev().collect::(), "سلام"); + assert!(shape_arabic("سلام", false).unwrap().iter().map(|g| g.text.as_str()).collect::() == "سلام"); + } + + #[test] + fn marks_keep_their_own_text_and_no_advance() { + if !face_or_skip() { + return; + } + let c = shape_arabic("بَ", true).unwrap(); + assert_eq!(c.len(), 2, "{c:?}"); + let mark = c.iter().find(|c| c.text == "\u{064E}").unwrap(); + assert_eq!(mark.advance, 0.0); + let outline = arabic_glyph(mark.glyphs[0].0).unwrap(); + assert!(!outline.contours.is_empty() && outline.bbox[1] > 0.0, "the fatha sits above the baseline"); + } + + #[test] + fn spaces_and_missing_characters() { + if !face_or_skip() { + return; + } + let space = shape_arabic(" ", true).unwrap(); + assert!(space[0].advance > 0.0); + assert!(arabic_glyph(space[0].glyphs[0].0).unwrap().contours.is_empty()); + assert!(arabic_has('ب') && !arabic_has('日')); + assert_eq!(shape_arabic("日", true), Err(GlyphError::Missing)); + assert_eq!(arabic_glyph(u32::MAX).map(|_| ()), Err(GlyphError::Missing)); + assert!(shape_arabic("", true).unwrap().is_empty()); + } +} diff --git a/crates/fonts/src/craft.rs b/crates/fonts/src/craft.rs index 2b9e8b92..36d45002 100644 --- a/crates/fonts/src/craft.rs +++ b/crates/fonts/src/craft.rs @@ -49,10 +49,23 @@ pub fn ui_arabic_fonts() -> Vec<&'static CraftFont> { arabic(CRAFT_FONTS.iter()) } +/// The face for Arabic text written into PDFs: the first `Arab` face. `None` when built without +/// craft-fonts or when its revision has no Arabic face. +pub fn document_arabic_font() -> Option<&'static CraftFont> { + ui_arabic_fonts().into_iter().next() +} + fn arabic<'a>(faces: impl IntoIterator) -> Vec<&'a CraftFont> { faces.into_iter().filter(|f| f.covers("Arab")).collect() } +/// The `Telu` craft-fonts faces for Telugu interface text (the Telugu catalog, file names, +/// document titles), in manifest order. Empty when built without craft-fonts or when it has no +/// Telugu face. +pub fn ui_telugu_fonts() -> Vec<&'static CraftFont> { + CRAFT_FONTS.iter().filter(|f| f.covers("Telu")).collect() +} + /// Interface CJK faces in fallback order for the UI language: Simplified Chinese first when /// `prefer_hans`, otherwise Japanese first (the historical default). /// @@ -78,12 +91,47 @@ fn order_cjk<'a>(faces: impl IntoIterator, prefer_hans: bo /// The face for Japanese text written into PDFs (serif document text): Shippori Mincho, then /// BIZ UDMincho, then any other regular `Jpan` face. `None` without craft-fonts. pub fn document_japanese_font() -> Option<&'static CraftFont> { - let jpan = || CRAFT_FONTS.iter().filter(|f| f.covers("Jpan")); - ["Shippori Mincho", "BIZ UDMincho"] - .iter() - .find_map(|family| jpan().find(|f| f.family == *family && f.style == "Regular")) - .or_else(|| jpan().find(|f| f.style == "Regular")) - .or_else(|| jpan().next()) + document_face(CRAFT_FONTS, true, false) +} + +/// A real Japanese document face matching serif/sans and weight where available. +/// Sans text prefers BIZ UDPGothic Bold or Regular. Missing weights fall back to Regular; +/// serif text keeps the document Mincho preference. No synthetic weight or slant is applied. +/// `None` without craft-fonts. The small web input currently has only Gothic Regular. +pub fn document_japanese_font_for_style(serif: bool, bold: bool) -> Option<&'static CraftFont> { + document_face(CRAFT_FONTS, serif, bold) +} + +/// Every `Jpan` face that can stand in for the document text, the best match for the style first +/// (the face [`document_japanese_font_for_style`] returns), then the others. Not every face has +/// every glyph (their coverage of e.g. Cyrillic differs), so a caller can move on to the next one. +/// Empty without craft-fonts. +pub fn document_japanese_fonts_for_style(serif: bool, bold: bool) -> Vec<&'static CraftFont> { + document_faces(CRAFT_FONTS, serif, bold) +} + +fn document_face(faces: &[CraftFont], serif: bool, bold: bool) -> Option<&CraftFont> { + document_faces(faces, serif, bold).into_iter().next() +} + +fn document_faces(faces: &[CraftFont], serif: bool, bold: bool) -> Vec<&CraftFont> { + let jpan = || faces.iter().filter(|f| f.covers("Jpan")); + let mut preferred = Vec::new(); + if !serif { + let style = if bold { "Bold" } else { "Regular" }; + preferred.extend(jpan().find(|f| f.family == "BIZ UDPGothic" && f.style == style)); + preferred.extend(jpan().find(|f| f.family == "BIZ UDPGothic" && f.style == "Regular")); + } + for family in ["Shippori Mincho", "BIZ UDMincho"] { + preferred.extend(jpan().find(|f| f.family == family && f.style == "Regular")); + } + let mut out: Vec<&CraftFont> = Vec::new(); + for face in preferred.into_iter().chain(jpan().filter(|f| f.style == "Regular")).chain(jpan()) { + if !out.iter().any(|f| std::ptr::eq(*f, face)) { + out.push(face); + } + } + out } const fn str_eq(a: &str, b: &str) -> bool { @@ -149,6 +197,29 @@ mod tests { assert_eq!(ja, ["BIZ UDPGothic", "Shippori Mincho", "FakeHans"]); } + #[test] + fn document_faces_match_style_without_inventing_missing_weights() { + let faces = [ + CraftFont { family: "Shippori Mincho", style: "Regular", scripts: &["Jpan"], bytes: b"serif" }, + CraftFont { family: "BIZ UDPGothic", style: "Regular", scripts: &["Jpan"], bytes: b"sans" }, + CraftFont { family: "BIZ UDPGothic", style: "Bold", scripts: &["Jpan"], bytes: b"bold" }, + CraftFont { family: "BIZ UDPGothic", style: "Bold", scripts: &["Latn"], bytes: b"not-japanese" }, + ]; + assert_eq!(document_face(&faces, false, false).unwrap().bytes, b"sans"); + assert_eq!(document_face(&faces, false, true).unwrap().bytes, b"bold"); + assert_eq!(document_face(&faces, true, false).unwrap().bytes, b"serif"); + assert_eq!(document_face(&faces, true, true).unwrap().bytes, b"serif"); + assert_eq!(document_face(&faces[..2], false, true).unwrap().bytes, b"sans"); + assert_eq!(document_face(&faces[..1], false, true).unwrap().bytes, b"serif"); + // Every Japanese face is a candidate, the style's match first and none twice. + let order: Vec<&[u8]> = document_faces(&faces, false, true).iter().map(|f| f.bytes).collect(); + assert_eq!(order, [b"bold".as_slice(), b"sans", b"serif"]); + let order: Vec<&[u8]> = document_faces(&faces, true, false).iter().map(|f| f.bytes).collect(); + assert_eq!(order, [b"serif".as_slice(), b"sans", b"bold"]); + assert!(document_face(&faces[3..], false, true).is_none()); + assert!(document_face(&[], false, true).is_none()); + } + #[test] fn arabic_faces_are_picked_by_script_in_manifest_order() { // Synthetic faces: the filter must not depend on the real build input. diff --git a/crates/fonts/src/lib.rs b/crates/fonts/src/lib.rs index 9c9c7821..ee94566d 100644 --- a/crates/fonts/src/lib.rs +++ b/crates/fonts/src/lib.rs @@ -5,14 +5,17 @@ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] +mod arabic; mod craft; mod encodings; pub mod pdf; mod script; +pub use arabic::{ShapedCluster, arabic_glyph, arabic_has, shape_arabic}; pub use craft::{ - CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_japanese_font, ui_arabic_fonts, ui_chinese_fonts, ui_cjk_fonts, ui_japanese_fonts, + CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_arabic_font, document_japanese_font, document_japanese_font_for_style, + document_japanese_fonts_for_style, ui_arabic_fonts, ui_chinese_fonts, ui_cjk_fonts, ui_japanese_fonts, ui_telugu_fonts, }; -pub use script::{GlyphError, GlyphOutline, MAX_SIGNATURE_CHARS, ScriptOutline, japanese_glyph, script_outline}; +pub use script::{GlyphError, GlyphOutline, MAX_SIGNATURE_CHARS, ScriptOutline, japanese_glyph, japanese_glyph_from, script_outline}; /// Approximate advance of `s` in Helvetica (or Arial) at `size` points. pub fn helvetica_width(s: &str, size: f64) -> f64 { @@ -36,15 +39,51 @@ pub fn helvetica_width(s: &str, size: f64) -> f64 { units * size / 1000.0 } +/// Approximate advance of `s` at `size` points in a CJK font: full-width characters take one +/// em, half-width forms half of one, and everything else is measured as Helvetica. +pub fn cjk_width(s: &str, size: f64) -> f64 { + let mut buf = [0u8; 4]; + s.chars() + .map(|c| match c { + '\u{ff61}'..='\u{ffdc}' | '\u{ffe8}'..='\u{ffee}' => size * 0.5, + _ if is_wide(c) => size, + _ => helvetica_width(c.encode_utf8(&mut buf), size), + }) + .sum() +} + +/// East Asian wide and full-width characters (Unicode UAX #11 W and F, by block). +fn is_wide(c: char) -> bool { + matches!(c, + '\u{1100}'..='\u{115f}' // Hangul Jamo initials + | '\u{2e80}'..='\u{303e}' // CJK radicals, Kangxi, ideographic description, CJK symbols + | '\u{3041}'..='\u{33ff}' // kana, Bopomofo, Hangul compatibility, Kanbun, CJK compatibility + | '\u{3400}'..='\u{4dbf}' // CJK extension A + | '\u{4e00}'..='\u{9fff}' // CJK unified ideographs + | '\u{a000}'..='\u{a4cf}' // Yi + | '\u{ac00}'..='\u{d7a3}' // Hangul syllables + | '\u{f900}'..='\u{faff}' // CJK compatibility ideographs + | '\u{fe30}'..='\u{fe4f}' // CJK compatibility forms + | '\u{ff00}'..='\u{ff60}' // full-width forms + | '\u{ffe0}'..='\u{ffe6}' + | '\u{20000}'..='\u{3fffd}' // supplementary ideographic planes + ) +} + /// Greedy line breaking within `width` points (paragraphs split on newlines; words longer /// than a line are broken by character). pub fn wrap(text: &str, size: f64, width: f64) -> Vec { + wrap_with(text, size, width, helvetica_width) +} + +/// [`wrap`], measuring with `measure` (for example [`cjk_width`]). +pub fn wrap_with(text: &str, size: f64, width: f64, measure: impl Fn(&str, f64) -> f64) -> Vec { let mut lines = Vec::new(); for para in text.split(['\n', '\r']) { let mut line = String::new(); for word in para.split(' ') { let candidate = if line.is_empty() { word.to_string() } else { format!("{line} {word}") }; - if helvetica_width(&candidate, size) <= width || line.is_empty() && helvetica_width(word, size) <= width { + if measure(&candidate, size) <= width || line.is_empty() && measure(word, size) <= width { line = candidate; continue; } @@ -52,7 +91,7 @@ pub fn wrap(text: &str, size: f64, width: f64) -> Vec { lines.push(std::mem::take(&mut line)); } for ch in word.chars() { - if !line.is_empty() && helvetica_width(&format!("{line}{ch}"), size) > width { + if !line.is_empty() && measure(&format!("{line}{ch}"), size) > width { lines.push(std::mem::take(&mut line)); } line.push(ch); @@ -63,28 +102,88 @@ pub fn wrap(text: &str, size: f64, width: f64) -> Vec { lines } -/// Encode text in WinAnsiEncoding (ISO 32000-2 Annex D); unmappable characters become `?`. +/// The WinAnsiEncoding (ISO 32000-2 Annex D) code for `c`, if the encoding has one. +/// +/// Codes 0x80–0x9F come from the same table the text extractor decodes with, so encoding and +/// decoding agree (Š š Ž ž Œ œ Ÿ ƒ † ‡ ˆ ˜ ‰ ‹ › as well as quotes, dashes, € and ™). +pub fn win_ansi_byte(c: char) -> Option { + match c { + '\u{20}'..='\u{7e}' | '\u{a0}'..='\u{ff}' => u8::try_from(u32::from(c)).ok(), + _ => { + let high = encodings::WIN_ANSI.get(0x80..0xa0)?; + let i = high.iter().position(|&u| u != 0 && u == u32::from(c))?; + u8::try_from(0x80 + i).ok() + } + } +} + +/// Encode text in WinAnsiEncoding (ISO 32000-2 Annex D); tabs become spaces and unmappable +/// characters become `?`. pub fn win_ansi(s: &str) -> Vec { - s.chars() - .map(|c| match c { - '\u{20}'..='\u{7e}' => c as u8, - '\u{a0}'..='\u{ff}' => c as u32 as u8, - '€' => 0x80, - '‚' => 0x82, - '„' => 0x84, - '…' => 0x85, - '‘' => 0x91, - '’' => 0x92, - '“' => 0x93, - '”' => 0x94, - '•' => 0x95, - '–' => 0x96, - '—' => 0x97, - '™' => 0x99, - '\t' => b' ', - _ => b'?', + s.chars().map(|c| if c == '\t' { b' ' } else { win_ansi_byte(c).unwrap_or(b'?') }).collect() +} + +/// The character encoding of a predefined Unicode CMap (`UniJIS-UTF16-H`, `UniGB-UCS2-H`, …; +/// ISO 32000-2 §9.7.5.2), which maps Unicode text straight to codes. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UnicodeCMap { + Ucs2, + Utf16, + Utf8, + Utf32, +} + +impl UnicodeCMap { + /// The encoding of the predefined CMap `name`, if it is a horizontal Unicode one. Vertical + /// (`-V`) CMaps are refused: field appearances are laid out horizontally, so their glyphs + /// would be drawn stacked down the field instead of across it. + pub fn from_name(name: &[u8]) -> Option { + let name = std::str::from_utf8(name).ok()?; + if !name.starts_with("Uni") || !name.ends_with("-H") { + return None; + } + name.split('-').find_map(|part| match part { + "UCS2" => Some(Self::Ucs2), + "UTF16" => Some(Self::Utf16), + "UTF8" => Some(Self::Utf8), + "UTF32" => Some(Self::Utf32), + _ => None, }) - .collect() + } + + /// Encode `s`; tabs become spaces, and characters UCS-2 can't hold become `?`. + pub fn encode(self, s: &str) -> Vec { + let mut out = Vec::with_capacity(s.len().saturating_mul(2)); + let mut units = [0u16; 2]; + let mut buf = [0u8; 4]; + for c in s.chars() { + let c = if c == '\t' { ' ' } else { c }; + match self { + Self::Ucs2 => { + let unit = u16::try_from(u32::from(c)).unwrap_or(u16::from(b'?')); + out.extend_from_slice(&unit.to_be_bytes()); + } + Self::Utf16 => { + for unit in c.encode_utf16(&mut units) { + out.extend_from_slice(&unit.to_be_bytes()); + } + } + Self::Utf8 => out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes()), + Self::Utf32 => out.extend_from_slice(&u32::from(c).to_be_bytes()), + } + } + out + } +} + +/// Whether [`win_ansi`] keeps `c` rather than writing `?` (a tab becomes a space). +pub fn win_ansi_encodable(c: char) -> bool { + c == '\t' || win_ansi_byte(c).is_some() +} + +/// The first character of `s` that [`win_ansi`] can't encode and would write as `?`. +pub fn first_non_win_ansi(s: &str) -> Option { + s.chars().find(|c| !win_ansi_encodable(*c)) } /// Bytes as a PDF literal string, `(` … `)`, with delimiters escaped. @@ -107,6 +206,18 @@ pub fn literal(bytes: &[u8]) -> Vec { mod tests { use super::*; + #[test] + fn win_ansi_encodability_matches_the_encoder() { + for c in ['a', 'é', 'ÿ', '€', '—', '™', '\t', '?'] { + assert!(win_ansi_encodable(c), "{c:?}"); + } + for c in ['中', 'あ', 'Ω', 'č', 'ă', '☃', '😀'] { + assert!(!win_ansi_encodable(c), "{c:?}"); + } + assert_eq!(first_non_win_ansi("Café — 5€"), None); + assert_eq!(first_non_win_ansi("Dvořák"), Some('ř')); + } + #[test] fn widths_wrap_and_encode() { assert!(helvetica_width("MMMM", 10.0) > helvetica_width("iiii", 10.0) * 2.0); @@ -119,4 +230,43 @@ mod tests { assert_eq!(win_ansi("Café — 5€ ☃"), b"Caf\xe9 \x97 5\x80 ?"); assert_eq!(literal(b"a(b)\\c"), b"(a\\(b\\)\\\\c)"); } + + #[test] + fn win_ansi_covers_the_0x80_to_0x9f_glyphs() { + // Every character in the WinAnsiEncoding table encodes to its code and back (#347). + for (code, &u) in encodings::WIN_ANSI.iter().enumerate() { + let Some(c) = char::from_u32(u).filter(|_| u != 0) else { continue }; + assert_eq!(win_ansi_byte(c), Some(code as u8), "{c:?} (U+{u:04X})"); + assert_eq!(win_ansi(&c.to_string()), [code as u8]); + } + assert_eq!( + win_ansi("Šta? žaba, œuvre — „ok“ ‰ ƒ † ‡ ˆ ˜ ‹›Ÿ"), + b"\x8ata? \x9eaba, \x9cuvre \x97 \x84ok\x93 \x89 \x83 \x86 \x87 \x88 \x98 \x8b\x9b\x9f" + ); + // Not in WinAnsi: č ć still need the fallback font; undefined codes are never produced. + assert_eq!(win_ansi("čć\u{81}\u{8d}\t"), b"???? "); + assert_eq!(win_ansi_byte('\0'), None); + } + + #[test] + fn unicode_cmaps_encode_cjk_text() { + assert_eq!(UnicodeCMap::from_name(b"UniJIS-UTF16-H"), Some(UnicodeCMap::Utf16)); + assert_eq!(UnicodeCMap::from_name(b"UniGB-UCS2-V"), None); + assert_eq!(UnicodeCMap::from_name(b"UniJIS-UCS2-HW-H"), Some(UnicodeCMap::Ucs2)); + assert_eq!(UnicodeCMap::from_name(b"UniKS-UTF8-H"), Some(UnicodeCMap::Utf8)); + assert_eq!(UnicodeCMap::from_name(b"UniJIS2004-UTF32-H"), Some(UnicodeCMap::Utf32)); + assert_eq!(UnicodeCMap::from_name(b"Identity-H"), None); + assert_eq!(UnicodeCMap::from_name(b"90ms-RKSJ-H"), None); + assert_eq!(UnicodeCMap::from_name(b"\xff\xfe"), None); + assert_eq!(UnicodeCMap::Utf16.encode("令A"), [0x4e, 0xe4, 0x00, 0x41]); + assert_eq!(UnicodeCMap::Utf16.encode("𠀋"), [0xd8, 0x40, 0xdc, 0x0b]); + assert_eq!(UnicodeCMap::Ucs2.encode("𠀋\t"), [0x00, b'?', 0x00, b' ']); + assert_eq!(UnicodeCMap::Utf8.encode("令"), "令".as_bytes()); + assert_eq!(UnicodeCMap::Utf32.encode("令"), [0, 0, 0x4e, 0xe4]); + assert_eq!(cjk_width("令和", 10.0), 20.0); + assert_eq!(cjk_width("ア", 10.0), 5.0); + assert_eq!(cjk_width("A", 10.0), helvetica_width("A", 10.0)); + let lines = wrap_with("日本語のテキストは文字単位で折り返されます", 10.0, 45.0, cjk_width); + assert!(lines.len() > 3 && lines.iter().all(|l| cjk_width(l, 10.0) <= 45.0), "{lines:?}"); + } } diff --git a/crates/fonts/src/pdf.rs b/crates/fonts/src/pdf.rs index 42c87d18..aeee43e1 100644 --- a/crates/fonts/src/pdf.rs +++ b/crates/fonts/src/pdf.rs @@ -47,7 +47,7 @@ pub struct Metrics { pub composite: bool, /// Code → Unicode text. unicode: HashMap, - /// `/BaseFont`, and whether it is a subset (`ABCDEF+Name`: other glyphs are missing). + /// `/BaseFont` (or Type 3 descriptor's `/FontName`), and whether it is a subset (`ABCDEF+Name`: other glyphs are missing). pub base_font: String, pub subset: bool, /// Whether the PDF identifies this face as bold or italic. These come from the @@ -68,12 +68,125 @@ fn dict(doc: &Document, o: Option<&Object>) -> Option { o.and_then(|o| doc.resolve(o).as_dict().cloned()) } -fn hex_bytes(tok: &str) -> Option> { - let t = tok.trim_start_matches('<').trim_end_matches('>'); - if !t.len().is_multiple_of(2) || t.is_empty() { - return None; +/// Most entries a CMap may add, so a small stream of overlapping `bfrange`s can't take minutes +/// or gigabytes; a full 2-byte code space is 65 536. Entries past it are ignored. +const MAX_CMAP_ENTRIES: usize = 1 << 20; +/// Most codespace or CID ranges kept from a CMap; Adobe's largest published CMaps have a few +/// thousand. +const MAX_CMAP_RANGES: usize = 1 << 16; + +/// A token of a CMap stream (PostScript syntax, ISO 32000-2 §9.10.3). +#[derive(Debug, PartialEq)] +enum Tok<'a> { + /// A `<…>` string; `None` when it isn't an even number of hex digits. + Hex(Option>), + Open, + Close, + /// An operator, number or name (with its `/`), a `(…)` string or a `<<`/`>>`. + Word(&'a [u8]), +} + +/// Splits a CMap into tokens without assuming whitespace between them (`<01><0041>` is two +/// strings), skipping `%` comments. +struct Lexer<'a> { + data: &'a [u8], + pos: usize, +} + +impl<'a> Lexer<'a> { + fn new(data: &'a [u8]) -> Self { + Self { data, pos: 0 } + } + + fn at(&self, i: usize) -> Option { + self.data.get(i).copied() + } +} + +fn is_cmap_space(b: u8) -> bool { + matches!(b, 0 | b'\t' | b'\n' | 0x0c | b'\r' | b' ') +} + +fn is_cmap_delimiter(b: u8) -> bool { + matches!(b, b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%') +} + +impl<'a> Iterator for Lexer<'a> { + type Item = Tok<'a>; + + fn next(&mut self) -> Option> { + loop { + let b = self.at(self.pos)?; + if is_cmap_space(b) { + self.pos += 1; + } else if b == b'%' { + while self.at(self.pos).is_some_and(|c| c != b'\n' && c != b'\r') { + self.pos += 1; + } + } else { + break; + } + } + let start = self.pos; + let b = self.at(start)?; + self.pos += 1; + match b { + b'[' => Some(Tok::Open), + b']' => Some(Tok::Close), + b'<' | b'>' if self.at(self.pos) == Some(b) => { + self.pos += 1; + Some(Tok::Word(self.data.get(start..self.pos)?)) + } + b'<' => { + let mut digits = Vec::new(); + let mut valid = true; + loop { + match self.at(self.pos) { + // Unterminated: what's left is one bad string. + None => return Some(Tok::Hex(None)), + Some(b'>') => break, + Some(c) if is_cmap_space(c) => {} + Some(c) => match (c as char).to_digit(16) { + // A hex digit is below 16, so it fits a u8. + Some(d) => digits.push(d as u8), + None => valid = false, + }, + } + self.pos += 1; + } + self.pos += 1; + if !valid || digits.is_empty() || !digits.len().is_multiple_of(2) { + return Some(Tok::Hex(None)); + } + // `chunks` never yields an empty slice, so `p[0]` exists. + Some(Tok::Hex(Some(digits.chunks(2).map(|p| p[0] << 4 | p.get(1).copied().unwrap_or(0)).collect()))) + } + b'(' => { + // Literal strings nest and escape their parentheses; only skipping them matters. + let mut depth = 1usize; + while depth > 0 { + match self.at(self.pos) { + None => break, + Some(b'\\') => self.pos += 1, + Some(b'(') => depth += 1, + Some(b')') => depth -= 1, + _ => {} + } + self.pos += 1; + } + Some(Tok::Word(self.data.get(start..self.pos.min(self.data.len()))?)) + } + _ => { + // A name keeps its `/`; other delimiters (`)`, `>`, `{`, `}`) stand alone. + if b == b'/' || !is_cmap_delimiter(b) { + while self.at(self.pos).is_some_and(|c| !is_cmap_space(c) && !is_cmap_delimiter(c)) { + self.pos += 1; + } + } + Some(Tok::Word(self.data.get(start..self.pos)?)) + } + } } - (0..t.len()).step_by(2).map(|i| u8::from_str_radix(&t[i..i + 2], 16).ok()).collect() } fn be(b: &[u8]) -> u32 { @@ -89,47 +202,64 @@ fn style_from_name(name: &str) -> (bool, bool) { ) } -/// Codespace ranges and CID mappings from an embedded CMap stream. +/// The next entry of a `begin… end…` section: `N` tokens, or `None` at its `end` operator or +/// the end of the stream. +fn entry<'a, const N: usize>(toks: &mut Lexer<'a>, end: &[u8]) -> Option<[Tok<'a>; N]> { + let mut out = Vec::with_capacity(N); + for _ in 0..N { + match toks.next()? { + Tok::Word(w) if w == end => return None, + t => out.push(t), + } + } + out.try_into().ok() +} + +fn number(t: &Tok) -> Option { + match t { + Tok::Word(w) => std::str::from_utf8(w).ok()?.parse().ok(), + _ => None, + } +} + +/// Codespace ranges and CID mappings from an embedded CMap stream, at most +/// [`MAX_CMAP_RANGES`] of each (every glyph searches them). #[allow(clippy::type_complexity)] fn parse_cmap(data: &[u8]) -> (Vec<(usize, Vec, Vec)>, Vec<(u32, u32, u32)>) { - let text = String::from_utf8_lossy(data); - let toks: Vec<&str> = text.split_whitespace().collect(); let (mut spaces, mut cids) = (Vec::new(), Vec::new()); - let mut i = 0; - while i < toks.len() { - match toks[i] { - "begincodespacerange" => { - i += 1; - while i + 1 < toks.len() && toks[i] != "endcodespacerange" { - if let (Some(lo), Some(hi)) = (hex_bytes(toks[i]), hex_bytes(toks[i + 1])) + let mut toks = Lexer::new(data); + while let Some(t) = toks.next() { + match t { + Tok::Word(b"begincodespacerange") => { + while let Some([lo, hi]) = entry(&mut toks, b"endcodespacerange") { + if let (Tok::Hex(Some(lo)), Tok::Hex(Some(hi))) = (lo, hi) && lo.len() == hi.len() + && spaces.len() < MAX_CMAP_RANGES { spaces.push((lo.len(), lo, hi)); } - i += 2; } } - "begincidrange" => { - i += 1; - while i + 2 < toks.len() && toks[i] != "endcidrange" { - if let (Some(lo), Some(hi), Ok(c)) = (hex_bytes(toks[i]), hex_bytes(toks[i + 1]), toks[i + 2].parse::()) { - cids.push((be(&lo), be(&hi), c)); + Tok::Word(b"begincidrange") => { + while let Some([lo, hi, c]) = entry(&mut toks, b"endcidrange") { + if let (Tok::Hex(Some(lo)), Tok::Hex(Some(hi)), Some(c)) = (&lo, &hi, number(&c)) + && cids.len() < MAX_CMAP_RANGES + { + cids.push((be(lo), be(hi), c)); } - i += 3; } } - "begincidchar" => { - i += 1; - while i + 1 < toks.len() && toks[i] != "endcidchar" { - if let (Some(code), Ok(c)) = (hex_bytes(toks[i]), toks[i + 1].parse::()) { - cids.push((be(&code), be(&code), c)); + Tok::Word(b"begincidchar") => { + while let Some([code, c]) = entry(&mut toks, b"endcidchar") { + if let (Tok::Hex(Some(code)), Some(c)) = (&code, number(&c)) + && cids.len() < MAX_CMAP_RANGES + { + cids.push((be(code), be(code), c)); } - i += 2; } } _ => {} } - i += 1; } spaces.sort_by_key(|s| s.0); (spaces, cids) @@ -162,7 +292,16 @@ impl Metrics { pub fn from_dict(doc: &Document, font: &Dict) -> Self { let mut m = Self::read_metrics(doc, font); - let base = String::from_utf8_lossy(font.name(b"BaseFont").unwrap_or(b"")).into_owned(); + let base = font + .name(b"BaseFont") + .map(|name| String::from_utf8_lossy(name).into_owned()) + .or_else(|| { + (font.name(b"Subtype") == Some(b"Type3")) + .then(|| dict(doc, font.get(b"FontDescriptor"))) + .flatten() + .and_then(|d| d.name(b"FontName").map(|name| String::from_utf8_lossy(name).into_owned())) + }) + .unwrap_or_default(); m.subset = base.len() > 7 && base.as_bytes()[6] == b'+' && base[..6].bytes().all(|b| b.is_ascii_uppercase()); let (bold, italic) = style_from_name(&base); m.bold |= bold; @@ -263,11 +402,18 @@ impl Metrics { let weight = d.get(b"FontWeight").and_then(|v| doc.resolve(v).as_f64()).unwrap_or(0.0); m.italic |= flags & 64 != 0 || angle.abs() > 0.1; m.bold |= flags & 262_144 != 0 || weight >= 600.0; - let a = d.get(b"Ascent").and_then(|v| doc.resolve(v).as_f64()).unwrap_or(0.0) / 1000.0; - let de = d.get(b"Descent").and_then(|v| doc.resolve(v).as_f64()).unwrap_or(0.0) / 1000.0; + // Type 3 descriptors may omit these metrics; the glyph-space FontBBox still applies. + let ascent = d.get(b"Ascent").and_then(|v| doc.resolve(v).as_f64()); + let descent = d.get(b"Descent").and_then(|v| doc.resolve(v).as_f64()); // Fonts often claim 0; never shrink the glyph box below a sensible minimum. - m.ascent = if a > 0.3 { a.min(1.5) } else { 0.9 }; - m.descent = if de < -0.05 { de.max(-0.6) } else { -0.25 }; + if ascent.is_some() || subtype != b"Type3" { + let a = ascent.unwrap_or(0.0) / 1000.0; + m.ascent = if a > 0.3 { a.min(1.5) } else { 0.9 }; + } + if descent.is_some() || subtype != b"Type3" { + let de = descent.unwrap_or(0.0) / 1000.0; + m.descent = if de < -0.05 { de.max(-0.6) } else { -0.25 }; + } } m } @@ -296,7 +442,7 @@ impl Metrics { if self.cid_map.is_empty() { return code; } - self.cid_map.iter().find(|(lo, hi, _)| code >= *lo && code <= *hi).map_or(0, |(lo, _, c)| c + (code - lo)) + self.cid_map.iter().find(|(lo, hi, _)| code >= *lo && code <= *hi).map_or(0, |(lo, _, c)| c.saturating_add(code - lo)) } /// The advance of `code` in text space per unit of font size. @@ -376,60 +522,66 @@ fn utf16(b: &[u8]) -> String { String::from_utf16_lossy(&units) } -/// `bfchar`/`bfrange` entries of a ToUnicode CMap. +/// `bfchar`/`bfrange` entries of a ToUnicode CMap, at most [`MAX_CMAP_ENTRIES`] of them. fn parse_to_unicode(data: &[u8], out: &mut HashMap) { - let text = String::from_utf8_lossy(data); - // Tokenise, keeping arrays' brackets as tokens. - let spaced = text.replace('[', " [ ").replace(']', " ] "); - let toks: Vec<&str> = spaced.split_whitespace().collect(); - let mut i = 0; - while i < toks.len() { - match toks[i] { - "beginbfchar" => { - i += 1; - while i + 1 < toks.len() && toks[i] != "endbfchar" { - if let (Some(c), Some(u)) = (hex_bytes(toks[i]), hex_bytes(toks[i + 1])) { + let mut budget = MAX_CMAP_ENTRIES; + let mut toks = Lexer::new(data); + while let Some(t) = toks.next() { + match t { + Tok::Word(b"beginbfchar") => { + while let Some([c, u]) = entry(&mut toks, b"endbfchar") { + if let (Tok::Hex(Some(c)), Tok::Hex(Some(u))) = (c, u) { + let Some(left) = budget.checked_sub(1) else { return }; + budget = left; out.insert(be(&c), utf16(&u)); } - i += 2; } } - "beginbfrange" => { - i += 1; - while i + 2 < toks.len() && toks[i] != "endbfrange" { - let (Some(lo), Some(hi)) = (hex_bytes(toks[i]), hex_bytes(toks[i + 1])) else { break }; + Tok::Word(b"beginbfrange") => { + while let Some([lo, hi, dst]) = entry(&mut toks, b"endbfrange") { + let (Tok::Hex(Some(lo)), Tok::Hex(Some(hi))) = (lo, hi) else { break }; let (lo, hi) = (be(&lo), be(&hi)); - if toks[i + 2] == "[" { - let mut j = i + 3; - let mut code = lo; - while j < toks.len() && toks[j] != "]" { - if let Some(u) = hex_bytes(toks[j]) { - out.insert(code, utf16(&u)); + match dst { + // One destination per code, from `lo` up to `hi`. + Tok::Open => { + let mut code = (hi >= lo).then_some(lo); + for t in toks.by_ref() { + let Some(c) = code else { + if t == Tok::Close { + break; + } + continue; + }; + match t { + Tok::Close => break, + Tok::Hex(Some(u)) => { + let Some(left) = budget.checked_sub(1) else { return }; + budget = left; + out.insert(c, utf16(&u)); + } + // A bad entry still stands for its code. + _ => {} + } + code = c.checked_add(1).filter(|c| *c <= hi); } - code += 1; - j += 1; } - i = j + 1; - } else { - if let Some(u) = hex_bytes(toks[i + 2]) - && hi >= lo - && hi - lo < 65536 - { + Tok::Hex(Some(u)) if hi >= lo && hi - lo < 65536 => { let mut units: Vec = u.chunks(2).map(|c| u16::from_be_bytes([c[0], *c.get(1).unwrap_or(&0)])).collect(); for code in lo..=hi { + let Some(left) = budget.checked_sub(1) else { return }; + budget = left; out.insert(code, String::from_utf16_lossy(&units)); if let Some(last) = units.last_mut() { *last = last.wrapping_add(1); } } } - i += 3; + _ => {} } } } _ => {} } - i += 1; } } @@ -468,13 +620,14 @@ fn unicode_map(doc: &Document, font: &Dict, composite: bool) -> HashMap code = (*n).max(0) as u32, + // Out-of-range codes map nothing a simple font can show. + Object::Int(n) => code = u32::try_from((*n).max(0)).unwrap_or(u32::MAX), Object::Name(n) => { match glyph_unicode(&String::from_utf8_lossy(n)) { Some(c) => out.insert(code, c.to_string()), None => out.remove(&code), }; - code += 1; + code = code.saturating_add(1); } _ => {} } @@ -505,6 +658,36 @@ mod tests { d } + #[test] + fn type3_descriptor_names_the_font_without_changing_its_metrics() { + let mut doc = Document::new_empty(); + let mut f = font( + &mut doc, + vec![ + ("Subtype", Object::name("Type3")), + ( + "FontMatrix", + Object::Array(vec![Object::Real(0.001), Object::Int(0), Object::Int(0), Object::Real(0.001), Object::Int(0), Object::Int(0)]), + ), + ("FontBBox", Object::Array(vec![Object::Int(0), Object::Int(-300), Object::Int(1000), Object::Int(1000)])), + ], + ); + let before = Metrics::from_dict(&doc, &f); + let mut descriptor = Dict::new(); + descriptor.set(b"Type".to_vec(), Object::name("FontDescriptor")); + descriptor.set(b"FontName".to_vec(), Object::name("ExampleMincho-Regular")); + descriptor.set(b"Flags".to_vec(), Object::Int(6)); + descriptor.set(b"ItalicAngle".to_vec(), Object::Int(0)); + let reference = doc.add(Object::Dict(descriptor)); + f.set(b"FontDescriptor".to_vec(), Object::Ref(reference)); + let after = Metrics::from_dict(&doc, &f); + assert_eq!(after.base_font, "ExampleMincho-Regular"); + assert_eq!((after.ascent, after.descent, after.scale), (before.ascent, before.descent, before.scale)); + assert!(!after.bold && !after.italic && !after.subset); + f.set(b"BaseFont".to_vec(), Object::name("ExplicitName")); + assert_eq!(Metrics::from_dict(&doc, &f).base_font, "ExplicitName", "an explicit BaseFont takes precedence"); + } + #[test] fn simple_fonts_decode_and_encode_through_their_encoding() { let mut doc = Document::new_empty(); @@ -591,4 +774,74 @@ mod tests { assert_eq!(glyph_unicode("u1F600"), Some('😀')); assert_eq!(glyph_unicode("nonsense"), None); } + + fn to_unicode(cmap: &[u8]) -> HashMap { + let mut out = HashMap::new(); + parse_to_unicode(cmap, &mut out); + out + } + + #[test] + fn cmap_tokens_need_no_space_between_them() { + // Hex strings, brackets and operators may touch; comments and literal strings are skipped. + let cmap = b"/CIDSystemInfo << /Registry (Adobe (x) \\) beginbfchar) >> def % beginbfchar <0001> <0058>\n\ + 2 beginbfchar<0003><0020><0010><00 E9>endbfchar \ + 1 beginbfrange<0024><0026><0041>endbfrange 1 beginbfrange<0030><0031>[<0048><0069>]endbfrange"; + let map = to_unicode(cmap); + let mut got: Vec<_> = map.iter().map(|(c, u)| (*c, u.as_str())).collect(); + got.sort_unstable(); + assert_eq!(got, [(3, " "), (0x10, "é"), (0x24, "A"), (0x25, "B"), (0x26, "C"), (0x30, "H"), (0x31, "i")]); + let (spaces, cids) = parse_cmap(b"1 begincodespacerange<00>endcodespacerange 1 begincidrange<20><7E>1 endcidrange"); + assert_eq!(spaces, [(1, vec![0], vec![0xff])]); + assert_eq!(cids, [(0x20, 0x7e, 1)]); + } + + #[test] + fn malformed_cmaps_never_panic() { + // Non-ASCII bytes inside hex strings once split a UTF-8 character and panicked. + for cmap in [ + "1 beginbfchar <0é0> <0041> <0042> endbfchar".as_bytes(), + "1 beginbfrange <0é0> <00é> <0041> endbfrange".as_bytes(), + b"1 beginbfchar <0001> <004", + b"1 beginbfchar <123> <0041> <0002> <0003> endbfchar", + b"1 beginbfrange <0001> [<0041>] endbfrange", + b"1 beginbfrange <0001> <0002> [<0041> (", + b"1 beginbfrange <0001>", + b"\xff\xfe beginbfchar \x80<\xc3\xa9> <0041> endbfchar %", + ] { + to_unicode(cmap); + parse_cmap(cmap); + } + // Code counters at the top of the range stop instead of overflowing. + let map = to_unicode(b"1 beginbfrange [<0041> <0042> <0043>] endbfrange"); + assert_eq!(map.len(), 2); + assert_eq!(map.get(&u32::MAX).map(String::as_str), Some("B")); + // A malformed array entry keeps its place, so the next one maps the next code. + let map = to_unicode(b"1 beginbfrange <0001> <0003> [<0041> <0043>] endbfrange"); + assert_eq!((map.get(&2), map.get(&3).map(String::as_str)), (None, Some("C"))); + let mut m = Metrics::fallback(); + (_, m.cid_map) = parse_cmap(b"1 begincidrange <0000> 4294967295 endcidrange"); + assert_eq!(m.cid(5), u32::MAX); + let mut doc = Document::new_empty(); + let mut enc = Dict::new(); + enc.set(b"Differences".to_vec(), Object::Array(vec![Object::Int(i64::MAX), Object::name("A"), Object::name("B")])); + let f = font(&mut doc, vec![("Subtype", Object::name("Type1")), ("Encoding", Object::Dict(enc))]); + assert_eq!(unicode_map(&doc, &f, false).get(&u32::MAX).map(String::as_str), Some("B")); + } + + #[test] + fn a_cmap_adds_at_most_max_entries() { + // 200 ranges of 65 536 codes would be 13 million strings from 4 KB of stream. + let mut cmap = b"200 beginbfrange ".to_vec(); + for k in 0..200u32 { + cmap.extend(format!("<{:08X}> <{:08X}> <0041> ", k << 16, (k << 16) | 0xffff).bytes()); + } + cmap.extend(b"endbfrange"); + assert_eq!(to_unicode(&cmap).len(), MAX_CMAP_ENTRIES); + let mut many = b"begincidrange ".to_vec(); + for k in 0..MAX_CMAP_RANGES + 10 { + many.extend(format!("<{k:08X}> <{k:08X}> 1 ").bytes()); + } + assert_eq!(parse_cmap(&many).1.len(), MAX_CMAP_RANGES); + } } diff --git a/crates/fonts/src/script.rs b/crates/fonts/src/script.rs index 23000ffb..04695ebe 100644 --- a/crates/fonts/src/script.rs +++ b/crates/fonts/src/script.rs @@ -2,7 +2,7 @@ use skrifa::instance::{LocationRef, Size}; use skrifa::outline::{DrawSettings, OutlinePen}; -use skrifa::{FontRef, MetadataProvider}; +use skrifa::{FontRef, GlyphId, MetadataProvider}; static FONT: &[u8] = include_bytes!("../../../assets/fonts/DancingScript.ttf"); @@ -129,6 +129,12 @@ impl OutlinePen for Flatten { /// outline data. [`GlyphError::NoFont`] when the build has no Japanese face. pub fn japanese_glyph(ch: char) -> Result { let Some(face) = crate::document_japanese_font() else { return Err(GlyphError::NoFont) }; + japanese_glyph_from(face, ch) +} + +/// The bounded outline and advance from the explicitly selected craft-fonts face. +/// Uses the same complexity and missing-glyph checks as [`japanese_glyph`]. +pub fn japanese_glyph_from(face: &crate::CraftFont, ch: char) -> Result { let Ok(font) = FontRef::new(face.bytes) else { return Err(GlyphError::Missing) }; let loc = LocationRef::default(); let metrics = font.metrics(Size::unscaled(), loc); @@ -139,6 +145,13 @@ pub fn japanese_glyph(ch: char) -> Result { if !width.is_finite() || width <= 0.0 || width > 2.0 { return Err(GlyphError::Missing); } + bounded_outline(&font, gid, width) +} + +/// Glyph `gid` of `font` in em units with advance `width`, bounded like [`japanese_glyph`]. +pub(crate) fn bounded_outline(font: &FontRef, gid: GlyphId, width: f64) -> Result { + let loc = LocationRef::default(); + let scale = 1.0 / font.metrics(Size::unscaled(), loc).units_per_em.max(1) as f64; let Some(glyph) = font.outline_glyphs().get(gid) else { return Err(GlyphError::Missing) }; let mut pen = Flatten::new(scale); let _ = glyph.draw(DrawSettings::unhinted(Size::unscaled(), loc), &mut pen); diff --git a/crates/forms/Cargo.toml b/crates/forms/Cargo.toml index 3f085ee6..ce6a73a5 100644 --- a/crates/forms/Cargo.toml +++ b/crates/forms/Cargo.toml @@ -17,3 +17,4 @@ thiserror = { workspace = true } [dev-dependencies] hayro-syntax = { workspace = true } +pdfcraft-render = { workspace = true } diff --git a/crates/forms/README.md b/crates/forms/README.md index 27459bab..0fa6d0de 100644 --- a/crates/forms/README.md +++ b/crates/forms/README.md @@ -39,5 +39,6 @@ delete_field(&mut doc, "email")?; ## Not yet JavaScript actions (format, keystroke, validate, calculate; M6.4/M6.5), rich-text values, -`/MK /R` rotation, the Appearance (colours, fonts), Actions, Format, Validate and Calculate -property tabs, FDF/XFDF import and export (M6.7). +the Appearance (colours, fonts), Actions, Format, Validate and Calculate property tabs, +FDF/XFDF import and export (M6.7). `/MK /R` rotation (0, 90, 180, 270) is drawn into the +appearance. diff --git a/crates/forms/src/appearance.rs b/crates/forms/src/appearance.rs index f13f53f8..ea88f675 100644 --- a/crates/forms/src/appearance.rs +++ b/crates/forms/src/appearance.rs @@ -5,12 +5,18 @@ //! marked-content block as viewers expect. Supported: single-line, multiline (wrapped), comb, //! password, quadding, auto font size (`0 Tf`), combo boxes and list boxes (selection shown). //! -//! The `/DA` font is used when the form's `/DR` defines it as a simple font; text is encoded in -//! WinAnsi. Otherwise (composite fonts, missing resources) Helvetica is used, so text is always -//! visible. Widths use the approximate Helvetica metrics of `pdfcraft-fonts`. +//! The `/DA` font is used when the form's `/DR` defines it as a simple font (text encoded in +//! WinAnsi) or as a composite font with a predefined Unicode CMap such as `UniJIS-UTF16-H`, as +//! Japanese forms use (text encoded in that CMap). Otherwise (other composite fonts, missing +//! resources) Helvetica is used, so text is always visible. Widths use the approximate metrics +//! of `pdfcraft-fonts` (Helvetica, or one em per full-width character in composite fonts). +//! +//! `/MK /R` (0, 90, 180, 270, counterclockwise) lays the content out in the upright box and sets +//! the appearance `/Matrix` so that box lands on the widget rectangle. Any other value is 0, and +//! rotation 0 leaves `/Matrix` off. use pdfcraft_cos::{Dict, Document, Object, Stream}; -use pdfcraft_fonts::{helvetica_width, literal, win_ansi, wrap}; +use pdfcraft_fonts::{UnicodeCMap, cjk_width, helvetica_width, literal, win_ansi, wrap_with}; use crate::{Field, FieldKind, Widget, acroform, flags}; @@ -26,6 +32,57 @@ pub fn fmt(v: f64) -> String { n(v) } +/// `/MK /R` as a quadrant, or 0 when it is missing or not a multiple of 90. +pub(crate) fn mk_rotation(doc: &Document, wd: &Dict) -> i64 { + let Some(raw) = wd.get(b"MK") else { return 0 }; + let mk_obj = doc.resolve(raw); + let Some(mk) = mk_obj.as_dict() else { return 0 }; + let Some(r_obj) = mk.get(b"R") else { return 0 }; + let resolved = doc.resolve(r_obj); + let Some(r) = degrees(&resolved) else { return 0 }; + match r.rem_euclid(360) { + q @ (0 | 90 | 180 | 270) => q, + _ => 0, + } +} + +fn degrees(o: &Object) -> Option { + if let Some(i) = o.as_int() { + return Some(i); + } + let v = o.as_f64()?; + (v.is_finite() && v.abs() <= 1.0e6).then_some(v.round() as i64) +} + +/// Where an appearance is drawn: the upright layout box, and the `/Matrix` that turns it onto +/// the widget rectangle. Rotation 0 has no matrix. +#[derive(Clone, Copy, Debug)] +pub(crate) struct Placement { + pub layout_w: f64, + pub layout_h: f64, + pub matrix: Option<[f64; 6]>, +} + +pub(crate) fn placement(rotation: i64, page_w: f64, page_h: f64) -> Placement { + let (layout_w, layout_h, matrix) = match rotation { + // (x, y) -> (page_w - y, x): text that ran to the right runs upward. + 90 => (page_h, page_w, Some([0.0, 1.0, -1.0, 0.0, page_w, 0.0])), + 180 => (page_w, page_h, Some([-1.0, 0.0, 0.0, -1.0, page_w, page_h])), + // (x, y) -> (y, page_h - x): text that ran to the right runs downward. + 270 => (page_h, page_w, Some([0.0, -1.0, 1.0, 0.0, 0.0, page_h])), + _ => (page_w, page_h, None), + }; + Placement { layout_w, layout_h, matrix } +} + +pub(crate) fn set_form_box(d: &mut Dict, place: Placement) { + let box_ = [0.0, 0.0, place.layout_w, place.layout_h]; + d.set(b"BBox".to_vec(), Object::Array(box_.iter().map(|v| Object::Real(*v)).collect())); + if let Some(m) = place.matrix { + d.set(b"Matrix".to_vec(), Object::Array(m.iter().map(|v| Object::Real(*v)).collect())); + } +} + /// A parsed default appearance string: font resource name, size (0 = auto) and colour operator. #[derive(Clone, Debug, PartialEq)] pub struct Da { @@ -53,8 +110,32 @@ pub fn parse_da(da: &str) -> Da { out } -/// The font resource for `name` from the form's `/DR`, if it is a simple font we can encode for. -fn dr_font(doc: &Document, name: &str) -> Option { +/// How a field font's text is encoded and measured. +#[derive(Clone, Copy, Debug, PartialEq)] +enum FontText { + WinAnsi, + /// A composite font with a predefined Unicode CMap. + Unicode(UnicodeCMap), +} + +impl FontText { + fn encode(self, s: &str) -> Vec { + match self { + Self::WinAnsi => win_ansi(s), + Self::Unicode(cmap) => cmap.encode(s), + } + } + + fn width(self, s: &str, size: f64) -> f64 { + match self { + Self::WinAnsi => helvetica_width(s, size), + Self::Unicode(_) => cjk_width(s, size), + } + } +} + +/// The font resource for `name` from the form's `/DR`, if it is a font we can encode for. +fn dr_font(doc: &Document, name: &str) -> Option<(Object, FontText)> { let af = acroform(doc)?; let dr = doc.resolve(af.get(b"DR")?); let fonts = doc.resolve(dr.as_dict()?.get(b"Font")?); @@ -63,13 +144,17 @@ fn dr_font(doc: &Document, name: &str) -> Option { let fd = font.as_dict()?; match fd.name(b"Subtype") { Some(b"Type1" | b"TrueType" | b"MMType1") => {} + // Japanese, Chinese and Korean forms: a CID font addressed by Unicode (a non-embedded + // `/HeiseiMin-W3` with `/UniJIS-UCS2-H`, say). Embedded CMap streams and Identity-H need + // the font's own mapping, so those still fall back to Helvetica. + Some(b"Type0") => return Some((entry, FontText::Unicode(UnicodeCMap::from_name(fd.name(b"Encoding")?)?))), _ => return None, } // Symbolic fonts (ZapfDingbats, Symbol) can't show WinAnsi text. if matches!(fd.name(b"BaseFont"), Some(b"ZapfDingbats" | b"Symbol")) { return None; } - Some(entry) + Some((entry, FontText::WinAnsi)) } fn helvetica() -> Object { @@ -192,12 +277,15 @@ pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[Stri }; let wobj = doc.get(w.obj); let wd = wobj.as_dict().cloned().unwrap_or_default(); - let (width, height) = ((w.rect[2] - w.rect[0]).max(1.0), (w.rect[3] - w.rect[1]).max(1.0)); + let (page_w, page_h) = ((w.rect[2] - w.rect[0]).max(1.0), (w.rect[3] - w.rect[1]).max(1.0)); + let place = placement(mk_rotation(doc, &wd), page_w, page_h); + let (width, height) = (place.layout_w, place.layout_h); let da = parse_da(wd.get(b"DA").and_then(|o| doc.resolve(o).as_string().map(|s| s.to_text())).as_deref().unwrap_or(&f.da)); - let (font_name, font_obj) = match dr_font(doc, &da.font) { - Some(o) => (da.font.clone(), o), - None => ("Helv".to_string(), helvetica()), + let (font_name, font_obj, enc) = match dr_font(doc, &da.font) { + Some((o, enc)) => (da.font.clone(), o, enc), + None => ("Helv".to_string(), helvetica(), FontText::WinAnsi), }; + let width_of = |text: &str, size: f64| enc.width(text, size); let (mut c, bw) = frame(doc, &wd, width, height); let pad = 2.0 + bw; let inner_w = (width - 2.0 * pad).max(1.0); @@ -205,11 +293,11 @@ pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[Stri let mut body: Vec = Vec::new(); let show = |body: &mut Vec, x: f64, y: f64, text: &str| { body.extend(format!("1 0 0 1 {} {} Tm ", n(x), n(y)).bytes()); - body.extend(literal(&win_ansi(text))); + body.extend(literal(&enc.encode(text))); body.extend_from_slice(b" Tj\n"); }; let x_for = |text: &str, size: f64| -> f64 { - let tw = helvetica_width(text, size); + let tw = width_of(text, size); match q { 1 => pad + (inner_w - tw) / 2.0, 2 => width - pad - tw, @@ -249,12 +337,12 @@ pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[Stri if size == 0.0 { // Auto size: the largest size (≤ 12) whose wrapped lines fit the height. size = 12.0; - while size > 4.0 && wrap(&text, size, inner_w).len() as f64 * size * 1.15 > height - 2.0 * pad { + while size > 4.0 && wrap_with(&text, size, inner_w, width_of).len() as f64 * size * 1.15 > height - 2.0 * pad { size -= 0.5; } } let mut y = height - pad - size * 0.85; - for line in wrap(&text, size, inner_w) { + for line in wrap_with(&text, size, inner_w, width_of) { if y < -size { break; } @@ -264,7 +352,7 @@ pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[Stri } else { if size == 0.0 { size = ((height - 2.0 * pad) / 1.15).clamp(4.0, 12.0); - let tw = helvetica_width(&text, size); + let tw = width_of(&text, size); if tw > inner_w && !comb { size = (size * inner_w / tw).max(4.0); } @@ -276,7 +364,7 @@ pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[Stri let cell = width / cells as f64; for (i, ch) in text.chars().take(cells).enumerate() { let s = ch.to_string(); - show(&mut body, cell * i as f64 + (cell - helvetica_width(&s, size)) / 2.0, y, &s); + show(&mut body, cell * i as f64 + (cell - width_of(&s, size)) / 2.0, y, &s); } } else { show(&mut body, x_for(&text, size), y, &text); @@ -307,7 +395,7 @@ pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[Stri let mut d = Dict::new(); d.set(b"Type".to_vec(), Object::name("XObject")); d.set(b"Subtype".to_vec(), Object::name("Form")); - d.set(b"BBox".to_vec(), Object::Array([0.0, 0.0, width, height].iter().map(|v| Object::Real(*v)).collect())); + set_form_box(&mut d, place); d.set(b"Resources".to_vec(), Object::Dict(res)); Stream::flate(d, &content) } @@ -318,20 +406,22 @@ pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[Stri pub fn check_box_states(doc: &mut Document, w: &Widget, kind: FieldKind, on_name: &str) -> Dict { let wobj = doc.get(w.obj); let wd = wobj.as_dict().cloned().unwrap_or_default(); - let (width, height) = ((w.rect[2] - w.rect[0]).max(1.0), (w.rect[3] - w.rect[1]).max(1.0)); + let (page_w, page_h) = ((w.rect[2] - w.rect[0]).max(1.0), (w.rect[3] - w.rect[1]).max(1.0)); + let place = placement(mk_rotation(doc, &wd), page_w, page_h); + let (width, height) = (place.layout_w, place.layout_h); let (frame_c, _) = frame(doc, &wd, width, height); let style = crate::author::CheckStyle::of_widget(doc, &wd, kind); let mut form = |content: String| -> Object { let mut d = Dict::new(); d.set(b"Type".to_vec(), Object::name("XObject")); d.set(b"Subtype".to_vec(), Object::name("Form")); - d.set(b"BBox".to_vec(), Object::Array([0.0, 0.0, width, height].iter().map(|v| Object::Real(*v)).collect())); + set_form_box(&mut d, place); Object::Ref(doc.add(Object::Stream(Stream::flate(d, content.as_bytes())))) }; let s = width.min(height); let mark = check_mark(style, width / 2.0, height / 2.0, s); let mut nd = Dict::new(); - nd.set(on_name.as_bytes().to_vec(), form(format!("{frame_c}{mark}"))); + nd.set(crate::name_bytes(on_name), form(format!("{frame_c}{mark}"))); nd.set(b"Off".to_vec(), form(frame_c)); let mut ap = Dict::new(); ap.set(b"N".to_vec(), Object::Dict(nd)); diff --git a/crates/forms/src/author.rs b/crates/forms/src/author.rs index d51abda7..53af944a 100644 --- a/crates/forms/src/author.rs +++ b/crates/forms/src/author.rs @@ -91,6 +91,10 @@ pub struct FieldProps { pub actions: Option>, /// Options tab: the mark of a check box or radio button (every widget). pub check_style: Option, + /// Position tab: rotate one widget (`/MK /R`) to 0, 90, 180 or 270 degrees counterclockwise. + /// Changing between horizontal and vertical swaps that widget's width and height around its + /// center, unless [`Self::rect`] is also set. + pub rotation: Option<(usize, i64)>, } fn invalid(m: impl Into) -> Result { @@ -524,6 +528,7 @@ pub fn add_field(doc: &mut Document, page: usize, rect: [f64; 4], kind: &NewFiel tab: usize::MAX, locked: false, hidden: false, + rotation: 0, }; let ap = appearance::check_box_states(doc, &w, FieldKind::Radio, export); doc.update_dict(widget, |d| d.set(b"AP".to_vec(), Object::Dict(ap)))?; @@ -569,15 +574,18 @@ pub fn redraw_field(doc: &mut Document, name: &str) -> Result<(), FormError> { d } }; - doc.update_dict(w.obj, |d| d.set(b"AP".to_vec(), Object::Dict(ap)))?; + let existing = crate::merged_appearance(doc, w.obj, &ap); + doc.update_dict(w.obj, |d| d.set(b"AP".to_vec(), Object::Dict(existing)))?; } Ok(()) } -fn frame_only(doc: &Document, w: &Widget) -> (String, f64, f64) { - let (width, height) = ((w.rect[2] - w.rect[0]).max(1.0), (w.rect[3] - w.rect[1]).max(1.0)); +fn frame_only(doc: &Document, w: &Widget) -> (String, crate::appearance::Placement) { + let (page_w, page_h) = ((w.rect[2] - w.rect[0]).max(1.0), (w.rect[3] - w.rect[1]).max(1.0)); let wobj = doc.get(w.obj); let wd = wobj.as_dict().cloned().unwrap_or_default(); + let place = crate::appearance::placement(crate::appearance::mk_rotation(doc, &wd), page_w, page_h); + let (width, height) = (place.layout_w, place.layout_h); let mk = wd.get(b"MK").and_then(|m| m.as_dict()).cloned().unwrap_or_default(); let col = |k: &[u8]| -> Option { let v: Vec = mk.get(k)?.as_array()?.iter().filter_map(|x| x.as_f64()).collect(); @@ -590,14 +598,14 @@ fn frame_only(doc: &Document, w: &Widget) -> (String, f64, f64) { if let Some(bc) = col(b"BC") { c.push_str(&format!("{bc} RG 1 w 0.5 0.5 {:.3} {:.3} re S\n", width - 1.0, height - 1.0)); } - (c, width, height) + (c, place) } -fn form_stream(width: f64, height: f64, content: Vec, resources: Dict) -> Stream { +fn form_stream(place: crate::appearance::Placement, content: Vec, resources: Dict) -> Stream { let mut d = Dict::new(); d.set(b"Type".to_vec(), Object::name("XObject")); d.set(b"Subtype".to_vec(), Object::name("Form")); - d.set(b"BBox".to_vec(), Object::Array([0.0, 0.0, width, height].iter().map(|v| Object::Real(*v)).collect())); + crate::appearance::set_form_box(&mut d, place); d.set(b"Resources".to_vec(), Object::Dict(resources)); Stream::flate(d, &content) } @@ -605,11 +613,12 @@ fn form_stream(width: f64, height: f64, content: Vec, resources: Dict) -> St /// A push button: background, border, its icon (`/MK /I`, scaled to fit and centred) and its /// centred caption (unless the layout is icon only, `/TP 1`). fn button_appearance(doc: &Document, w: &Widget) -> Stream { - let (mut c, width, height) = frame_only(doc, w); + let (mut c, place) = frame_only(doc, w); + let (width, height) = (place.layout_w, place.layout_h); let wobj = doc.get(w.obj); let mk = wobj.as_dict().and_then(|d| d.get(b"MK")).map(|m| doc.resolve(m)).and_then(|m| m.as_dict().cloned()).unwrap_or_default(); - let caption = mk.get(b"CA").and_then(|c| c.as_string()).map(|s| s.to_text()).unwrap_or_default(); - let icon_only = mk.int(b"TP") == Some(1); + let caption = mk.get(b"CA").map(|c| doc.resolve(c)).and_then(|c| c.as_string().map(|s| s.to_text())).unwrap_or_default(); + let icon_only = mk.get(b"TP").and_then(|t| doc.resolve(t).as_int()) == Some(1); let mut content = std::mem::take(&mut c).into_bytes(); let mut xobjects = Dict::new(); if let Some(icon) = mk.get(b"I").and_then(Object::as_ref) @@ -646,7 +655,7 @@ fn button_appearance(doc: &Document, w: &Widget) -> Stream { if !xobjects.is_empty() { res.set(b"XObject".to_vec(), Object::Dict(xobjects)); } - form_stream(width, height, content, res) + form_stream(place, content, res) } /// Give a push button (an image field) the picture `image` (an image XObject of `px` pixels): @@ -662,7 +671,8 @@ pub fn set_button_icon(doc: &mut Document, name: &str, image: ObjRef, px: (u32, xo.set(b"Im0".to_vec(), Object::Ref(image)); let mut res = Dict::new(); res.set(b"XObject".to_vec(), Object::Dict(xo)); - let icon = doc.add(Object::Stream(form_stream(w, h, format!("q {w} 0 0 {h} 0 0 cm /Im0 Do Q").into_bytes(), res))); + let icon = + doc.add(Object::Stream(form_stream(crate::appearance::placement(0, w, h), format!("q {w} 0 0 {h} 0 0 cm /Im0 Do Q").into_bytes(), res))); for wd in &f.widgets { let mut mk = doc.get(wd.obj).as_dict().and_then(|d| d.get(b"MK").map(|m| doc.resolve(m))).and_then(|m| m.as_dict().cloned()).unwrap_or_default(); @@ -676,14 +686,43 @@ pub fn set_button_icon(doc: &mut Document, name: &str, image: ObjRef, px: (u32, } fn empty_box(doc: &Document, w: &Widget) -> Stream { - let (c, width, height) = frame_only(doc, w); - form_stream(width, height, c.into_bytes(), Dict::new()) + let (c, place) = frame_only(doc, w); + form_stream(place, c.into_bytes(), Dict::new()) +} + +/// Swap width and height, keeping the center. `None` when that is not a usable widget. +fn swapped_around_center(r: [f64; 4]) -> Option<[f64; 4]> { + if !r.iter().all(|v| v.is_finite()) { + return None; + } + let (x0, y0, x1, y1) = (r[0].min(r[2]), r[1].min(r[3]), r[0].max(r[2]), r[1].max(r[3])); + let (cx, cy) = ((x0 + x1) / 2.0, (y0 + y1) / 2.0); + let (nw, nh) = (y1 - y0, x1 - x0); + let out = [cx - nw / 2.0, cy - nh / 2.0, cx + nw / 2.0, cy + nh / 2.0]; + (out.iter().all(|v| v.is_finite()) && out[2] - out[0] >= 4.0 && out[3] - out[1] >= 4.0).then_some(out) } /// Change a field's properties (General and Options tabs) and redraw it. pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result { let all = fields(doc); let f = all.iter().find(|f| f.name == name).ok_or_else(|| FormError::NoSuchField(name.into()))?.clone(); + // Validated before anything is written, so a refused rotation leaves the document unchanged. + // `Some(Some(rect))`: the widget's axis changes and it gets the swapped rectangle. + let mut rotated_rect: Option> = None; + if let Some((wi, rot)) = props.rotation { + if !matches!(rot, 0 | 90 | 180 | 270) { + return invalid("rotation must be 0, 90, 180 or 270 degrees"); + } + let Some(w) = f.widgets.get(wi) else { + return invalid(format!("{name} has no widget {}", wi.saturating_add(1))); + }; + let axis_change = (w.rotation % 180 == 0) != (rot % 180 == 0); + rotated_rect = Some(None); + if axis_change && props.rect.is_none() { + let Some(rect) = swapped_around_center(w.rect) else { return invalid("the field is too small") }; + rotated_rect = Some(Some(rect)); + } + } // A locked field only takes unlocking (Acrobat greys its properties out). if f.locked() && props.locked != Some(false) && *props != (FieldProps { locked: props.locked, ..FieldProps::default() }) { return invalid(format!("{name} is locked: unlock it first")); @@ -809,7 +848,7 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result d.set(b"DV".to_vec(), Object::name(v)), + Some(Some(v)) if matches!(f.kind, FieldKind::CheckBox | FieldKind::Radio) => d.set(b"DV".to_vec(), Object::Name(crate::name_bytes(v))), Some(Some(v)) => d.set(b"DV".to_vec(), PdfString::text(v)), Some(None) => { d.remove(b"DV"); @@ -943,6 +982,26 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result

, pub value: Value, - /// Check buttons: the on value then the off value; choice lists: the items. + /// Check buttons: the on value then the off value; choice lists: the items as shown. pub items: Vec, + /// Choice lists: the bound values saved to the data (``), one per shown + /// item; empty when the shown text is the value. + pub item_values: Vec, + /// Choice lists: how the list opens. + pub choice: ChoiceList, pub max_chars: Option, pub tooltip: Option, pub access: Access, @@ -427,6 +443,43 @@ pub struct Field { pub numeric: bool, } +impl Field { + /// A choice list's value as saved to the data: an item's shown text becomes its saved + /// value (an editable list keeps typed text); several values stay on separate lines. + /// Other fields keep `v`. + pub fn saved_value(&self, v: &str) -> String { + if self.ui != Ui::ChoiceList { + return v.to_string(); + } + // The data holds saved values: one of those is itself, even when another item shows + // the same text (Designer's "specify item values" lists often show 0, 1, 2 and save + // 1, 2, 3). + let saved = |one: &str| { + if self.item_values.is_empty() || self.item_values.iter().any(|s| s == one) { + return one.to_string(); + } + self.items.iter().position(|shown| shown == one).and_then(|i| self.item_values.get(i)).cloned().unwrap_or_else(|| one.to_string()) + }; + if self.choice.multi { + // Each value once, and never more than there are items. + let mut out: Vec = Vec::new(); + for l in v.lines().map(str::trim).filter(|l| !l.is_empty()) { + let s = saved(l); + if !out.contains(&s) { + out.push(s); + } + if out.len() >= self.items.len().max(1) { + break; + } + } + out.join("\n") + } else { + // One value: the first line, should the data hold several. + saved(v.lines().map(str::trim).find(|l| !l.is_empty()).unwrap_or("")) + } + } +} + #[derive(Clone, Debug, Default, PartialEq)] pub struct Draw { pub common: Common, diff --git a/crates/xfa/src/parse.rs b/crates/xfa/src/parse.rs index 6fded48e..0bf90081 100644 --- a/crates/xfa/src/parse.rs +++ b/crates/xfa/src/parse.rs @@ -13,6 +13,8 @@ const MAX_DEPTH: usize = 64; const MAX_NODES: u32 = 2_000_000; /// Largest decoded inline image. const MAX_IMAGE: usize = 32 << 20; +/// Most items a choice list keeps. +const MAX_ITEMS: usize = 1000; /// Longest text run kept. const MAX_TEXT: usize = 1 << 20; @@ -50,6 +52,23 @@ fn children<'a, 'd>(n: XmlNode<'a, 'd>, name: &'a str) -> impl Iterator Option> { + let mut out = Vec::new(); + for d in n.descendants() { + if d.is_text() + && let Some(t) = d.text() + { + out.extend(t.bytes().filter(|b| !b.is_ascii_whitespace())); + if out.len() > MAX_IMAGE / 3 * 4 + 4 { + return None; + } + } + } + Some(out) +} + fn text_of(n: XmlNode) -> String { let mut s = String::new(); for d in n.descendants() { @@ -302,7 +321,10 @@ fn parse_value(n: Option, warnings: &mut Vec) -> Value { return Value::Text(text_of(c)); } "image" => { - let raw: Vec = text_of(c).bytes().filter(|b| !b.is_ascii_whitespace()).collect(); + let Some(raw) = image_text_of(c) else { + warnings.push("an image is larger than 32 MB; it is left out".into()); + return Value::Empty; + }; let data = if c.attribute("transferEncoding").is_none_or(|e| e == "base64") { match base64::engine::general_purpose::STANDARD.decode(&raw) { Ok(d) => d, @@ -353,13 +375,35 @@ fn parse_caption(n: Option, warnings: &mut Vec) -> Option Vec { - // Several lists may exist (display and bound values); the first one is what the - // widget shows / the check button's on and off values. - let Some(items) = children(n, "items").find(|i| i.attribute("presence") != Some("hidden")).or_else(|| child(n, "items")) else { - return Vec::new(); +/// The field's item lists: what is shown (the first `` that is not the saved values, +/// also a check button's on and off values) and, for choice lists, the values saved to the +/// data (``, usually hidden), empty when the shown text is the value. +fn parse_items(n: XmlNode, warnings: &mut Vec) -> (Vec, Vec) { + let lists: Vec = children(n, "items").collect(); + let is_saved = |i: &XmlNode| i.attribute("save") == Some("1") || i.attribute("presence") == Some("hidden"); + let mut over = false; + let mut texts = |i: &XmlNode| -> Vec { + let all: Vec = i.children().filter(|c| c.is_element()).map(text_of).take(MAX_ITEMS + 1).collect(); + if all.len() > MAX_ITEMS { + over = true; + } + all.into_iter().take(MAX_ITEMS).collect() }; - items.children().filter(|c| c.is_element()).map(text_of).take(1000).collect() + let shown = lists.iter().find(|i| !is_saved(i)).or(lists.first()); + let saved = lists.iter().find(|i| is_saved(i) && shown.is_none_or(|s| s != *i)); + let shown: Vec = shown.map(&mut texts).unwrap_or_default(); + let mut saved: Vec = saved.map(&mut texts).unwrap_or_default(); + if over && warnings.len() < 100 { + warnings.push(format!( + "{} has more than {MAX_ITEMS} items; the rest are left out", + n.attribute("name").map_or_else(|| "a choice list".to_string(), |name| format!("the choice list {name}")) + )); + } + // A saved list shorter than the shown one is padded with the shown text. + if !saved.is_empty() && saved.len() < shown.len() { + saved.extend(shown.iter().skip(saved.len()).cloned()); + } + (shown, saved) } fn parse_field(n: XmlNode, warnings: &mut Vec) -> Field { @@ -384,6 +428,18 @@ fn parse_field(n: XmlNode, warnings: &mut Vec) -> Field { let scripts = parse_events(n); let numeric = matches!(kind, Ui::NumericEdit) || value_el.is_some_and(|v| v.children().any(|c| c.is_element() && matches!(c.tag_name().name(), "decimal" | "integer" | "float"))); + let (items, item_values) = parse_items(n, warnings); + let choice = match (kind == Ui::ChoiceList, ui_el) { + (true, Some(e)) => { + let open = e.attribute("open").unwrap_or("onEntry"); + ChoiceList { + list_box: matches!(open, "always" | "multiSelect"), + multi: open == "multiSelect", + editable: e.attribute("textEntry").is_some_and(|t| t == "1" || t == "true"), + } + } + _ => ChoiceList::default(), + }; let validate_el = child(n, "validate"); let validate_message = validate_el .and_then(|v| child(v, "message")) @@ -401,7 +457,9 @@ fn parse_field(n: XmlNode, warnings: &mut Vec) -> Field { multiline: ui_el.is_some_and(|e| e.attribute("multiLine").is_some_and(|m| m == "1" || m == "true")), caption: parse_caption(child(n, "caption"), warnings), value: parse_value(value_el, warnings), - items: parse_items(n), + items, + item_values, + choice, max_chars, tooltip: child(n, "assist").and_then(|a| child(a, "toolTip")).map(text_of).map(|t| t.trim().to_string()).filter(|t| !t.is_empty()), access: match n.attribute("access") { diff --git a/crates/xfa/src/pdf.rs b/crates/xfa/src/pdf.rs index 538dc214..811ac326 100644 --- a/crates/xfa/src/pdf.rs +++ b/crates/xfa/src/pdf.rs @@ -29,11 +29,39 @@ pub fn jpeg_size(data: &[u8]) -> Option<(u32, u32)> { } fn jpeg_frame(data: &[u8]) -> Option<(u32, u32, u8)> { + jpeg_info(data).map(|(w, h, c, _)| (w, h, c)) +} + +/// The SHA-256 of a picture's bytes, to find its XObject again. +fn content_hash(data: &[u8]) -> [u8; 32] { + use sha2::Digest; + sha2::Sha256::digest(data).into() +} + +fn hex32(h: &[u8; 32]) -> Vec { + h.iter().flat_map(|b| format!("{b:02x}").into_bytes()).collect() +} + +fn parse_hex32(s: &[u8]) -> Option<[u8; 32]> { + let mut out = [0u8; 32]; + if s.len() != 64 { + return None; + } + for (o, pair) in out.iter_mut().zip(s.as_chunks::<2>().0) { + *o = u8::from_str_radix(std::str::from_utf8(pair).ok()?, 16).ok()?; + } + Some(out) +} + +/// Width, height, components and whether an Adobe APP14 segment is present (such CMYK JPEGs +/// store inverted values). +fn jpeg_info(data: &[u8]) -> Option<(u32, u32, u8, bool)> { if data.get(0..2)? != [0xFF, 0xD8] { return None; } let mut i = 2usize; let mut guard = 0; + let mut adobe = false; while i + 4 <= data.len() && guard < 10_000 { guard += 1; if *data.get(i)? != 0xFF { @@ -50,12 +78,15 @@ fn jpeg_frame(data: &[u8]) -> Option<(u32, u32, u8)> { continue; } let len = u16::from_be_bytes([*data.get(i + 2)?, *data.get(i + 3)?]) as usize; + if marker == 0xEE && data.get(i + 4..i + 9) == Some(b"Adobe") { + adobe = true; + } let sof = matches!(marker, 0xC0..=0xCF) && !matches!(marker, 0xC4 | 0xC8 | 0xCC); if sof { let h = u16::from_be_bytes([*data.get(i + 5)?, *data.get(i + 6)?]) as u32; let w = u16::from_be_bytes([*data.get(i + 7)?, *data.get(i + 8)?]) as u32; let comps = *data.get(i + 9)?; - return (w > 0 && h > 0).then_some((w, h, comps)); + return (w > 0 && h > 0).then_some((w, h, comps, adobe)); } i = i.checked_add(2)?.checked_add(len.max(2))?; } @@ -125,8 +156,20 @@ pub struct Written { pub warnings: Vec, } +/// On an image XObject written for a picture: the SHA-256 (hex) of the picture's bytes. A +/// re-layout (after every script event) finds the XObjects on the pages it replaces by this, +/// and reuses them instead of decoding and embedding the same picture again. Nothing is kept +/// in the catalog, and a match is by the picture's own hash, never by a map read from the file. +const IMAGE_SHA_KEY: &[u8] = b"PCXfaSHA256"; +/// Most pages searched for earlier pictures. +const MAX_PAGES_SEARCHED: usize = 10_000; +/// Most pictures remembered. +const MAX_REMEMBERED_IMAGES: usize = 1000; + struct Emitter<'a> { doc: &'a mut Document, + /// Image XObjects by content hash: those from earlier passes, then this one's. + images: HashMap<[u8; 32], ObjRef>, fields: Vec, radio_groups: HashMap)>, /// The on state selected in each radio group, from the data. @@ -173,11 +216,14 @@ impl Emitter<'_> { } let mut top_level = true; match &w.kind { - WidgetKind::Text | WidgetKind::Date(_) => { + WidgetKind::Text | WidgetKind::Date(_) | WidgetKind::Password => { d.set(b"FT".to_vec(), Object::name("Tx")); if w.multiline { ff |= 1 << 12; } + if w.kind == WidgetKind::Password { + ff |= 1 << 13; + } if let Some(m) = w.max_chars { d.set(b"MaxLen".to_vec(), Object::Int(m.min(100_000) as i64)); } @@ -202,6 +248,63 @@ impl Emitter<'_> { d.set(b"AA".to_vec(), Object::Dict(aa)); } } + WidgetKind::Choice { options, list_box, multi, editable } => { + d.set(b"FT".to_vec(), Object::name("Ch")); + if !list_box { + ff |= 1 << 17; + if *editable { + ff |= 1 << 18; + } + } + if *multi { + ff |= 1 << 21; + } + let opt: Vec = options + .iter() + .map(|(saved, shown)| { + if saved == shown { + Object::String(PdfString::text(shown)) + } else { + Object::Array(vec![Object::String(PdfString::text(saved)), Object::String(PdfString::text(shown))]) + } + }) + .collect(); + d.set(b"Opt".to_vec(), Object::Array(opt)); + let q = match w.h_align { + crate::model::HAlign::Center => 1, + crate::model::HAlign::Right => 2, + _ => 0, + }; + if q != 0 { + d.set(b"Q".to_vec(), Object::Int(q)); + } + let values = |v: &str| -> Object { + let picked: Vec<&str> = if *multi { v.lines().filter(|l| !l.is_empty()).collect() } else { vec![v] }; + match picked.as_slice() { + [one] => Object::String(PdfString::text(one)), + many => Object::Array(many.iter().map(|s| Object::String(PdfString::text(s))).collect()), + } + }; + if let Some(v) = &w.value { + d.set(b"V".to_vec(), values(v)); + // The selected indices (ISO 32000-2 Table 231): ascending, each once. Where + // two options save the same value the first is meant; the data can't say. + let picked: Vec<&str> = if *multi { v.lines().filter(|l| !l.is_empty()).collect() } else { vec![v.as_str()] }; + let mut idx: Vec = picked.iter().filter_map(|p| options.iter().position(|(saved, _)| saved == p)).collect(); + idx.sort_unstable(); + idx.dedup(); + if !idx.is_empty() { + d.set(b"I".to_vec(), Object::Array(idx.into_iter().map(|i| Object::Int(i as i64)).collect())); + } + } + if let Some(v) = &w.default { + // Already a saved value (the layout maps defaults as it maps data). + d.set(b"DV".to_vec(), values(v)); + } + } + WidgetKind::Signature => { + d.set(b"FT".to_vec(), Object::name("Sig")); + } WidgetKind::CheckBox { on, .. } => { d.set(b"FT".to_vec(), Object::name("Btn")); let on = state_name(on); @@ -309,6 +412,85 @@ impl Emitter<'_> { Ok(Some(r)) } + /// An image XObject for a picture: JPEGs as they are (`DCTDecode`), PNGs and GIFs decoded + /// to samples (`FlateDecode`, with a soft mask for transparency). `None`, with a warning, + /// for anything else. + fn image_xobject(&mut self, data: &[u8], content_type: &str) -> Option { + let hash = content_hash(data); + if let Some(&r) = self.images.get(&hash) + && let Object::Stream(s) = &*self.doc.get(r) + && s.dict.name(b"Subtype") == Some(b"Image") + { + return Some(r); + } + let r = self.new_image_xobject(data, content_type, &hash)?; + if self.images.len() < MAX_REMEMBERED_IMAGES { + self.images.insert(hash, r); + } + Some(r) + } + + fn new_image_xobject(&mut self, data: &[u8], content_type: &str, hash: &[u8; 32]) -> Option { + let what = if content_type.is_empty() { "picture" } else { content_type }; + let mut d = Dict::new(); + d.set(IMAGE_SHA_KEY.to_vec(), Object::String(PdfString::literal(hex32(hash)))); + d.set(b"Type".to_vec(), Object::name("XObject")); + d.set(b"Subtype".to_vec(), Object::name("Image")); + d.set(b"BitsPerComponent".to_vec(), Object::Int(8)); + match crate::image::kind(data) { + Some(crate::image::Kind::Jpeg) => { + let Some((iw, ih, comps, adobe)) = jpeg_info(data) else { + self.warnings.push(format!("a {what} image is not a readable JPEG and is left out")); + return None; + }; + d.set(b"Width".to_vec(), Object::Int(i64::from(iw))); + d.set(b"Height".to_vec(), Object::Int(i64::from(ih))); + d.set( + b"ColorSpace".to_vec(), + Object::name(match comps { + 1 => "DeviceGray", + 4 => "DeviceCMYK", + _ => "DeviceRGB", + }), + ); + if comps == 4 && adobe { + // Adobe APP14 JPEGs store inverted CMYK. + d.set(b"Decode".to_vec(), Object::Array([1, 0, 1, 0, 1, 0, 1, 0].iter().map(|v| Object::Int(*v)).collect())); + } + d.set(b"Filter".to_vec(), Object::name("DCTDecode")); + Some(self.doc.add(Object::Stream(Stream::from_raw(d, data.to_vec())))) + } + Some(crate::image::Kind::Png | crate::image::Kind::Gif) => { + let img = match crate::image::decode(data) { + Ok(img) => img, + Err(why) => { + self.warnings.push(why); + return None; + } + }; + d.set(b"Width".to_vec(), Object::Int(i64::from(img.width))); + d.set(b"Height".to_vec(), Object::Int(i64::from(img.height))); + d.set(b"ColorSpace".to_vec(), Object::name(img.color_space)); + if let Some(alpha) = &img.alpha { + let mut m = Dict::new(); + m.set(b"Type".to_vec(), Object::name("XObject")); + m.set(b"Subtype".to_vec(), Object::name("Image")); + m.set(b"Width".to_vec(), Object::Int(i64::from(img.width))); + m.set(b"Height".to_vec(), Object::Int(i64::from(img.height))); + m.set(b"ColorSpace".to_vec(), Object::name("DeviceGray")); + m.set(b"BitsPerComponent".to_vec(), Object::Int(8)); + let mr = self.doc.add(Object::Stream(Stream::flate(m, alpha))); + d.set(b"SMask".to_vec(), Object::Ref(mr)); + } + Some(self.doc.add(Object::Stream(Stream::flate(d, &img.samples)))) + } + None => { + self.warnings.push(format!("a {what} image is not a JPEG, PNG or GIF and is left out")); + None + } + } + } + fn page(&mut self, page: &Page, pages_ref: ObjRef) -> Result { let page_ref = self.doc.add(Object::Dict(Dict::new())); let mut content: Vec = Vec::new(); @@ -346,33 +528,7 @@ impl Emitter<'_> { content.extend_from_slice(b" Tj ET\n"); } Item::Image { rect, data, content_type } => { - let Some((iw, ih, comps)) = jpeg_frame(data) else { - self.warnings.push(format!( - "a {} image is not a JPEG and is left out", - if content_type.is_empty() { "picture" } else { content_type.as_str() } - )); - continue; - }; - let mut d = Dict::new(); - d.set(b"Type".to_vec(), Object::name("XObject")); - d.set(b"Subtype".to_vec(), Object::name("Image")); - d.set(b"Width".to_vec(), Object::Int(i64::from(iw))); - d.set(b"Height".to_vec(), Object::Int(i64::from(ih))); - d.set(b"BitsPerComponent".to_vec(), Object::Int(8)); - d.set( - b"ColorSpace".to_vec(), - Object::name(match comps { - 1 => "DeviceGray", - 4 => "DeviceCMYK", - _ => "DeviceRGB", - }), - ); - if comps == 4 { - // Adobe APP14 JPEGs store inverted CMYK. - d.set(b"Decode".to_vec(), Object::Array([1, 0, 1, 0, 1, 0, 1, 0].iter().map(|v| Object::Int(*v)).collect())); - } - d.set(b"Filter".to_vec(), Object::name("DCTDecode")); - let r = self.doc.add(Object::Stream(Stream::from_raw(d, data.as_ref().clone()))); + let Some(r) = self.image_xobject(data, content_type) else { continue }; let name = format!("Im{i}"); xobjects.set(name.clone().into_bytes(), Object::Ref(r)); content.extend(format!("q {} 0 0 {} {} {} cm /{name} Do Q\n", n(rect.w), n(rect.h), n(rect.x), n(h - rect.bottom())).bytes()); @@ -413,6 +569,30 @@ impl Emitter<'_> { /// Replace the document's pages with the laid-out form and add its fields to the AcroForm. The /// XFA packets and everything else in the catalog stay. +/// The image XObjects earlier passes wrote for pictures, by the SHA-256 each records: those on +/// the pages under `pages` (the flat page list [`write_form`] writes). +fn earlier_images(doc: &Document, pages: ObjRef) -> HashMap<[u8; 32], ObjRef> { + let mut out = HashMap::new(); + let kids = doc.get(pages).as_dict().and_then(|d| d.get(b"Kids").map(|k| doc.resolve(k))).and_then(|k| k.as_array().cloned()).unwrap_or_default(); + for page in kids.iter().filter_map(Object::as_ref).take(MAX_PAGES_SEARCHED) { + let page = doc.get(page); + let Some(resources) = page.as_dict().and_then(|p| p.get(b"Resources")).map(|r| doc.resolve(r)) else { continue }; + let Some(xobjects) = resources.as_dict().and_then(|r| r.get(b"XObject")).map(|x| doc.resolve(x)) else { continue }; + for r in xobjects.as_dict().into_iter().flat_map(|x| x.iter()).filter_map(|(_, v)| v.as_ref()) { + if out.len() >= MAX_REMEMBERED_IMAGES { + return out; + } + if let Object::Stream(s) = &*doc.get(r) + && s.dict.name(b"Subtype") == Some(b"Image") + && let Some(hash) = s.dict.get(IMAGE_SHA_KEY).and_then(Object::as_string).and_then(|h| parse_hex32(&h.bytes)) + { + out.insert(hash, r); + } + } + } + out +} + pub fn write_form(doc: &mut Document, form: &Form) -> Result { let catalog_ref = doc.root().ok_or_else(|| XfaError::Malformed("the document has no catalog".into()))?; let mut catalog = doc.get(catalog_ref).as_dict().cloned().ok_or_else(|| XfaError::Malformed("the catalog is not a dictionary".into()))?; @@ -428,8 +608,17 @@ pub fn write_form(doc: &mut Document, form: &Form) -> Result r } }; - let mut em = - Emitter { doc, fields: Vec::new(), radio_groups: HashMap::new(), radio_selected: HashMap::new(), field_count: 0, warnings: Vec::new() }; + // Pictures embedded by earlier passes, from the pages this pass replaces. + let images = earlier_images(doc, pages_ref); + let mut em = Emitter { + doc, + images, + fields: Vec::new(), + radio_groups: HashMap::new(), + radio_selected: HashMap::new(), + field_count: 0, + warnings: Vec::new(), + }; let mut kids = Vec::new(); for page in &form.pages { kids.push(Object::Ref(em.page(page, pages_ref)?)); diff --git a/crates/xfa/src/script.rs b/crates/xfa/src/script.rs index 53b2cf50..c2088dee 100644 --- a/crates/xfa/src/script.rs +++ b/crates/xfa/src/script.rs @@ -241,15 +241,15 @@ impl Walker<'_> { fn field(&mut self, f: &Field, parent_som: &str, index: usize, radio: Option<&str>) -> FormNode { let som = child_som(parent_som, f.common.name.as_deref().or(Some("field")), index); - let data_value = self.data.and_then(|d| d.text_at(&som_to_path(&som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty()); + let data_value = self.data.and_then(|d| d.value_at(&som_to_path(&som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty()); let value = match (radio, data_value) { // A radio button's value is its on value when the group picked it. (Some(selected), _) => { let on = f.items.first().cloned().unwrap_or_default(); if selected == on { on } else { String::new() } } - (None, Some(v)) => v, - (None, None) => f.value.plain().map(|v| v.trim().to_string()).unwrap_or_default(), + (None, Some(v)) => f.saved_value(&v), + (None, None) => f.value.plain().map(|v| f.saved_value(v.trim())).unwrap_or_default(), }; self.push_events(&som, &f.scripts, f.calculate.as_ref(), f.validate.as_ref(), f.validate_message.as_deref()); FormNode { diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index d067b14d..4a4d8ad5 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -7,8 +7,9 @@ use crate::data::{ DatasetsWrite, FieldData, FieldDatum, build_data, iso_to_pattern, parse_datasets, pattern_to_iso, read_values, som_to_path, write_datasets, }; use crate::data::{add_data_instances, remove_data_instance, write_data_value}; -use crate::fixtures::{scripted_template, shell, static_shell, template, template_with_data}; +use crate::fixtures::{TINY_GIF, fields_template, scripted_template, shell, static_shell, template, template_with_data, tiny_png}; use crate::layout::Item; +use crate::layout::WidgetKind; use crate::model::*; use crate::script::{NodeKind, Overrides, apply_overrides, fields_by_som, form_tree, overrides, rerender, set_overrides}; @@ -762,3 +763,281 @@ fn top_fields(doc: &Document) -> Vec<(String, pdfcraft_cos::Dict)> { }) .collect() } + +#[test] +fn choice_lists_signatures_passwords_and_pictures_become_fields_and_images() { + let form = layout_xml(&fields_template("")).unwrap(); + let p1 = &form.pages[0]; + let w = widgets(p1); + let country = w.iter().find(|w| w.name == "country").unwrap(); + assert_eq!( + country.kind, + WidgetKind::Choice { + options: vec![("CA".into(), "Canada".into()), ("FR".into(), "France".into()), ("JP".into(), "Japan".into())], + list_box: false, + multi: false, + editable: false, + } + ); + assert_eq!(country.value.as_deref(), Some("FR"), "the template default, as a saved value"); + let langs = w.iter().find(|w| w.name == "langs").unwrap(); + assert!( + matches!(&langs.kind, WidgetKind::Choice { options, list_box: true, multi: true, editable: false } if options.len() == 3 && options[0] == ("English".into(), "English".into())) + ); + let other = w.iter().find(|w| w.name == "other").unwrap(); + assert!(matches!(&other.kind, WidgetKind::Choice { list_box: false, multi: false, editable: true, .. })); + assert_eq!(w.iter().find(|w| w.name == "pin").unwrap().kind, WidgetKind::Password); + assert_eq!(w.iter().find(|w| w.name == "sign").unwrap().kind, WidgetKind::Signature); + assert!(w.iter().all(|w| w.name != "photo" && w.name != "code"), "image and barcode fields make no widget"); + // The PNG in the image field and the GIF in the draw are pictures, fitted to their boxes. + let images: Vec<(&Rect, usize)> = + p1.items.iter().filter_map(|i| if let Item::Image { rect, data, .. } = i { Some((rect, data.len())) } else { None }).collect(); + assert_eq!(images.len(), 2, "{images:?}"); + assert!((images[0].0.w - 72.0).abs() < 0.01 && (images[0].0.h - 72.0).abs() < 0.01, "a square picture fills a 1in square: {:?}", images[0].0); + assert_eq!(images[1].1, TINY_GIF.len()); + let t = texts(p1); + assert!(t.iter().any(|s| s == "12345"), "the barcode's value as text: {t:?}"); + assert!(form.warnings.iter().any(|w| w.contains("barcode")) && form.warnings.iter().any(|w| w.contains("image fields")), "{:?}", form.warnings); + + // Written into the PDF: /Ch fields with their options and flags, a /Sig field, a password + // field, and Flate images (the PNG with a soft mask). + let mut doc = Document::open(Arc::new(shell(&fields_template("")))).unwrap(); + let report = render_into(&mut doc).unwrap(); + assert_eq!(report.fields, 6, "country, langs, other, pin, sign, summary"); + let bytes = write_incremental(&doc, &SaveOptions::default()).unwrap(); + let doc = Document::open(Arc::new(bytes)).unwrap(); + let fields = pdfcraft_forms_free_fields(&doc); + let field = |n: &str| fields.iter().find(|(name, _)| name == n).map(|(_, d)| d.clone()).unwrap_or_else(|| panic!("no field {n}")); + let country = field("country"); + assert_eq!(country.name(b"FT"), Some(b"Ch".as_slice())); + assert_eq!(country.int(b"Ff"), Some(1 << 17), "a combo box"); + let opt = doc.resolve(country.get(b"Opt").unwrap()).as_array().unwrap().clone(); + assert_eq!(opt.len(), 3); + let pair = doc.resolve(&opt[1]).as_array().unwrap().clone(); + assert_eq!(pair[0].as_string().unwrap().to_text(), "FR"); + assert_eq!(pair[1].as_string().unwrap().to_text(), "France"); + assert_eq!(country.get(b"V").and_then(|v| v.as_string()).map(|s| s.to_text()).as_deref(), Some("FR")); + assert_eq!(country.get(b"DV").and_then(|v| v.as_string()).map(|s| s.to_text()).as_deref(), Some("FR")); + let langs = field("langs"); + assert_eq!(langs.int(b"Ff"), Some(1 << 21), "a multi-select list box"); + let opt = doc.resolve(langs.get(b"Opt").unwrap()).as_array().unwrap().clone(); + assert_eq!(opt[0].as_string().unwrap().to_text(), "English", "same shown text and value: a plain string"); + assert_eq!(field("other").int(b"Ff"), Some((1 << 17) | (1 << 18)), "an editable combo box"); + assert_eq!(field("pin").int(b"Ff"), Some(1 << 13), "a password field"); + assert_eq!(field("sign").name(b"FT"), Some(b"Sig".as_slice())); + let streams: Vec = doc + .object_numbers() + .into_iter() + .filter_map(|n| if let Object::Stream(st) = &*doc.get(pdfcraft_cos::ObjRef { num: n, generation: 0 }) { Some(st.clone()) } else { None }) + .filter(|s| s.dict.name(b"Subtype") == Some(b"Image")) + .collect(); + assert_eq!(streams.len(), 3, "the PNG, its soft mask, the GIF"); + let png = streams.iter().find(|s| s.dict.contains(b"SMask")).expect("the PNG has a soft mask"); + assert_eq!(png.dict.name(b"ColorSpace"), Some(b"DeviceRGB".as_slice())); + assert_eq!(png.dict.name(b"Filter"), Some(b"FlateDecode".as_slice())); + assert_eq!(png.decoded().unwrap().len(), 12, "2 × 2 RGB samples"); + let gif = streams.iter().find(|s| s.dict.int(b"Width") == Some(1)).expect("the 1 × 1 GIF"); + assert_eq!(gif.dict.name(b"ColorSpace"), Some(b"DeviceGray".as_slice()), "a white pixel is gray"); + assert_eq!(gif.decoded().unwrap(), vec![255]); +} + +/// The terminal field dictionaries by name (one level: this fixture has no hierarchy). +fn pdfcraft_forms_free_fields(doc: &Document) -> Vec<(String, pdfcraft_cos::Dict)> { + let root = doc.get(doc.root().unwrap()); + let acro = doc.resolve(root.as_dict().unwrap().get(b"AcroForm").unwrap()); + let fields = doc.resolve(acro.as_dict().unwrap().get(b"Fields").unwrap()).as_array().unwrap().clone(); + fields + .iter() + .filter_map(|f| { + let d = doc.resolve(f).as_dict()?.clone(); + let name = d.get(b"T").map(|t| doc.resolve(t)).and_then(|t| t.as_string().map(|s| s.to_text()))?; + Some((name, d)) + }) + .collect() +} + +#[test] +fn choice_values_come_from_the_data_as_saved_values_and_several_for_lists() { + // Shown text in the data is taken too (an editable list the user typed into saves text). + let xml = fields_template("
JapanEnglish\nSpanishtyped
"); + let form = layout_xml(&xml).unwrap(); + let w = widgets(&form.pages[0]); + assert_eq!(w.iter().find(|w| w.name == "country").unwrap().value.as_deref(), Some("JP")); + assert_eq!(w.iter().find(|w| w.name == "langs").unwrap().value.as_deref(), Some("English\nSpanish")); + assert_eq!(w.iter().find(|w| w.name == "other").unwrap().value.as_deref(), Some("typed")); + let mut doc = Document::open(Arc::new(shell(&xml))).unwrap(); + render_into(&mut doc).unwrap(); + let bytes = write_incremental(&doc, &SaveOptions::default()).unwrap(); + let doc = Document::open(Arc::new(bytes)).unwrap(); + let fields = pdfcraft_forms_free_fields(&doc); + let langs = &fields.iter().find(|(n, _)| n == "langs").unwrap().1; + let v = doc.resolve(langs.get(b"V").unwrap()).as_array().unwrap().clone(); + assert_eq!(v.iter().map(|s| s.as_string().unwrap().to_text()).collect::>(), vec!["English", "Spanish"]); + let i = doc.resolve(langs.get(b"I").unwrap()).as_array().unwrap().clone(); + assert_eq!(i.iter().map(|o| o.as_int().unwrap()).collect::>(), vec![0, 2]); +} + +#[test] +fn pictures_are_sized_from_their_headers_and_hostile_ones_are_refused_unallocated() { + use crate::image::{Kind, decode, kind, size}; + assert_eq!(kind(&tiny_png()), Some(Kind::Png)); + assert_eq!(size(&tiny_png()), Some((2, 2))); + assert_eq!(kind(TINY_GIF), Some(Kind::Gif)); + assert_eq!(size(TINY_GIF), Some((1, 1))); + assert_eq!(kind(b"BM..."), None); + assert_eq!(size(b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR"), None, "a header cut short"); + let decoded = decode(&tiny_png()).unwrap(); + assert_eq!((decoded.width, decoded.height, decoded.color_space), (2, 2, "DeviceRGB")); + assert_eq!(decoded.samples, vec![255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0]); + assert_eq!(decoded.alpha, Some(vec![255, 255, 255, 0])); + assert_eq!(decode(TINY_GIF).unwrap().samples, vec![255]); + // A PNG claiming 100,000 × 100,000 pixels is refused from its header, before any buffer. + let mut huge = tiny_png(); + huge[16..20].copy_from_slice(&100_000u32.to_be_bytes()); + huge[20..24].copy_from_slice(&100_000u32.to_be_bytes()); + assert_eq!(size(&huge), None, "no size for layout: the box is used"); + assert!(decode(&huge).unwrap_err().contains("million pixels")); + // Truncated and corrupt data are errors, not panics. + let cut = &tiny_png()[..30]; + assert!(decode(cut).is_err()); + let mut bad = tiny_png(); + for b in bad.iter_mut().skip(40) { + *b ^= 0x55; + } + assert!(decode(&bad).is_err() || decode(&bad).is_ok()); + let mut gif = TINY_GIF.to_vec(); + gif.truncate(12); + assert!(decode(&gif).is_err()); + // A form whose picture is broken still lays out, with a warning, and keeps the rest. + let xml = fields_template("") + .replace("image/gif", "image/gif\" transferEncoding=\"none") + .replace("", "AAAA"); + let mut doc = Document::open(Arc::new(shell(&xml))).unwrap(); + let report = render_into(&mut doc).unwrap(); + assert_eq!(report.fields, 6); + assert!(report.warnings.iter().any(|w| w.contains("image")), "{:?}", report.warnings); +} + +#[test] +fn choice_values_are_saved_values_first_deduplicated_and_defaults_are_saved_values_too() { + // Designer's "specify item values": shown 0..3, saved 1..4. The data's 1 means the item + // shown as 0, not the item shown as 1. + let tpl = fields_template("
1
") + .replace("CanadaFranceJapan", "0123") + .replace("CAFRJP", "1234") + .replace("FR", "1"); + let form = layout_xml(&tpl).unwrap(); + let w = widgets(&form.pages[0]); + assert_eq!(w.iter().find(|w| w.name == "country").unwrap().value.as_deref(), Some("1")); + let (t, _) = parse(&tpl).unwrap(); + let tree = form_tree(&t, parse_datasets(&tpl).as_ref(), &Overrides::default()); + let node = tree.root.children.iter().find(|n| n.name == "page1").unwrap().children.iter().find(|n| n.name == "country").unwrap(); + assert_eq!(node.value, "1", "scripts read the saved value"); + // Several values: each once, never more than there are items, written as ascending indices; + // a single-select list takes the first line; defaults are saved values. + let tpl = fields_template("
Japan\nCanadaSpanish\nEnglish\nEnglish\nSpanish
") + .replace("FR", "France") + .replace( + "EnglishFrenchSpanish", + "EnglishFrenchSpanishEnglish\nFrench", + ); + let form = layout_xml(&tpl).unwrap(); + let w = widgets(&form.pages[0]); + assert_eq!(w.iter().find(|w| w.name == "langs").unwrap().value.as_deref(), Some("Spanish\nEnglish")); + assert_eq!(w.iter().find(|w| w.name == "country").unwrap().value.as_deref(), Some("JP")); + assert_eq!(w.iter().find(|w| w.name == "country").unwrap().default.as_deref(), Some("FR")); + let mut doc = Document::open(Arc::new(shell(&tpl))).unwrap(); + render_into(&mut doc).unwrap(); + let doc = Document::open(Arc::new(write_incremental(&doc, &SaveOptions::default()).unwrap())).unwrap(); + let fields = pdfcraft_forms_free_fields(&doc); + let langs = &fields.iter().find(|(n, _)| n == "langs").unwrap().1; + let i = doc.resolve(langs.get(b"I").unwrap()).as_array().unwrap().clone(); + assert_eq!(i.iter().map(|o| o.as_int().unwrap()).collect::>(), vec![0, 2]); + let dv = doc.resolve(langs.get(b"DV").unwrap()).as_array().unwrap().clone(); + assert_eq!(dv.iter().map(|s| s.as_string().unwrap().to_text()).collect::>(), vec!["English", "French"]); + let country = &fields.iter().find(|(n, _)| n == "country").unwrap().1; + assert_eq!(country.get(b"DV").and_then(|v| v.as_string()).map(|s| s.to_text()).as_deref(), Some("FR")); + assert_eq!(doc.resolve(country.get(b"I").unwrap()).as_array().unwrap().len(), 1); + // Picks stored as children are read too. + let tpl = fields_template("
EnglishSpanish
"); + let form = layout_xml(&tpl).unwrap(); + assert_eq!(widgets(&form.pages[0]).iter().find(|w| w.name == "langs").unwrap().value.as_deref(), Some("English\nSpanish")); + // More items than the cap: kept to the cap, and said so. + let many: String = (0..1500).map(|i| format!("i{i}")).collect(); + let tpl = fields_template("").replace("AB", &format!("{many}")); + let form = layout_xml(&tpl).unwrap(); + let other = widgets(&form.pages[0]).into_iter().find(|w| w.name == "other").unwrap(); + assert!(matches!(&other.kind, WidgetKind::Choice { options, .. } if options.len() == 1000)); + assert!(form.warnings.iter().any(|w| w.contains("other") && w.contains("1000 items")), "{:?}", form.warnings); +} + +#[test] +fn pictures_are_embedded_once_across_relayouts_and_bad_data_pictures_are_reported() { + use crate::image::{decode, size}; + let tpl = fields_template("
JP
"); + let mut doc = Document::open(Arc::new(shell(&tpl))).unwrap(); + render_into(&mut doc).unwrap(); + let (t, _) = parse(&tpl).unwrap(); + rerender(&mut doc, &t).unwrap(); + rerender(&mut doc, &t).unwrap(); + let bytes = write_incremental(&doc, &SaveOptions::default()).unwrap(); + let doc = Document::open(Arc::new(bytes)).unwrap(); + let images = doc + .object_numbers() + .into_iter() + .filter( + |n| matches!(&*doc.get(pdfcraft_cos::ObjRef { num: *n, generation: 0 }), Object::Stream(s) if s.dict.name(b"Subtype") == Some(b"Image")), + ) + .count(); + assert_eq!(images, 3, "the PNG, its soft mask and the GIF, however many times the form was laid out"); + // No private map in the catalog: earlier pictures are found on the pages, by their hash. + let root = doc.get(doc.root().unwrap()); + assert!(!root.as_dict().unwrap().contains(b"PCXfaImages")); + // Pictures in the data that aren't pictures: a warning naming the field, the template's + // picture shown instead. + for (data, what) in [("Qk0AAAAA", "not a JPEG, PNG or GIF"), ("not base64!", "not base64")] { + let tpl = fields_template(&format!("
{data}
")); + let form = layout_xml(&tpl).unwrap(); + assert!(form.warnings.iter().any(|w| w.contains("photo") && w.contains(what)), "{what}: {:?}", form.warnings); + assert_eq!(form.pages[0].items.iter().filter(|i| matches!(i, Item::Image { .. })).count(), 2); + } + // A hostile PNG header: no size for layout (the draw keeps its box), refused at decode. + let mut tall = tiny_png(); + tall[16..20].copy_from_slice(&1u32.to_be_bytes()); + tall[20..24].copy_from_slice(&4_000_000_000u32.to_be_bytes()); + assert_eq!(size(&tall), None); + assert!(decode(&tall).unwrap_err().contains("million pixels")); + use base64::Engine; + let b64 = base64::engine::general_purpose::STANDARD.encode(&tall); + let tpl = fields_template("").replace( + r#""#, + &format!( + r#"{b64}"# + ), + ); + let form = layout_xml(&tpl).unwrap(); + assert_eq!(form.pages.len(), 1, "a picture of unknown size takes its box, not a page"); + // A GIF whose frame is far larger than its 1 × 1 screen is refused before it is allocated. + let mut gif = b"GIF89a\x01\x00\x01\x00\x80\x00\x00\xff\xff\xff\x00\x00\x00,\x00\x00\x00\x00".to_vec(); + gif.extend_from_slice(&11_000u16.to_le_bytes()); + gif.extend_from_slice(&11_000u16.to_le_bytes()); + gif.extend_from_slice(b"\x00\x02\x02D\x01\x00;"); + let started = std::time::Instant::now(); + assert!(decode(&gif).unwrap_err().contains("million pixels")); + assert!(started.elapsed() < std::time::Duration::from_secs(2)); + // A template picture over 1 MiB is read whole (text is cut at 1 MiB, pictures are not). + let big = "A".repeat(1_600_000); + let tpl = fields_template("").replace(r#""#, &format!(r#"{big}"#)); + let (t, _) = parse(&tpl).unwrap(); + fn find_image(nodes: &[Node]) -> Option { + nodes.iter().find_map(|n| match n { + Node::Draw(d) => match &d.value { + Value::Image { data, .. } => Some(data.len()), + _ => None, + }, + Node::Subform(s) => find_image(&s.children), + _ => None, + }) + } + assert!(find_image(&t.root.children).is_some_and(|n| n > 1_200_000)); +} diff --git a/crates/xfdf/src/lib.rs b/crates/xfdf/src/lib.rs index c41f8da2..19008061 100644 --- a/crates/xfdf/src/lib.rs +++ b/crates/xfdf/src/lib.rs @@ -13,6 +13,7 @@ use std::collections::HashMap; use std::fmt::Write as _; +use std::io::Write as _; use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString}; use pdfcraft_forms::{FieldKind, FieldValue}; @@ -151,7 +152,7 @@ fn nums_of(doc: &Document, o: Option<&Object>) -> Vec { fn text_of(doc: &Document, d: &Dict, k: &[u8]) -> Option { d.get(k).and_then(|o| match &*doc.resolve(o) { Object::String(s) => Some(s.to_text()), - Object::Name(n) => Some(String::from_utf8_lossy(n).into_owned()), + Object::Name(n) => Some(pdfcraft_forms::name_text(n)), _ => None, }) } @@ -350,9 +351,13 @@ fn pdf_string(s: &str) -> Vec { v } -/// FDF with field values and/or comments (direct objects only; appearances are not carried). +/// FDF with field values and/or comments (appearances are not carried). Each comment is an +/// indirect object of the FDF, so a reply's `/IRT` can point at its parent's object there +/// (§12.7.8.3.1); nothing else refers back into the source document. pub fn export_fdf(doc: &Document, comments: bool, fields: bool, file: &str) -> Vec { let mut body: Vec = b"<< /FDF << ".to_vec(); + // Comment dictionaries, written after the root as objects 2, 3, … + let mut annots: Vec = Vec::new(); if fields { body.extend_from_slice(b"/Fields ["); for f in pdfcraft_forms::fields(doc) { @@ -365,7 +370,7 @@ pub fn export_fdf(doc: &Document, comments: bool, fields: bool, file: &str) -> V match f.kind { FieldKind::CheckBox | FieldKind::Radio => { let v = f.value.first().cloned().unwrap_or_else(|| "Off".into()); - pdfcraft_cos::serialize(&Object::Name(v.into_bytes()), &mut body); + pdfcraft_cos::serialize(&Object::Name(pdfcraft_forms::name_bytes(&v)), &mut body); } FieldKind::List if f.value.len() > 1 => { pdfcraft_cos::serialize(&Object::Array(f.value.iter().map(|v| Object::String(PdfString::text(v))).collect()), &mut body); @@ -377,44 +382,71 @@ pub fn export_fdf(doc: &Document, comments: bool, fields: bool, file: &str) -> V body.extend_from_slice(b"] "); } if comments { - body.extend_from_slice(b"/Annots ["); + // Every exported comment with its source reference, numbered in export order. + let mut picked: Vec<(Option, Dict, usize)> = Vec::new(); for (pi, p) in pdfcraft_model::pages(doc).iter().enumerate() { for a in p.dict.get(b"Annots").map(|a| doc.resolve(a)).and_then(|a| a.as_array().cloned()).unwrap_or_default() { - let Some(mut d) = doc.resolve(&a).as_dict().cloned() else { continue }; - let Some(sub) = d.name(b"Subtype") else { continue }; - if !SUBTYPES.iter().any(|(s, _)| s.as_bytes() == sub) { - continue; + let Some(d) = doc.resolve(&a).as_dict().cloned() else { continue }; + if d.name(b"Subtype").is_some_and(|sub| SUBTYPES.iter().any(|(s, _)| s.as_bytes() == sub)) { + picked.push((a.as_ref(), d, pi)); } - // Only direct values travel: references into this file would dangle. - for k in [&b"P"[..], b"AP", b"Popup", b"IRT", b"Parent", b"FS", b"Sound"] { - d.remove(k); - } - let keys: Vec> = d.iter().filter(|(_, v)| matches!(v, Object::Ref(_))).map(|(k, _)| k.clone()).collect(); - for k in keys { - let Some(o) = d.get(&k) else { continue }; - let v = (*doc.resolve(o)).clone(); - if matches!(v, Object::Stream(_)) { - d.remove(&k); - } else { - d.set(k, v); - } - } - d.set(b"Page".to_vec(), Object::Int(pi as i64)); - pdfcraft_cos::serialize(&Object::Dict(d), &mut body); - body.push(b' '); } } + let numbers: HashMap = + picked.iter().enumerate().filter_map(|(i, (r, _, _))| Some(((*r)?, u32::try_from(i).ok()?.checked_add(2)?))).collect(); + for (_, mut d, pi) in picked { + // A reply points at its parent's object in this FDF; a parent that isn't exported + // leaves the reply unthreaded. + let irt = d.get(b"IRT").and_then(Object::as_ref).and_then(|r| numbers.get(&r)).copied(); + // Only direct values travel: references into this file would dangle. + for k in [&b"P"[..], b"AP", b"Popup", b"IRT", b"Parent", b"FS", b"Sound"] { + d.remove(k); + } + let keys: Vec> = d.iter().filter(|(_, v)| matches!(v, Object::Ref(_))).map(|(k, _)| k.clone()).collect(); + for k in keys { + let Some(o) = d.get(&k) else { continue }; + let v = (*doc.resolve(o)).clone(); + if matches!(v, Object::Stream(_)) { + d.remove(&k); + } else { + d.set(k, v); + } + } + if let Some(num) = irt { + d.set(b"IRT".to_vec(), Object::Ref(ObjRef::new(num, 0))); + } + d.set(b"Page".to_vec(), Object::Int(pi as i64)); + annots.push(d); + } + body.extend_from_slice(b"/Annots ["); + for i in 0..annots.len() { + let _ = write!(body, "{} 0 R ", i + 2); + } body.extend_from_slice(b"] "); } body.extend_from_slice(b"/F "); body.extend(pdf_string(file)); body.extend_from_slice(b" >> >>"); - let mut out = b"%FDF-1.2\n%\xE2\xE3\xCF\xD3\n1 0 obj\n".to_vec(); + let mut out = b"%FDF-1.2\n%\xE2\xE3\xCF\xD3\n".to_vec(); + let mut offsets = Vec::with_capacity(annots.len() + 1); + offsets.push(out.len()); + out.extend_from_slice(b"1 0 obj\n"); out.extend(body); - out.extend_from_slice(b"\nendobj\ntrailer\n<< /Root 1 0 R >>\n%%EOF\n"); + out.extend_from_slice(b"\nendobj\n"); + for (i, d) in annots.into_iter().enumerate() { + offsets.push(out.len()); + let _ = writeln!(out, "{} 0 obj", i + 2); + pdfcraft_cos::serialize(&Object::Dict(d), &mut out); + out.extend_from_slice(b"\nendobj\n"); + } + let xref = out.len(); + let _ = write!(out, "xref\n0 {}\n0000000000 65535 f \n", offsets.len() + 1); + for o in offsets { + let _ = writeln!(out, "{o:010} 00000 n "); + } + let _ = write!(out, "trailer\n<< /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n"); out } - /// The XML element name for a field name (letters, digits, `_`, `-`, `.`). fn xml_name(name: &str) -> String { let mut s: String = name.chars().map(|c| if c.is_alphanumeric() || matches!(c, '_' | '-' | '.') { c } else { '_' }).collect(); @@ -727,7 +759,7 @@ fn walk_fdf(fdf: &Document, list: &[Object], prefix: &str, out: &mut Vec<(String if let Some(v) = d.get(b"V").map(|v| fdf.resolve(v)) { let vals: Vec = match &*v { Object::String(s) => vec![s.to_text()], - Object::Name(n) => vec![String::from_utf8_lossy(n).into_owned()], + Object::Name(n) => vec![pdfcraft_forms::name_text(n)], Object::Array(a) => a.iter().filter_map(|x| fdf.resolve(x).as_string().map(|s| s.to_text())).collect(), _ => Vec::new(), }; @@ -817,10 +849,16 @@ fn import_fdf(doc: &mut Document, bytes: &[u8]) -> Result { if let Some(list) = body.get(b"Fields").map(|f| fdf.resolve(f)).and_then(|f| f.as_array().cloned()) { walk_fdf(&fdf, &list, "", &mut values, 0); } - // Comments. + // Comments: parents first, then replies (whose /IRT needs the parent's new reference). let pages: Vec = pdfcraft_model::pages(doc).iter().map(|p| p.obj).collect(); + let mut placed: HashMap = HashMap::new(); + let mut later: Vec<(ObjRef, Option, Option)> = Vec::new(); for a in body.get(b"Annots").map(|a| fdf.resolve(a)).and_then(|a| a.as_array().cloned()).unwrap_or_default() { - let Object::Dict(mut d) = deep(&fdf, &a, 0) else { continue }; + let Some(mut src) = fdf.resolve(&a).as_dict().cloned() else { continue }; + // The parent is another comment: link it below rather than copying it in. + let irt = src.get(b"IRT").cloned(); + src.remove(b"IRT"); + let Object::Dict(mut d) = deep(&fdf, &Object::Dict(src), 0) else { continue }; let Some(page) = d.get(b"Page").and_then(Object::as_int).and_then(|p| usize::try_from(p).ok()).and_then(|p| pages.get(p).copied()) else { continue; }; @@ -829,9 +867,23 @@ fn import_fdf(doc: &mut Document, bytes: &[u8]) -> Result { } d.remove(b"Page"); d.set(b"P".to_vec(), Object::Ref(page)); - place(doc, page, d)?; + let r = place(doc, page, d)?; + if let Some(fr) = a.as_ref() { + placed.insert(fr, r); + } + if let Some(irt) = irt { + let parent_name = fdf.resolve(&irt).as_dict().and_then(|p| text_of(&fdf, p, b"NM")); + later.push((r, irt.as_ref(), parent_name)); + } report.comments += 1; } + for (r, parent_obj, parent_name) in later { + // The parent imported from this file, else one already in the document with its name. + let parent = parent_obj.and_then(|p| placed.get(&p).copied()).or_else(|| parent_name.and_then(|nm| find_by_name(doc, &nm))); + if let Some(parent) = parent.filter(|p| *p != r) { + doc.update_dict(r, |d| d.set(b"IRT".to_vec(), Object::Ref(parent)))?; + } + } apply_values(doc, &values, &mut report); Ok(report) } diff --git a/crates/xfdf/src/tests.rs b/crates/xfdf/src/tests.rs index 07d079a5..ca1debe4 100644 --- a/crates/xfdf/src/tests.rs +++ b/crates/xfdf/src/tests.rs @@ -129,6 +129,121 @@ fn fdf_round_trips_values_and_comments() { assert_eq!(values(&dst), values(&src)); assert!(r.comments >= 3, "{r:?}"); assert!(summaries(&dst).iter().any(|s| s.contents.as_deref() == Some("scribble"))); + assert_eq!(comment_view(&dst), comment_view(&src), "replies keep their thread"); +} + +/// One page with a check box whose on state is the Shift-JIS name 「はい」 (`/#82#CD#82#A2`). +fn shift_jis_check_box() -> Document { + let objs = [ + "<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>", + "<< /Type /Page /Parent 2 0 R /Annots [5 0 R] >>", + "<< /Fields [5 0 R] >>", + "<< /Type /Annot /Subtype /Widget /FT /Btn /T (agree) /V /Off /AS /Off /Rect [50 700 64 714] /P 3 0 R /AP << /N << /Off 6 0 R /#82#CD#82#A2 6 0 R >> >> >>", + "<< /Length 0 >>\nstream\n\nendstream", + ]; + let mut out = b"%PDF-1.7\n".to_vec(); + let mut offsets = Vec::new(); + for (i, o) in objs.iter().enumerate() { + offsets.push(out.len()); + out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes()); + } + let xref = out.len(); + out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes()); + for o in offsets { + out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes()); + } + out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes()); + Document::open(Arc::new(out)).unwrap() +} + +#[test] +fn non_utf8_check_box_states_round_trip() { + let sjis_hai: &[u8] = b"\x82\xcd\x82\xa2"; + let mut src = shift_jis_check_box(); + set_value(&mut src, "agree", &FieldValue::Check(true)).unwrap(); + // FDF writes the state's own bytes, not its #XX text. + let fdf = export_fdf(&src, false, true, "form.pdf"); + let name: &[u8] = b"/#82#CD#82#A2"; + assert!(fdf.windows(name.len()).any(|w| w == name), "{}", String::from_utf8_lossy(&fdf)); + for data in [fdf, export_xfdf(&src, false, true, "form.pdf").into_bytes()] { + let mut dst = shift_jis_check_box(); + assert_eq!(import(&mut dst, &data).unwrap().fields, 1); + assert_eq!(values(&dst), values(&src)); + let w = fields(&dst).into_iter().next().unwrap().widgets[0].obj; + assert_eq!(dst.get(w).as_dict().unwrap().name(b"AS"), Some(sjis_hai)); + } +} + +/// Who each comment replies to, by name, with its review state. +fn threads(doc: &Document) -> Vec<(Option, Option, Option)> { + summaries(doc).into_iter().map(|s| (s.name, s.in_reply_to, s.state)).collect() +} + +/// #333: a note with two replies and an Accepted review status keeps its thread through FDF. +#[test] +fn fdf_keeps_replies_and_review_status_on_their_note() { + let mut src = blank(); + let meta = |id: &str| Meta { date: Some("D:20261002120000Z".into()), id: id.into() }; + let note = Shape::Note { at: [100.0, 100.0], icon: pdfcraft_annot::NoteIcon::Comment }; + add_annotation( + &mut src, + &NewAnnotation { page: 1, style: Style::default_for(¬e), shape: note, contents: "Literal Note".into(), author: "Alpha".into() }, + &meta("note"), + ) + .unwrap(); + let index = summaries(&src).iter().find(|s| s.subtype == "Text").map(|s| s.index).unwrap(); + add_reply(&mut src, 1, index, "Reply One", "Beta", &meta("one")).unwrap(); + add_reply(&mut src, 1, index, "Reply Two", "Gamma", &meta("two")).unwrap(); + pdfcraft_annot::set_review_state(&mut src, 1, index, pdfcraft_annot::ReviewState::Accepted, "Review", &meta("status")).unwrap(); + let want = threads(&src); + assert_eq!(want.iter().filter(|t| t.1.as_deref() == Some("note")).count(), 3, "{want:?}"); + + let fdf = export_fdf(&src, true, false, "notes.pdf"); + // Every cross-reference entry points at its object (other readers trust the table). + let tail = std::str::from_utf8(&fdf[fdf.windows(5).position(|w| w == b"xref\n").unwrap()..]).unwrap(); + let xref: usize = tail.lines().skip_while(|l| *l != "startxref").nth(1).unwrap().parse().unwrap(); + assert!(fdf[xref..].starts_with(b"xref\n0 6\n"), "{tail}"); + for (num, line) in tail.lines().skip(3).take(5).enumerate() { + let at: usize = line[..10].parse().unwrap(); + assert!(fdf[at..].starts_with(format!("{} 0 obj\n", num + 1).as_bytes()), "object {}: {line}", num + 1); + } + let mut dst = blank(); + assert_eq!(import(&mut dst, &fdf).unwrap().comments, 4); + assert_eq!(threads(&dst), want); + assert_eq!(comment_view(&dst), comment_view(&src)); + // Importing again replaces the thread instead of duplicating or detaching it. + import(&mut dst, &fdf).unwrap(); + assert_eq!(threads(&dst), want); +} + +/// FDFs written before #333 (direct comment dictionaries, no /IRT) still import. +#[test] +fn fdf_with_direct_comments_still_imports() { + let fdf = b"%FDF-1.2\n1 0 obj\n<< /FDF << /Annots [<< /Type /Annot /Subtype /Text /Rect [10 10 30 30] /Contents (old) /NM (a) /Page 0 >>] >> >>\nendobj\ntrailer\n<< /Root 1 0 R >>\n%%EOF\n"; + let mut dst = blank(); + assert_eq!(import(&mut dst, fdf).unwrap().comments, 1); + assert_eq!(threads(&dst), vec![(Some("a".into()), None, None)]); +} + +/// /IRT pointing at itself, at a missing object, at a non-comment or in a cycle neither panics +/// nor loops; only a real parent is linked. +#[test] +fn hostile_fdf_reply_parents_are_ignored() { + let fdf = b"%FDF-1.2\n1 0 obj\n<< /FDF << /Annots [2 0 R 3 0 R 4 0 R 5 0 R 6 0 R] >> >>\nendobj\n\ +2 0 obj\n<< /Subtype /Text /Rect [0 0 9 9] /NM (self) /IRT 2 0 R /Page 0 >>\nendobj\n\ +3 0 obj\n<< /Subtype /Text /Rect [0 0 9 9] /NM (missing) /IRT 99 0 R /Page 0 >>\nendobj\n\ +4 0 obj\n<< /Subtype /Text /Rect [0 0 9 9] /NM (cycle-a) /IRT 5 0 R /Page 0 >>\nendobj\n\ +5 0 obj\n<< /Subtype /Text /Rect [0 0 9 9] /NM (cycle-b) /IRT 4 0 R /Page 0 >>\nendobj\n\ +6 0 obj\n<< /Subtype /Text /Rect [0 0 9 9] /NM (number) /IRT 42 /Page 99 >>\nendobj\n\ +trailer\n<< /Root 1 0 R >>\n%%EOF\n"; + let mut dst = blank(); + assert_eq!(import(&mut dst, fdf).unwrap().comments, 4); + let t = threads(&dst); + let parent = |nm: &str| t.iter().find(|x| x.0.as_deref() == Some(nm)).and_then(|x| x.1.clone()); + assert_eq!(parent("self"), None); + assert_eq!(parent("missing"), None); + assert_eq!((parent("cycle-a").as_deref(), parent("cycle-b").as_deref()), (Some("cycle-b"), Some("cycle-a"))); } #[test] diff --git a/docs/development.md b/docs/development.md index adcfd973..aa46e0cd 100644 --- a/docs/development.md +++ b/docs/development.md @@ -8,9 +8,12 @@ collects what the desktop app reads from its environment and where it writes its The desktop app writes its `log` records to standard error and to `logs/pdfcraft.log` in its settings folder, next to `app.ron`: Linux and FreeBSD `~/.local/share/pdfcraft/logs/` (or `$XDG_DATA_HOME/pdfcraft/logs/`), macOS `~/Library/Application Support/PdfCraft/logs/`, Windows -`%APPDATA%\PdfCraft\data\logs\`. A start from a desktop menu, Finder or the Start menu has no -terminal, so this file is what to attach to a bug report: a failed autosave, a page that would not -render, a render worker that could not start and the report of an internal error all land there. +`%APPDATA%\PdfCraft\data\logs\`. In portable mode (a `portable.txt` file next to the executable, as +the Windows portable zip ships) the settings folder is `PdfCraftData\` beside the executable, and +crash recovery and new digital IDs go there too. A start from a desktop menu, Finder or the Start +menu has no terminal, so this file is what to attach to a bug report: a failed autosave, a page that +would not render, a render worker that could not start and the report of an internal error all land +there. Each launch moves the previous log to `pdfcraft.1.log` (and that one to `pdfcraft.2.log`), so the log of a run that crashed survives the next start. The file stops growing at 16 MiB. `--version` writes no file. diff --git a/docs/editing-images.md b/docs/editing-images.md new file mode 100644 index 00000000..86a01c72 --- /dev/null +++ b/docs/editing-images.md @@ -0,0 +1,23 @@ +# Editing existing images and figures + +In **Edit a PDF → Edit text and images**, click a figure to select it. Drag the selected +figure to move it, drag a corner to resize it proportionally, or press Delete to remove +that placement. The context menu also rotates or flips it. Changes support undo and +survive saving/reopening. + +PDF figures are not always bitmap images. Imported diagrams often use a **Form XObject**, +which packages text, vector paths, and nested images into one object. PdfCraft selects +this object using its `BBox` and optional `Matrix` and edits it as a whole. Its original +content, resources, clipping box, and transparency settings remain intact. Moving or +deleting one use of a shared figure does not alter its other placements or pages. + +**Replace Image** and **Save Image As** apply only to raster Image XObjects; these menu +items are disabled for grouped Form artwork. The tool does not ungroup Forms, edit their +individual internal elements, select arbitrary ungrouped vector paths, or edit inline +images (`BI`/`ID`/`EI`). A Form's bounding box may include whitespace. + +For automation, `page_images` lists both kinds with `kind: "image"` or `kind: "form"`. +Forms have `pixels: [0, 0]`, since they have no single bitmap resolution. Use the returned +number with `image_edit` (move, rotate, flip_horizontal, flip_vertical, delete). +Coordinates are top-left-origin page points. `image_save` and `image_edit`'s replace action +return a clear error for Form artwork. diff --git a/docs/localization.md b/docs/localization.md index 582c830a..43f77e38 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -1,8 +1,8 @@ # Interface language -Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Čeština**, **Português (Brasil)**, **Español** or **Français**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged. +Choose **Menu > Edit > Preferences…** (Command-comma on macOS, Ctrl-comma elsewhere) **> Interface language** and select **Auto**, **English**, **日本語**, **简体中文**, **繁體中文**, **Русский**, **Български**, **Čeština**, **Português (Brasil)**, **Deutsch**, **Español**, **Français**, **తెలుగు**, **Magyar** or **Українська**. The change applies immediately and persists between launches. Command ids, document contents and file names are unchanged. -**Auto** (the default) follows the system language: `LC_ALL`, `LC_MESSAGES` or `LANG`, then the preferred-languages list on macOS. Any Portuguese locale (`pt_BR`, `pt_PT`) uses the Brazilian catalog, any French locale (`fr`, `fr_FR`, `fr_CA`, `fr_BE`) the French one, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one; other Chinese locales (`zh`, `zh_CN`, `zh_SG`, `zh-Hans`) the Simplified Chinese one. A system language without a catalog shows English. On Windows, **Auto** uses the Windows display languages, in preference order, when no supported locale is selected by those environment variables. The query asks Windows directly (`GetUserPreferredUILanguages`, through the `sys-locale` crate) without starting a process, and is cached for the process. If detection fails or the display language has no catalog, PdfCraft shows English. +**Auto** (the default) follows the system language: `LC_ALL`, `LC_MESSAGES` or `LANG`, then the preferred-languages list on macOS. Any Portuguese locale (`pt_BR`, `pt_PT`) uses the Brazilian catalog, any French locale (`fr`, `fr_FR`, `fr_CA`, `fr_BE`) the French one, any German locale (`de`, `de_DE`, `de_AT`, `de_CH`) the German one, and Chinese locales for Taiwan, Hong Kong and Macau (`zh_TW`, `zh_HK`, `zh_MO`, `zh-Hant`) the Traditional Chinese one; other Chinese locales (`zh`, `zh_CN`, `zh_SG`, `zh-Hans`) the Simplified Chinese one. Telugu locales (`te`, `te_IN`) use the Telugu catalog, Bulgarian locales (`bg`, `bg_BG`) the Bulgarian one, and Hungarian locales (`hu`, `hu_HU`) the Hungarian one, and Ukrainian locales (`uk`, `uk_UA`) the Ukrainian one. A system language without a catalog shows English. On Windows, **Auto** uses the Windows display languages, in preference order, when no supported locale is selected by those environment variables. Neutral `C`/`POSIX` locale values (including `C.UTF-8`) inherited from a launcher do not override the Windows display language; explicitly named supported locales such as `fr_FR.UTF-8` still do. The query asks Windows directly (`GetUserPreferredUILanguages`, through the `sys-locale` crate) without starting a process, and is cached for the process. If detection fails or the display language has no catalog, PdfCraft shows English. The control channel exposes the setting through `ui.set`: @@ -10,22 +10,26 @@ The control channel exposes the setting through `ui.set`: {"method":"ui.set","params":{"key":"language","value":"ja"}} ``` -The value is `auto` or a language code (`en`, `ja`, `zh-hans`, `zh-hant`, `cs`, `pt-br`, `es`, `fr`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `cs`, `pt-br`, `es` or `fr`). Unknown values return an error without changing the current setting. Preferences saved before this setting existed follow the system language. +The value is `auto` or a language code (`en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr`, `te`, `hu`, `uk`), in any case. `ui.state` reports `language` as written in the preferences (`auto`, `en`, `ja`, `zh-hans`, `zh-hant`, `ru`, `bg`, `cs`, `pt-br`, `de`, `es`, `fr`, `te`, `hu` or `uk`). Unknown values return an error without changing the current setting. Preferences saved before this setting existed follow the system language. -Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese, Traditional Chinese, Simplified Chinese, Spanish and French translate them (about 1,800 entries each); Czech and Brazilian Portuguese cover the menus so far. Untranslated labels use English. Error details that come from the engine or the operating system are shown as they are, inside a translated frame ("操作「…」に失敗しました: …"). The command palette matches the translated label, the English label and the command id. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing. +Dialogs, panels, menus, notices and history labels go through the catalog (`tl!`). Japanese, Traditional Chinese, Simplified Chinese, Russian, Bulgarian, German, Spanish, French, Telugu, Hungarian and Ukrainian translate them (about 1,800 entries each); Czech and Brazilian Portuguese cover the menus so far. Untranslated labels use English. Error details that come from the engine or the operating system are shown as they are, inside a translated frame ("操作「…」に失敗しました: …"). The command palette matches the translated label, the English label and the command id. Vertical Japanese PDF rendering is an existing viewer feature; this change does not add vertical text editing. -Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Czech, Brazilian Portuguese, Spanish and French need only the bundled Latin faces (tested in `tests/fonts.rs`). +The Ukrainian catalog has 2,047 entries, written from the meaning of the English labels using Ukrainian computing vocabulary. Commands use infinitives; fixed-count messages use count-neutral wording. Plural-aware messages use the three [CLDR Ukrainian integer forms](https://www.unicode.org/cldr/charts/48/supplemental/language_plural_rules.html#uk): `1 сторінка`, `2 сторінки`, `5 сторінок`; 11–14 take the third form, while 21 and 22 return to the first and second. Calendar dates use genitive month names (`9 жовтня 2026`), while headings use standalone names (`Жовтень 2026`). Home recommendation cards grow to fit translated text; the toolbar search hint truncates before the shortcut, with the full label available on hover. + +Japanese interface text uses BIZ UDPGothic from [craft-fonts](https://github.com/storytold/craft-fonts), an optional build input that every release includes (`CRAFT_FONTS_DIR`; see the README). A build made without it has no Japanese face, so Japanese labels show replacement boxes. Chinese is drawn with the same craft-fonts CJK faces (in Simplified Chinese the `Hans` faces come first, so a line never mixes faces with different baselines), so a character none of them has shows a replacement box (and every Chinese character does in a build without craft-fonts). Russian, Bulgarian, Czech, Brazilian Portuguese, German, Spanish, French and Hungarian need only the bundled Latin faces (tested in `tests/fonts.rs`). Hungarian uses the same one/other plural rule as English: 1 is singular, and every other count — including 0 — is the other form. After a numeral the noun stays singular (`1 oldal`, `2 oldal`). Telugu is drawn with a craft-fonts `Telu` face (Noto Sans Telugu, kept in the web build too), whose conjuncts egui shapes; a build without one shows Telugu labels as replacement boxes, and the release-pinned craft-fonts input has no Telugu face yet. There is no right-to-left interface language yet. File names and document titles in Arabic or Hebrew are drawn with a craft-fonts `Arab` face when the build input has one; on desktop, a character no embedded face has is drawn with one font already installed on the machine (`PDFCRAFT_SYSTEM_FONTS=0` turns this off). The tab strip, the recent-files list and Properties show such names in display order; other places (bookmarks, comments, search results) have the glyphs but may order mixed-direction text wrongly. -Simplified Chinese and French coverage tests, following PhotoCraft's complete-catalog approach, check every registered command, All tools label and `tl!("literal")` in the UI source. Simplified Chinese also checks direct `i18n::t` calls and multiline literals, plus the About tabs, contributor name modes, sort options, table headers and model columns. Generated history labels, contributor summaries and diagnostics are tested with user-supplied names and error details. Catalog coverage does not guarantee glyph coverage: the release-pinned craft-fonts input currently has no `Hans` face, and the web build embeds only BIZ UDPGothic Regular. Chinese font delivery remains separate work, as discussed in [#112](https://github.com/storytold/pdfcraft/pull/112) and [#140](https://github.com/storytold/pdfcraft/pull/140). +Simplified Chinese, French and German coverage tests, following PhotoCraft's complete-catalog approach, check every registered command, All tools label and `tl!("literal")` in the UI source. Simplified Chinese also checks direct `i18n::t` calls and multiline literals, plus the About tabs, contributor name modes, sort options, table headers and model columns. Generated history labels, contributor summaries and diagnostics are tested with user-supplied names and error details. Catalog coverage does not guarantee glyph coverage: the release-pinned craft-fonts input currently has no `Hans` face, and the web build embeds only BIZ UDPGothic Regular. Chinese font delivery remains separate work, as discussed in [#112](https://github.com/storytold/pdfcraft/pull/112) and [#140](https://github.com/storytold/pdfcraft/pull/140). + +German labels keep nouns capitalized, including tool names inserted into notices. The certificate viewer uses the `certificate` context for `Subject` (the certificate holder), while document properties keep the plain translation (the topic). Search hints reserve room for the shortcut, and Home cards limit their title and description to the available rows; hovering shows the full text when it is shortened. ## How translations work PdfCraft uses the same system as PhotoCraft (`crates/ui-egui/src/i18n/`): - Strings in the code stay in English and serve as lookup keys. At draw time, `tl!("Save")` returns the text in the current language, and falls back to English when there is no entry. -- Each language has a catalog, `crates/ui-egui/src/i18n/.tsv`, with one entry per line: `contextEnglishtranslation`. An empty context marks a plain string. `@id` keys an entry by command id (for example `file.saveAs`), so one menu item can read differently from another with the same English label. `@plural` holds plural forms. Any other context disambiguates an English word with several meanings (`tr_ctx`). The header of `ja.tsv` documents the format and escapes. +- Each language has a catalog, `crates/ui-egui/src/i18n/.tsv`, with one entry per line: `contextEnglishtranslation`. An empty context marks a plain string. `@id` keys an entry by command id (for example `file.saveAs`), so one menu item can read differently from another with the same English label. `@plural` holds plural forms. Any other context disambiguates an English word with several meanings (`tl_ctx!` in UI code, for example the signature pad's "Type" button against the "Type" column). The header of `ja.tsv` documents the format and escapes. - Placeholders (`{name}`) must appear in both columns. A trailing `…` must be kept. `i18n::fmt` fills placeholders in one pass, so a value such as a file name containing `{n}` is inserted as written. - Catalogs are validated strictly: unknown escapes, unknown `@` contexts, placeholder or ellipsis mismatches, wrong plural form counts and duplicates are errors. The tests require the bundled catalogs to have none; at run time a bad line is skipped (and logged) and that string shows in English. - Catalogs are assets: each `.tsv` has an `ATTRIBUTION.toml` entry with `kind = "translation"` and its SHA-256, checked by `cargo xtask assets`. diff --git a/docs/mcp.md b/docs/mcp.md new file mode 100644 index 00000000..5465c5b9 --- /dev/null +++ b/docs/mcp.md @@ -0,0 +1,45 @@ +# MCP conventions + +Start explicitly with `pdfcraft-cli mcp [--root DIR] [--compact]`. The server uses JSON lines on +stdin/stdout, opens no port, and stops on EOF. `--root` confines all reads and writes, including +commands invoked through the common tools. Logs stay on stderr. + +| Tool | Arguments | +|---|---| +| `command_list` | `doc?`, `filter?`, `enabled_only?`; returns `{commands: [...]}` with mapped tool schemas | +| `command_run` | `id`, `params?` (the mapped task tool's arguments, including `doc` when required) | +| `command_batch` | `steps: [{id, params?}]`, `stop_on_error?` (default true) | +| `doc_inspect` | `doc?`; one document's info or `{documents: [...]}` for the session | +| `render_preview` | `doc?`, `page?` (1-based, default 1), `max_side?` (1–4096, default 1024) | + +All existing task tools remain available. Compact mode lists these five tools alongside its +existing core set and `tool_search` / `tool_call`. There are no legacy-name aliases. The full +[automation guide](../crates/automation/README.md) describes the tools, resources, coordinate +system, root confinement, and errors. + +```text +command_run {"id":"file.open","params":{"path":"input.pdf"}} + → {"doc":1,...} +command_list {"doc":1,"filter":"rotate"} +command_run {"id":"page.rotate","params":{"doc":1,"degrees":90}} +render_preview {"doc":1,"page":1,"max_side":512} +``` + +Every tool has a title and the four MCP annotation hints. Unknown top-level argument keys return +JSON-RPC `-32602`; command parameter keys that do not belong to the mapped task tool are ignored +with warnings. Task failures and escaped tool panics return `isError: true`, and the server keeps +serving. A failed batch includes per-step errors and is marked `isError: true`. + +Exports complete synchronously. A call's `progressToken` and cancellation notifications are +harmlessly ignored; the server does not provide background export progress or cancellation. + +## Resources and protocol versions + +`pdfcraft://document` returns the session's document information (as `doc_inspect` without +arguments), including an empty `documents` array before any PDF is opened. `pdfcraft://commands` +returns the command catalog (as `command_list`). Existing per-document info, text and page-image +resources and their templates are preserved. + +Unknown top-level argument keys are refused with JSON-RPC `-32602` (invalid params) instead of +being ignored, so a misspelled argument is reported rather than silently dropped. Clients that +sent extra keys before get an error now. diff --git a/docs/releasing.md b/docs/releasing.md index 1b561644..30900549 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -82,6 +82,9 @@ from Preview). Files opened from Finder arrive as Apple events, which - **Notarization:** the app is zipped and sent with `xcrun notarytool submit --wait`, the ticket is stapled, and the result is checked with `codesign --verify`, `stapler validate` and `spctl`. The app ships on a drag-to-Applications DMG, which is signed and notarized too. + Its Finder window (background, icon size and positions) comes from + [`packaging/macos/dmg/`](../packaging/macos/dmg/README.md), and its volume is named `PdfCraft` + without the version, which the window's background needs; the DMG file name keeps the version. - **CLI:** `pdfcraft-cli` is signed and notarized as a zip. A bare executable can't hold a stapled ticket, so Gatekeeper looks it up online the first time a downloaded copy runs. @@ -113,8 +116,10 @@ neither the MSI nor the portable zip needs the Visual C++ redistributable. child process before signing, so the MSI isn't held open when signtool runs. The ARM64 install smoke test checks that both all-users shortcuts point at the installed `pdfcraft.exe` and are removed on uninstall, and that `INSTALLDESKTOPSHORTCUT=0` skips the desktop one. -- The portable zip holds both executables, the README, the licences, and the OFL licence of each - embedded craft-fonts family. +- The portable zip holds both executables, the README, the licences, the OFL licence of each + embedded craft-fonts family, and `portable.txt`. That marker beside `pdfcraft.exe` keeps the + settings, logs, crash recovery and new digital IDs in `PdfCraftData\` next to the exe instead of + `%APPDATA%` and `%LOCALAPPDATA%` (`crates/ui-egui/src/portable.rs`). - **Signing:** `packaging/windows/sign.ps1` signs both executables and the MSI with `signtool` (SHA-256, RFC 3161 timestamp), from a `.pfx` (`WINDOWS_CERTIFICATE`) or Azure Trusted Signing (`AZURE_*`), whichever is configured. It is the one place to change when signing changes. diff --git a/packaging/linux/ai.storyteller.pdfcraft.desktop b/packaging/linux/ai.storyteller.pdfcraft.desktop index f32d625d..f7928d62 100644 --- a/packaging/linux/ai.storyteller.pdfcraft.desktop +++ b/packaging/linux/ai.storyteller.pdfcraft.desktop @@ -3,12 +3,12 @@ Type=Application Name=PdfCraft GenericName=PDF Editor Comment=Read, organize, combine, split and secure PDFs -Exec=pdfcraft %F +Exec=pdfcraft %U TryExec=pdfcraft Icon=ai.storyteller.pdfcraft Terminal=false StartupNotify=true StartupWMClass=pdfcraft -Categories=Office;Viewer;Graphics; +Categories=Office;Viewer; Keywords=pdf;viewer;editor;annotate;sign;forms;merge;split; MimeType=application/pdf; diff --git a/packaging/macos/dmg/README.md b/packaging/macos/dmg/README.md new file mode 100644 index 00000000..ec902e30 --- /dev/null +++ b/packaging/macos/dmg/README.md @@ -0,0 +1,44 @@ +# macOS DMG window + +What Finder shows when the DMG opens: a 660 × 400 pt background with the app icon and the +`Applications` link side by side. `package.sh` copies these files into the image; nothing here is +generated at build time, so the DMG still builds with `hdiutil makehybrid` (no mounted device, no +Finder scripting on CI, nothing extra installed in the signing job). + +| File | What | +|---|---| +| `background.svg` | Source of the background: the app icon (`assets/app-icon/pdfcraft.svg`, linked, not copied) cropped as a cover on the PdfCraft colour field (`#12a58a`), Ink and Paper. Its text (Inter, JetBrains Mono) is outlined, so rendering it needs no fonts. No paper grain: noise doesn't compress, and this keeps `background.tiff` small. | +| `background.tiff` | The background at 1x (660 × 400 px, 72 dpi) and 2x (1320 × 800 px, 144 dpi) in one HiDPI TIFF (16-colour palette, Deflate, sRGB). Goes to `.background/background.tiff`. | +| `dmg-layout.DS_Store` | Finder's view settings for the volume: window size, icon size 128, PdfCraft.app at (326, 205), `Applications` at (574, 205), and the background. Goes to `.DS_Store` in the image; named so it isn't mistaken for (or ignored like) a Finder-generated `.DS_Store`. | +| `generate.py` | Writes `background.tiff` and `dmg-layout.DS_Store` from the SVG and the layout above. | + +## The volume name has no version + +The mounted volume is called `PdfCraft`, not `PdfCraft `. The layout points at the background +through an alias that includes the volume name, and Finder resolves it by that name: with a +versioned name the window keeps its size and icon positions but shows no background (tested). +The DMG file name (`pdfcraft--macos-.dmg`) still carries the version. + +## Rules + +- **Finder draws the icon labels in black in light and dark mode** when a window has a background, + so the area under both icons stays light (Paper). +- **Nothing goes inside the icon boxes:** artwork keeps 10 pt clear of each 128 pt icon box and of + the label strip under it. + +## Regenerate + +Edit `background.svg` (or the layout constants in `generate.py`), then run, on any OS: + +```sh +pip install 'pillow>=12' ds_store==1.3.3 mac_alias==2.2.3 +python3 packaging/macos/dmg/generate.py # needs resvg on PATH +``` + +Both outputs are byte-for-byte reproducible and carry nothing from the machine that ran it: the +SVG renders without any fonts, and `dmg-layout.DS_Store` is written from scratch, its background +alias holding only the volume name and `/.background/background.tiff`. If you add text to the SVG, +outline it (`usvg` from resvg converts text to paths). The window is 660 × 432 with Finder's 32 pt +title bar; the content area is 660 × 400. + +This follows VectorCraft's `packaging/macos/dmg/` (storytold/vectorcraft#493). diff --git a/packaging/macos/dmg/background.svg b/packaging/macos/dmg/background.svg new file mode 100644 index 00000000..749b6a49 --- /dev/null +++ b/packaging/macos/dmg/background.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/macos/dmg/background.tiff b/packaging/macos/dmg/background.tiff new file mode 100644 index 00000000..119a02d3 Binary files /dev/null and b/packaging/macos/dmg/background.tiff differ diff --git a/packaging/macos/dmg/dmg-layout.DS_Store b/packaging/macos/dmg/dmg-layout.DS_Store new file mode 100644 index 00000000..33dd6ed8 Binary files /dev/null and b/packaging/macos/dmg/dmg-layout.DS_Store differ diff --git a/packaging/macos/dmg/generate.py b/packaging/macos/dmg/generate.py new file mode 100644 index 00000000..27adba06 --- /dev/null +++ b/packaging/macos/dmg/generate.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +"""Regenerate background.tiff and dmg-layout.DS_Store for the macOS DMG window (see README.md here). + +Needs resvg on PATH and: + pip install 'pillow>=12' ds_store==1.3.3 mac_alias==2.2.3 + +Runs on any OS, no Mac needed. Both outputs come from this script alone, so they carry nothing +from the machine that made them: no local paths, user or disk names, dates or volume UUIDs. + + python3 packaging/macos/dmg/generate.py +""" + +import datetime +import os +import shutil +import subprocess +import sys +import tempfile + +from ds_store import DSStore +from mac_alias import Alias, TargetInfo, VolumeInfo +from PIL import Image, ImageCms + +HERE = os.path.dirname(os.path.abspath(__file__)) + +# Keep in sync with package.sh (volume name) and README.md (layout). +VOLUME = "PdfCraft" +WIDTH, HEIGHT = 660, 400 # window content, pt; the background's 1x size +TITLE_BAR = 32 +ICON_SIZE = 128 +PALETTE = 16 # colours in background.tiff +ICONS = {"PdfCraft.app": (326, 205), "Applications": (574, 205)} +# A fixed date for the alias and the colour profile. Finder never matches it against the image +# (each build is a new volume): it finds the background by volume name and path. +FIXED_DATE = datetime.datetime(2026, 10, 8, tzinfo=datetime.timezone.utc) + + +def render(svg, width, out): + # The SVG's text is already outlined and no fonts are loaded, so every machine draws the + # same pixels. + subprocess.run( + ["resvg", "--skip-system-fonts", "-w", str(width), svg, out], + check=True, + ) + with Image.open(out) as im: + return im.convert("RGB") + + +def quantize(im): + # No dithering: dither noise compresses badly and the artwork has flat fills. + return im.quantize(PALETTE, method=Image.Quantize.FASTOCTREE, dither=Image.Dither.NONE) + + +def srgb_profile(): + # LittleCMS's built-in sRGB ("No copyright, use freely"), with its creation date fixed. + icc = bytearray(ImageCms.ImageCmsProfile(ImageCms.createProfile("sRGB")).tobytes()) + d = FIXED_DATE + icc[24:36] = b"".join(v.to_bytes(2, "big") for v in (d.year, d.month, d.day, 0, 0, 0)) + if icc[84:100] != bytes(16): + sys.exit("error: the sRGB profile has an ID; recompute it after fixing the date") + return bytes(icc) + + +def write_tiff(svg, out): + """1x (72 dpi) and 2x (144 dpi) in one TIFF, as `tiffutil -cathidpicheck` writes them.""" + with tempfile.TemporaryDirectory() as tmp: + one, two = ( + quantize(render(svg, WIDTH * s, os.path.join(tmp, f"{s}x.png"))) for s in (1, 2) + ) + two.encoderinfo = {"dpi": (144, 144)} # the rest as the first page + # Opaque 16-colour palette, Deflate: the artwork is three inks plus antialiasing, and a palette + # keeps the file small (RGB was ~230 KB; repository assets must stay small). As PhotoCraft's. + one.save( + out, + "TIFF", + save_all=True, + append_images=[two], + compression="tiff_adobe_deflate", # brand-ok: Pillow's name for TIFF compression 8 (Deflate) + dpi=(72, 72), + icc_profile=srgb_profile(), + ) + with Image.open(out) as im: + im.seek(1) + if im.info.get("dpi") != (144, 144): + sys.exit("error: the 2x page isn't 144 dpi (needs Pillow 12 or later)") + strips = [] + for page in (0, 1): + im.seek(page) + strips += zip(im.tag_v2[273], im.tag_v2[279]) # StripOffsets, StripByteCounts + # TIFF keeps directories and tag data on even offsets. When a strip ends on an odd one, libtiff + # leaves the padding byte after it uninitialised, so the file changed from run to run with + # identical pixels. Zero it: nothing refers to that byte. + starts = {o for o, _ in strips} + with open(out, "r+b") as f: + for o, n in strips: + if (o + n) % 2 and o + n not in starts: + f.seek(o + n) + f.write(bytes(1)) + + +def background_alias(): + # What Finder stores for /.background/background.tiff on the volume, minus anything about the + # machine (dmgbuild writes the same kind of alias from a mounted image). Disk type 5 (ejectable), + # the volume flags and the folder/file ids (25, 26) are what Finder wrote on a test image; the ids + # are only hints, as the volume's creation date. + volume = VolumeInfo(VOLUME, FIXED_DATE, b"H+", 5, 0x0D02, b"\0\0") + volume.posix_path = f"/Volumes/{VOLUME}" + target = TargetInfo(0, "background.tiff", 25, 26, FIXED_DATE, b"\0\0\0\0", b"\0\0\0\0") + target.folder_name = ".background" + target.cnid_path = [25] + target.carbon_path = f"{VOLUME}:.background:\0background.tiff" + target.posix_path = "/.background/background.tiff" + return Alias(volume=volume, target=target).to_bytes() + + +def write_ds_store(out): + if os.path.exists(out): + os.remove(out) + with DSStore.open(out, "w+") as d: + d["."]["bwsp"] = { + "ShowStatusBar": False, + "ShowToolbar": False, + "ShowTabView": False, + "ContainerShowSidebar": False, + "ShowSidebar": False, + "WindowBounds": f"{{{{200, 528}}, {{{WIDTH}, {HEIGHT + TITLE_BAR}}}}}", + } + d["."]["icvp"] = { + "viewOptionsVersion": 1, + "backgroundType": 2, + "backgroundImageAlias": background_alias(), + "backgroundColorRed": 1.0, + "backgroundColorGreen": 1.0, + "backgroundColorBlue": 1.0, + "gridOffsetX": 0.0, + "gridOffsetY": 0.0, + "gridSpacing": 100.0, + "arrangeBy": "none", + "showIconPreview": True, + "showItemInfo": False, + "labelOnBottom": True, + "textSize": 16.0, + "iconSize": float(ICON_SIZE), + } + d["."]["vSrn"] = ("long", 1) + for name, pos in ICONS.items(): + d[name]["Iloc"] = pos + + +def main(): + if not shutil.which("resvg"): + sys.exit("error: resvg not found (brew install resvg / cargo install resvg --locked)") + write_tiff(os.path.join(HERE, "background.svg"), os.path.join(HERE, "background.tiff")) + write_ds_store(os.path.join(HERE, "dmg-layout.DS_Store")) + + +if __name__ == "__main__": + main() diff --git a/packaging/macos/package.sh b/packaging/macos/package.sh index 7f69443b..507e911d 100755 --- a/packaging/macos/package.sh +++ b/packaging/macos/package.sh @@ -130,10 +130,15 @@ STAGE="$WORK/dmg" mkdir -p "$STAGE" ditto "$APP" "$STAGE/PdfCraft.app" ln -s /Applications "$STAGE/Applications" +# Finder window layout: background, icon size and positions (packaging/macos/dmg/README.md). +mkdir -p "$STAGE/.background" +cp "$HERE/dmg/background.tiff" "$STAGE/.background/background.tiff" +cp "$HERE/dmg/dmg-layout.DS_Store" "$STAGE/.DS_Store" rm -f "$DMG" "$WORK/raw.dmg" # makehybrid + convert builds the image without attaching a device, unlike `create -srcfolder`, # which is flaky on CI runners ("Resource busy") and hangs in sandboxed sessions. -hdiutil makehybrid -hfs -hfs-volume-name "PdfCraft $VERSION" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE" +# The volume name has no version: .DS_Store finds the background through an alias that includes it. +hdiutil makehybrid -hfs -hfs-volume-name "PdfCraft" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE" hdiutil convert "$WORK/raw.dmg" -format UDZO -imagekey zlib-level=9 -o "$DMG" rm -f "$WORK/raw.dmg" sign "$DMG" diff --git a/packaging/windows/package.ps1 b/packaging/windows/package.ps1 index f6fc6a47..14588668 100644 --- a/packaging/windows/package.ps1 +++ b/packaging/windows/package.ps1 @@ -5,7 +5,7 @@ .DESCRIPTION Produces, in $env:DIST (default: dist/release): pdfcraft--windows-.msi per-machine installer (WiX v5) - pdfcraft--windows--portable.zip pdfcraft.exe + pdfcraft-cli.exe + pdfcraft--windows--portable.zip pdfcraft.exe + pdfcraft-cli.exe + portable.txt The binaries link the C runtime statically (+crt-static), so neither the MSI nor the portable zip needs the Visual C++ redistributable. Signing is delegated to sign.ps1 (skipped with a @@ -122,6 +122,10 @@ if ($env:CRAFT_FONTS_DIR) { if (Test-Path $ofl) { Copy-Item $ofl (Join-Path $Portable "OFL-$($_.Name).txt") } } } +# portable.txt beside pdfcraft.exe switches on portable mode: settings, logs, recovery files and new +# digital IDs go to PdfCraftData\ next to the exe instead of %APPDATA% (#157; see +# crates/ui-egui/src/portable.rs). +Copy-Item (Join-Path $PSScriptRoot 'portable.txt') $Portable $Zip = Join-Path $Dist "pdfcraft-$Version-windows-$Arch-portable.zip" Remove-Item -Force $Zip -ErrorAction SilentlyContinue Compress-Archive -Path $Portable -DestinationPath $Zip diff --git a/packaging/windows/portable.txt b/packaging/windows/portable.txt new file mode 100644 index 00000000..9586f930 --- /dev/null +++ b/packaging/windows/portable.txt @@ -0,0 +1,9 @@ +PdfCraft portable mode + +While this file sits next to pdfcraft.exe, PdfCraft keeps its settings, log files, crash-recovery +autosaves and new digital IDs in the PdfCraftData folder beside it, and writes nothing to +%APPDATA% or %LOCALAPPDATA%. Copy the whole folder to move PdfCraft with its settings. + +Delete this file to use the normal per-user folders (%APPDATA%\PdfCraft and +%LOCALAPPDATA%\PdfCraft) instead. If PdfCraftData can't be written (for example on a read-only +drive), PdfCraft says so and uses the per-user folders instead. diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 16674649..2f8d8eeb 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -274,8 +274,8 @@ milestone = "M1" status = "shipped" commands = ["file.save"] tools = ["doc_save"] -evidence = ["crates/ui-egui/tests/editing.rs::save_writes_an_incremental_update_and_clears_dirty", "crates/engine/src/tests.rs::save_is_incremental_stamps_mod_date_and_rebases", "crates/organize/src/tests.rs::successive_incremental_saves_stack_revisions"] -notes = "" +evidence = ["crates/ui-egui/tests/editing.rs::save_writes_an_incremental_update_and_clears_dirty", "crates/engine/src/tests.rs::save_is_incremental_stamps_mod_date_and_rebases", "crates/organize/src/tests.rs::successive_incremental_saves_stack_revisions", "crates/automation/src/lib.rs::staging_never_writes_through_a_file_planted_at_its_name", "crates/ui-egui/src/editing.rs::saving_never_writes_through_a_file_planted_at_the_staging_name"] +notes = "The atomic write stages to a new, randomly named file created with create_new, so a file or link planted at the staging path is never written through (desktop app, automation and CLI)." [[feature]] id = "core.save-as-full-rewrite" @@ -656,10 +656,22 @@ area = "A" tier = "P1" milestone = "M3" status = "shipped" +commands = ["file.open_recent", "file.clear_recent"] +tools = [] +evidence = ["crates/ui-egui/tests/ui.rs::persistence_round_trips_and_tolerates_garbage", "crates/ui-egui/tests/commands.rs::the_open_recent_menu_lists_files_and_opens_one", "crates/ui-egui/tests/commands.rs::the_open_recent_menu_is_disabled_while_the_list_is_empty", "crates/ui-egui/tests/commands.rs::clear_recent_files_at_the_foot_of_open_recent_empties_the_list", "crates/ui-egui/tests/commands.rs::clear_recent_files_runs_from_the_palette_and_is_saved", "crates/ui-egui/tests/ui.rs::home_removes_one_recent_file_or_clears_them_all"] +notes = "never reads other apps' lists; File ▸ Open Recent lists them (#290), an already-open file focuses its tab instead of reopening it; Clear Recent Files at the foot of Open Recent, on Home and in the palette, and a remove button on each Home row (#430); missing: the row's remove button shows on pointer hover only (keyboard users clear the whole list)" + +[[feature]] +id = "core.reopen-last-session" +title = "Reopen the files from the last session at startup" +area = "A" +tier = "P2" +milestone = "M3" +status = "shipped" commands = [] tools = [] -evidence = ["crates/ui-egui/tests/ui.rs::persistence_round_trips_and_tolerates_garbage"] -notes = "never reads other apps' lists" +evidence = ["crates/ui-egui/tests/editing.rs::the_last_session_reopens_its_files_pages_and_active_tab", "crates/ui-egui/tests/editing.rs::quitting_with_unsaved_changes_still_remembers_every_file", "crates/ui-egui/tests/editing.rs::the_last_session_is_off_by_default_and_skips_missing_and_duplicate_files", "crates/ui-egui/src/last_session.rs::malformed_sessions_are_skipped_and_capped"] +notes = "Desktop, off by default (Preferences ▸ Documents and view, #442): the files open when PdfCraft last closed reopen at the page each showed, with the same tab in front; files named on the command line open after them, in front; files that are gone, and documents the Recovery dialog offers, are skipped. Quitting with unsaved changes still remembers every tab. Not yet: zoom, layout and scroll position within the page (#418); only the first encrypted file asks for its password (later ones stay closed), and a file opened after it dismisses that prompt; a save that fails partway through a quit can leave the remembered list from that quit; never-saved documents and the web build have no paths to reopen." [[feature]] id = "core.usage-rights-preserve" @@ -746,8 +758,8 @@ milestone = "M2" status = "partial" commands = [] tools = [] -evidence = ["crates/render/src/raster.rs::huge_cid_width_ranges_terminate"] -notes = "CID-to-Unicode tables are M2.4" +evidence = ["crates/render/src/raster.rs::huge_cid_width_ranges_terminate", "crates/render/src/raster.rs::non_embedded_japanese_cid_fonts_draw_with_a_craft_fonts_face", "crates/render/src/raster.rs::mincho_substitute_covers_half_width_katakana"] +notes = "CID-to-Unicode tables are M2.4; a non-embedded Adobe-Japan1 font is drawn in a craft-fonts Japanese face (Mincho or Gothic by name; BIZ UDMincho for Mincho, which covers half-width katakana) when built with craft-fonts; non-embedded GB1, CNS1 and Korea1 fonts still draw nothing (no allowed face)" [[feature]] id = "view.render-image-codecs" @@ -770,8 +782,8 @@ milestone = "M2" status = "shipped" commands = [] tools = [] -evidence = ["crates/render/src/raster.rs::widget_appearance_states", "crates/render/src/raster.rs::highlights_without_an_appearance_are_drawn"] -notes = "a Highlight without /AP is drawn from /QuadPoints, /C and /CA; other comment types without /AP are not drawn yet" +evidence = ["crates/render/src/raster.rs::widget_appearance_states", "crates/render/src/raster.rs::highlights_without_an_appearance_are_drawn", "crates/engine/src/tests.rs::comments_without_appearances_are_drawn_but_not_saved"] +notes = "comments without /AP (FreeText, Ink, notes, stamps, shapes, markup) are drawn from their dictionaries in the page view, page_render and page images, never saved (a read-only document still gets the renderer's Highlight fallback only); Link borders without /AP and printing are not covered yet" [[feature]] id = "view.render-error-isolation" @@ -1107,7 +1119,7 @@ status = "shipped" commands = [] tools = [] evidence = ["crates/ui-egui/tests/control.rs::state_and_view_options", "crates/ui-egui/tests/ui.rs::theme_and_view_options_apply", "crates/ui-egui/tests/viewer.rs::two_page_view_steps_a_spread_at_a_time", "crates/ui-egui/tests/viewer.rs::arrow_and_page_keys_move_through_a_scrolling_document"] -notes = "in two-page view, next and previous page move a whole spread (with or without a cover page); → / ← turn pages in every layout, ↓ / ↑ scroll a line and Page Down / Page Up a screen (the page in single-page view), as in Acrobat" +notes = "in two-page view, next and previous page move a whole spread (with or without a cover page); → / ← turn pages in every layout, ↓ / ↑ scroll a line and Page Down / Page Up a screen (the page in single-page view), as in Acrobat; in single-page view ↓ / ↑ turn the page once there is nothing left to scroll that way, like the wheel" [[feature]] id = "view.goto-page-label" @@ -1142,8 +1154,8 @@ milestone = "M3" status = "shipped" commands = [] tools = [] -evidence = ["crates/ui-egui/tests/view.rs::clicking_a_link_goes_to_its_destination"] -notes = "GoTo and URI links; other action types (JavaScript, Launch, named actions) arrive with M6" +evidence = ["crates/ui-egui/tests/view.rs::clicking_a_link_goes_to_its_destination", "crates/ui-egui/tests/view.rs::link_destinations_scroll_to_their_position", "crates/render/src/inspect.rs::bookmarks_and_links_keep_their_destination_view"] +notes = "GoTo and URI links; other action types (JavaScript, Launch, named actions) arrive with M6. Destinations keep their position and zoom (/XYZ, /Fit, /FitH, /FitV, /FitR, named); the /FitB* content box is read as the whole page" [[feature]] id = "view.annotation-hover" @@ -1166,8 +1178,8 @@ milestone = "M3" status = "shipped" commands = [] tools = ["doc_list"] -evidence = ["crates/ui-egui/tests/editing.rs::combining_files_opens_a_new_unsaved_tab", "crates/ui-egui/tests/control.rs::state_and_view_options", "crates/ui-egui/tests/viewer.rs::each_document_keeps_its_own_scroll_position"] -notes = "each tab keeps its own scroll position (#189)" +evidence = ["crates/ui-egui/tests/editing.rs::combining_files_opens_a_new_unsaved_tab", "crates/ui-egui/tests/control.rs::state_and_view_options", "crates/ui-egui/tests/viewer.rs::each_document_keeps_its_own_scroll_position", "crates/ui-egui/tests/viewer.rs::a_new_document_opens_at_the_top_after_a_relaunch"] +notes = "each tab keeps its own scroll position (#189), and a newly opened one starts at the top, not where an earlier session left the scroll" [[feature]] id = "view.multiple-windows" @@ -1238,8 +1250,8 @@ milestone = "M3" status = "shipped" commands = [] tools = ["doc_info"] -evidence = ["crates/ui-egui/tests/ui.rs::opening_a_pdf_shows_comments_and_bookmarks", "crates/ui-egui/tests/control.rs::state_and_view_options"] -notes = "" +evidence = ["crates/ui-egui/tests/ui.rs::opening_a_pdf_shows_comments_and_bookmarks", "crates/ui-egui/tests/control.rs::state_and_view_options", "crates/ui-egui/tests/view.rs::bookmark_destinations_scroll_to_their_position"] +notes = "A bookmark goes to its destination's position and zoom, not just its page" [[feature]] id = "view.highlight-current-bookmark" @@ -1466,8 +1478,8 @@ milestone = "M3" status = "shipped" commands = [] tools = [] -evidence = ["crates/ui-egui/tests/ui.rs::drag_selects_text_and_copy_returns_it", "crates/ui-egui/tests/ui.rs::selection_works_in_every_view_rotation"] -notes = "" +evidence = ["crates/ui-egui/tests/ui.rs::drag_selects_text_and_copy_returns_it", "crates/ui-egui/tests/ui.rs::selection_works_in_every_view_rotation", "crates/ui-egui/tests/text_select.rs::quick_clicks_select_a_word_then_the_line_then_the_page", "crates/ui-egui/tests/text_select.rs::quick_clicks_with_the_highlighter_mark_only_the_word", "crates/ui-egui/tests/text_select.rs::shift_click_extends_the_selection_from_its_anchor"] +notes = "Drag to select; ⇧-click extends the selection to the click, keeping its anchor (same page only); with the Select tool, quick clicks select a word (two), the line (three) or all the text on the page (four). The markup and Redact tools mark a double-clicked word and stop there. Missing: selection across pages (dragging or ⇧-clicking onto another page), paragraph selection, and ⌘A across the whole document (it selects the current page)." [[feature]] id = "view.copy-text" @@ -1608,10 +1620,10 @@ area = "B" tier = "P1" milestone = "M3" status = "shipped" -commands = [] +commands = ["file.pin_folder"] tools = [] -evidence = ["crates/ui-egui/tests/ui.rs::home_shows_welcome_and_tools"] -notes = "" +evidence = ["crates/ui-egui/tests/ui.rs::home_shows_welcome_and_tools", "crates/ui-egui/tests/folders.rs::a_pinned_folder_lists_its_newest_pdfs_on_home", "crates/ui-egui/tests/folders.rs::clicking_a_pinned_pdf_opens_it_and_adds_it_to_recent", "crates/ui-egui/tests/folders.rs::pinned_folders_survive_a_restart"] +notes = "Desktop: folders pinned to Home (File ▸ Pin folder to Home…, up to 8) list their newest PDFs, read on a worker thread and refreshed every few seconds while Home shows; a folder that can't be read (moved, offline synced drive) stays pinned and says so. Pinned folders are local paths, so a cloud drive works through its synced folder (iCloud Drive, OneDrive, Dropbox). Not yet: subfolders, search within a pinned folder, the web build." [[feature]] id = "view.all-tools" @@ -1806,8 +1818,8 @@ milestone = "M7" status = "shipped" commands = ["edit.edit_text"] tools = ["text_edit"] -evidence = ["crates/edit/src/tests.rs::japanese_paragraph_keeps_ideographic_spaces", "crates/edit/src/tests.rs::paragraphs_are_found_and_rewrapped", "crates/edit/src/tests.rs::text_lines_are_found_and_replaced_in_place", "crates/ui-egui/tests/editing.rs::editing_existing_text_in_place", "crates/automation/tests/automation.rs::editing_existing_text_through_tools"] -notes = "edited paragraphs are rewrapped to the box width with the font's own advance widths and the original line spacing, starting at the original text matrix; text state and fill colour restored after; a single line grows to the right margin; U+3000 ideographic spaces are preserved (including paragraph edges and repeated spaces); Japanese font-style fallback remains limited (#184)" +evidence = ["crates/edit/src/tests.rs::japanese_fallback_reports_the_face_after_save_and_reopen", "crates/edit/src/tests.rs::japanese_paragraph_keeps_ideographic_spaces", "crates/edit/src/tests.rs::paragraphs_are_found_and_rewrapped", "crates/edit/src/tests.rs::text_lines_are_found_and_replaced_in_place", "crates/ui-egui/tests/editing.rs::editing_existing_text_in_place", "crates/automation/tests/automation.rs::editing_existing_text_through_tools"] +notes = "edited paragraphs are rewrapped to the box width with the font's own advance widths and the original line spacing, starting at the original text matrix; text state and fill colour restored after; a single line grows to the right margin; U+3000 ideographic spaces are preserved (including paragraph edges and repeated spaces); Japanese Type 3 fallback reports its face through FontDescriptor/FontName after editing and save/reopen; source family/weight matching remains limited (#184)" [[feature]] id = "edit.text-move-resize" @@ -1938,8 +1950,8 @@ milestone = "M7" status = "shipped" commands = ["edit.text"] tools = ["page_add_text", "content_update"] -evidence = ["crates/ui-egui/tests/content.rs::typing_moving_styling_and_deleting_added_content", "crates/ui-egui/tests/content.rs::clicking_elsewhere_keeps_the_text_and_starts_a_new_box", "crates/automation/tests/automation.rs::adding_content_through_tools", "crates/edit/src/tests.rs::added_text_and_images_are_page_content_that_stays_editable", "crates/engine/src/tests.rs::added_text_and_images_render_and_stay_editable"] -notes = "page content, editable later (retype, move, restyle); standard fonts, Windows-1252 text. Each click on the page keeps the box being typed and starts a new one; Done and Discard beside the box" +evidence = ["crates/ui-egui/tests/content.rs::typing_moving_styling_and_deleting_added_content", "crates/ui-egui/tests/content.rs::clicking_elsewhere_keeps_the_text_and_starts_a_new_box", "crates/automation/tests/automation.rs::adding_content_through_tools", "crates/edit/src/tests.rs::added_text_and_images_are_page_content_that_stays_editable", "crates/engine/src/tests.rs::added_text_and_images_render_and_stay_editable", "crates/edit/src/tests.rs::added_arabic_text_is_shaped_in_display_order_and_stays_searchable"] +notes = "page content, editable later (retype, move, restyle); standard fonts, Windows-1252 text. Arabic text is shaped with the craft-fonts Arab face (Type 3 outlines with ToUnicode) and laid out in bidirectional display order; it is drawn regular whatever the bold/italic setting, Hebrew and other right-to-left scripts are not covered, and typing in the box is still left to right (misc.rtl-ui). Each click on the page keeps the box being typed and starts a new one; Done and Discard beside the box" [[feature]] id = "edit.find-replace" @@ -2035,7 +2047,7 @@ status = "shipped" commands = ["edit.edit_text"] tools = ["image_edit"] evidence = ["crates/edit/src/tests.rs::page_images_move_turn_replace_and_delete", "crates/ui-egui/tests/editing.rs::editing_existing_images_on_the_page", "crates/automation/tests/automation.rs::editing_page_images_through_tools"] -notes = "existing page images: rotate either way about the centre, flip horizontally or vertically (a q/cm/Q wrapper); added images too" +notes = "existing raster images and grouped Form artwork: rotate either way about the centre, flip horizontally or vertically (a q/cm/Q wrapper); Forms stay intact as groups; added images too" [[feature]] id = "edit.image-move-resize" @@ -2046,8 +2058,8 @@ milestone = "M7" status = "shipped" commands = ["edit.edit_text"] tools = ["image_edit"] -evidence = ["crates/edit/src/tests.rs::page_images_move_turn_replace_and_delete", "crates/ui-egui/tests/editing.rs::editing_existing_images_on_the_page", "crates/automation/tests/automation.rs::editing_page_images_through_tools"] -notes = "existing page images: select, drag to move, drag a corner to resize keeping proportions; Delete removes; added images too" +evidence = ["crates/edit/src/tests.rs::form_artwork_edits_only_the_selected_placement", "crates/ui-egui/tests/editing.rs::editing_existing_form_artwork_on_the_page", "crates/automation/tests/automation.rs::editing_form_artwork_through_tools", "crates/edit/src/tests.rs::page_images_move_turn_replace_and_delete", "crates/ui-egui/tests/editing.rs::editing_existing_images_on_the_page", "crates/automation/tests/automation.rs::editing_page_images_through_tools"] +notes = "existing raster images and grouped Form artwork: select, drag to move, drag a corner to resize keeping proportions; Delete removes one placement. Forms include their nested content as a group; individual internal elements and inline images are not supported. See docs/editing-images.md; added images too" [[feature]] id = "edit.image-extract" @@ -2202,8 +2214,8 @@ milestone = "M7" status = "shipped" commands = ["edit.link"] tools = ["link_add"] -evidence = ["crates/annot/src/tests.rs::links_are_added_edited_listed_and_removed", "crates/automation/tests/automation.rs::links_through_tools", "crates/ui-egui/tests/document_links.rs::a_link_asks_first_and_opens_when_allowed", "crates/ui-egui/tests/document_links.rs::a_button_uri_action_asks_first"] -notes = "Opening a document's link or button URI asks first; only http, https and mailto addresses are offered (#90, #91)." +evidence = ["crates/annot/src/tests.rs::links_are_added_edited_listed_and_removed", "crates/automation/tests/automation.rs::links_through_tools", "crates/ui-egui/tests/document_links.rs::a_link_asks_first_and_opens_when_allowed", "crates/ui-egui/tests/document_links.rs::a_button_uri_action_asks_first", "crates/ui-egui/tests/document_links.rs::an_email_link_cannot_attach_a_local_file", "crates/ui-egui/tests/document_links.rs::a_lookalike_host_is_shown_in_punycode_and_flagged"] +notes = "Opening a document's link or button URI asks first; only http, https and mailto addresses are offered (#90, #91), and an email address that asks the email app to attach or insert a local file is refused. The prompt shows the host as the browser receives it: international hosts in punycode, lookalikes that mix alphabets flagged, numeric hosts in their usual form." [[feature]] id = "edit.action-launch" @@ -2638,8 +2650,8 @@ milestone = "M4" status = "shipped" commands = ["page.insert"] tools = ["page_insert_file"] -evidence = ["crates/organize/src/tests.rs::inserting_pages_from_another_file_is_an_incremental_edit", "crates/ui-egui/tests/editing.rs::inserting_a_file_goes_after_the_selection_and_undoes"] -notes = "" +evidence = ["crates/organize/src/tests.rs::inserting_pages_from_another_file_is_an_incremental_edit", "crates/ui-egui/tests/editing.rs::inserting_a_file_goes_after_the_selection_and_undoes", "crates/ui-egui/tests/editing.rs::plus_between_pages_inserts_picked_files_there_in_order"] +notes = "After the selection (toolbar, File menu) or at any gap of the page grid (a + between pages and at both ends); several files at once, in order; images and .txt files are converted first; the inserted pages end up selected. The page_insert_file tool takes PDFs only. Missing: choosing which pages of the file to insert in the app." [[feature]] id = "organize.insert-from-clipboard" @@ -2683,11 +2695,11 @@ title = "Insert by dropping files onto the page grid" area = "D" tier = "P1" milestone = "M4" -status = "planned" -commands = [] +status = "partial" +commands = ["page.organize"] tools = [] -evidence = [] -notes = "" +evidence = ["crates/ui-egui/tests/editing.rs::files_dropped_between_pages_are_inserted_there", "crates/ui-egui/tests/editing.rs::files_dropped_before_the_pointer_is_known_wait_for_it", "crates/ui-egui/tests/editing.rs::files_dropped_outside_the_page_grid_still_open_as_documents"] +notes = "Desktop: PDFs, images and .txt files dropped on the Organize page grid are inserted at the gap under the pointer, in order, and selected (elsewhere a dropped file still opens as a document). While the files hover, a bar at the gap and a + badge follow the pointer; on macOS the pointer is read from AppKit, because the windowing layer sends no moves during a file drag. Missing: on Windows and Linux there is no live position, so the grid is only outlined and the files go to the gap the pointer is at right after the drop (or the end after a second); the web build (dropped files open as documents). Covered by simulated drop events; a real drag from Finder is not in the automated tests." [[feature]] id = "organize.extract-pages" @@ -2698,8 +2710,8 @@ milestone = "M4" status = "shipped" commands = ["page.extract"] tools = ["page_extract"] -evidence = ["crates/ui-egui/tests/editing.rs::extract_button_copies_selected_pages_to_a_new_tab", "crates/automation/tests/automation.rs::combine_extract_and_split"] -notes = "" +evidence = ["crates/ui-egui/tests/editing.rs::extract_button_copies_selected_pages_to_a_new_tab", "crates/automation/tests/automation.rs::combine_extract_and_split", "crates/organize/src/tests.rs::extract_and_split_keep_the_pdfx_output_intent_and_identification"] +notes = "Carries the PDF/X output intent and identification (#263) in a new XMP packet; the source XMP (dates, other schemas) is not copied." [[feature]] id = "organize.extract-separate-files" @@ -2818,8 +2830,8 @@ milestone = "M4" status = "shipped" commands = ["page.split"] tools = ["doc_split"] -evidence = ["crates/ui-egui/tests/editing.rs::split_dialog_writes_one_file_per_part", "crates/organize/src/tests.rs::split_ranges_cover_every_page_once", "crates/organize/src/tests.rs::split_produces_standalone_documents_with_metadata"] -notes = "" +evidence = ["crates/ui-egui/tests/editing.rs::split_dialog_writes_one_file_per_part", "crates/organize/src/tests.rs::split_ranges_cover_every_page_once", "crates/organize/src/tests.rs::split_produces_standalone_documents_with_metadata", "crates/organize/src/tests.rs::extract_and_split_keep_the_pdfx_output_intent_and_identification"] +notes = "Each part carries the PDF/X output intent and identification (#263), as Extract does." [[feature]] id = "organize.split-before-pages" @@ -2890,8 +2902,8 @@ milestone = "M4" status = "shipped" commands = ["page.combine"] tools = ["doc_combine"] -evidence = ["crates/organize/src/tests.rs::combine_concatenates_with_a_bookmark_per_file", "crates/ui-egui/tests/editing.rs::combining_files_opens_a_new_unsaved_tab", "crates/automation/tests/automation.rs::combine_extract_and_split"] -notes = "" +evidence = ["crates/organize/src/tests.rs::combine_concatenates_with_a_bookmark_per_file", "crates/ui-egui/tests/editing.rs::combining_files_opens_a_new_unsaved_tab", "crates/ui-egui/tests/editing.rs::combine_files_opens_in_its_own_tab", "crates/ui-egui/tests/editing.rs::combine_lists_size_and_warns_before_combining", "crates/ui-egui/tests/editing.rs::a_password_protected_file_is_unlocked_with_its_password_and_combined", "crates/ui-egui/tests/editing.rs::a_folder_adds_its_pdfs_in_order_and_optionally_its_subfolders", "crates/ui-egui/tests/ui.rs::pdfs_dropped_on_the_combine_tab_join_its_list", "crates/automation/tests/automation.rs::combine_extract_and_split", "crates/automation/tests/automation.rs::combine_opens_protected_files_with_their_passwords", "crates/organize/src/tests.rs::combine_keeps_pdfx_when_every_source_agrees", "crates/organize/src/tests.rs::combine_claims_no_pdfx_when_the_sources_disagree"] +notes = "Combine files is its own tab: a table of the files (name, pages, size, modified, warnings) with column sorting, resizing and moving; files, folders (with subfolders) and open documents can be added, or dropped on the tab. Problems show before Combine (password, security settings that withhold page assembly, bad page ranges, duplicates); protected files are unlocked with their open or permissions password (also doc_combine passwords). The result is never encrypted, and is not yet offered for protection in the same step. A PDF/X result (output intent, GTS_PDFXVersion, Trapped, XMP pdfxid) only when every source declares the same version and output intent (#263). When they differ the result claims none, and nothing reports that yet; Acrobat behaviour not observed." [[feature]] id = "organize.combine-bookmarks" @@ -2915,7 +2927,7 @@ status = "shipped" commands = ["page.combine"] tools = ["doc_combine"] evidence = ["crates/ui-egui/tests/editing.rs::combine_files_takes_chosen_pages_in_the_order_listed", "crates/automation/tests/automation.rs::combine_extract_and_split"] -notes = "Combine files list: order (move up/down), remove, add files, a page range per file (such as 1-3, 6; in the order given); bookmarks to left-out pages lose their destination" +notes = "A page range per file (such as 1-3, 6; in the order given), checked as it is typed, with the page count it takes; bookmarks to left-out pages lose their destination" [[feature]] id = "organize.combine-sort" @@ -2923,11 +2935,11 @@ title = "Sort and reorder files before combining" area = "D" tier = "P2" milestone = "M4" -status = "planned" -commands = [] -tools = [] -evidence = [] -notes = "" +status = "shipped" +commands = ["page.combine"] +tools = ["doc_combine"] +evidence = ["crates/ui-egui/tests/editing.rs::column_headings_sort_the_list_and_flip_on_the_second_click", "crates/ui-egui/tests/editing.rs::ctrl_and_shift_clicks_select_several_files_that_move_and_go_together", "crates/ui-egui/tests/editing.rs::undo_and_redo_restore_the_combine_list", "crates/automation/tests/automation.rs::combine_extract_and_split"] +notes = "Sort by any column (name in natural order, pages, size, modified, warnings), ascending or descending; multi-select (Ctrl/Shift) and move up/down, Alt+arrows or drag; every change undoable. doc_combine combines in the order its paths are given." [[feature]] id = "organize.combine-options" @@ -2935,11 +2947,11 @@ title = "Combine options (file size, convert to PDF/A, include open files)" area = "D" tier = "P2" milestone = "M11" -status = "planned" -commands = [] +status = "partial" +commands = ["page.combine"] tools = [] -evidence = [] -notes = "" +evidence = ["crates/ui-egui/tests/editing.rs::open_documents_can_be_added_to_combine"] +notes = "Include open files: Add open documents (with their unsaved changes). Missing: file-size options and converting the result to PDF/A." [[feature]] id = "organize.resource-dedupe" @@ -3494,8 +3506,8 @@ milestone = "M5" status = "shipped" commands = ["comment.highlight"] tools = ["comment_add"] -evidence = ["crates/ui-egui/tests/comments.rs::dragging_over_text_with_the_highlighter_highlights_it", "crates/engine/src/tests.rs::highlight_multiplies_over_text"] -notes = "drag over text, or pick the tool with text selected; agents can highlight by text search" +evidence = ["crates/ui-egui/tests/comments.rs::dragging_over_text_with_the_highlighter_highlights_it", "crates/ui-egui/tests/comments.rs::a_new_highlight_opens_its_note_for_typing", "crates/engine/src/tests.rs::highlight_multiplies_over_text"] +notes = "drag over text, or pick the tool with text selected; a new highlight opens its note for typing in the Comments panel; agents can highlight by text search" [[feature]] id = "comment.underline" @@ -3602,8 +3614,8 @@ milestone = "M5" status = "shipped" commands = ["comment.ink"] tools = ["comment_add"] -evidence = ["crates/ui-egui/tests/comments.rs::shapes_and_ink_are_drawn_by_dragging"] -notes = "one ink annotation per stroke" +evidence = ["crates/ui-egui/tests/comments.rs::shapes_and_ink_are_drawn_by_dragging", "crates/annot/src/tests.rs::ink_strokes_are_smooth_curves_through_their_points"] +notes = "one ink annotation per stroke; strokes are drawn as smooth curves through their points, as in Acrobat" [[feature]] id = "comment.eraser" @@ -3758,8 +3770,8 @@ milestone = "M5" status = "partial" commands = [] tools = ["comment_add"] -evidence = ["crates/annot/src/tests.rs::every_tool_creates_a_drawable_comment"] -notes = "None, OpenArrow and ClosedArrow are drawn; other endings are kept but not redrawn" +evidence = ["crates/annot/src/tests.rs::every_line_ending_is_drawn", "crates/automation/tests/automation.rs::line_endings_through_the_tool", "crates/ui-egui/tests/comments.rs::line_ending_properties_change_and_undo"] +notes = "All ten /LE styles (None, Square, Circle, Diamond, OpenArrow, ClosedArrow, Butt, ROpenArrow, RClosedArrow, Slash) are drawn on lines, polylines and callouts. comment_add endings and Comment properties set them. Dashed borders are drawn. Cloudy borders are still polygon-only." [[feature]] id = "comment.properties-dialog" @@ -4302,8 +4314,8 @@ milestone = "M3" status = "shipped" commands = [] tools = [] -evidence = ["crates/ui-egui/tests/view.rs::highlight_fields_tints_the_field_area"] -notes = "" +evidence = ["crates/ui-egui/tests/view.rs::highlight_fields_tints_the_field_area", "crates/ui-egui/tests/view.rs::required_radio_buttons_get_a_round_red_border", "crates/ui-egui/tests/view.rs::field_highlighting_is_remembered"] +notes = "remembered for the next document and session; required fields get a red border (round for radio buttons); the tint is drawn over the field, where Acrobat paints the highlight colour behind it" [[feature]] id = "form.field-appearance-display" @@ -4385,9 +4397,9 @@ tier = "P0" milestone = "M6" status = "partial" commands = ["form.fields"] -tools = ["form_fill"] -evidence = ["crates/forms/src/tests.rs::text_fields_get_new_appearances", "crates/forms/src/tests.rs::choices_accept_exports_or_display_text"] -notes = "auto-size, comb, multiline, password, quadding, /MK colours; approximate Helvetica metrics, no /MK rotation yet" +tools = ["form_fill", "form_set_props"] +evidence = ["crates/forms/src/tests.rs::text_fields_get_new_appearances", "crates/forms/src/tests.rs::choices_accept_exports_or_display_text", "crates/forms/src/tests.rs::rotated_fields_draw_in_their_quadrant", "crates/forms/src/tests.rs::a_rotated_field_draws_along_the_vertical_edge", "crates/automation/tests/automation.rs::rotating_a_field_through_tools", "crates/ui-egui/tests/prepare.rs::position_tab_rotates_a_field"] +notes = "auto-size, comb, multiline, password, quadding, /MK colours and /MK /R rotation (0, 90, 180, 270); approximate Helvetica metrics" [[feature]] id = "form.rich-text-fields" @@ -5130,8 +5142,8 @@ milestone = "M12" status = "partial" commands = ["file.open"] tools = ["doc_open", "doc_info", "form_fields", "form_fill", "js_run"] -evidence = ["crates/xfa/src/tests.rs::layout_places_captions_fields_and_sizes_containers_to_their_content", "crates/xfa/src/tests.rs::tables_break_across_pages_and_repeat_their_header_row", "crates/xfa/src/tests.rs::rendering_into_the_pdf_replaces_the_placeholder_page_and_adds_fields", "crates/engine/src/tests.rs::dynamic_xfa_forms_are_laid_out_on_open_filled_and_saved_incrementally", "crates/automation/tests/automation.rs::dynamic_xfa_forms_open_render_fill_and_save_through_tools", "crates/ui-egui/tests/xfa.rs::a_dynamic_xfa_form_is_laid_out_and_filled", "crates/xfa/src/tests.rs::nested_width_less_subforms_lay_out_in_polynomial_time", "crates/js/src/tests.rs::instance_managers_add_remove_and_count", "crates/xfa/src/tests.rs::the_form_tree_carries_values_presence_instances_and_the_events", "crates/engine/src/tests.rs::xfa_scripts_initialize_calculate_validate_toggle_and_add_rows", "crates/automation/tests/automation.rs::xfa_scripts_run_for_buttons_and_field_changes_through_tools", "crates/ui-egui/tests/xfa.rs::xfa_buttons_run_their_scripts_in_the_app", "crates/engine/src/tests.rs::hostile_xfa_open_scripts_are_merged_capped_kept_from_printing_and_noted", "crates/engine/src/tests.rs::runaway_xfa_calculations_at_open_end_quickly_with_a_report", "crates/xfa/src/tests.rs::the_live_form_of_nested_repeating_subforms_stops_at_its_node_budget", "crates/xfa/src/tests.rs::data_ops_of_one_event_are_one_datasets_rewrite"] -notes = "laid out from the template on open by the xfa crate (positioned, tb, lr-tb, table/row, page masters, breaks, repeated header rows, captions, rich text, JPEG images) into real pages and AcroForm fields (text, date with AFDate formats, check boxes, radio groups, buttons with reset/print/save/URL), filled from the datasets packet (including rows another viewer added), saved as an incremental revision with the values merged into the datasets so Adobe's viewers show them; doc_info reports xfa_layout. XFA JavaScript and FormCalc (form.xfa-formcalc) run in the js crate's XFA object model (xfa.form, SOM resolution, rawValue, presence, access, instance managers, xfa.host, xfa.layout, xfa.event): initialize and calculate on open, exit, validate and calculate after a field changes, click for buttons (through js_run and the app), with rows added and removed, subforms shown and hidden, and the form laid out again, all as one undo step and kept in the file. Hostile scripts are bounded per event (merged and capped effects, messages and console lines, lower loop limits and a time budget on open and calculate, one datasets rewrite per event, print/save/links/focus from clicks only, a runaway script abandoned and the form's scripts turned off) and what open-time scripts changed is noted. Missing: loops inside nested calls are only stopped by abandoning their thread, and script memory is not capped; the change event per keystroke; xfa.template, connections and data descriptions; a hidden instance of a repeating subform hides them all; choice lists are text fields; signature, image and barcode fields are blank; PNG images; keep rules; metrics are approximate and Acrobat fidelity is eyeballed on real forms, not measured (#60)" +evidence = ["crates/xfa/src/tests.rs::layout_places_captions_fields_and_sizes_containers_to_their_content", "crates/xfa/src/tests.rs::tables_break_across_pages_and_repeat_their_header_row", "crates/xfa/src/tests.rs::rendering_into_the_pdf_replaces_the_placeholder_page_and_adds_fields", "crates/engine/src/tests.rs::dynamic_xfa_forms_are_laid_out_on_open_filled_and_saved_incrementally", "crates/automation/tests/automation.rs::dynamic_xfa_forms_open_render_fill_and_save_through_tools", "crates/ui-egui/tests/xfa.rs::a_dynamic_xfa_form_is_laid_out_and_filled", "crates/xfa/src/tests.rs::nested_width_less_subforms_lay_out_in_polynomial_time", "crates/js/src/tests.rs::instance_managers_add_remove_and_count", "crates/xfa/src/tests.rs::the_form_tree_carries_values_presence_instances_and_the_events", "crates/engine/src/tests.rs::xfa_scripts_initialize_calculate_validate_toggle_and_add_rows", "crates/automation/tests/automation.rs::xfa_scripts_run_for_buttons_and_field_changes_through_tools", "crates/ui-egui/tests/xfa.rs::xfa_buttons_run_their_scripts_in_the_app", "crates/engine/src/tests.rs::hostile_xfa_open_scripts_are_merged_capped_kept_from_printing_and_noted", "crates/engine/src/tests.rs::runaway_xfa_calculations_at_open_end_quickly_with_a_report", "crates/xfa/src/tests.rs::the_live_form_of_nested_repeating_subforms_stops_at_its_node_budget", "crates/xfa/src/tests.rs::data_ops_of_one_event_are_one_datasets_rewrite", "crates/xfa/src/tests.rs::choice_lists_signatures_passwords_and_pictures_become_fields_and_images", "crates/xfa/src/tests.rs::choice_values_come_from_the_data_as_saved_values_and_several_for_lists", "crates/xfa/src/tests.rs::pictures_are_sized_from_their_headers_and_hostile_ones_are_refused_unallocated", "crates/engine/src/tests.rs::xfa_choice_lists_signature_and_picture_fields_open_fill_script_and_save"] +notes = "laid out from the template on open by the xfa crate (positioned, tb, lr-tb, table/row, page masters, breaks, repeated header rows, captions, rich text, JPEG/PNG/GIF images) into real pages and AcroForm fields (text, password, date with AFDate formats, check boxes, radio groups, drop-downs and list boxes with shown/saved item pairs and multi-select, signature fields, image fields showing their picture (pictures decoded once per document, at most 20 million pixels, and reused across re-layouts), buttons with reset/print/save/URL), filled from the datasets packet (including rows another viewer added), saved as an incremental revision with the values merged into the datasets so Adobe's viewers show them; doc_info reports xfa_layout. XFA JavaScript and FormCalc (form.xfa-formcalc) run in the js crate's XFA object model (xfa.form, SOM resolution, rawValue, presence, access, instance managers, xfa.host, xfa.layout, xfa.event): initialize and calculate on open, exit, validate and calculate after a field changes, click for buttons (through js_run and the app), with rows added and removed, subforms shown and hidden, and the form laid out again, all as one undo step and kept in the file. Hostile scripts are bounded per event (merged and capped effects, messages and console lines, lower loop limits and a time budget on open and calculate, one datasets rewrite per event, print/save/links/focus from clicks only, a runaway script abandoned and the form's scripts turned off) and what open-time scripts changed is noted. Missing: loops inside nested calls are only stopped by abandoning their thread, and script memory is not capped; the change event per keystroke; xfa.template, connections and data descriptions; a hidden instance of a repeating subform hides them all; choosing a new picture for an image field; barcodes are drawn as their text, not bars; keep rules; metrics are approximate and Acrobat fidelity is eyeballed on real forms, not measured (#60)" [[feature]] id = "form.xfa-formcalc" @@ -5211,11 +5223,11 @@ title = "Fill & Sign: typed, drawn or image signature and initials" area = "F" tier = "P0" milestone = "M5" -status = "partial" +status = "shipped" commands = ["sign.fill.signature", "sign.fill.initials", "sign.fill.signature.change", "sign.fill.initials.change", "sign.fill.signature.remove", "sign.fill.initials.remove"] -tools = ["fill_sign_add"] -evidence = ["crates/ui-egui/tests/fill_sign.rs::typed_signatures_and_initials", "crates/ui-egui/tests/fill_sign.rs::signing_draws_a_signature_once_and_places_it", "crates/ui-egui/tests/fill_sign.rs::changing_saved_signatures_and_initials_preserves_placed_marks", "crates/ui-egui/tests/fill_sign.rs::changing_drawn_signatures_can_be_cancelled_or_switched_to_type", "crates/ui-egui/tests/fill_sign.rs::saved_signature_cards_remove_and_add_without_changing_the_document", "crates/ui-egui/tests/fill_sign.rs::long_typed_names_fit_the_placed_signature_and_keep_every_outline", "crates/ui-egui/src/fill_sign.rs::long_signature_previews_have_clear_margins", "crates/ui-egui/tests/control.rs::saved_signature_can_be_changed_through_the_control_channel", "crates/fonts/src/script.rs::a_name_becomes_outlines", "crates/fonts/src/script.rs::long_names_keep_every_glyph_with_bounded_outline_work", "crates/automation/tests/automation.rs::fill_and_sign_through_tools"] -notes = "signature and initials, typed (Dancing Script outlines as filled paths, no font embedded) or drawn, saved in settings; preview cards with add/change/remove controls in the panel and picker, also through ui.command; changes leave placed annotations untouched; long names fit within 150 pt with overhangs included in preview/PDF bounds; image signatures not yet" +tools = ["fill_sign_add", "comment_edit", "comment_image_preview"] +evidence = ["crates/ui-egui/tests/fill_sign.rs::drawn_and_typed_signatures_stay_upright_on_rotated_pages", "crates/automation/tests/automation.rs::typed_signatures_stay_upright_on_rotated_pages", "crates/ui-egui/tests/fill_sign.rs::image_signature_corners_restore_original_aspect_after_edge_resize_and_reopen", "crates/ui-egui/src/comments.rs::proportional_corners_keep_the_opposite_anchor_and_edges_stretch_one_axis", "crates/ui-egui/tests/fill_sign.rs::image_signature_live_resize_and_move_preserve_background_and_hide_moving_handles", "crates/ui-egui/tests/fill_sign.rs::embedded_image_signature_live_gestures_follow_document_and_view_rotation", "crates/automation/tests/automation.rs::image_signature_preview_layers_are_read_only_and_survive_encrypted_save", "crates/engine/src/signature_image.rs::embedded_signature_preview_preserves_alpha_without_editing_the_document", "crates/engine/src/signature_image.rs::oversized_embedded_signature_preview_is_rejected_before_decoding", "crates/ui-egui/tests/fill_sign.rs::image_signature_preview_follows_pointer_at_page_size_with_zoom_and_rotation", "crates/ui-egui/tests/fill_sign.rs::image_signature_placement_selects_resize_handles_and_requires_reselecting_to_repeat", "crates/ui-egui/tests/fill_sign.rs::typed_signatures_and_initials", "crates/ui-egui/tests/fill_sign.rs::signing_draws_a_signature_once_and_places_it", "crates/ui-egui/tests/fill_sign.rs::changing_saved_signatures_and_initials_preserves_placed_marks", "crates/ui-egui/tests/fill_sign.rs::changing_drawn_signatures_can_be_cancelled_or_switched_to_type", "crates/ui-egui/tests/fill_sign.rs::saved_signature_cards_remove_and_add_without_changing_the_document", "crates/ui-egui/tests/fill_sign.rs::long_typed_names_fit_the_placed_signature_and_keep_every_outline", "crates/ui-egui/src/fill_sign.rs::long_signature_previews_have_clear_margins", "crates/ui-egui/tests/control.rs::saved_signature_can_be_changed_through_the_control_channel", "crates/fonts/src/script.rs::a_name_becomes_outlines", "crates/fonts/src/script.rs::long_names_keep_every_glyph_with_bounded_outline_work", "crates/automation/tests/automation.rs::fill_and_sign_through_tools", "crates/ui-egui/tests/fill_sign.rs::image_signatures_and_initials_can_be_imported_placed_and_remembered", "crates/ui-egui/tests/fill_sign.rs::image_import_cancel_clear_and_errors_preserve_saved_signatures", "crates/engine/src/signature_image.rs::image_signatures_preserve_alpha_and_fit_the_signature_box", "crates/engine/src/signature_image.rs::invalid_and_oversized_signature_images_are_rejected", "crates/engine/src/signature_image.rs::jpeg_signatures_are_normalized_to_png", "crates/engine/src/signature_image.rs::jpeg_signature_orientation_is_applied_before_preview_and_embedding", "crates/engine/src/signature_image.rs::very_wide_image_signatures_keep_the_requested_rectangle", "crates/automation/tests/automation.rs::image_signatures_through_tools_preserve_transparency_and_survive_save"] +notes = "signature and initials, typed (Dancing Script outlines as filled paths, no font embedded), drawn or imported from local PNG/JPEG images; all three are placed upright and anchored as displayed on pages with /Rotate (#299); saved in settings with preview cards and add/change/remove controls; changes leave placed annotations untouched; images preserve alpha, aspect ratio and EXIF orientation, fit within 150 pt by 32 pt (24 pt for initials), and are normalized without source paths or metadata; image previews follow the pointer at placement size with zoom and rotation; signatures/initials place once then select the new annotation; embedded image signatures resize and move live, hiding the frame during movement and restoring it on release; corner handles preserve the original aspect ratio of the embedded image with the opposite corner anchored, while edge midpoint handles stretch width or height independently; cached background layers preserve page content and alpha, including reopened/encrypted documents; Escape cancels a gesture without committing; stamps resize through selection handles or comment_edit with their original appearance; native and web asynchronous pickers; fill_sign_add accepts path instead of text; image imports capped at 4 MiB, 4 megapixels and 4096 pixels per side" [[feature]] id = "form.fill-sign-flatten" @@ -5519,7 +5531,7 @@ status = "shipped" commands = ["redact.search"] tools = ["redact_mark"] evidence = ["crates/redact/src/patterns.rs::phone_numbers", "crates/redact/src/patterns.rs::credit_cards_need_a_valid_checksum", "crates/automation/tests/automation.rs::redacting_through_tools"] -notes = "phone, e-mail, credit card (Luhn), SSN, dates (English US forms)" +notes = "phone (North American, E.164 with + country code, national with trunk 0), e-mail, credit card (Luhn), SSN, dates (English US forms)" [[feature]] id = "protect.redact-pattern-locales" @@ -5786,8 +5798,8 @@ milestone = "M9" status = "shipped" commands = ["sign.digital"] tools = ["sign_document"] -evidence = ["crates/sign/tests/crypto.rs::opens_every_openssl_flavour_of_pkcs12", "crates/sign/tests/pdf.rs::signing_then_validating_with_and_without_trust", "crates/ui-egui/tests/signing.rs::certifying_without_a_visible_signature", "crates/automation/tests/automation.rs::digital_ids_signing_and_validation_through_tools"] -notes = "PBES2 (AES, 3DES) and legacy SHA-1 3DES/RC2 files; RSA, P-256, P-384" +evidence = ["crates/sign/tests/crypto.rs::opens_every_openssl_flavour_of_pkcs12", "crates/sign/tests/crypto.rs::opens_pkcs12_as_windows_writes_it", "crates/sign/tests/pdf.rs::signing_then_validating_with_and_without_trust", "crates/ui-egui/tests/signing.rs::certifying_without_a_visible_signature", "crates/automation/tests/automation.rs::digital_ids_signing_and_validation_through_tools"] +notes = "PBES2 (AES, 3DES) and legacy SHA-1 3DES/RC2 files; RSA, P-256, P-384; both the DER OpenSSL writes and the BER Windows writes (indefinite lengths, segmented AuthenticatedSafe)" [[feature]] id = "sign.macos-keychain" @@ -6798,12 +6810,12 @@ id = "create.create-multiple" title = "Convert many files to PDF in one run" area = "J" tier = "P1" -milestone = "M13" -status = "planned" -commands = [] -tools = [] -evidence = [] -notes = "" +milestone = "M10" +status = "shipped" +commands = ["create.multiple"] +tools = ["doc_create_multiple"] +evidence = ["crates/engine/src/tests.rs::mixed_files_convert_and_combine_in_order", "crates/engine/src/tests.rs::files_that_cannot_be_converted_are_refused_clearly", "crates/automation/tests/automation.rs::creating_from_multiple_files_through_tools", "crates/ui-egui/tests/create.rs::multiple_files_open_as_one_document_in_the_page_grid", "crates/ui-egui/tests/editing.rs::plus_between_pages_inserts_picked_files_there_in_order", "crates/ui-egui/tests/editing.rs::save_pages_writes_what_the_grid_shows"] +notes = "Create ▸ Multiple files: the picked PDFs, images (PNG, JPEG, JPEG 2000, TIFF, GIF, BMP) and .txt files open as one unsaved document in the page grid, in the order picked (a bookmark per file); there pages are removed, reordered, more files inserted at any gap (a + button, or dropped there), and Save pages writes what is shown. One PDF per file is offered by the doc_create_multiple tool only (mode separate), not in the app. Missing: Office, HTML and Markdown inputs, PDF Portfolio output, per-file image resolution choice, progress and cancel for large batches (conversion runs on the UI thread). The combined file has combine's limits: untagged, no page labels or document info carried over, and its per-file bookmarks are not updated when pages are later moved between files." [[feature]] id = "create.image-conversion-options" @@ -6862,8 +6874,8 @@ milestone = "M10" status = "partial" commands = ["export.docx"] tools = ["doc_export_office"] -evidence = ["crates/export/src/lib.rs::docx_is_a_word_package", "crates/export/src/lib.rs::grid_text_becomes_a_real_table_in_every_format", "crates/export/src/lib.rs::word_opens_text_with_control_characters_and_long_pages", "crates/engine/src/tests.rs::exporting_office_files_keeps_images", "crates/automation/tests/automation.rs::exporting_to_word_html_and_rtf", "crates/ui-egui/tests/compare.rs::export_to_word_html_and_rtf"] -notes = "Flowing Word document: paragraphs, Heading 1/2 from larger text, bold/italic, images inline, page breaks, and detected tables with spans/empty cells; checked with macOS textutil and opened in Microsoft Word (Windows). Characters XML forbids are dropped; the page size stays within Word's 22 in. Not yet: columns, fonts and colours, exact layout mode." +evidence = ["crates/export/src/lib.rs::docx_is_a_word_package", "crates/export/src/lib.rs::text_colour_survives_every_format", "crates/engine/src/tests.rs::exporting_office_files_keeps_text_colour", "crates/export/src/lib.rs::grid_text_becomes_a_real_table_in_every_format", "crates/export/src/lib.rs::word_opens_text_with_control_characters_and_long_pages", "crates/engine/src/tests.rs::exporting_office_files_keeps_images", "crates/automation/tests/automation.rs::exporting_to_word_html_and_rtf", "crates/ui-egui/tests/compare.rs::export_to_word_html_and_rtf"] +notes = "Flowing Word document: paragraphs, Heading 1/2 from larger text, bold/italic, text colour (fill as sRGB; gray/CMYK converted naively, colour spaces via cs/sc read as black), images inline, page breaks, and detected tables with spans/empty cells; checked with macOS textutil and opened in Microsoft Word (Windows). Characters XML forbids are dropped; the page size stays within Word's 22 in. Not yet: columns, fonts, colour per run within a paragraph, exact layout mode." [[feature]] id = "create.export-rtf" @@ -6874,8 +6886,8 @@ milestone = "M10" status = "shipped" commands = ["export.rtf"] tools = ["doc_export_office"] -evidence = ["crates/export/src/lib.rs::rtf_escapes_and_sizes", "crates/export/src/lib.rs::word_opens_text_with_control_characters_and_long_pages", "crates/automation/tests/automation.rs::exporting_to_word_html_and_rtf", "crates/ui-egui/tests/compare.rs::export_to_word_html_and_rtf"] -notes = "Paragraphs with sizes, bold, italic, Unicode; page breaks. Images are left out." +evidence = ["crates/export/src/lib.rs::rtf_escapes_and_sizes", "crates/export/src/lib.rs::text_colour_survives_every_format", "crates/export/src/lib.rs::word_opens_text_with_control_characters_and_long_pages", "crates/automation/tests/automation.rs::exporting_to_word_html_and_rtf", "crates/ui-egui/tests/compare.rs::export_to_word_html_and_rtf"] +notes = "Paragraphs with sizes, bold, italic, text colour (colortbl), Unicode; page breaks. Images are left out." [[feature]] id = "create.export-xlsx" @@ -6958,8 +6970,8 @@ milestone = "M10" status = "partial" commands = ["export.html"] tools = ["doc_export_office"] -evidence = ["crates/export/src/lib.rs::html_has_headings_paragraphs_and_images_in_order", "crates/export/src/lib.rs::grid_text_becomes_a_real_table_in_every_format", "crates/automation/tests/automation.rs::exporting_to_word_html_and_rtf", "crates/ui-egui/tests/compare.rs::export_to_word_html_and_rtf"] -notes = "Single HTML file with headings, paragraphs, inline images, and detected tables with spans/empty cells. Not yet: multi-page folder output, CSS from fonts." +evidence = ["crates/export/src/lib.rs::html_has_headings_paragraphs_and_images_in_order", "crates/export/src/lib.rs::text_colour_survives_every_format", "crates/export/src/lib.rs::grid_text_becomes_a_real_table_in_every_format", "crates/automation/tests/automation.rs::exporting_to_word_html_and_rtf", "crates/ui-egui/tests/compare.rs::export_to_word_html_and_rtf"] +notes = "Single HTML file with headings, paragraphs (with text colour), inline images, and detected tables with spans/empty cells. Not yet: multi-page folder output, CSS from fonts." [[feature]] id = "create.export-text" @@ -7107,7 +7119,7 @@ status = "shipped" commands = ["optimize.reduce"] tools = ["doc_reduce"] evidence = ["crates/ui-egui/tests/create.rs::reduce_file_size_writes_a_compact_copy", "crates/engine/src/tests.rs::create_and_reduce", "crates/automation/tests/automation.rs::creating_and_reducing_through_tools", "crates/automation/tests/automation.rs::optimizing_through_tools"] -notes = "images above 225 ppi to 150 ppi JPEG medium, thumbnails dropped, resources merged, object streams" +notes = "images above 225 ppi to 150 ppi JPEG medium, thumbnails dropped, resources merged, object streams; runs in the background with a progress bar and Cancel" [[feature]] id = "optimize.compatibility-level" @@ -7130,8 +7142,8 @@ milestone = "M11" status = "partial" commands = ["optimize.advanced"] tools = ["doc_optimize"] -evidence = ["crates/ui-egui/tests/create.rs::the_pdf_optimizer_dialog_saves_an_optimized_copy", "crates/automation/tests/automation.rs::optimizing_through_tools"] -notes = "Images, Discard Objects, Discard User Data, Clean Up; no Fonts or Transparency panels, no space audit" +evidence = ["crates/ui-egui/tests/create.rs::the_pdf_optimizer_dialog_saves_an_optimized_copy", "crates/automation/tests/automation.rs::optimizing_through_tools", "crates/ui-egui/src/optimize_ui.rs::the_progress_card_shows_the_stage_and_cancels", "crates/engine/src/tests.rs::an_optimize_job_reports_its_stages_and_can_be_cancelled"] +notes = "Images, Discard Objects, Discard User Data, Clean Up; runs in the background with a progress bar (per image) and Cancel; no Fonts or Transparency panels, no space audit" [[feature]] id = "optimize.downsample-images" @@ -9337,8 +9349,8 @@ tier = "P0" milestone = "M13" status = "shipped" commands = [] -tools = [] -evidence = ["crates/automation/tests/automation.rs::mcp_session_over_stdio", "crates/automation/tests/automation.rs::mcp_errors"] +tools = ["command_list", "command_run", "command_batch", "doc_inspect", "render_preview"] +evidence = ["crates/automation/tests/automation.rs::mcp_session_over_stdio", "crates/automation/tests/automation.rs::mcp_errors", "crates/automation/tests/automation.rs::conventions_core_tools_preserve_root_and_batch_errors", "crates/automation/tests/automation.rs::conventions_annotations_and_strict_keys_in_both_modes"] notes = "" [[feature]] @@ -9362,7 +9374,7 @@ milestone = "M13" status = "shipped" commands = [] tools = ["doc_info", "text_extract", "page_render"] -evidence = ["crates/automation/tests/automation.rs::mcp_resources_expose_open_documents", "crates/automation/src/mcp.rs::resource_uris_parse"] +evidence = ["crates/automation/tests/automation.rs::mcp_resources_expose_open_documents", "crates/automation/src/mcp.rs::resource_uris_parse", "crates/automation/tests/automation.rs::conventions_resources_and_modern_results"] notes = "resources/list, resources/templates/list and resources/read: pdfcraft://doc/{doc}/info (JSON), /text, /page/{page}/text, /page/{page}/image?dpi= (PNG)" [[feature]] @@ -9555,7 +9567,7 @@ status = "na" commands = [] tools = [] evidence = [] -notes = "Adobe cloud; use OS file dialogs and synced folders" +notes = "Adobe cloud; use OS file dialogs and synced folders, which can be pinned to Home (view.home-view)" [[feature]] id = "misc.cross-device-sync" @@ -9806,8 +9818,8 @@ milestone = "M14" status = "partial" commands = ["app.preferences"] tools = [] -evidence = ["crates/ui-egui/src/i18n/mod.rs::windows_display_language_resolves_chinese_scripts", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_query_returns_a_locale_tag", "crates/ui-egui/src/i18n/mod.rs::language_persists_and_invalid_input_keeps_current_language", "crates/ui-egui/src/i18n/mod.rs::bundled_catalogs_are_consistent", "crates/ui-egui/src/i18n/mod.rs::japanese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_history_and_diagnostics_preserve_user_values", "crates/ui-egui/tests/control.rs::simplified_chinese_signature_prompts_and_errors_keep_document_state", "crates/ui-egui/tests/control.rs::language_switch_preserves_document_and_command_ids", "crates/ui-egui/tests/control.rs::japanese_controls_and_search_keep_command_ids", "crates/ui-egui/tests/control.rs::japanese_dialogs_errors_and_custom_action_names", "crates/ui-egui/src/i18n/mod.rs::french_is_registered", "crates/ui-egui/src/i18n/mod.rs::french_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::french_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::french_history_and_diagnostics_preserve_user_values"] -notes = "Preferences switches Auto (system language)/English/Japanese/Simplified Chinese/Traditional Chinese/Czech/Brazilian Portuguese/Spanish/French and persists; ui.set language auto|en|ja|zh-hans|zh-hant|cs|pt-br|es|fr and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese, Traditional Chinese, Simplified Chinese, Spanish and French cover commands, dialogs, panels, shortcuts and history labels; Czech and Brazilian Portuguese the menus. French locales (fr, fr_FR, fr_CA, fr_BE) select the French catalog. Engine/OS error details stay untranslated. Auto detects the Windows display language after locale environment overrides, with Chinese script/region mapping and English fallback. Missing: Czech and Brazilian Portuguese dialogs, Chinese font delivery, a full localization audit." +evidence = ["crates/ui-egui/src/i18n/mod.rs::windows_display_language_resolves_chinese_scripts", "crates/ui-egui/src/i18n/mod.rs::neutral_locale_does_not_override_display_language", "crates/ui-egui/src/i18n/mod.rs::windows_display_language_query_returns_a_locale_tag", "crates/ui-egui/src/i18n/mod.rs::language_persists_and_invalid_input_keeps_current_language", "crates/ui-egui/src/i18n/mod.rs::bundled_catalogs_are_consistent", "crates/ui-egui/src/i18n/mod.rs::japanese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::simplified_chinese_history_and_diagnostics_preserve_user_values", "crates/ui-egui/tests/control.rs::simplified_chinese_signature_prompts_and_errors_keep_document_state", "crates/ui-egui/tests/control.rs::language_switch_preserves_document_and_command_ids", "crates/ui-egui/tests/control.rs::japanese_controls_and_search_keep_command_ids", "crates/ui-egui/tests/control.rs::japanese_dialogs_errors_and_custom_action_names", "crates/ui-egui/src/i18n/mod.rs::french_is_registered", "crates/ui-egui/src/i18n/mod.rs::french_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::french_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::french_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::german_certificate_subject_has_a_distinct_context", "crates/ui-egui/src/widgets.rs::search_placeholder_never_overlaps_the_shortcut", "crates/ui-egui/src/home.rs::translated_card_description_leaves_room_for_the_action", "crates/ui-egui/tests/control.rs::german_preferences_and_search_keep_command_ids", "crates/ui-egui/src/i18n/mod.rs::telugu_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::telugu_labels_have_glyphs", "crates/ui-egui/src/i18n/mod.rs::hungarian_is_registered", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::hungarian_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::hungarian_history_and_diagnostics_preserve_user_values", "crates/ui-egui/src/i18n/mod.rs::ukrainian_is_registered_and_persists", "crates/ui-egui/src/i18n/mod.rs::ukrainian_integer_plurals_cover_teens_and_large_counts", "crates/ui-egui/src/i18n/mod.rs::ukrainian_calendar_uses_month_case_for_dates", "crates/ui-egui/src/i18n/mod.rs::ukrainian_catalog_does_not_leave_english_ui_text", "crates/ui-egui/src/i18n/mod.rs::ukrainian_covers_commands_and_catalogue", "crates/ui-egui/src/i18n/mod.rs::ukrainian_covers_ui_literals", "crates/ui-egui/src/i18n/mod.rs::ukrainian_history_and_diagnostics_preserve_user_values"] +notes = "Preferences switches Auto (system language)/English/Japanese/Simplified Chinese/Traditional Chinese/Czech/Brazilian Portuguese/German/Spanish/French/Telugu/Hungarian/Ukrainian and persists; ui.set language auto|en|ja|zh-hans|zh-hant|cs|pt-br|de|es|fr|te|hu|uk and ui.state expose it. TSV catalogs per language (same system as PhotoCraft); every dialog, panel and notice goes through tl!. Japanese, Traditional Chinese, Simplified Chinese, German, Spanish, French and Hungarian cover commands, dialogs, panels, shortcuts and history labels; Czech and Brazilian Portuguese the menus. French locales (fr, fr_FR, fr_CA, fr_BE) select the French catalog, German locales (de, de_DE, de_AT, de_CH) the German one, Hungarian locales (hu, hu_HU) the Hungarian one. German search hints reserve shortcut space, Home cards bound translated text, certificate Subject has its own context, and inserted tool names keep their capitalization. Engine/OS error details stay untranslated. Auto detects the Windows display language after explicitly named supported locale overrides; neutral C/POSIX launcher locales are skipped on Windows, with Chinese script/region mapping and English fallback. Missing: Czech and Brazilian Portuguese dialogs, Chinese and Telugu font delivery in releases, a full localization audit." [[feature]] id = "misc.prefs-reading" diff --git a/vendor/README.md b/vendor/README.md index 862f5744..408621f9 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -7,12 +7,13 @@ the fix. Vendoring copyleft code is never allowed (plan/adr/0001). | Crate | Version | Licence | Patches | Test | |---|---|---|---|---| -| hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments); (9) `image_size_ok`: images and masks over 2^28 pixels are not decoded (a fuzzed `/W 4294967295` stencil mask allocated 4 GiB); (10) `MAX_NESTED_PAINTS`: nested paints (forms, Type 3 glyphs and tiling patterns inside one another) share a per-page budget of 50,000 (a glyph showing eight glyphs of its own font, sixteen deep, hung); (11) depends on skrifa 0.47 instead of 0.42 (upstream read-fonts fixed a Type 1 tokenizer that recursed through runs of numbers: a fuzzed font hung or overflowed the stack), with the one API change (`Charset::glyph_id` returns `Option`); (12) annotation appearances whose `/BBox` or `/Matrix` collapses to a line or a point are skipped (mapping them onto `/Rect` divided by zero and handed the device infinite or NaN matrices); (13) a Highlight annotation without a usable normal appearance is drawn from its `/QuadPoints`, `/C` and `/CA` with Multiply blending, as pdfcraft-annot draws highlights (it was not drawn at all); (14) simple-font `/Widths` (`read_widths`): entries only for codes 0–255 (one entry per code below `/FirstChar` was allocated: `4294967295` asked for 34 GB), an entry that isn't a number leaves its code missing instead of ending the array, and a `/LastChar` below `/FirstChar` gives no widths instead of discarding the font for a standard one; (15) the appearance box is mapped onto `/Rect` corner to corner: the translation now accounts for the scale (an appearance whose box didn't start at the origin and differed in size from `/Rect` was drawn shifted) | (1) `raster::tests::widget_appearance_states`; (2, 3) `xtask text-oracle` on corpus/pdfjs issue918; (4) `raster::tests::layer_override_hides_content`; (5) `raster::tests::soft_mask_without_group_colour_space_is_applied`; (6) `raster::tests::self_referencing_tiling_pattern_terminates`, `raster::tests::self_referencing_type3_glyph_terminates`; (7) `raster::tests::huge_cid_width_ranges_terminate`; (8) `raster::tests::hiding_comments_keeps_fields`; (9) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (10) `raster::tests::type3_glyphs_that_fan_out_into_themselves_finish`; (11) `raster::tests::type1_font_programs_with_long_runs_of_numbers_load_quickly`; (12) `raster::tests::degenerate_annotation_appearances_are_skipped`; (13) `raster::tests::highlights_without_an_appearance_are_drawn`; (14) `raster::tests::malformed_simple_font_widths_keep_later_widths_and_the_font`, `raster::tests::huge_simple_font_first_char_terminates`; (15) `raster::tests::annotation_appearance_box_maps_onto_rect` | +| hayro-interpret | 0.7.0 | Apache-2.0 OR MIT | (1) select `/AP /N` appearance states via `/AS` (checkboxes, radios, stateful icons) and honour the NoView flag; (2) Type3 Unicode fallback to `/Encoding` glyph names, then printable ASCII; (3) `Type3Glyph::advance_width`; (4) `InterpreterSettings::ocg_overrides` for viewer layer toggles; (5) soft masks whose group lacks `/CS` were dropped (Chrome gradient text); (6) paint nesting cap `MAX_PAINT_NESTING` for tiling patterns and Type 3 glyphs (self-referencing content overflowed the stack); (7) CID width ranges in `/W` and `/W2` clamped to `MAX_CID` (a `0 4294967295 w` range hung); (8) `InterpreterSettings::hide_comments` skips markup annotations but keeps widgets and links (View ▸ Hide all comments); (9) `image_size_ok`: images and masks over 2^28 pixels are not decoded (a fuzzed `/W 4294967295` stencil mask allocated 4 GiB); (10) `MAX_NESTED_PAINTS`: nested paints (forms, Type 3 glyphs and tiling patterns inside one another) share a per-page budget of 50,000 (a glyph showing eight glyphs of its own font, sixteen deep, hung); (11) depends on skrifa 0.47 instead of 0.42 (upstream read-fonts fixed a Type 1 tokenizer that recursed through runs of numbers: a fuzzed font hung or overflowed the stack), with the one API change (`Charset::glyph_id` returns `Option`); (12) annotation appearances whose `/BBox` or `/Matrix` collapses to a line or a point are skipped (mapping them onto `/Rect` divided by zero and handed the device infinite or NaN matrices); (13) a Highlight annotation without a usable normal appearance is drawn from its `/QuadPoints`, `/C` and `/CA` with Multiply blending, as pdfcraft-annot draws highlights (it was not drawn at all); (14) simple-font `/Widths` (`read_widths`): entries only for codes 0–255 (one entry per code below `/FirstChar` was allocated: `4294967295` asked for 34 GB), an entry that isn't a number leaves its code missing instead of ending the array, and a `/LastChar` below `/FirstChar` gives no widths instead of discarding the font for a standard one; (15) the appearance box is mapped onto `/Rect` corner to corner: the translation now accounts for the scale (an appearance whose box didn't start at the origin and differed in size from `/Rect` was drawn shifted); (16) a clip set by `W`/`W*` takes effect after whichever path-painting operator ends the path (`S`, `f`, `B`, … as well as `n`; ISO 32000-2 §8.5.4): it was applied only on `n`, so `re W* S` clipped nothing and the clip stayed pending for a later, unrelated `n`; (17) `InterpreterSettings::cancelled`: once set, interpretation stops before the next content operator (a render pool sets it for every worker when dropped, after an edit or on closing, and for one worker when the watchdog gives up on its render, so renders whose answers nobody can receive stop instead of running to the end) | (1) `raster::tests::widget_appearance_states`; (2, 3) `xtask text-oracle` on corpus/pdfjs issue918; (4) `raster::tests::layer_override_hides_content`; (5) `raster::tests::soft_mask_without_group_colour_space_is_applied`; (6) `raster::tests::self_referencing_tiling_pattern_terminates`, `raster::tests::self_referencing_type3_glyph_terminates`; (7) `raster::tests::huge_cid_width_ranges_terminate`; (8) `raster::tests::hiding_comments_keeps_fields`; (9) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (10) `raster::tests::type3_glyphs_that_fan_out_into_themselves_finish`; (11) `raster::tests::type1_font_programs_with_long_runs_of_numbers_load_quickly`; (12) `raster::tests::degenerate_annotation_appearances_are_skipped`; (13) `raster::tests::highlights_without_an_appearance_are_drawn`; (14) `raster::tests::malformed_simple_font_widths_keep_later_widths_and_the_font`, `raster::tests::huge_simple_font_first_char_terminates`; (15) `raster::tests::annotation_appearance_box_maps_onto_rect`; (16) `raster::tests::clipping_path_applies_after_any_painting_operator`; (17) `raster::tests::dropping_the_pool_cancels_its_renders`, `raster::tests::the_watchdog_stops_the_render_it_gives_up_on` | -| hayro | 0.7.1 | Apache-2.0 OR MIT | (1) `RenderSettings::{x_offset, y_offset}` to render one tile of a large page; (2) images over `MAX_IMAGE_PIXELS` (2^28) are skipped (a fuzzed `/W 4294967295` hung in resampling); (3) `tiling_cell_scale` keeps a tiling pattern's cell pixmap within 3000 px a side whatever its `/XStep`/`/YStep` (a fuzzed step far beyond `/BBox` allocated 2 GB); (4) stroke widths clamped to 4 × (canvas width + height) device pixels (a fuzzed `/LW 9223372036854775807` allocated 10 GB in stroke expansion) | (1) `raster::tests::tiles_match_full_render`; (2) `raster::tests::absurd_image_dimensions_are_skipped`; (3) `raster::tests::tiling_cells_stay_small_whatever_the_step`; (4) `raster::tests::absurd_predictor_columns_and_line_widths_render` | -| hayro-jbig2 | 0.3.0 | Apache-2.0 OR MIT | (1) at most 2^28 pixels per bitmap, page bitmap, halftone grid and grayscale image (each side could be 65535: 4.3 billion pixels decoded one by one); (2) at most 2^26 symbol pixels per symbol dictionary (a fuzzed dictionary decoded for minutes) | (1) `raster::tests::jbig2_regions_of_billions_of_pixels_are_refused`; (2) the nightly `cargo xtask fuzz` finding, which now finishes in about a second | -| lopdf | 0.45.0 | MIT | (1) PNG predictor rows longer than the data are refused before their buffers are allocated (a fuzzed `/Columns 4294967295` allocated two 4 GiB rows); (2) predictor row widths use saturating arithmetic | (1) `inspect::tests::png_predictor_rows_longer_than_the_data_are_refused_before_allocating` | +| hayro | 0.7.1 | Apache-2.0 OR MIT | (1) `RenderSettings::{x_offset, y_offset}` to render one tile of a large page; (2) images over `MAX_IMAGE_PIXELS` (2^28) are skipped (a fuzzed `/W 4294967295` hung in resampling); (3) `tiling_cell_scale` keeps a tiling pattern's cell pixmap within 3000 px a side whatever its `/XStep`/`/YStep` (a fuzzed step far beyond `/BBox` allocated 2 GB); (4) stroke widths clamped to 4 × (canvas width + height) device pixels (a fuzzed `/LW 9223372036854775807` allocated 10 GB in stroke expansion); (5) `usable_dash_pattern`: a stroke whose dash pattern could cut it into more than `MAX_DASHES_PER_STROKE` (250,000) pieces, counting kurbo's restart of the pattern at every subpath and `ClosePath`, is drawn solid (a fuzzed `/D [[11] 0]` on a line from x = 92234775807 allocated over 5 GB, `[0.000001] 0 d` on a 20 pt line over 10 GB); (6) a dash pattern with a negative or non-finite entry, or a period that isn't positive, is drawn solid (`[-1 -1] 0 d` looped for ever in kurbo); (7) `render_size` and `render_into`: render into a buffer the caller allocates (PdfCraft renders into pixel-aligned memory that the GUI takes over as texture data); `render` is `render_into` on a new pixmap | (1) `raster::tests::tiles_match_full_render`; (2) `raster::tests::absurd_image_dimensions_are_skipped`; (3) `raster::tests::tiling_cells_stay_small_whatever_the_step`; (4) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (5) `raster::tests::dash_patterns_that_cut_a_stroke_into_billions_of_pieces_render`; (6) `raster::tests::invalid_dash_patterns_terminate_and_are_drawn_solid`; (7) `raster::tests::pixels_are_a_plain_renders_bytes_in_a_buffer_a_gui_can_take_over` | +| hayro-jbig2 | 0.3.0 | Apache-2.0 OR MIT | (1) at most 2^28 pixels per bitmap, page bitmap, halftone grid and grayscale image (each side could be 65535: 4.3 billion pixels decoded one by one); (2) at most 2^26 symbol pixels per symbol dictionary (a fuzzed dictionary decoded for minutes); (3) a symbol dictionary's height classes are capped at one per new symbol and its export runs at `2 × symbols + 1`, each plus `EXTRA_EMPTY_STEPS` (1024): an empty height class or a zero-length export run doesn't advance, and past the end of its data the arithmetic decoder can decode them for ever (one changed byte hung the renderer) | (1) `raster::tests::jbig2_regions_of_billions_of_pixels_are_refused`; (2) the nightly `cargo xtask fuzz` finding, which now finishes in about a second; (3) `raster::tests::jbig2_symbol_dictionaries_that_never_advance_terminate` | +| lopdf | 0.45.0 | MIT | (1) PNG predictor rows longer than the data are refused before their buffers are allocated (a fuzzed `/Columns 4294967295` allocated two 4 GiB rows), and empty frames return without row buffers; (2) predictor row widths use saturating arithmetic or reject overflow; (3) sub-byte TIFF Predictor 2 scratch is bounded by the samples present in the input instead of the declared `/Colors`, aligned with [lopdf#589](https://github.com/J-F-Liu/lopdf/pull/589) | (1) `inspect::tests::png_predictor_rows_longer_than_the_data_are_refused_before_allocating`, `inspect::tests::empty_png_predictor_frames_do_not_allocate_rows`; (2) `inspect::tests::tiff_subbyte_predictor_row_width_overflow_is_refused`; (3) `inspect::tests::tiff_subbyte_predictor_scratch_is_bounded_by_available_samples`, `inspect::tests::tiff_subbyte_predictors_keep_components_rows_and_padding` | | hayro-syntax | 0.7.2 | Apache-2.0 OR MIT | (1) Page-tree cycle guard in `resolve_pages` (visited set + depth cap): a `/Kids` loop among compressed objects overflowed the stack and aborted the process; (2) `ccitt_size_ok`: CCITT images wider than 2^20 columns or over 2^28 pixels are not decoded (a fuzzed `/Columns 4294967295` allocated 4 GiB); (3) Flate/LZW predictor row widths are saturating `usize`, a zero width is refused and a row longer than the data is not allocated (a fuzzed `/Columns 9223372036854775807` wrapped to a 2^61-byte allocation and aborted); (4) the inline-image end-of-data check (`EI`) doesn't nest inside its own look-ahead and examines at most 256 candidates and steps (50 images with "EI" in their data ran for over ten minutes); (5) a stream whose `/Length` is wrong is recovered only when `stream` is the next token — scanning ahead to a later object's `stream` made an indirect appearance-state dictionary (Quartz checkboxes: `/AP /N` → `<< /Yes … /Off … >>`) render as a non-form, so the widget was skipped | (1) `cos/tests/writer.rs::page_tree_loops_in_object_streams_do_not_crash_readers`; (2) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (3) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (4) `raster::tests::inline_images_with_ei_in_their_data_parse_in_linear_time`; (5) `raster::tests::indirect_appearance_state_dict_is_not_another_stream` | +| winit | 0.30.13 | Apache-2.0 | (1) on Windows 11 (build 22000 and later), `WM_DPICHANGED` applies the rect Windows suggests. It used to nudge the window onto the monitor `MonitorFromWindow` names, which during a drag is still the monitor being left; the nudge sent another `WM_DPICHANGED`, so a window dragged to a monitor with another scale factor grew and stayed on the wrong monitor at the wrong scale (#324). Backport of winit master 488c036a ([winit#4341](https://github.com/rust-windowing/winit/pull/4341)), which no 0.30 release has; Windows 10 keeps the old adjustment, as on winit master. The published crate's asset files (`docs/res/*.svg`, `examples/data/*.png`) are left out, so its `window` example doesn't build | (1) `pdfcraft` `tests::winit_carries_the_windows_11_monitor_scale_fix` keeps the patch in the build; the drag itself needs two monitors with different scale factors on Windows 11 | Temporary: hayro is the bootstrap renderer and lopdf the bootstrap inspector (ADR-0004); M2.6 replaces them with our own devices and `cos`/`model`. diff --git a/vendor/hayro-interpret/src/context.rs b/vendor/hayro-interpret/src/context.rs index 5a2f2ab8..c5cea5cf 100644 --- a/vendor/hayro-interpret/src/context.rs +++ b/vendor/hayro-interpret/src/context.rs @@ -274,10 +274,6 @@ impl<'a> Context<'a> { &mut self.last_point } - pub(crate) fn clip(&self) -> &Option { - &self.clip - } - pub(crate) fn clip_mut(&mut self) -> &mut Option { &mut self.clip } diff --git a/vendor/hayro-interpret/src/interpret/mod.rs b/vendor/hayro-interpret/src/interpret/mod.rs index 52201206..e77ff75e 100644 --- a/vendor/hayro-interpret/src/interpret/mod.rs +++ b/vendor/hayro-interpret/src/interpret/mod.rs @@ -5,7 +5,7 @@ use crate::convert::{convert_line_cap, convert_line_join}; use crate::device::Device; use crate::font::{Font, FontData, FontQuery, StandardFont}; use crate::interpret::path::{ - close_path, fill_path, fill_path_impl, fill_stroke_path, stroke_path, + apply_pending_clip, close_path, fill_path, fill_path_impl, fill_stroke_path, stroke_path, }; use crate::interpret::state::{TextStateFont, handle_gs}; use crate::interpret::text::TextRenderingMode; @@ -25,6 +25,7 @@ use hayro_syntax::page::{Page, Resources}; use kurbo::{Affine, BezPath, Point, Shape}; use smallvec::smallvec; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; pub(crate) mod path; pub(crate) mod state; @@ -109,6 +110,10 @@ pub struct InterpreterSettings { /// PdfCraft patch: viewer overrides for optional content groups (object number, generation, /// visible), applied on top of the document's default configuration (Layers panel toggles). pub ocg_overrides: Arc>, + /// PdfCraft patch: once this is `true`, interpretation stops before the next content + /// operator. What was drawn is then incomplete, so only set it when the output will be + /// thrown away (a viewer's render whose result nobody can receive any more). + pub cancelled: Option>, } impl Default for InterpreterSettings { @@ -129,6 +134,7 @@ impl Default for InterpreterSettings { render_annotations: true, hide_comments: false, ocg_overrides: Arc::new(Vec::new()), + cancelled: None, } } } @@ -325,6 +331,10 @@ pub fn interpret<'a>( context.save_state(); while let Some(op) = ops.next() { + // PdfCraft patch: stop when the caller cancelled (see `InterpreterSettings::cancelled`). + if context.settings.cancelled.as_ref().is_some_and(|c| c.load(Ordering::Relaxed)) { + break; + } match op { TypedInstruction::SaveState(_) => context.save_state(), TypedInstruction::StrokeColorDeviceRgb(s) => { @@ -479,15 +489,7 @@ pub fn interpret<'a>( stroke_path(context, device); } TypedInstruction::EndPath(_) => { - if let Some(clip) = *context.clip() - && !context.path().elements().is_empty() - { - let clip_path = context.get().ctm * context.path().clone(); - context.push_clip_path(clip_path, clip, device); - - *(context.clip_mut()) = None; - } - + apply_pending_clip(context, device); context.path_mut().truncate(0); } TypedInstruction::NonStrokeColor(c) => { diff --git a/vendor/hayro-interpret/src/interpret/path.rs b/vendor/hayro-interpret/src/interpret/path.rs index e1e7d96a..838ea099 100644 --- a/vendor/hayro-interpret/src/interpret/path.rs +++ b/vendor/hayro-interpret/src/interpret/path.rs @@ -12,12 +12,14 @@ pub(crate) fn fill_path<'a>( fill_rule: FillRule, ) { fill_path_impl(context, device, fill_rule, None); + apply_pending_clip(context, device); context.path_mut().truncate(0); } pub(crate) fn stroke_path<'a>(context: &mut Context<'a>, device: &mut impl Device<'a>) { stroke_path_impl(context, device, None); + apply_pending_clip(context, device); context.path_mut().truncate(0); } @@ -29,10 +31,23 @@ pub(crate) fn fill_stroke_path<'a>( ) { fill_path_impl(context, device, fill_rule, None); stroke_path_impl(context, device, None); + apply_pending_clip(context, device); context.path_mut().truncate(0); } +/// PdfCraft patch: a clip set by `W` / `W*` takes effect once the path-painting operator that +/// ends the path has painted it, whichever operator that is (ISO 32000-2 §8.5.4). Upstream +/// applied it only on `n`, and otherwise left it pending for some later, unrelated `n`. +pub(crate) fn apply_pending_clip<'a>(context: &mut Context<'a>, device: &mut impl Device<'a>) { + if let Some(rule) = context.clip_mut().take() + && !context.path().elements().is_empty() + { + let clip_path = context.get().ctm * context.path().clone(); + context.push_clip_path(clip_path, rule, device); + } +} + pub(crate) fn close_path(context: &mut Context<'_>) { // This is necessary to prevent artifacts (see for example issue 157), // but it does cause some weird lines (maybe conflation artifacts) in diff --git a/vendor/hayro-jbig2/src/decode/symbol.rs b/vendor/hayro-jbig2/src/decode/symbol.rs index ad804252..f1aa8a66 100644 --- a/vendor/hayro-jbig2/src/decode/symbol.rs +++ b/vendor/hayro-jbig2/src/decode/symbol.rs @@ -29,6 +29,15 @@ const MAX_SYMBOLS: u32 = u16::MAX as u32; /// file decoding for minutes; real dictionaries hold about a million pixels. const MAX_DICTIONARY_PIXELS: u64 = 1 << 26; +/// PdfCraft patch: decoding steps that decode nothing, allowed beyond what a valid dictionary can +/// need. A height class without a symbol and an export run of length 0 don't move their loops +/// on, and past the end of its data the arithmetic decoder can decode them for ever: one changed +/// byte in a symbol dictionary hung the renderer. A valid dictionary needs at most one height +/// class per new symbol, and `2 × symbols + 1` export runs (they alternate between exported and +/// not, so at most one zero-length run before each symbol's). Encoders don't write empty steps +/// back to back; this tolerates a thousand. +const EXTRA_EMPTY_STEPS: u64 = 1024; + /// Decode a symbol dictionary segment (7.4.2, 6.5). pub(crate) fn decode( header: &SymbolDictionaryHeader<'_>, @@ -79,7 +88,14 @@ pub(crate) fn decode( } }; + // PdfCraft patch: see `EXTRA_EMPTY_STEPS`. + let mut height_classes_left = u64::from(num_new_symbols) + EXTRA_EMPTY_STEPS; + while ctx.symbols_decoded_count < num_new_symbols { + if height_classes_left == 0 { + bail!(SymbolError::Invalid); + } + height_classes_left -= 1; let height_class_delta = read_height_class_delta(&mut ctx)?.ok_or(SymbolError::UnexpectedOob)?; @@ -694,8 +710,14 @@ fn export_symbols(ctx: &mut SymbolDecodeContext<'_>) -> Result> { let mut exported = Vec::with_capacity(ctx.header.num_exported_symbols as usize); let mut index: u32 = 0; let mut should_export = false; + // PdfCraft patch: see `EXTRA_EMPTY_STEPS`. + let mut runs_left = u64::from(total_symbols) * 2 + 1 + EXTRA_EMPTY_STEPS; while index < total_symbols { + if runs_left == 0 { + bail!(SymbolError::Invalid); + } + runs_left -= 1; let run_length = read_run_length()?; let end_index = index.checked_add(run_length).ok_or(OverflowError::Index)?; diff --git a/vendor/hayro/src/lib.rs b/vendor/hayro/src/lib.rs index 79c2f683..7acc4851 100644 --- a/vendor/hayro/src/lib.rs +++ b/vendor/hayro/src/lib.rs @@ -127,19 +127,56 @@ pub fn render<'a>( interpreter_settings: &InterpreterSettings, render_settings: &RenderSettings, ) -> Pixmap { - let (x_scale, y_scale) = (render_settings.x_scale, render_settings.y_scale); - let (width, height) = page.render_dimensions(); - let (scaled_width, scaled_height) = ((width * x_scale) as f64, (height * y_scale) as f64); - let initial_transform = Affine::translate((-render_settings.x_offset as f64, -render_settings.y_offset as f64)) - * Affine::scale_non_uniform(x_scale as f64, y_scale as f64) - * page.initial_transform(true).to_kurbo(); + let (pix_width, pix_height) = render_size(page, render_settings); + let mut pixmap = Pixmap::new(pix_width, pix_height); + render_into( + page, + cache, + interpreter_settings, + render_settings, + pixmap.data_as_u8_slice_mut(), + ); + pixmap +} - let (pix_width, pix_height) = ( +/// PdfCraft patch: the size in pixels of what [`render`] produces with these settings. +pub fn render_size(page: &Page<'_>, render_settings: &RenderSettings) -> (u16, u16) { + let (width, height) = page.render_dimensions(); + let (scaled_width, scaled_height) = ( + (width * render_settings.x_scale) as f64, + (height * render_settings.y_scale) as f64, + ); + ( render_settings.width.unwrap_or(scaled_width.floor() as u16), render_settings .height .unwrap_or(scaled_height.floor() as u16), - ); + ) +} + +/// PdfCraft patch: [`render`] into `buffer`, premultiplied RGBA8 rows of [`render_size`] +/// pixels, so the caller chooses the allocation (one aligned to whole pixels can then become +/// texture data without a copy). `buffer.len()` must be width × height × 4; any other length +/// leaves `buffer` untouched (vello_cpu asserts on the length, and a panic must not be reachable). +pub fn render_into<'a>( + page: &'a Page<'a>, + cache: &RenderCache<'a>, + interpreter_settings: &InterpreterSettings, + render_settings: &RenderSettings, + buffer: &mut [u8], +) { + let (x_scale, y_scale) = (render_settings.x_scale, render_settings.y_scale); + let initial_transform = Affine::translate((-render_settings.x_offset as f64, -render_settings.y_offset as f64)) + * Affine::scale_non_uniform(x_scale as f64, y_scale as f64) + * page.initial_transform(true).to_kurbo(); + + let (pix_width, pix_height) = render_size(page, render_settings); + let expected = (pix_width as usize) + .checked_mul(pix_height as usize) + .and_then(|n| n.checked_mul(4)); + if expected != Some(buffer.len()) { + return; + } let mut state = Context::new( initial_transform, Rect::new(0.0, 0.0, pix_width as f64, pix_height as f64), @@ -174,11 +211,14 @@ pub fn render<'a>( device.pop_clip_path(); - let mut pixmap = Pixmap::new(pix_width, pix_height); let mut resources = vello_cpu::Resources::default(); - device.ctx.render_to_pixmap(&mut resources, &mut pixmap); - - pixmap + device.ctx.render_to_buffer( + &mut resources, + buffer, + pix_width, + pix_height, + vc_settings.render_mode, + ); } // Just a convenience method for testing. diff --git a/vendor/hayro/src/renderer.rs b/vendor/hayro/src/renderer.rs index 3bf5ccec..ebf3cdc5 100644 --- a/vendor/hayro/src/renderer.rs +++ b/vendor/hayro/src/renderer.rs @@ -23,6 +23,76 @@ use vello_cpu::{ /// PdfCraft patch: largest image drawn, in pixels (2^28 ≈ 268 MP, well above real page images). const MAX_IMAGE_PIXELS: u64 = 1 << 28; + +/// PdfCraft patch: most pieces one dashed stroke may be cut into. Stroke expansion keeps every +/// dash, so a pattern that is tiny next to its path asks for billions: a fuzzed `/D [[11] 0]` on +/// a line from x = 92234775807 allocated over 5 GB, and `[0.000001] 0 d` on a 20 pt line over +/// 10 GB. Measured in release builds: a stroke cut into 250,000 pieces renders in about 16 ms and +/// 75 MB at its peak (a million took 80 ms and 290 MB). Dotted and dashed lines in real documents +/// stay far below this. +const MAX_DASHES_PER_STROKE: f64 = 250_000.0; + +/// PdfCraft patch: the dash pattern to stroke `path` with. Empty (a solid line) when the pattern +/// is invalid, or when it could cut the path into more than [`MAX_DASHES_PER_STROKE`] pieces. +/// ISO 32000-2 §8.4.3.6 requires nonnegative entries that are not all zero; a pattern whose +/// period isn't positive (`[-1 -1] 0 d`) made kurbo's search for the starting dash loop for ever. +fn usable_dash_pattern(path: &BezPath, dashes: &[f32]) -> Vec { + if dashes.iter().any(|d| !d.is_finite() || *d < 0.0) { + return Vec::new(); + } + let sum: f64 = dashes.iter().map(|d| f64::from(*d)).sum(); + // kurbo repeats an odd-length pattern once, as SVG does, so a period covers it twice. + let (period, per_period) = if dashes.len() % 2 == 1 { + (2.0 * sum, dashes.len().saturating_mul(2)) + } else { + (sum, dashes.len()) + }; + if !period.is_finite() || period <= 0.0 { + return Vec::new(); + } + let (length, restarts) = control_polygon_length(path); + // kurbo restarts the pattern at every subpath and after every `ClosePath`, so each restart + // can begin up to a whole period of pieces however short its segment is. + let pieces = (length / period + restarts as f64) * per_period as f64; + if !pieces.is_finite() || pieces > MAX_DASHES_PER_STROKE { + return Vec::new(); + } + dashes.iter().map(|d| f64::from(*d)).collect() +} + +/// PdfCraft patch: the length of `path`'s control polygon, which a curve never exceeds, and how +/// many times kurbo restarts a dash pattern along it (each `MoveTo` and `ClosePath`). +fn control_polygon_length(path: &BezPath) -> (f64, usize) { + let (mut length, mut restarts) = (0.0, 0usize); + let (mut start, mut last) = (Point::ORIGIN, Point::ORIGIN); + for el in path.elements() { + match *el { + kurbo::PathEl::MoveTo(p) => { + (start, last) = (p, p); + restarts = restarts.saturating_add(1); + } + kurbo::PathEl::LineTo(p) => { + length += last.distance(p); + last = p; + } + kurbo::PathEl::QuadTo(c, p) => { + length += last.distance(c) + c.distance(p); + last = p; + } + kurbo::PathEl::CurveTo(c1, c2, p) => { + length += last.distance(c1) + c1.distance(c2) + c2.distance(p); + last = p; + } + kurbo::PathEl::ClosePath => { + length += last.distance(start); + last = start; + restarts = restarts.saturating_add(1); + } + } + } + (length, restarts) +} + pub(crate) struct Renderer { pub(crate) ctx: RenderContext, pub(crate) inside_pattern: bool, @@ -61,7 +131,7 @@ impl Renderer { } } - fn set_stroke_properties(&mut self, stroke_props: &StrokeProps, is_text: bool) { + fn set_stroke_properties(&mut self, stroke_props: &StrokeProps, is_text: bool, path: &BezPath) { let threshold = if is_text { 0.25 } else { 1.0 }; // Best-effort attempt to ensure a line width of at least 1.0, as required by the PDF @@ -91,8 +161,13 @@ impl Renderer { miter_limit: stroke_props.miter_limit as f64, start_cap: stroke_props.line_cap, end_cap: stroke_props.line_cap, - dash_pattern: stroke_props.dash_array.iter().map(|n| *n as f64).collect(), - dash_offset: stroke_props.dash_offset as f64, + // PdfCraft patch: see `usable_dash_pattern`. + dash_pattern: usable_dash_pattern(path, &stroke_props.dash_array).into(), + dash_offset: if stroke_props.dash_offset.is_finite() { + stroke_props.dash_offset as f64 + } else { + 0.0 + }, }; self.ctx.set_stroke(stroke); @@ -598,7 +673,7 @@ impl Renderer { is_text: bool, ) { self.ctx.set_transform(transform); - self.set_stroke_properties(stroke_props, is_text); + self.set_stroke_properties(stroke_props, is_text, path); let clip_path = self.set_paint(paint, path, true); if let Some(clip_path) = clip_path.as_ref() { diff --git a/vendor/lopdf/src/filters/png.rs b/vendor/lopdf/src/filters/png.rs index e9390799..027097f4 100644 --- a/vendor/lopdf/src/filters/png.rs +++ b/vendor/lopdf/src/filters/png.rs @@ -86,10 +86,15 @@ pub fn decode_row(filter: FilterType, bpp: usize, previous: &[u8], current: &mut } pub fn decode_frame(content: &[u8], bpp: usize, bytes_per_row: usize) -> Result> { + // PdfCraft patch: an empty frame needs no row buffers, even for an absurd declared width. + if content.is_empty() { + return Ok(Vec::new()); + } + // PdfCraft patch: each row is a filter byte plus `bytes_per_row` bytes, so a row longer // than the data can never be read (`read_exact` below fails). Refuse it before allocating: // a fuzzed `/Columns 4294967295` allocated two 4 GiB rows here. - if !content.is_empty() && bytes_per_row >= content.len() { + if bytes_per_row >= content.len() { return Err(Error::new(ErrorKind::InvalidData, "PNG predictor row is longer than the data")); } let mut previous = Vec::new(); diff --git a/vendor/lopdf/src/object.rs b/vendor/lopdf/src/object.rs index 843d80ac..a406e44d 100644 --- a/vendor/lopdf/src/object.rs +++ b/vendor/lopdf/src/object.rs @@ -1277,7 +1277,12 @@ impl Stream { } } 1 | 2 | 4 => { - let stride = (columns * colors * bits).div_ceil(8); + // PdfCraft patch: reject overflowing packed row widths (lopdf#589). + let stride = columns + .checked_mul(colors) + .and_then(|samples| samples.checked_mul(bits)) + .ok_or(DecompressError::Predictor("predictor row size overflows"))? + .div_ceil(8); if stride > 0 { for row in data.chunks_mut(stride) { Self::reverse_tiff_predictor2_subbyte(row, columns, colors, bits); @@ -1299,8 +1304,9 @@ impl Stream { /// `2^bits`, then repacked MSB-first; the row's trailing padding bits are kept. fn reverse_tiff_predictor2_subbyte(row: &mut [u8], columns: usize, colors: usize, bits: usize) { let mask = (1u16 << bits) - 1; - let samples = columns.saturating_mul(colors).min(row.len() * 8 / bits); // PdfCraft patch: saturating - let mut acc = vec![0u16; colors]; + // PdfCraft patch: bound scratch to the samples present in the input (lopdf#589). + let samples = columns.saturating_mul(colors).min(row.len().saturating_mul(8 / bits)); + let mut acc = vec![0u16; colors.min(samples)]; let mut out = vec![0u8; row.len()]; let mut in_bit = 0usize; let mut out_buf = 0u32; diff --git a/vendor/winit/.cargo-ok b/vendor/winit/.cargo-ok new file mode 100644 index 00000000..5f8b7958 --- /dev/null +++ b/vendor/winit/.cargo-ok @@ -0,0 +1 @@ +{"v":1} \ No newline at end of file diff --git a/vendor/winit/.cargo_vcs_info.json b/vendor/winit/.cargo_vcs_info.json new file mode 100644 index 00000000..6a632833 --- /dev/null +++ b/vendor/winit/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "e9809ef54b18499bb4f2cac945719ecc2a61061b" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/vendor/winit/Cargo.lock b/vendor/winit/Cargo.lock new file mode 100644 index 00000000..fa62c188 --- /dev/null +++ b/vendor/winit/Cargo.lock @@ -0,0 +1,2187 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79faae4620f45232f599d9bc7b290f88247a0834162c4495ab2f02d60004adfb" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.6.0", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "arrayref" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytemuck" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" + +[[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.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.6.0", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +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 = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[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" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +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 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", + "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 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.10.0", + "libc", +] + +[[package]] +name = "core-text" +version = "20.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" +dependencies = [ + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types", + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crossfont" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a650099c323679b4d6fb6858c2c512660ff5118f6b0ffb1bfc6cadcd54002b14" +dependencies = [ + "core-foundation 0.9.4", + "core-foundation-sys", + "core-graphics 0.23.2", + "core-text", + "dwrote", + "foreign-types", + "freetype-rs", + "libc", + "log", + "objc2", + "objc2-foundation", + "once_cell", + "pkg-config", + "winapi", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "cstr" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68523903c8ae5aacfa32a0d9ae60cadeb764e1da14ee0d26b1f3089f13a54636" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "ctor-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b" + +[[package]] +name = "cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +dependencies = [ + "serde", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[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" +dependencies = [ + "mint", + "serde", +] + +[[package]] +name = "dwrote" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b" +dependencies = [ + "lazy_static", + "libc", + "serde", + "serde_derive", + "winapi", + "wio", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "freetype-rs" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5442dee36ca09604133580dc0553780e867936bb3cbef3275859e889026d2b17" +dependencies = [ + "bitflags 2.6.0", + "freetype-sys", + "libc", +] + +[[package]] +name = "freetype-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "image" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" +dependencies = [ + "bytemuck", + "byteorder-lite", + "num-traits", + "png", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libredox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" +dependencies = [ + "bitflags 2.6.0", + "libc", + "redox_syscall 0.4.1", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mint" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.6.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle 0.4.3", + "raw-window-handle 0.5.2", + "raw-window-handle 0.6.2", + "thiserror", +] + +[[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", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[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-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.6.0", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2", + "objc2-contacts", + "objc2-foundation", +] + +[[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.6.0", + "block2", + "dispatch", + "libc", + "objc2", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "orbclient" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f0d54bde9774d3a51dcf281a5def240c71996bc6ca05d2c847ec8b2b216166" +dependencies = [ + "libredox", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owned_ttf_parser" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490d3a563d3122bf7c911a59b0add9389e5ec0f5f0c3ac6b91ff235a0e6a7f90" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 0.38.44", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "raw-window-handle" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b800beb9b6e7d2df1fe337c9e3d04e3af22a124460fb4c30fcc22c9117cefb41" +dependencies = [ + "cty", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys 0.4.14", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.59.0", +] + +[[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 = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "crossfont", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.6.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "softbuffer" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "cfg_aliases", + "core-graphics 0.24.0", + "fastrand", + "foreign-types", + "js-sys", + "log", + "memmap2", + "objc2", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle 0.6.2", + "redox_syscall 0.5.8", + "rustix 0.38.44", + "tiny-xlib", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.59.0", + "x11rb", +] + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[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", + "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 = "tiny-xlib" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d52f22673960ad13af14ff4025997312def1223bfa7c8e4949d099e6b3d5d1c" +dependencies = [ + "as-raw-xcb-connection", + "ctor-lite", + "libloading", + "pkg-config", + "tracing", +] + +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-web" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e6a141feebd51f8d91ebfd785af50fca223c570b86852166caa3b141defe7c" +dependencies = [ + "js-sys", + "tracing-core", + "tracing-subscriber", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "ttf-parser" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8686b91785aff82828ed725225925b33b4fde44c4bb15876e5f7c832724c420a" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[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 = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.0.8", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.6.0", + "rustix 1.0.8", + "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.6.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef9489a8df197ebf3a8ce8a7a7f0a2320035c3743f3c1bd0bdbccf07ce64f95" +dependencies = [ + "rustix 0.38.44", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fd38cdad69b56ace413c6bcc1fbf5acc5e2ef4af9d5f8f1f9570c0c83eae175" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +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 = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[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-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[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", + "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_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[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_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[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_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[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_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winit" +version = "0.30.13" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.6.0", + "block2", + "bytemuck", + "calloop", + "cfg_aliases", + "concurrent-queue", + "console_error_panic_hook", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "cursor-icon", + "dpi", + "image", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle 0.4.3", + "raw-window-handle 0.5.2", + "raw-window-handle 0.6.2", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "serde", + "smithay-client-toolkit", + "smol_str", + "softbuffer", + "tracing", + "tracing-subscriber", + "tracing-web", + "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 = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 0.38.44", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xcursor" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d491ee231a51ae64a5b762114c3ac2104b967aadba1de45c86ca42cf051513b7" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.6.0", + "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 = "yeslogic-fontconfig-sys" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb6b23999a8b1a997bf47c7bb4d19ad4029c3327bb3386ebe0a5ff584b33c7a" +dependencies = [ + "cstr", + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/vendor/winit/Cargo.toml b/vendor/winit/Cargo.toml new file mode 100644 index 00000000..4bfded8a --- /dev/null +++ b/vendor/winit/Cargo.toml @@ -0,0 +1,553 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.70.0" +name = "winit" +version = "0.30.13" +authors = [ + "The winit contributors", + "Pierre Krieger ", +] +build = "build.rs" +include = [ + "/build.rs", + "/docs", + "/examples", + "/FEATURES.md", + "/LICENSE", + "/src", + "!/src/platform_impl/web/script", + "/src/platform_impl/web/script/**/*.min.js", + "/tests", +] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Cross-platform window creation library." +documentation = "https://docs.rs/winit" +readme = "README.md" +keywords = ["windowing"] +categories = ["gui"] +license = "Apache-2.0" +repository = "https://github.com/rust-windowing/winit" + +[package.metadata.docs.rs] +features = [ + "rwh_04", + "rwh_05", + "rwh_06", + "serde", + "mint", + "android-native-activity", +] +targets = [ + "i686-pc-windows-msvc", + "x86_64-pc-windows-msvc", + "x86_64-apple-darwin", + "i686-unknown-linux-gnu", + "x86_64-unknown-linux-gnu", + "x86_64-apple-ios", + "aarch64-linux-android", + "wasm32-unknown-unknown", +] +rustdoc-args = [ + "--cfg", + "docsrs", +] + +[features] +android-game-activity = ["android-activity/game-activity"] +android-native-activity = ["android-activity/native-activity"] +default = [ + "rwh_06", + "x11", + "wayland", + "wayland-dlopen", + "wayland-csd-adwaita", +] +mint = ["dpi/mint"] +rwh_04 = [ + "dep:rwh_04", + "ndk/rwh_04", +] +rwh_05 = [ + "dep:rwh_05", + "ndk/rwh_05", +] +rwh_06 = [ + "dep:rwh_06", + "ndk/rwh_06", +] +serde = [ + "dep:serde", + "cursor-icon/serde", + "smol_str/serde", + "dpi/serde", +] +wayland = [ + "wayland-client", + "wayland-backend", + "wayland-protocols", + "wayland-protocols-plasma", + "sctk", + "ahash", + "memmap2", +] +wayland-csd-adwaita = [ + "sctk-adwaita", + "sctk-adwaita/ab_glyph", +] +wayland-csd-adwaita-crossfont = [ + "sctk-adwaita", + "sctk-adwaita/crossfont", +] +wayland-csd-adwaita-notitle = ["sctk-adwaita"] +wayland-dlopen = ["wayland-backend/dlopen"] +x11 = [ + "x11-dl", + "bytemuck", + "percent-encoding", + "xkbcommon-dl/x11", + "x11rb", +] + +[lib] +name = "winit" +path = "src/lib.rs" + +[[example]] +name = "child_window" +path = "examples/child_window.rs" + +[[example]] +name = "control_flow" +path = "examples/control_flow.rs" + +[[example]] +name = "pump_events" +path = "examples/pump_events.rs" + +[[example]] +name = "run_on_demand" +path = "examples/run_on_demand.rs" + +[[example]] +name = "window" +path = "examples/window.rs" +doc-scrape-examples = true + +[[example]] +name = "x11_embed" +path = "examples/x11_embed.rs" + +[[test]] +name = "send_objects" +path = "tests/send_objects.rs" + +[[test]] +name = "serde_objects" +path = "tests/serde_objects.rs" + +[[test]] +name = "sync_object" +path = "tests/sync_object.rs" + +[dependencies.bitflags] +version = "2" + +[dependencies.cursor-icon] +version = "1.1.0" + +[dependencies.dpi] +version = "0.1.1" + +[dependencies.rwh_04] +version = "0.4" +optional = true +package = "raw-window-handle" + +[dependencies.rwh_05] +version = "0.5.2" +features = ["std"] +optional = true +package = "raw-window-handle" + +[dependencies.rwh_06] +version = "0.6" +features = ["std"] +optional = true +package = "raw-window-handle" + +[dependencies.serde] +version = "1" +features = ["serde_derive"] +optional = true + +[dependencies.smol_str] +version = "0.2.0" + +[dependencies.tracing] +version = "0.1.40" +default-features = false + +[dev-dependencies.image] +version = "0.25.0" +features = ["png"] +default-features = false + +[dev-dependencies.tracing] +version = "0.1.40" +features = ["log"] +default-features = false + +[dev-dependencies.tracing-subscriber] +version = "0.3.18" +features = ["env-filter"] + +[build-dependencies.cfg_aliases] +version = "0.2.1" + +[target.'cfg(all(target_family = "wasm", target_feature = "atomics"))'.dependencies.atomic-waker] +version = "1" + +[target.'cfg(all(target_family = "wasm", target_feature = "atomics"))'.dependencies.concurrent-queue] +version = "2" +default-features = false + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.ahash] +version = "0.8.7" +features = ["no-rng"] +optional = true + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.bytemuck] +version = "1.13.1" +optional = true +default-features = false + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.calloop] +version = "0.13.0" + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.libc] +version = "0.2.64" + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.memmap2] +version = "0.9.0" +optional = true + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.percent-encoding] +version = "2.0" +optional = true + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.rustix] +version = "0.38.4" +features = [ + "std", + "system", + "thread", + "process", +] +default-features = false + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.sctk] +version = "0.19.2" +features = ["calloop"] +optional = true +default-features = false +package = "smithay-client-toolkit" + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.sctk-adwaita] +version = "0.10.1" +optional = true +default-features = false + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.wayland-backend] +version = "0.3.10" +features = ["client_system"] +optional = true +default-features = false + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.wayland-client] +version = "0.31.10" +optional = true + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.wayland-protocols] +version = "0.32.8" +features = ["staging"] +optional = true + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.wayland-protocols-plasma] +version = "0.3.8" +features = ["client"] +optional = true + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.x11-dl] +version = "2.19.1" +optional = true + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.x11rb] +version = "0.13.0" +features = [ + "allow-unsafe-code", + "dl-libxcb", + "randr", + "resource_manager", + "xinput", + "xkb", +] +optional = true +default-features = false + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies.xkbcommon-dl] +version = "0.4.2" + +[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies.block2] +version = "0.5.1" + +[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies.core-foundation] +version = "0.9.3" + +[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies.objc2] +version = "0.5.2" +features = ["relax-sign-encoding"] + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dev-dependencies.softbuffer] +version = "0.4.0" +features = [ + "x11", + "x11-dlopen", + "wayland", + "wayland-dlopen", +] +default-features = false + +[target.'cfg(target_family = "wasm")'.dependencies.js-sys] +version = "0.3.70" + +[target.'cfg(target_family = "wasm")'.dependencies.pin-project] +version = "1" + +[target.'cfg(target_family = "wasm")'.dependencies.wasm-bindgen] +version = "0.2.93" + +[target.'cfg(target_family = "wasm")'.dependencies.wasm-bindgen-futures] +version = "0.4.43" + +[target.'cfg(target_family = "wasm")'.dependencies.web-time] +version = "1" + +[target.'cfg(target_family = "wasm")'.dependencies.web_sys] +version = "0.3.70" +features = [ + "AbortController", + "AbortSignal", + "Blob", + "BlobPropertyBag", + "console", + "CssStyleDeclaration", + "Document", + "DomException", + "DomRect", + "DomRectReadOnly", + "Element", + "Event", + "EventTarget", + "FocusEvent", + "HtmlCanvasElement", + "HtmlElement", + "HtmlImageElement", + "ImageBitmap", + "ImageBitmapOptions", + "ImageBitmapRenderingContext", + "ImageData", + "IntersectionObserver", + "IntersectionObserverEntry", + "KeyboardEvent", + "MediaQueryList", + "MessageChannel", + "MessagePort", + "Navigator", + "Node", + "OrientationLockType", + "OrientationType", + "PageTransitionEvent", + "Permissions", + "PermissionState", + "PermissionStatus", + "PointerEvent", + "PremultiplyAlpha", + "ResizeObserver", + "ResizeObserverBoxOptions", + "ResizeObserverEntry", + "ResizeObserverOptions", + "ResizeObserverSize", + "Screen", + "ScreenOrientation", + "Url", + "VisibilityState", + "WheelEvent", + "Window", + "Worker", +] +package = "web-sys" + +[target.'cfg(target_family = "wasm")'.dev-dependencies.console_error_panic_hook] +version = "0.1" + +[target.'cfg(target_family = "wasm")'.dev-dependencies.tracing-web] +version = "0.1" + +[target.'cfg(target_os = "android")'.dependencies.android-activity] +version = "0.6.0" + +[target.'cfg(target_os = "android")'.dependencies.ndk] +version = "0.9.0" +default-features = false + +[target.'cfg(target_os = "ios")'.dependencies.objc2-foundation] +version = "0.2.2" +features = [ + "block2", + "dispatch", + "NSArray", + "NSEnumerator", + "NSGeometry", + "NSObjCRuntime", + "NSOperation", + "NSString", + "NSProcessInfo", + "NSThread", + "NSSet", +] + +[target.'cfg(target_os = "ios")'.dependencies.objc2-ui-kit] +version = "0.2.2" +features = [ + "UIApplication", + "UIDevice", + "UIEvent", + "UIGeometry", + "UIGestureRecognizer", + "UITextInput", + "UITextInputTraits", + "UIOrientation", + "UIPanGestureRecognizer", + "UIPinchGestureRecognizer", + "UIResponder", + "UIRotationGestureRecognizer", + "UIScreen", + "UIScreenMode", + "UITapGestureRecognizer", + "UITouch", + "UITraitCollection", + "UIView", + "UIViewController", + "UIWindow", +] + +[target.'cfg(target_os = "macos")'.dependencies.core-graphics] +version = "0.23.1" + +[target.'cfg(target_os = "macos")'.dependencies.objc2-app-kit] +version = "0.2.2" +features = [ + "NSAppearance", + "NSApplication", + "NSBitmapImageRep", + "NSButton", + "NSColor", + "NSControl", + "NSCursor", + "NSDragging", + "NSEvent", + "NSGraphics", + "NSGraphicsContext", + "NSImage", + "NSImageRep", + "NSMenu", + "NSMenuItem", + "NSOpenGLView", + "NSPasteboard", + "NSResponder", + "NSRunningApplication", + "NSScreen", + "NSTextInputClient", + "NSTextInputContext", + "NSView", + "NSWindow", + "NSWindowScripting", + "NSWindowTabGroup", +] + +[target.'cfg(target_os = "macos")'.dependencies.objc2-foundation] +version = "0.2.2" +features = [ + "block2", + "dispatch", + "NSArray", + "NSAttributedString", + "NSData", + "NSDictionary", + "NSDistributedNotificationCenter", + "NSEnumerator", + "NSKeyValueObserving", + "NSNotification", + "NSObjCRuntime", + "NSPathUtilities", + "NSProcessInfo", + "NSRunLoop", + "NSString", + "NSThread", + "NSValue", +] + +[target.'cfg(target_os = "redox")'.dependencies.orbclient] +version = "0.3.47" +default-features = false + +[target.'cfg(target_os = "redox")'.dependencies.redox_syscall] +version = "0.4.1" + +[target.'cfg(target_os = "windows")'.dependencies.unicode-segmentation] +version = "1.7.1" + +[target.'cfg(target_os = "windows")'.dependencies.windows-sys] +version = "0.52.0" +features = [ + "Win32_Devices_HumanInterfaceDevice", + "Win32_Foundation", + "Win32_Globalization", + "Win32_Graphics_Dwm", + "Win32_Graphics_Gdi", + "Win32_Media", + "Win32_System_Com_StructuredStorage", + "Win32_System_Com", + "Win32_System_LibraryLoader", + "Win32_System_Ole", + "Win32_Security", + "Win32_System_SystemInformation", + "Win32_System_SystemServices", + "Win32_System_Threading", + "Win32_System_WindowsProgramming", + "Win32_UI_Accessibility", + "Win32_UI_Controls", + "Win32_UI_HiDpi", + "Win32_UI_Input_Ime", + "Win32_UI_Input_KeyboardAndMouse", + "Win32_UI_Input_Pointer", + "Win32_UI_Input_Touch", + "Win32_UI_Shell", + "Win32_UI_TextServices", + "Win32_UI_WindowsAndMessaging", +] diff --git a/vendor/winit/Cargo.toml.orig b/vendor/winit/Cargo.toml.orig new file mode 100644 index 00000000..71f7c581 --- /dev/null +++ b/vendor/winit/Cargo.toml.orig @@ -0,0 +1,378 @@ +[package] +name = "winit" +version = "0.30.13" +authors = [ + "The winit contributors", + "Pierre Krieger ", +] +description = "Cross-platform window creation library." +keywords = ["windowing"] +readme = "README.md" +documentation = "https://docs.rs/winit" +categories = ["gui"] +rust-version.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +include = [ + "/build.rs", + "/docs", + "/examples", + "/FEATURES.md", + "/LICENSE", + "/src", + "!/src/platform_impl/web/script", + "/src/platform_impl/web/script/**/*.min.js", + "/tests", +] + +[package.metadata.docs.rs] +features = [ + "rwh_04", + "rwh_05", + "rwh_06", + "serde", + "mint", + # Enabled to get docs to compile + "android-native-activity", +] +# These are all tested in CI +targets = [ + # Windows + "i686-pc-windows-msvc", + "x86_64-pc-windows-msvc", + # macOS + "x86_64-apple-darwin", + # Unix (X11 & Wayland) + "i686-unknown-linux-gnu", + "x86_64-unknown-linux-gnu", + # iOS + "x86_64-apple-ios", + # Android + "aarch64-linux-android", + # Web + "wasm32-unknown-unknown", +] +rustdoc-args = ["--cfg", "docsrs"] + +# Features are documented in either `lib.rs` or under `winit::platform`. +[features] +default = ["rwh_06", "x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] +x11 = ["x11-dl", "bytemuck", "percent-encoding", "xkbcommon-dl/x11", "x11rb"] +wayland = [ + "wayland-client", + "wayland-backend", + "wayland-protocols", + "wayland-protocols-plasma", + "sctk", + "ahash", + "memmap2", +] +wayland-dlopen = ["wayland-backend/dlopen"] +wayland-csd-adwaita = ["sctk-adwaita", "sctk-adwaita/ab_glyph"] +wayland-csd-adwaita-crossfont = ["sctk-adwaita", "sctk-adwaita/crossfont"] +wayland-csd-adwaita-notitle = ["sctk-adwaita"] +android-native-activity = ["android-activity/native-activity"] +android-game-activity = ["android-activity/game-activity"] +serde = ["dep:serde", "cursor-icon/serde", "smol_str/serde", "dpi/serde"] +mint = ["dpi/mint"] +rwh_04 = ["dep:rwh_04", "ndk/rwh_04"] +rwh_05 = ["dep:rwh_05", "ndk/rwh_05"] +rwh_06 = ["dep:rwh_06", "ndk/rwh_06"] + +[build-dependencies] +cfg_aliases = "0.2.1" + +[dependencies] +bitflags = "2" +cursor-icon = "1.1.0" +dpi = { version = "0.1.1", path = "dpi" } +rwh_04 = { package = "raw-window-handle", version = "0.4", optional = true } +rwh_05 = { package = "raw-window-handle", version = "0.5.2", features = [ + "std", +], optional = true } +rwh_06 = { package = "raw-window-handle", version = "0.6", features = [ + "std", +], optional = true } +serde = { workspace = true, optional = true } +smol_str = "0.2.0" +tracing = { version = "0.1.40", default-features = false } + +[dev-dependencies] +image = { version = "0.25.0", default-features = false, features = ["png"] } +tracing = { version = "0.1.40", default-features = false, features = ["log"] } +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +winit = { path = ".", features = ["rwh_05"] } + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dev-dependencies] +softbuffer = { version = "0.4.0", default-features = false, features = [ + "x11", + "x11-dlopen", + "wayland", + "wayland-dlopen", +] } + +[target.'cfg(target_os = "android")'.dependencies] +android-activity = "0.6.0" +ndk = { version = "0.9.0", default-features = false } + +[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] +block2 = "0.5.1" +core-foundation = "0.9.3" +objc2 = { version = "0.5.2", features = ["relax-sign-encoding"] } + +[target.'cfg(target_os = "macos")'.dependencies] +core-graphics = "0.23.1" + +[target.'cfg(target_os = "macos")'.dependencies.objc2-foundation] +version = "0.2.2" +features = [ + "block2", + "dispatch", + "NSArray", + "NSAttributedString", + "NSData", + "NSDictionary", + "NSDistributedNotificationCenter", + "NSEnumerator", + "NSKeyValueObserving", + "NSNotification", + "NSObjCRuntime", + "NSPathUtilities", + "NSProcessInfo", + "NSRunLoop", + "NSString", + "NSThread", + "NSValue", +] + +[target.'cfg(target_os = "macos")'.dependencies.objc2-app-kit] +version = "0.2.2" +features = [ + "NSAppearance", + "NSApplication", + "NSBitmapImageRep", + "NSButton", + "NSColor", + "NSControl", + "NSCursor", + "NSDragging", + "NSEvent", + "NSGraphics", + "NSGraphicsContext", + "NSImage", + "NSImageRep", + "NSMenu", + "NSMenuItem", + "NSOpenGLView", + "NSPasteboard", + "NSResponder", + "NSRunningApplication", + "NSScreen", + "NSTextInputClient", + "NSTextInputContext", + "NSView", + "NSWindow", + "NSWindowScripting", + "NSWindowTabGroup", +] + +[target.'cfg(target_os = "ios")'.dependencies.objc2-foundation] +version = "0.2.2" +features = [ + "block2", + "dispatch", + "NSArray", + "NSEnumerator", + "NSGeometry", + "NSObjCRuntime", + "NSOperation", + "NSString", + "NSProcessInfo", + "NSThread", + "NSSet", +] + +[target.'cfg(target_os = "ios")'.dependencies.objc2-ui-kit] +version = "0.2.2" +features = [ + "UIApplication", + "UIDevice", + "UIEvent", + "UIGeometry", + "UIGestureRecognizer", + "UITextInput", + "UITextInputTraits", + "UIOrientation", + "UIPanGestureRecognizer", + "UIPinchGestureRecognizer", + "UIResponder", + "UIRotationGestureRecognizer", + "UIScreen", + "UIScreenMode", + "UITapGestureRecognizer", + "UITouch", + "UITraitCollection", + "UIView", + "UIViewController", + "UIWindow", +] + +[target.'cfg(target_os = "windows")'.dependencies] +unicode-segmentation = "1.7.1" + +[target.'cfg(target_os = "windows")'.dependencies.windows-sys] +version = "0.52.0" +features = [ + "Win32_Devices_HumanInterfaceDevice", + "Win32_Foundation", + "Win32_Globalization", + "Win32_Graphics_Dwm", + "Win32_Graphics_Gdi", + "Win32_Media", + "Win32_System_Com_StructuredStorage", + "Win32_System_Com", + "Win32_System_LibraryLoader", + "Win32_System_Ole", + "Win32_Security", + "Win32_System_SystemInformation", + "Win32_System_SystemServices", + "Win32_System_Threading", + "Win32_System_WindowsProgramming", + "Win32_UI_Accessibility", + "Win32_UI_Controls", + "Win32_UI_HiDpi", + "Win32_UI_Input_Ime", + "Win32_UI_Input_KeyboardAndMouse", + "Win32_UI_Input_Pointer", + "Win32_UI_Input_Touch", + "Win32_UI_Shell", + "Win32_UI_TextServices", + "Win32_UI_WindowsAndMessaging", +] + +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_os = "ios", target_os = "macos"))))'.dependencies] +ahash = { version = "0.8.7", features = ["no-rng"], optional = true } +bytemuck = { version = "1.13.1", default-features = false, optional = true } +calloop = "0.13.0" +libc = "0.2.64" +memmap2 = { version = "0.9.0", optional = true } +percent-encoding = { version = "2.0", optional = true } +rustix = { version = "0.38.4", default-features = false, features = [ + "std", + "system", + "thread", + "process", +] } +sctk = { package = "smithay-client-toolkit", version = "0.19.2", default-features = false, features = [ + "calloop", +], optional = true } +sctk-adwaita = { version = "0.10.1", default-features = false, optional = true } +wayland-backend = { version = "0.3.10", default-features = false, features = [ + "client_system", +], optional = true } +wayland-client = { version = "0.31.10", optional = true } +wayland-protocols = { version = "0.32.8", features = [ + "staging", +], optional = true } +wayland-protocols-plasma = { version = "0.3.8", features = [ + "client", +], optional = true } +x11-dl = { version = "2.19.1", optional = true } +x11rb = { version = "0.13.0", default-features = false, features = [ + "allow-unsafe-code", + "dl-libxcb", + "randr", + "resource_manager", + "xinput", + "xkb", +], optional = true } +xkbcommon-dl = "0.4.2" + +[target.'cfg(target_os = "redox")'.dependencies] +orbclient = { version = "0.3.47", default-features = false } +redox_syscall = "0.4.1" + +[target.'cfg(target_family = "wasm")'.dependencies] +js-sys = "0.3.70" +pin-project = "1" +wasm-bindgen = "0.2.93" +wasm-bindgen-futures = "0.4.43" +web-time = "1" +web_sys = { package = "web-sys", version = "0.3.70", features = [ + "AbortController", + "AbortSignal", + "Blob", + "BlobPropertyBag", + "console", + "CssStyleDeclaration", + "Document", + "DomException", + "DomRect", + "DomRectReadOnly", + "Element", + "Event", + "EventTarget", + "FocusEvent", + "HtmlCanvasElement", + "HtmlElement", + "HtmlImageElement", + "ImageBitmap", + "ImageBitmapOptions", + "ImageBitmapRenderingContext", + "ImageData", + "IntersectionObserver", + "IntersectionObserverEntry", + "KeyboardEvent", + "MediaQueryList", + "MessageChannel", + "MessagePort", + "Navigator", + "Node", + "OrientationLockType", + "OrientationType", + "PageTransitionEvent", + "Permissions", + "PermissionState", + "PermissionStatus", + "PointerEvent", + "PremultiplyAlpha", + "ResizeObserver", + "ResizeObserverBoxOptions", + "ResizeObserverEntry", + "ResizeObserverOptions", + "ResizeObserverSize", + "Screen", + "ScreenOrientation", + "Url", + "VisibilityState", + "WheelEvent", + "Window", + "Worker", +] } + +[target.'cfg(all(target_family = "wasm", target_feature = "atomics"))'.dependencies] +atomic-waker = "1" +concurrent-queue = { version = "2", default-features = false } + +[target.'cfg(target_family = "wasm")'.dev-dependencies] +console_error_panic_hook = "0.1" +tracing-web = "0.1" + +[[example]] +doc-scrape-examples = true +name = "window" + +[workspace] +resolver = "2" +members = ["dpi"] + +[workspace.package] +rust-version = "1.70.0" +repository = "https://github.com/rust-windowing/winit" +license = "Apache-2.0" +edition = "2021" + +[workspace.dependencies] +serde = { version = "1", features = ["serde_derive"] } +mint = "0.5.6" diff --git a/vendor/winit/FEATURES.md b/vendor/winit/FEATURES.md new file mode 100644 index 00000000..2ae94770 --- /dev/null +++ b/vendor/winit/FEATURES.md @@ -0,0 +1,248 @@ +# Winit Scope + +Winit aims to expose an interface that abstracts over window creation and input handling and can +be used to create both games and applications. It supports the following main graphical platforms: +- Desktop + - Windows + - macOS + - Unix + - via X11 + - via Wayland + - Redox OS, via Orbital +- Mobile + - iOS + - Android +- Web + +Most platforms expose capabilities that cannot be meaningfully transposed onto others. Winit does not +aim to support every single feature of every platform, but rather to abstract over the common features +available everywhere. In this context, APIs exposed in winit can be split into different "support tiers": + +- **Core:** Features that are essential to providing a well-formed abstraction over each platform's + windowing and input APIs. +- **Platform:** Platform-specific features that can't be meaningfully exposed through a common API and + cannot be implemented outside of Winit without exposing a significant amount of Winit's internals + or interfering with Winit's abstractions. +- **Usability:** Features that are not strictly essential to Winit's functionality, but provide meaningful + usability improvements and cannot be reasonably implemented in an external crate. These are + generally optional and exposed through Cargo features. + +Core features are taken care of by the core Winit maintainers. Platform features are not. +When a platform feature is submitted, the submitter is considered the expert in the +feature and may be asked to support the feature should it break in the future. + +Winit ***does not*** directly expose functionality for drawing inside windows or creating native +menus, but ***does*** commit to providing APIs that higher-level crates can use to implement that +functionality. + +## `1.0` and stability + +When all core features are implemented to the satisfaction of the Winit maintainers, Winit 1.0 will +be released and the library will enter maintenance mode. For the most part, new core features will not +be added past this point. New platform features may be accepted and exposed through point releases. + +### Tier upgrades +Some platform features could, in theory, be exposed across multiple platforms, but have not gone +through the implementation work necessary to function on all platforms. When one of these features +gets implemented across all platforms, a PR can be opened to upgrade the feature to a core feature. +If that gets accepted, the platform-specific functions get deprecated and become permanently +exposed through the core, cross-platform API. + +# Features + +## Extending this section + +If your PR makes notable changes to Winit's features, please update this section as follows: + +- If your PR adds a new feature, add a brief description to the relevant section. If the feature is a core + feature, add a row to the feature matrix and describe what platforms the feature has been implemented on. + +- If your PR begins a new API rework, add a row to the `Pending API Reworks` table. If the PR implements the + API rework on all relevant platforms, please move it to the `Completed API Reworks` table. + +- If your PR implements an already-existing feature on a new platform, either mark the feature as *completed*, + or mark it as *mostly completed* and link to an issue describing the problems with the implementation. + +## Core + +### Windowing +- **Window initialization**: Winit allows the creation of a window +- **Providing pointer to init OpenGL**: Winit provides the necessary pointers to initialize a working opengl context +- **Providing pointer to init Vulkan**: Same as OpenGL but for Vulkan +- **Window decorations**: The windows created by winit are properly decorated, and the decorations can + be deactivated +- **Window decorations toggle**: Decorations can be turned on or off after window creation +- **Window resizing**: The windows created by winit can be resized and generate the appropriate events + when they are. The application can precisely control its window size if desired. +- **Window resize increments**: When the window gets resized, the application can choose to snap the window's + size to specific values. +- **Window transparency**: Winit allows the creation of windows with a transparent background. +- **Window maximization**: The windows created by winit can be maximized upon creation. +- **Window maximization toggle**: The windows created by winit can be maximized and unmaximized after + creation. +- **Window minimization**: The windows created by winit can be minimized after creation. +- **Fullscreen**: The windows created by winit can be put into fullscreen mode. +- **Fullscreen toggle**: The windows created by winit can be switched to and from fullscreen after + creation. +- **Exclusive fullscreen**: Winit allows changing the video mode of the monitor + for fullscreen windows and, if applicable, captures the monitor for exclusive + use by this application. +- **HiDPI support**: Winit assists developers in appropriately scaling HiDPI content. +- **Popup / modal windows**: Windows can be created relative to the client area of other windows, and parent + windows can be disabled in favor of popup windows. This feature also guarantees that popup windows + get drawn above their owner. + + +### System Information +- **Monitor list**: Retrieve the list of monitors and their metadata, including which one is primary. +- **Video mode query**: Monitors can be queried for their supported fullscreen video modes (consisting of resolution, refresh rate, and bit depth). + +### Input Handling +- **Mouse events**: Generating mouse events associated with pointer motion, click, and scrolling events. +- **Mouse set location**: Forcibly changing the location of the pointer. +- **Cursor locking**: Locking the cursor inside the window so it cannot move. +- **Cursor confining**: Confining the cursor to the window bounds so it cannot leave them. +- **Cursor icon**: Changing the cursor icon or hiding the cursor. +- **Cursor image**: Changing the cursor to your own image. +- **Cursor hittest**: Handle or ignore mouse events for a window. +- **Touch events**: Single-touch events. +- **Touch pressure**: Touch events contain information about the amount of force being applied. +- **Multitouch**: Multi-touch events, including cancellation of a gesture. +- **Keyboard events**: Properly processing keyboard events using the user-specified keymap and + translating keypresses into UTF-8 characters, handling dead keys and IMEs. +- **Drag & Drop**: Dragging content into winit, detecting when content enters, drops, or if the drop is cancelled. +- **Raw Device Events**: Capturing input from input devices without any OS filtering. +- **Gamepad/Joystick events**: Capturing input from gamepads and joysticks. +- **Device movement events**: Capturing input from the device gyroscope and accelerometer. + +## Platform +### Windows +* Setting the name of the internal window class +* Setting the taskbar icon +* Setting the parent window +* Setting a menu bar +* `WS_EX_NOREDIRECTIONBITMAP` support +* Theme the title bar according to Windows 10 Dark Mode setting or set a preferred theme +* Changing a system-drawn backdrop +* Setting the window border color +* Setting the title bar background color +* Setting the title color +* Setting the corner rounding preference + +### macOS +* Window activation policy +* Window movable by background +* Transparent titlebar +* Hidden titlebar +* Hidden titlebar buttons +* Full-size content view +* Accepts first mouse +* Set a preferred theme and get current theme. + +### Unix +* Window urgency +* X11 Window Class +* X11 Override Redirect Flag +* GTK Theme Variant +* Base window size +* Setting the X11 parent window + +### iOS +* Get the `UIScreen` object pointer +* Setting the `UIView` hidpi factor +* Valid orientations +* Home indicator visibility +* Status bar visibility and style +* Deferring system gestures +* Getting the device idiom +* Getting the preferred video mode + +### Web +* Get if the systems preferred color scheme is "dark" + +## Compatibility Matrix + +Legend: + +- ✔️: Works as intended +- ▢: Mostly works, but some bugs are known +- ❌: Missing feature or large bugs making it unusable +- **N/A**: Not applicable for this platform +- ❓: Unknown status + +### Windowing +|Feature |Windows|MacOS |Linux x11 |Linux Wayland |Android|iOS |Web |Redox OS| +|-------------------------------- | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | ------ | +|Window initialization |✔️ |✔️ |▢[#5] |✔️ |▢[#33]|▢[#33] |✔️ |✔️ | +|Providing pointer to init OpenGL |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ | +|Providing pointer to init Vulkan |✔️ |✔️ |✔️ |✔️ |✔️ |❓ |**N/A**|**N/A** | +|Window decorations |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**|✔️ | +|Window decorations toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**|**N/A** | +|Window resizing |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ |✔️ | +|Window resize increments |✔️ |✔️ |✔️ |❌ |**N/A**|**N/A**|**N/A**|**N/A** | +|Window transparency |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|N/A |✔️ | +|Window blur |❌ |❌ |❌ |✔️ |**N/A**|**N/A**|N/A |❌ | +|Window maximization |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**|**N/A** | +|Window maximization toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**|**N/A** | +|Window minimization |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**|**N/A** | +|Fullscreen |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |✔️ |**N/A** | +|Fullscreen toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |✔️ |**N/A** | +|Exclusive fullscreen |✔️ |✔️ |✔️ |**N/A** |❌ |✔️ |**N/A**|**N/A** | +|HiDPI support |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |❌ | +|Popup windows |❌ |❌ |❌ |❌ |❌ |❌ |**N/A**|**N/A** | + +### System information +|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |Web |Redox OS| +|---------------- | ----- | ---- | ------- | ----------- | ----- | ------- | -------- | ------ | +|Monitor list |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A**|❌ | +|Video mode query |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A**|❌ | + +### Input handling +|Feature |Windows |MacOS |Linux x11|Linux Wayland|Android|iOS |Web |Redox OS| +|----------------------- | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | ------ | +|Mouse events |✔️ |▢[#63] |✔️ |✔️ |**N/A**|**N/A**|✔️ |✔️ | +|Mouse set location |✔️ |✔️ |✔️ |✔️(when locked) |**N/A**|**N/A**|**N/A**|**N/A** | +|Cursor locking |❌ |✔️ |❌ |✔️ |**N/A**|**N/A**|✔️ |❌ | +|Cursor confining |✔️ |❌ |✔️ |✔️ |**N/A**|**N/A**|❌ |❌ | +|Cursor icon |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ |**N/A** | +|Cursor image |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ |**N/A** | +|Cursor hittest |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❌ |❌ | +|Touch events |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A** | +|Touch pressure |✔️ |❌ |❌ |❌ |❌ |✔️ |✔️ |**N/A** | +|Multitouch |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |❌ |**N/A** | +|Keyboard events |✔️ |✔️ |✔️ |✔️ |✔️ |❌ |✔️ |✔️ | +|Drag & Drop |▢[#720] |▢[#720] |▢[#720] |▢[#720] |**N/A**|**N/A**|❓ |**N/A** | +|Raw Device Events |▢[#750] |▢[#750] |▢[#750] |❌ |❌ |❌ |❓ |**N/A** | +|Gamepad/Joystick events |❌[#804] |❌ |❌ |❌ |❌ |❌ |❓ |**N/A** | +|Device movement events |❓ |❓ |❓ |❓ |❌ |❌ |❓ |**N/A** | +|Drag window with cursor |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A** |**N/A** | +|Resize with cursor |✔️ |❌ |✔️ |✔️ |**N/A**|**N/A**|**N/A** |**N/A** | + +### Pending API Reworks +Changes in the API that have been agreed upon but aren't implemented across all platforms. + +|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |Web |Redox OS| +|------------------------------ | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | ------ | +|New API for HiDPI ([#315] [#319]) |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |❓ |❓ | +|Event Loop 2.0 ([#459]) |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |❓ |✔️ | +|Keyboard Input 2.0 ([#753]) |✔️ |✔️ |✔️ |✔️ |✔️ |❌ |✔️ |✔️ | + +### Completed API Reworks +|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |Web |Redox OS| +|------------------------------ | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | ------ | + +[#165]: https://github.com/rust-windowing/winit/issues/165 +[#219]: https://github.com/rust-windowing/winit/issues/219 +[#242]: https://github.com/rust-windowing/winit/issues/242 +[#306]: https://github.com/rust-windowing/winit/issues/306 +[#315]: https://github.com/rust-windowing/winit/issues/315 +[#319]: https://github.com/rust-windowing/winit/issues/319 +[#33]: https://github.com/rust-windowing/winit/issues/33 +[#459]: https://github.com/rust-windowing/winit/issues/459 +[#5]: https://github.com/rust-windowing/winit/issues/5 +[#63]: https://github.com/rust-windowing/winit/issues/63 +[#720]: https://github.com/rust-windowing/winit/issues/720 +[#721]: https://github.com/rust-windowing/winit/issues/721 +[#750]: https://github.com/rust-windowing/winit/issues/750 +[#753]: https://github.com/rust-windowing/winit/issues/753 +[#804]: https://github.com/rust-windowing/winit/issues/804 diff --git a/vendor/winit/LICENSE b/vendor/winit/LICENSE new file mode 100644 index 00000000..ad410e11 --- /dev/null +++ b/vendor/winit/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/vendor/winit/README.md b/vendor/winit/README.md new file mode 100644 index 00000000..ab958874 --- /dev/null +++ b/vendor/winit/README.md @@ -0,0 +1,70 @@ +# winit - Cross-platform window creation and management in Rust + +[![Crates.io](https://img.shields.io/crates/v/winit.svg)](https://crates.io/crates/winit) +[![Docs.rs](https://docs.rs/winit/badge.svg)](https://docs.rs/winit) +[![Master Docs](https://img.shields.io/github/actions/workflow/status/rust-windowing/winit/docs.yml?branch=master&label=master%20docs +)](https://rust-windowing.github.io/winit/winit/index.html) +[![CI Status](https://github.com/rust-windowing/winit/workflows/CI/badge.svg)](https://github.com/rust-windowing/winit/actions) + +```toml +[dependencies] +winit = "0.30.13" +``` + +## [Documentation](https://docs.rs/winit) + +For features _within_ the scope of winit, see [FEATURES.md](FEATURES.md). + +For features _outside_ the scope of winit, see [Are we GUI Yet?](https://areweguiyet.com/) and [Are we game yet?](https://arewegameyet.rs/), depending on what kind of project you're looking to do. + +## Contact Us + +Join us in our [![Matrix](https://img.shields.io/badge/Matrix-%23rust--windowing%3Amatrix.org-blueviolet.svg)](https://matrix.to/#/#rust-windowing:matrix.org) room. + +The maintainers have a meeting every friday at UTC 15. The meeting notes can be found [here](https://hackmd.io/@winit-meetings). + +## Usage + +Winit is a window creation and management library. It can create windows and lets you handle +events (for example: the window being resized, a key being pressed, a mouse movement, etc.) +produced by the window. + +Winit is designed to be a low-level brick in a hierarchy of libraries. Consequently, in order to +show something on the window you need to use the platform-specific getters provided by winit, or +another library. + +## CONTRIBUTING + +For contributing guidelines see [CONTRIBUTING.md](./CONTRIBUTING.md). + +## MSRV Policy + +This crate's Minimum Supported Rust Version (MSRV) is **1.70**. Changes to +the MSRV will be accompanied by a minor version bump. + +As a **tentative** policy, the upper bound of the MSRV is given by the following +formula: + +``` +min(sid, stable - 3) +``` + +Where `sid` is the current version of `rustc` provided by [Debian Sid], and +`stable` is the latest stable version of Rust. This bound may be broken in case of a major ecosystem shift or a security vulnerability. + +[Debian Sid]: https://packages.debian.org/sid/rustc + +The exception is for the Android platform, where a higher Rust version +must be used for certain Android features. In this case, the MSRV will be +capped at the latest stable version of Rust minus three. This inconsistency is +not reflected in Cargo metadata, as it is not powerful enough to expose this +restriction. + +All crates in the [`rust-windowing`] organizations have the +same MSRV policy. + +[`rust-windowing`]: https://github.com/rust-windowing + +### Platform-specific usage + +Check out the [`winit::platform`](https://rust-windowing.github.io/winit/winit/platform/index.html) module for platform-specific usage. diff --git a/vendor/winit/build.rs b/vendor/winit/build.rs new file mode 100644 index 00000000..6a4528b3 --- /dev/null +++ b/vendor/winit/build.rs @@ -0,0 +1,27 @@ +use cfg_aliases::cfg_aliases; + +fn main() { + // The script doesn't depend on our code. + println!("cargo:rerun-if-changed=build.rs"); + + // Setup cfg aliases. + cfg_aliases! { + // Systems. + android_platform: { target_os = "android" }, + web_platform: { all(target_family = "wasm", target_os = "unknown") }, + macos_platform: { target_os = "macos" }, + ios_platform: { target_os = "ios" }, + windows_platform: { target_os = "windows" }, + apple: { any(target_os = "ios", target_os = "macos") }, + free_unix: { all(unix, not(apple), not(android_platform), not(target_os = "emscripten")) }, + redox: { target_os = "redox" }, + + // Native displays. + x11_platform: { all(feature = "x11", free_unix, not(redox)) }, + wayland_platform: { all(feature = "wayland", free_unix, not(redox)) }, + orbital_platform: { redox }, + } + + // Winit defined cfgs. + println!("cargo:rustc-check-cfg=cfg(unreleased_changelogs)"); +} diff --git a/vendor/winit/examples/child_window.rs b/vendor/winit/examples/child_window.rs new file mode 100644 index 00000000..715996eb --- /dev/null +++ b/vendor/winit/examples/child_window.rs @@ -0,0 +1,88 @@ +#[cfg(all(feature = "rwh_06", any(x11_platform, macos_platform, windows_platform)))] +#[allow(deprecated)] +fn main() -> Result<(), impl std::error::Error> { + use std::collections::HashMap; + + use winit::dpi::{LogicalPosition, LogicalSize, Position}; + use winit::event::{ElementState, Event, KeyEvent, WindowEvent}; + use winit::event_loop::{ActiveEventLoop, EventLoop}; + use winit::raw_window_handle::HasRawWindowHandle; + use winit::window::Window; + + #[path = "util/fill.rs"] + mod fill; + + fn spawn_child_window(parent: &Window, event_loop: &ActiveEventLoop) -> Window { + let parent = parent.raw_window_handle().unwrap(); + let mut window_attributes = Window::default_attributes() + .with_title("child window") + .with_inner_size(LogicalSize::new(200.0f32, 200.0f32)) + .with_position(Position::Logical(LogicalPosition::new(0.0, 0.0))) + .with_visible(true); + // `with_parent_window` is unsafe. Parent window must be a valid window. + window_attributes = unsafe { window_attributes.with_parent_window(Some(parent)) }; + + event_loop.create_window(window_attributes).unwrap() + } + + let mut windows = HashMap::new(); + + let event_loop: EventLoop<()> = EventLoop::new().unwrap(); + let mut parent_window_id = None; + + event_loop.run(move |event: Event<()>, event_loop| { + match event { + Event::Resumed => { + let attributes = Window::default_attributes() + .with_title("parent window") + .with_position(Position::Logical(LogicalPosition::new(0.0, 0.0))) + .with_inner_size(LogicalSize::new(640.0f32, 480.0f32)); + let window = event_loop.create_window(attributes).unwrap(); + + parent_window_id = Some(window.id()); + + println!("Parent window id: {parent_window_id:?})"); + windows.insert(window.id(), window); + }, + Event::WindowEvent { window_id, event } => match event { + WindowEvent::CloseRequested => { + windows.clear(); + event_loop.exit(); + }, + WindowEvent::CursorEntered { device_id: _ } => { + // On x11, println when the cursor entered in a window even if the child window + // is created by some key inputs. + // the child windows are always placed at (0, 0) with size (200, 200) in the + // parent window, so we also can see this log when we move + // the cursor around (200, 200) in parent window. + println!("cursor entered in the window {window_id:?}"); + }, + WindowEvent::KeyboardInput { + event: KeyEvent { state: ElementState::Pressed, .. }, + .. + } => { + let parent_window = windows.get(&parent_window_id.unwrap()).unwrap(); + let child_window = spawn_child_window(parent_window, event_loop); + let child_id = child_window.id(); + println!("Child window created with id: {child_id:?}"); + windows.insert(child_id, child_window); + }, + WindowEvent::RedrawRequested => { + if let Some(window) = windows.get(&window_id) { + fill::fill_window(window); + } + }, + _ => (), + }, + _ => (), + } + }) +} + +#[cfg(all(feature = "rwh_06", not(any(x11_platform, macos_platform, windows_platform))))] +fn main() { + panic!( + "This example is supported only on x11, macOS, and Windows, with the `rwh_06` feature \ + enabled." + ); +} diff --git a/vendor/winit/examples/control_flow.rs b/vendor/winit/examples/control_flow.rs new file mode 100644 index 00000000..13cc9477 --- /dev/null +++ b/vendor/winit/examples/control_flow.rs @@ -0,0 +1,148 @@ +#![allow(clippy::single_match)] + +use std::thread; +#[cfg(not(web_platform))] +use std::time; + +use ::tracing::{info, warn}; +#[cfg(web_platform)] +use web_time as time; + +use winit::application::ApplicationHandler; +use winit::event::{ElementState, KeyEvent, StartCause, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; +use winit::keyboard::{Key, NamedKey}; +use winit::window::{Window, WindowId}; + +#[path = "util/fill.rs"] +mod fill; +#[path = "util/tracing.rs"] +mod tracing; + +const WAIT_TIME: time::Duration = time::Duration::from_millis(100); +const POLL_SLEEP_TIME: time::Duration = time::Duration::from_millis(100); + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +enum Mode { + #[default] + Wait, + WaitUntil, + Poll, +} + +fn main() -> Result<(), impl std::error::Error> { + #[cfg(web_platform)] + console_error_panic_hook::set_once(); + + tracing::init(); + + info!("Press '1' to switch to Wait mode."); + info!("Press '2' to switch to WaitUntil mode."); + info!("Press '3' to switch to Poll mode."); + info!("Press 'R' to toggle request_redraw() calls."); + info!("Press 'Esc' to close the window."); + + let event_loop = EventLoop::new().unwrap(); + + let mut app = ControlFlowDemo::default(); + event_loop.run_app(&mut app) +} + +#[derive(Default)] +struct ControlFlowDemo { + mode: Mode, + request_redraw: bool, + wait_cancelled: bool, + close_requested: bool, + window: Option, +} + +impl ApplicationHandler for ControlFlowDemo { + fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) { + info!("new_events: {cause:?}"); + + self.wait_cancelled = match cause { + StartCause::WaitCancelled { .. } => self.mode == Mode::WaitUntil, + _ => false, + } + } + + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window_attributes = Window::default_attributes().with_title( + "Press 1, 2, 3 to change control flow mode. Press R to toggle redraw requests.", + ); + self.window = Some(event_loop.create_window(window_attributes).unwrap()); + } + + fn window_event( + &mut self, + _event_loop: &ActiveEventLoop, + _window_id: WindowId, + event: WindowEvent, + ) { + info!("{event:?}"); + + match event { + WindowEvent::CloseRequested => { + self.close_requested = true; + }, + WindowEvent::KeyboardInput { + event: KeyEvent { logical_key: key, state: ElementState::Pressed, .. }, + .. + } => match key.as_ref() { + // WARNING: Consider using `key_without_modifiers()` if available on your platform. + // See the `key_binding` example + Key::Character("1") => { + self.mode = Mode::Wait; + warn!("mode: {:?}", self.mode); + }, + Key::Character("2") => { + self.mode = Mode::WaitUntil; + warn!("mode: {:?}", self.mode); + }, + Key::Character("3") => { + self.mode = Mode::Poll; + warn!("mode: {:?}", self.mode); + }, + Key::Character("r") => { + self.request_redraw = !self.request_redraw; + warn!("request_redraw: {}", self.request_redraw); + }, + Key::Named(NamedKey::Escape) => { + self.close_requested = true; + }, + _ => (), + }, + WindowEvent::RedrawRequested => { + let window = self.window.as_ref().unwrap(); + window.pre_present_notify(); + fill::fill_window(window); + }, + _ => (), + } + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + if self.request_redraw && !self.wait_cancelled && !self.close_requested { + self.window.as_ref().unwrap().request_redraw(); + } + + match self.mode { + Mode::Wait => event_loop.set_control_flow(ControlFlow::Wait), + Mode::WaitUntil => { + if !self.wait_cancelled { + event_loop + .set_control_flow(ControlFlow::WaitUntil(time::Instant::now() + WAIT_TIME)); + } + }, + Mode::Poll => { + thread::sleep(POLL_SLEEP_TIME); + event_loop.set_control_flow(ControlFlow::Poll); + }, + }; + + if self.close_requested { + event_loop.exit(); + } + } +} diff --git a/vendor/winit/examples/pump_events.rs b/vendor/winit/examples/pump_events.rs new file mode 100644 index 00000000..ad198cfa --- /dev/null +++ b/vendor/winit/examples/pump_events.rs @@ -0,0 +1,80 @@ +#![allow(clippy::single_match)] + +// Limit this example to only compatible platforms. +#[cfg(any(windows_platform, macos_platform, x11_platform, wayland_platform, android_platform,))] +fn main() -> std::process::ExitCode { + use std::process::ExitCode; + use std::thread::sleep; + use std::time::Duration; + + use winit::application::ApplicationHandler; + use winit::event::WindowEvent; + use winit::event_loop::{ActiveEventLoop, EventLoop}; + use winit::platform::pump_events::{EventLoopExtPumpEvents, PumpStatus}; + use winit::window::{Window, WindowId}; + + #[path = "util/fill.rs"] + mod fill; + + #[derive(Default)] + struct PumpDemo { + window: Option, + } + + impl ApplicationHandler for PumpDemo { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window_attributes = Window::default_attributes().with_title("A fantastic window!"); + self.window = Some(event_loop.create_window(window_attributes).unwrap()); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + _window_id: WindowId, + event: WindowEvent, + ) { + println!("{event:?}"); + + let window = match self.window.as_ref() { + Some(window) => window, + None => return, + }; + + match event { + WindowEvent::CloseRequested => event_loop.exit(), + WindowEvent::RedrawRequested => { + fill::fill_window(window); + window.request_redraw(); + }, + _ => (), + } + } + } + + let mut event_loop = EventLoop::new().unwrap(); + + tracing_subscriber::fmt::init(); + + let mut app = PumpDemo::default(); + + loop { + let timeout = Some(Duration::ZERO); + let status = event_loop.pump_app_events(timeout, &mut app); + + if let PumpStatus::Exit(exit_code) = status { + break ExitCode::from(exit_code as u8); + } + + // Sleep for 1/60 second to simulate application work + // + // Since `pump_events` doesn't block it will be important to + // throttle the loop in the app somehow. + println!("Update()"); + sleep(Duration::from_millis(16)); + } +} + +#[cfg(any(ios_platform, web_platform, orbital_platform))] +fn main() { + println!("This platform doesn't support pump_events."); +} diff --git a/vendor/winit/examples/run_on_demand.rs b/vendor/winit/examples/run_on_demand.rs new file mode 100644 index 00000000..5a277de1 --- /dev/null +++ b/vendor/winit/examples/run_on_demand.rs @@ -0,0 +1,99 @@ +#![allow(clippy::single_match)] + +// Limit this example to only compatible platforms. +#[cfg(any(windows_platform, macos_platform, x11_platform, wayland_platform,))] +fn main() -> Result<(), Box> { + use std::time::Duration; + + use winit::application::ApplicationHandler; + use winit::event::WindowEvent; + use winit::event_loop::{ActiveEventLoop, EventLoop}; + use winit::platform::run_on_demand::EventLoopExtRunOnDemand; + use winit::window::{Window, WindowId}; + + #[path = "util/fill.rs"] + mod fill; + + #[derive(Default)] + struct App { + idx: usize, + window_id: Option, + window: Option, + } + + impl ApplicationHandler for App { + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + if let Some(window) = self.window.as_ref() { + window.request_redraw(); + } + } + + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window_attributes = Window::default_attributes() + .with_title("Fantastic window number one!") + .with_inner_size(winit::dpi::LogicalSize::new(128.0, 128.0)); + let window = event_loop.create_window(window_attributes).unwrap(); + self.window_id = Some(window.id()); + self.window = Some(window); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + if event == WindowEvent::Destroyed && self.window_id == Some(window_id) { + println!( + "--------------------------------------------------------- Window {} Destroyed", + self.idx + ); + self.window_id = None; + event_loop.exit(); + return; + } + + let window = match self.window.as_mut() { + Some(window) => window, + None => return, + }; + + match event { + WindowEvent::CloseRequested => { + println!( + "--------------------------------------------------------- Window {} \ + CloseRequested", + self.idx + ); + fill::cleanup_window(window); + self.window = None; + }, + WindowEvent::RedrawRequested => { + fill::fill_window(window); + }, + _ => (), + } + } + } + + tracing_subscriber::fmt::init(); + + let mut event_loop = EventLoop::new().unwrap(); + + let mut app = App { idx: 1, ..Default::default() }; + event_loop.run_app_on_demand(&mut app)?; + + println!("--------------------------------------------------------- Finished first loop"); + println!("--------------------------------------------------------- Waiting 5 seconds"); + std::thread::sleep(Duration::from_secs(5)); + + app.idx += 1; + event_loop.run_app_on_demand(&mut app)?; + println!("--------------------------------------------------------- Finished second loop"); + Ok(()) +} + +#[cfg(not(any(windows_platform, macos_platform, x11_platform, wayland_platform,)))] +fn main() { + println!("This example is not supported on this platform"); +} diff --git a/vendor/winit/examples/util/fill.rs b/vendor/winit/examples/util/fill.rs new file mode 100644 index 00000000..31540c02 --- /dev/null +++ b/vendor/winit/examples/util/fill.rs @@ -0,0 +1,117 @@ +//! Fill the window buffer with a solid color. +//! +//! Launching a window without drawing to it has unpredictable results varying from platform to +//! platform. In order to have well-defined examples, this module provides an easy way to +//! fill the window buffer with a solid color. +//! +//! The `softbuffer` crate is used, largely because of its ease of use. `glutin` or `wgpu` could +//! also be used to fill the window buffer, but they are more complicated to use. + +#[allow(unused_imports)] +pub use platform::cleanup_window; +pub use platform::fill_window; + +#[cfg(all(feature = "rwh_05", not(any(target_os = "android", target_os = "ios"))))] +mod platform { + use std::cell::RefCell; + use std::collections::HashMap; + use std::mem; + use std::mem::ManuallyDrop; + use std::num::NonZeroU32; + + use softbuffer::{Context, Surface}; + use winit::window::{Window, WindowId}; + + thread_local! { + // NOTE: You should never do things like that, create context and drop it before + // you drop the event loop. We do this for brevity to not blow up examples. We use + // ManuallyDrop to prevent destructors from running. + // + // A static, thread-local map of graphics contexts to open windows. + static GC: ManuallyDrop>> = const { ManuallyDrop::new(RefCell::new(None)) }; + } + + /// The graphics context used to draw to a window. + struct GraphicsContext { + /// The global softbuffer context. + context: RefCell>, + + /// The hash map of window IDs to surfaces. + surfaces: HashMap>, + } + + impl GraphicsContext { + fn new(w: &Window) -> Self { + Self { + context: RefCell::new( + Context::new(unsafe { mem::transmute::<&'_ Window, &'static Window>(w) }) + .expect("Failed to create a softbuffer context"), + ), + surfaces: HashMap::new(), + } + } + + fn create_surface( + &mut self, + window: &Window, + ) -> &mut Surface<&'static Window, &'static Window> { + self.surfaces.entry(window.id()).or_insert_with(|| { + Surface::new(&self.context.borrow(), unsafe { + mem::transmute::<&'_ Window, &'static Window>(window) + }) + .expect("Failed to create a softbuffer surface") + }) + } + + fn destroy_surface(&mut self, window: &Window) { + self.surfaces.remove(&window.id()); + } + } + + pub fn fill_window(window: &Window) { + GC.with(|gc| { + let size = window.inner_size(); + let (Some(width), Some(height)) = + (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) + else { + return; + }; + + // Either get the last context used or create a new one. + let mut gc = gc.borrow_mut(); + let surface = + gc.get_or_insert_with(|| GraphicsContext::new(window)).create_surface(window); + + // Fill a buffer with a solid color. + const DARK_GRAY: u32 = 0xff181818; + + surface.resize(width, height).expect("Failed to resize the softbuffer surface"); + + let mut buffer = surface.buffer_mut().expect("Failed to get the softbuffer buffer"); + buffer.fill(DARK_GRAY); + buffer.present().expect("Failed to present the softbuffer buffer"); + }) + } + + #[allow(dead_code)] + pub fn cleanup_window(window: &Window) { + GC.with(|gc| { + let mut gc = gc.borrow_mut(); + if let Some(context) = gc.as_mut() { + context.destroy_surface(window); + } + }); + } +} + +#[cfg(not(all(feature = "rwh_05", not(any(target_os = "android", target_os = "ios")))))] +mod platform { + pub fn fill_window(_window: &winit::window::Window) { + // No-op on mobile platforms. + } + + #[allow(dead_code)] + pub fn cleanup_window(_window: &winit::window::Window) { + // No-op on mobile platforms. + } +} diff --git a/vendor/winit/examples/util/tracing.rs b/vendor/winit/examples/util/tracing.rs new file mode 100644 index 00000000..bab7ced7 --- /dev/null +++ b/vendor/winit/examples/util/tracing.rs @@ -0,0 +1,25 @@ +#[cfg(not(web_platform))] +pub fn init() { + use tracing_subscriber::filter::{EnvFilter, LevelFilter}; + + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::builder().with_default_directive(LevelFilter::INFO.into()).from_env_lossy(), + ) + .init(); +} + +#[cfg(web_platform)] +pub fn init() { + use tracing_subscriber::layer::SubscriberExt; + use tracing_subscriber::util::SubscriberInitExt; + + tracing_subscriber::registry() + .with( + tracing_subscriber::fmt::layer() + .with_ansi(false) + .without_time() + .with_writer(tracing_web::MakeWebConsoleWriter::new()), + ) + .init(); +} diff --git a/vendor/winit/examples/window.rs b/vendor/winit/examples/window.rs new file mode 100644 index 00000000..48afadf9 --- /dev/null +++ b/vendor/winit/examples/window.rs @@ -0,0 +1,1110 @@ +//! Simple winit application. + +use std::collections::HashMap; +use std::error::Error; +use std::fmt::Debug; +#[cfg(not(any(android_platform, ios_platform)))] +use std::num::NonZeroU32; +use std::sync::Arc; +use std::{fmt, mem}; + +use ::tracing::{error, info}; +use cursor_icon::CursorIcon; +#[cfg(not(any(android_platform, ios_platform)))] +use rwh_06::{DisplayHandle, HasDisplayHandle}; +#[cfg(not(any(android_platform, ios_platform)))] +use softbuffer::{Context, Surface}; + +use winit::application::ApplicationHandler; +use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize}; +use winit::event::{DeviceEvent, DeviceId, Ime, MouseButton, MouseScrollDelta, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, EventLoop}; +use winit::keyboard::{Key, ModifiersState}; +use winit::window::{ + Cursor, CursorGrabMode, CustomCursor, CustomCursorSource, Fullscreen, Icon, ResizeDirection, + Theme, Window, WindowId, +}; + +#[cfg(macos_platform)] +use winit::platform::macos::{OptionAsAlt, WindowAttributesExtMacOS, WindowExtMacOS}; +#[cfg(any(x11_platform, wayland_platform))] +use winit::platform::startup_notify::{ + self, EventLoopExtStartupNotify, WindowAttributesExtStartupNotify, WindowExtStartupNotify, +}; +#[cfg(x11_platform)] +use winit::platform::x11::WindowAttributesExtX11; + +#[path = "util/tracing.rs"] +mod tracing; + +/// The amount of points to around the window for drag resize direction calculations. +const BORDER_SIZE: f64 = 20.; + +fn main() -> Result<(), Box> { + #[cfg(web_platform)] + console_error_panic_hook::set_once(); + + tracing::init(); + + let event_loop = EventLoop::::with_user_event().build()?; + let _event_loop_proxy = event_loop.create_proxy(); + + // Wire the user event from another thread. + #[cfg(not(web_platform))] + std::thread::spawn(move || { + // Wake up the `event_loop` once every second and dispatch a custom event + // from a different thread. + info!("Starting to send user event every second"); + loop { + let _ = _event_loop_proxy.send_event(UserEvent::WakeUp); + std::thread::sleep(std::time::Duration::from_secs(1)); + } + }); + + let mut state = Application::new(&event_loop); + + event_loop.run_app(&mut state).map_err(Into::into) +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy)] +enum UserEvent { + WakeUp, +} + +/// Application state and event handling. +struct Application { + /// Custom cursors assets. + custom_cursors: Vec, + /// Application icon. + icon: Icon, + windows: HashMap, + /// Drawing context. + /// + /// With OpenGL it could be EGLDisplay. + #[cfg(not(any(android_platform, ios_platform)))] + context: Option>>, +} + +impl Application { + fn new(event_loop: &EventLoop) -> Self { + // SAFETY: we drop the context right before the event loop is stopped, thus making it safe. + #[cfg(not(any(android_platform, ios_platform)))] + let context = Some( + Context::new(unsafe { + std::mem::transmute::, DisplayHandle<'static>>( + event_loop.display_handle().unwrap(), + ) + }) + .unwrap(), + ); + + // You'll have to choose an icon size at your own discretion. On X11, the desired size + // varies by WM, and on Windows, you still have to account for screen scaling. Here + // we use 32px, since it seems to work well enough in most cases. Be careful about + // going too high, or you'll be bitten by the low-quality downscaling built into the + // WM. + let icon = load_icon(include_bytes!("data/icon.png")); + + info!("Loading cursor assets"); + let custom_cursors = vec![ + event_loop.create_custom_cursor(decode_cursor(include_bytes!("data/cross.png"))), + event_loop.create_custom_cursor(decode_cursor(include_bytes!("data/cross2.png"))), + event_loop.create_custom_cursor(decode_cursor(include_bytes!("data/gradient.png"))), + ]; + + Self { + #[cfg(not(any(android_platform, ios_platform)))] + context, + custom_cursors, + icon, + windows: Default::default(), + } + } + + fn create_window( + &mut self, + event_loop: &ActiveEventLoop, + _tab_id: Option, + ) -> Result> { + // TODO read-out activation token. + + #[allow(unused_mut)] + let mut window_attributes = Window::default_attributes() + .with_title("Winit window") + .with_transparent(true) + .with_window_icon(Some(self.icon.clone())); + + #[cfg(any(x11_platform, wayland_platform))] + if let Some(token) = event_loop.read_token_from_env() { + startup_notify::reset_activation_token_env(); + info!("Using token {:?} to activate a window", token); + window_attributes = window_attributes.with_activation_token(token); + } + + #[cfg(x11_platform)] + match std::env::var("X11_VISUAL_ID") { + Ok(visual_id_str) => { + info!("Using X11 visual id {visual_id_str}"); + let visual_id = visual_id_str.parse()?; + window_attributes = window_attributes.with_x11_visual(visual_id); + }, + Err(_) => info!("Set the X11_VISUAL_ID env variable to request specific X11 visual"), + } + + #[cfg(x11_platform)] + match std::env::var("X11_SCREEN_ID") { + Ok(screen_id_str) => { + info!("Placing the window on X11 screen {screen_id_str}"); + let screen_id = screen_id_str.parse()?; + window_attributes = window_attributes.with_x11_screen(screen_id); + }, + Err(_) => info!( + "Set the X11_SCREEN_ID env variable to place the window on non-default screen" + ), + } + + #[cfg(macos_platform)] + if let Some(tab_id) = _tab_id { + window_attributes = window_attributes.with_tabbing_identifier(&tab_id); + } + + #[cfg(web_platform)] + { + use winit::platform::web::WindowAttributesExtWebSys; + window_attributes = window_attributes.with_append(true); + } + + let window = event_loop.create_window(window_attributes)?; + + #[cfg(ios_platform)] + { + use winit::platform::ios::WindowExtIOS; + window.recognize_doubletap_gesture(true); + window.recognize_pinch_gesture(true); + window.recognize_rotation_gesture(true); + window.recognize_pan_gesture(true, 2, 2); + } + + let window_state = WindowState::new(self, window)?; + let window_id = window_state.window.id(); + info!("Created new window with id={window_id:?}"); + self.windows.insert(window_id, window_state); + Ok(window_id) + } + + fn handle_action(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, action: Action) { + // let cursor_position = self.cursor_position; + let window = self.windows.get_mut(&window_id).unwrap(); + info!("Executing action: {action:?}"); + match action { + Action::CloseWindow => { + let _ = self.windows.remove(&window_id); + }, + Action::CreateNewWindow => { + #[cfg(any(x11_platform, wayland_platform))] + if let Err(err) = window.window.request_activation_token() { + info!("Failed to get activation token: {err}"); + } else { + return; + } + + if let Err(err) = self.create_window(event_loop, None) { + error!("Error creating new window: {err}"); + } + }, + Action::ToggleResizeIncrements => window.toggle_resize_increments(), + Action::ToggleCursorVisibility => window.toggle_cursor_visibility(), + Action::ToggleResizable => window.toggle_resizable(), + Action::ToggleDecorations => window.toggle_decorations(), + Action::ToggleFullscreen => window.toggle_fullscreen(), + Action::ToggleMaximize => window.toggle_maximize(), + Action::ToggleImeInput => window.toggle_ime(), + Action::Minimize => window.minimize(), + Action::NextCursor => window.next_cursor(), + Action::NextCustomCursor => window.next_custom_cursor(&self.custom_cursors), + #[cfg(web_platform)] + Action::UrlCustomCursor => window.url_custom_cursor(event_loop), + #[cfg(web_platform)] + Action::AnimationCustomCursor => { + window.animation_custom_cursor(event_loop, &self.custom_cursors) + }, + Action::CycleCursorGrab => window.cycle_cursor_grab(), + Action::DragWindow => window.drag_window(), + Action::DragResizeWindow => window.drag_resize_window(), + Action::ShowWindowMenu => window.show_menu(), + Action::PrintHelp => self.print_help(), + #[cfg(macos_platform)] + Action::CycleOptionAsAlt => window.cycle_option_as_alt(), + Action::SetTheme(theme) => { + window.window.set_theme(theme); + // Get the resulting current theme to draw with + let actual_theme = theme.or_else(|| window.window.theme()).unwrap_or(Theme::Dark); + window.set_draw_theme(actual_theme); + }, + #[cfg(macos_platform)] + Action::CreateNewTab => { + let tab_id = window.window.tabbing_identifier(); + if let Err(err) = self.create_window(event_loop, Some(tab_id)) { + error!("Error creating new window: {err}"); + } + }, + Action::RequestResize => window.swap_dimensions(), + } + } + + fn dump_monitors(&self, event_loop: &ActiveEventLoop) { + info!("Monitors information"); + let primary_monitor = event_loop.primary_monitor(); + for monitor in event_loop.available_monitors() { + let intro = if primary_monitor.as_ref() == Some(&monitor) { + "Primary monitor" + } else { + "Monitor" + }; + + if let Some(name) = monitor.name() { + info!("{intro}: {name}"); + } else { + info!("{intro}: [no name]"); + } + + let PhysicalSize { width, height } = monitor.size(); + info!( + " Current mode: {width}x{height}{}", + if let Some(m_hz) = monitor.refresh_rate_millihertz() { + format!(" @ {}.{} Hz", m_hz / 1000, m_hz % 1000) + } else { + String::new() + } + ); + + let PhysicalPosition { x, y } = monitor.position(); + info!(" Position: {x},{y}"); + + info!(" Scale factor: {}", monitor.scale_factor()); + + info!(" Available modes (width x height x bit-depth):"); + for mode in monitor.video_modes() { + let PhysicalSize { width, height } = mode.size(); + let bits = mode.bit_depth(); + let m_hz = mode.refresh_rate_millihertz(); + info!(" {width}x{height}x{bits} @ {}.{} Hz", m_hz / 1000, m_hz % 1000); + } + } + } + + /// Process the key binding. + fn process_key_binding(key: &str, mods: &ModifiersState) -> Option { + KEY_BINDINGS + .iter() + .find_map(|binding| binding.is_triggered_by(&key, mods).then_some(binding.action)) + } + + /// Process mouse binding. + fn process_mouse_binding(button: MouseButton, mods: &ModifiersState) -> Option { + MOUSE_BINDINGS + .iter() + .find_map(|binding| binding.is_triggered_by(&button, mods).then_some(binding.action)) + } + + fn print_help(&self) { + info!("Keyboard bindings:"); + for binding in KEY_BINDINGS { + info!( + "{}{:<10} - {} ({})", + modifiers_to_string(binding.mods), + binding.trigger, + binding.action, + binding.action.help(), + ); + } + info!("Mouse bindings:"); + for binding in MOUSE_BINDINGS { + info!( + "{}{:<10} - {} ({})", + modifiers_to_string(binding.mods), + mouse_button_to_string(binding.trigger), + binding.action, + binding.action.help(), + ); + } + } +} + +impl ApplicationHandler for Application { + fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) { + info!("User event: {event:?}"); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + let window = match self.windows.get_mut(&window_id) { + Some(window) => window, + None => return, + }; + + match event { + WindowEvent::Resized(size) => { + window.resize(size); + }, + WindowEvent::Focused(focused) => { + if focused { + info!("Window={window_id:?} focused"); + } else { + info!("Window={window_id:?} unfocused"); + } + }, + WindowEvent::ScaleFactorChanged { scale_factor, .. } => { + info!("Window={window_id:?} changed scale to {scale_factor}"); + }, + WindowEvent::ThemeChanged(theme) => { + info!("Theme changed to {theme:?}"); + window.set_draw_theme(theme); + }, + WindowEvent::RedrawRequested => { + if let Err(err) = window.draw() { + error!("Error drawing window: {err}"); + } + }, + WindowEvent::Occluded(occluded) => { + window.set_occluded(occluded); + }, + WindowEvent::CloseRequested => { + info!("Closing Window={window_id:?}"); + self.windows.remove(&window_id); + }, + WindowEvent::ModifiersChanged(modifiers) => { + window.modifiers = modifiers.state(); + info!("Modifiers changed to {:?}", window.modifiers); + }, + WindowEvent::MouseWheel { delta, .. } => match delta { + MouseScrollDelta::LineDelta(x, y) => { + info!("Mouse wheel Line Delta: ({x},{y})"); + }, + MouseScrollDelta::PixelDelta(px) => { + info!("Mouse wheel Pixel Delta: ({},{})", px.x, px.y); + }, + }, + WindowEvent::KeyboardInput { event, is_synthetic: false, .. } => { + let mods = window.modifiers; + + // Dispatch actions only on press. + if event.state.is_pressed() { + let action = if let Key::Character(ch) = event.logical_key.as_ref() { + Self::process_key_binding(&ch.to_uppercase(), &mods) + } else { + None + }; + + if let Some(action) = action { + self.handle_action(event_loop, window_id, action); + } + } + }, + WindowEvent::MouseInput { button, state, .. } => { + let mods = window.modifiers; + if let Some(action) = + state.is_pressed().then(|| Self::process_mouse_binding(button, &mods)).flatten() + { + self.handle_action(event_loop, window_id, action); + } + }, + WindowEvent::CursorLeft { .. } => { + info!("Cursor left Window={window_id:?}"); + window.cursor_left(); + }, + WindowEvent::CursorMoved { position, .. } => { + info!("Moved cursor to {position:?}"); + window.cursor_moved(position); + }, + WindowEvent::ActivationTokenDone { token: _token, .. } => { + #[cfg(any(x11_platform, wayland_platform))] + { + startup_notify::set_activation_token_env(_token); + if let Err(err) = self.create_window(event_loop, None) { + error!("Error creating new window: {err}"); + } + } + }, + WindowEvent::Ime(event) => match event { + Ime::Enabled => info!("IME enabled for Window={window_id:?}"), + Ime::Preedit(text, caret_pos) => { + info!("Preedit: {}, with caret at {:?}", text, caret_pos); + }, + Ime::Commit(text) => { + info!("Committed: {}", text); + }, + Ime::Disabled => info!("IME disabled for Window={window_id:?}"), + }, + WindowEvent::PinchGesture { delta, .. } => { + window.zoom += delta; + let zoom = window.zoom; + if delta > 0.0 { + info!("Zoomed in {delta:.5} (now: {zoom:.5})"); + } else { + info!("Zoomed out {delta:.5} (now: {zoom:.5})"); + } + }, + WindowEvent::RotationGesture { delta, .. } => { + window.rotated += delta; + let rotated = window.rotated; + if delta > 0.0 { + info!("Rotated counterclockwise {delta:.5} (now: {rotated:.5})"); + } else { + info!("Rotated clockwise {delta:.5} (now: {rotated:.5})"); + } + }, + WindowEvent::PanGesture { delta, phase, .. } => { + window.panned.x += delta.x; + window.panned.y += delta.y; + info!("Panned ({delta:?})) (now: {:?}), {phase:?}", window.panned); + }, + WindowEvent::DoubleTapGesture { .. } => { + info!("Smart zoom"); + }, + WindowEvent::TouchpadPressure { .. } + | WindowEvent::HoveredFileCancelled + | WindowEvent::KeyboardInput { .. } + | WindowEvent::CursorEntered { .. } + | WindowEvent::AxisMotion { .. } + | WindowEvent::DroppedFile(_) + | WindowEvent::HoveredFile(_) + | WindowEvent::Destroyed + | WindowEvent::Touch(_) + | WindowEvent::Moved(_) => (), + } + } + + fn device_event( + &mut self, + _event_loop: &ActiveEventLoop, + device_id: DeviceId, + event: DeviceEvent, + ) { + info!("Device {device_id:?} event: {event:?}"); + } + + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + info!("Resumed the event loop"); + self.dump_monitors(event_loop); + + // Create initial window. + self.create_window(event_loop, None).expect("failed to create initial window"); + + self.print_help(); + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + if self.windows.is_empty() { + info!("No windows left, exiting..."); + event_loop.exit(); + } + } + + #[cfg(not(any(android_platform, ios_platform)))] + fn exiting(&mut self, _event_loop: &ActiveEventLoop) { + // We must drop the context here. + self.context = None; + } +} + +/// State of the window. +struct WindowState { + /// IME input. + ime: bool, + /// Render surface. + /// + /// NOTE: This surface must be dropped before the `Window`. + #[cfg(not(any(android_platform, ios_platform)))] + surface: Surface, Arc>, + /// The actual winit Window. + window: Arc, + /// The window theme we're drawing with. + theme: Theme, + /// Cursor position over the window. + cursor_position: Option>, + /// Window modifiers state. + modifiers: ModifiersState, + /// Occlusion state of the window. + occluded: bool, + /// Current cursor grab mode. + cursor_grab: CursorGrabMode, + /// The amount of zoom into window. + zoom: f64, + /// The amount of rotation of the window. + rotated: f32, + /// The amount of pan of the window. + panned: PhysicalPosition, + + #[cfg(macos_platform)] + option_as_alt: OptionAsAlt, + + // Cursor states. + named_idx: usize, + custom_idx: usize, + cursor_hidden: bool, +} + +impl WindowState { + fn new(app: &Application, window: Window) -> Result> { + let window = Arc::new(window); + + // SAFETY: the surface is dropped before the `window` which provided it with handle, thus + // it doesn't outlive it. + #[cfg(not(any(android_platform, ios_platform)))] + let surface = Surface::new(app.context.as_ref().unwrap(), Arc::clone(&window))?; + + let theme = window.theme().unwrap_or(Theme::Dark); + info!("Theme: {theme:?}"); + let named_idx = 0; + window.set_cursor(CURSORS[named_idx]); + + // Allow IME out of the box. + let ime = true; + window.set_ime_allowed(ime); + + let size = window.inner_size(); + let mut state = Self { + #[cfg(macos_platform)] + option_as_alt: window.option_as_alt(), + custom_idx: app.custom_cursors.len() - 1, + cursor_grab: CursorGrabMode::None, + named_idx, + #[cfg(not(any(android_platform, ios_platform)))] + surface, + window, + theme, + ime, + cursor_position: Default::default(), + cursor_hidden: Default::default(), + modifiers: Default::default(), + occluded: Default::default(), + rotated: Default::default(), + panned: Default::default(), + zoom: Default::default(), + }; + + state.resize(size); + Ok(state) + } + + pub fn toggle_ime(&mut self) { + self.ime = !self.ime; + self.window.set_ime_allowed(self.ime); + if let Some(position) = self.ime.then_some(self.cursor_position).flatten() { + self.window.set_ime_cursor_area(position, PhysicalSize::new(20, 20)); + } + } + + pub fn minimize(&mut self) { + self.window.set_minimized(true); + } + + pub fn cursor_moved(&mut self, position: PhysicalPosition) { + self.cursor_position = Some(position); + if self.ime { + self.window.set_ime_cursor_area(position, PhysicalSize::new(20, 20)); + } + } + + pub fn cursor_left(&mut self) { + self.cursor_position = None; + } + + /// Toggle maximized. + fn toggle_maximize(&self) { + let maximized = self.window.is_maximized(); + self.window.set_maximized(!maximized); + } + + /// Toggle window decorations. + fn toggle_decorations(&self) { + let decorated = self.window.is_decorated(); + self.window.set_decorations(!decorated); + } + + /// Toggle window resizable state. + fn toggle_resizable(&self) { + let resizable = self.window.is_resizable(); + self.window.set_resizable(!resizable); + } + + /// Toggle cursor visibility + fn toggle_cursor_visibility(&mut self) { + self.cursor_hidden = !self.cursor_hidden; + self.window.set_cursor_visible(!self.cursor_hidden); + } + + /// Toggle resize increments on a window. + fn toggle_resize_increments(&mut self) { + let new_increments = match self.window.resize_increments() { + Some(_) => None, + None => Some(LogicalSize::new(25.0, 25.0)), + }; + info!("Had increments: {}", new_increments.is_none()); + self.window.set_resize_increments(new_increments); + } + + /// Toggle fullscreen. + fn toggle_fullscreen(&self) { + let fullscreen = if self.window.fullscreen().is_some() { + None + } else { + Some(Fullscreen::Borderless(None)) + }; + + self.window.set_fullscreen(fullscreen); + } + + /// Cycle through the grab modes ignoring errors. + fn cycle_cursor_grab(&mut self) { + self.cursor_grab = match self.cursor_grab { + CursorGrabMode::None => CursorGrabMode::Confined, + CursorGrabMode::Confined => CursorGrabMode::Locked, + CursorGrabMode::Locked => CursorGrabMode::None, + }; + info!("Changing cursor grab mode to {:?}", self.cursor_grab); + if let Err(err) = self.window.set_cursor_grab(self.cursor_grab) { + error!("Error setting cursor grab: {err}"); + } + } + + #[cfg(macos_platform)] + fn cycle_option_as_alt(&mut self) { + self.option_as_alt = match self.option_as_alt { + OptionAsAlt::None => OptionAsAlt::OnlyLeft, + OptionAsAlt::OnlyLeft => OptionAsAlt::OnlyRight, + OptionAsAlt::OnlyRight => OptionAsAlt::Both, + OptionAsAlt::Both => OptionAsAlt::None, + }; + info!("Setting option as alt {:?}", self.option_as_alt); + self.window.set_option_as_alt(self.option_as_alt); + } + + /// Swap the window dimensions with `request_inner_size`. + fn swap_dimensions(&mut self) { + let old_inner_size = self.window.inner_size(); + let mut inner_size = old_inner_size; + + mem::swap(&mut inner_size.width, &mut inner_size.height); + info!("Requesting resize from {old_inner_size:?} to {inner_size:?}"); + + if let Some(new_inner_size) = self.window.request_inner_size(inner_size) { + if old_inner_size == new_inner_size { + info!("Inner size change got ignored"); + } else { + self.resize(new_inner_size); + } + } else { + info!("Request inner size is asynchronous"); + } + } + + /// Pick the next cursor. + fn next_cursor(&mut self) { + self.named_idx = (self.named_idx + 1) % CURSORS.len(); + info!("Setting cursor to \"{:?}\"", CURSORS[self.named_idx]); + self.window.set_cursor(Cursor::Icon(CURSORS[self.named_idx])); + } + + /// Pick the next custom cursor. + fn next_custom_cursor(&mut self, custom_cursors: &[CustomCursor]) { + self.custom_idx = (self.custom_idx + 1) % custom_cursors.len(); + let cursor = Cursor::Custom(custom_cursors[self.custom_idx].clone()); + self.window.set_cursor(cursor); + } + + /// Custom cursor from an URL. + #[cfg(web_platform)] + fn url_custom_cursor(&mut self, event_loop: &ActiveEventLoop) { + let cursor = event_loop.create_custom_cursor(url_custom_cursor()); + + self.window.set_cursor(cursor); + } + + /// Custom cursor from a URL. + #[cfg(web_platform)] + fn animation_custom_cursor( + &mut self, + event_loop: &ActiveEventLoop, + custom_cursors: &[CustomCursor], + ) { + use std::time::Duration; + use winit::platform::web::CustomCursorExtWebSys; + + let cursors = vec![ + custom_cursors[0].clone(), + custom_cursors[1].clone(), + event_loop.create_custom_cursor(url_custom_cursor()), + ]; + let cursor = CustomCursor::from_animation(Duration::from_secs(3), cursors).unwrap(); + let cursor = event_loop.create_custom_cursor(cursor); + + self.window.set_cursor(cursor); + } + + /// Resize the window to the new size. + fn resize(&mut self, size: PhysicalSize) { + info!("Resized to {size:?}"); + #[cfg(not(any(android_platform, ios_platform)))] + { + let (width, height) = match (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) + { + (Some(width), Some(height)) => (width, height), + _ => return, + }; + self.surface.resize(width, height).expect("failed to resize inner buffer"); + } + self.window.request_redraw(); + } + + /// Change the theme that things are drawn in. + fn set_draw_theme(&mut self, theme: Theme) { + self.theme = theme; + self.window.request_redraw(); + } + + /// Show window menu. + fn show_menu(&self) { + if let Some(position) = self.cursor_position { + self.window.show_window_menu(position); + } + } + + /// Drag the window. + fn drag_window(&self) { + if let Err(err) = self.window.drag_window() { + info!("Error starting window drag: {err}"); + } else { + info!("Dragging window Window={:?}", self.window.id()); + } + } + + /// Drag-resize the window. + fn drag_resize_window(&self) { + let position = match self.cursor_position { + Some(position) => position, + None => { + info!("Drag-resize requires cursor to be inside the window"); + return; + }, + }; + + let win_size = self.window.inner_size(); + let border_size = BORDER_SIZE * self.window.scale_factor(); + + let x_direction = if position.x < border_size { + ResizeDirection::West + } else if position.x > (win_size.width as f64 - border_size) { + ResizeDirection::East + } else { + // Use arbitrary direction instead of None for simplicity. + ResizeDirection::SouthEast + }; + + let y_direction = if position.y < border_size { + ResizeDirection::North + } else if position.y > (win_size.height as f64 - border_size) { + ResizeDirection::South + } else { + // Use arbitrary direction instead of None for simplicity. + ResizeDirection::SouthEast + }; + + let direction = match (x_direction, y_direction) { + (ResizeDirection::West, ResizeDirection::North) => ResizeDirection::NorthWest, + (ResizeDirection::West, ResizeDirection::South) => ResizeDirection::SouthWest, + (ResizeDirection::West, _) => ResizeDirection::West, + (ResizeDirection::East, ResizeDirection::North) => ResizeDirection::NorthEast, + (ResizeDirection::East, ResizeDirection::South) => ResizeDirection::SouthEast, + (ResizeDirection::East, _) => ResizeDirection::East, + (_, ResizeDirection::South) => ResizeDirection::South, + (_, ResizeDirection::North) => ResizeDirection::North, + _ => return, + }; + + if let Err(err) = self.window.drag_resize_window(direction) { + info!("Error starting window drag-resize: {err}"); + } else { + info!("Drag-resizing window Window={:?}", self.window.id()); + } + } + + /// Change window occlusion state. + fn set_occluded(&mut self, occluded: bool) { + self.occluded = occluded; + if !occluded { + self.window.request_redraw(); + } + } + + /// Draw the window contents. + #[cfg(not(any(android_platform, ios_platform)))] + fn draw(&mut self) -> Result<(), Box> { + if self.occluded { + info!("Skipping drawing occluded window={:?}", self.window.id()); + return Ok(()); + } + + const WHITE: u32 = 0xffffffff; + const DARK_GRAY: u32 = 0xff181818; + + let color = match self.theme { + Theme::Light => WHITE, + Theme::Dark => DARK_GRAY, + }; + + let mut buffer = self.surface.buffer_mut()?; + buffer.fill(color); + self.window.pre_present_notify(); + buffer.present()?; + Ok(()) + } + + #[cfg(any(android_platform, ios_platform))] + fn draw(&mut self) -> Result<(), Box> { + info!("Drawing but without rendering..."); + Ok(()) + } +} + +struct Binding { + trigger: T, + mods: ModifiersState, + action: Action, +} + +impl Binding { + const fn new(trigger: T, mods: ModifiersState, action: Action) -> Self { + Self { trigger, mods, action } + } + + fn is_triggered_by(&self, trigger: &T, mods: &ModifiersState) -> bool { + &self.trigger == trigger && &self.mods == mods + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Action { + CloseWindow, + ToggleCursorVisibility, + CreateNewWindow, + ToggleResizeIncrements, + ToggleImeInput, + ToggleDecorations, + ToggleResizable, + ToggleFullscreen, + ToggleMaximize, + Minimize, + NextCursor, + NextCustomCursor, + #[cfg(web_platform)] + UrlCustomCursor, + #[cfg(web_platform)] + AnimationCustomCursor, + CycleCursorGrab, + PrintHelp, + DragWindow, + DragResizeWindow, + ShowWindowMenu, + #[cfg(macos_platform)] + CycleOptionAsAlt, + SetTheme(Option), + #[cfg(macos_platform)] + CreateNewTab, + RequestResize, +} + +impl Action { + fn help(&self) -> &'static str { + match self { + Action::CloseWindow => "Close window", + Action::ToggleCursorVisibility => "Hide cursor", + Action::CreateNewWindow => "Create new window", + Action::ToggleImeInput => "Toggle IME input", + Action::ToggleDecorations => "Toggle decorations", + Action::ToggleResizable => "Toggle window resizable state", + Action::ToggleFullscreen => "Toggle fullscreen", + Action::ToggleMaximize => "Maximize", + Action::Minimize => "Minimize", + Action::ToggleResizeIncrements => "Use resize increments when resizing window", + Action::NextCursor => "Advance the cursor to the next value", + Action::NextCustomCursor => "Advance custom cursor to the next value", + #[cfg(web_platform)] + Action::UrlCustomCursor => "Custom cursor from an URL", + #[cfg(web_platform)] + Action::AnimationCustomCursor => "Custom cursor from an animation", + Action::CycleCursorGrab => "Cycle through cursor grab mode", + Action::PrintHelp => "Print help", + Action::DragWindow => "Start window drag", + Action::DragResizeWindow => "Start window drag-resize", + Action::ShowWindowMenu => "Show window menu", + #[cfg(macos_platform)] + Action::CycleOptionAsAlt => "Cycle option as alt mode", + Action::SetTheme(None) => "Change to the system theme", + Action::SetTheme(Some(Theme::Light)) => "Change to a light theme", + Action::SetTheme(Some(Theme::Dark)) => "Change to a dark theme", + #[cfg(macos_platform)] + Action::CreateNewTab => "Create new tab", + Action::RequestResize => "Request a resize", + } + } +} + +impl fmt::Display for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Debug::fmt(&self, f) + } +} + +fn decode_cursor(bytes: &[u8]) -> CustomCursorSource { + let img = image::load_from_memory(bytes).unwrap().to_rgba8(); + let samples = img.into_flat_samples(); + let (_, w, h) = samples.extents(); + let (w, h) = (w as u16, h as u16); + CustomCursor::from_rgba(samples.samples, w, h, w / 2, h / 2).unwrap() +} + +#[cfg(web_platform)] +fn url_custom_cursor() -> CustomCursorSource { + use std::sync::atomic::{AtomicU64, Ordering}; + + use winit::platform::web::CustomCursorExtWebSys; + + static URL_COUNTER: AtomicU64 = AtomicU64::new(0); + + CustomCursor::from_url( + format!("https://picsum.photos/128?random={}", URL_COUNTER.fetch_add(1, Ordering::Relaxed)), + 64, + 64, + ) +} + +fn load_icon(bytes: &[u8]) -> Icon { + let (icon_rgba, icon_width, icon_height) = { + let image = image::load_from_memory(bytes).unwrap().into_rgba8(); + let (width, height) = image.dimensions(); + let rgba = image.into_raw(); + (rgba, width, height) + }; + Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon") +} + +fn modifiers_to_string(mods: ModifiersState) -> String { + let mut mods_line = String::new(); + // Always add + since it's printed as a part of the bindings. + for (modifier, desc) in [ + (ModifiersState::SUPER, "Super+"), + (ModifiersState::ALT, "Alt+"), + (ModifiersState::CONTROL, "Ctrl+"), + (ModifiersState::SHIFT, "Shift+"), + ] { + if !mods.contains(modifier) { + continue; + } + + mods_line.push_str(desc); + } + mods_line +} + +fn mouse_button_to_string(button: MouseButton) -> &'static str { + match button { + MouseButton::Left => "LMB", + MouseButton::Right => "RMB", + MouseButton::Middle => "MMB", + MouseButton::Back => "Back", + MouseButton::Forward => "Forward", + MouseButton::Other(_) => "", + } +} + +/// Cursor list to cycle through. +const CURSORS: &[CursorIcon] = &[ + CursorIcon::Default, + CursorIcon::Crosshair, + CursorIcon::Pointer, + CursorIcon::Move, + CursorIcon::Text, + CursorIcon::Wait, + CursorIcon::Help, + CursorIcon::Progress, + CursorIcon::NotAllowed, + CursorIcon::ContextMenu, + CursorIcon::Cell, + CursorIcon::VerticalText, + CursorIcon::Alias, + CursorIcon::Copy, + CursorIcon::NoDrop, + CursorIcon::Grab, + CursorIcon::Grabbing, + CursorIcon::AllScroll, + CursorIcon::ZoomIn, + CursorIcon::ZoomOut, + CursorIcon::EResize, + CursorIcon::NResize, + CursorIcon::NeResize, + CursorIcon::NwResize, + CursorIcon::SResize, + CursorIcon::SeResize, + CursorIcon::SwResize, + CursorIcon::WResize, + CursorIcon::EwResize, + CursorIcon::NsResize, + CursorIcon::NeswResize, + CursorIcon::NwseResize, + CursorIcon::ColResize, + CursorIcon::RowResize, +]; + +const KEY_BINDINGS: &[Binding<&'static str>] = &[ + Binding::new("Q", ModifiersState::CONTROL, Action::CloseWindow), + Binding::new("H", ModifiersState::CONTROL, Action::PrintHelp), + Binding::new("F", ModifiersState::CONTROL, Action::ToggleFullscreen), + Binding::new("D", ModifiersState::CONTROL, Action::ToggleDecorations), + Binding::new("I", ModifiersState::CONTROL, Action::ToggleImeInput), + Binding::new("L", ModifiersState::CONTROL, Action::CycleCursorGrab), + Binding::new("P", ModifiersState::CONTROL, Action::ToggleResizeIncrements), + Binding::new("R", ModifiersState::CONTROL, Action::ToggleResizable), + Binding::new("R", ModifiersState::ALT, Action::RequestResize), + // M. + Binding::new("M", ModifiersState::CONTROL, Action::ToggleMaximize), + Binding::new("M", ModifiersState::ALT, Action::Minimize), + // N. + Binding::new("N", ModifiersState::CONTROL, Action::CreateNewWindow), + // C. + Binding::new("C", ModifiersState::CONTROL, Action::NextCursor), + Binding::new("C", ModifiersState::ALT, Action::NextCustomCursor), + #[cfg(web_platform)] + Binding::new( + "C", + ModifiersState::CONTROL.union(ModifiersState::SHIFT), + Action::UrlCustomCursor, + ), + #[cfg(web_platform)] + Binding::new( + "C", + ModifiersState::ALT.union(ModifiersState::SHIFT), + Action::AnimationCustomCursor, + ), + Binding::new("Z", ModifiersState::CONTROL, Action::ToggleCursorVisibility), + // K. + Binding::new("K", ModifiersState::empty(), Action::SetTheme(None)), + Binding::new("K", ModifiersState::SUPER, Action::SetTheme(Some(Theme::Light))), + Binding::new("K", ModifiersState::CONTROL, Action::SetTheme(Some(Theme::Dark))), + #[cfg(macos_platform)] + Binding::new("T", ModifiersState::SUPER, Action::CreateNewTab), + #[cfg(macos_platform)] + Binding::new("O", ModifiersState::CONTROL, Action::CycleOptionAsAlt), +]; + +const MOUSE_BINDINGS: &[Binding] = &[ + Binding::new(MouseButton::Left, ModifiersState::ALT, Action::DragResizeWindow), + Binding::new(MouseButton::Left, ModifiersState::CONTROL, Action::DragWindow), + Binding::new(MouseButton::Right, ModifiersState::CONTROL, Action::ShowWindowMenu), +]; diff --git a/vendor/winit/examples/x11_embed.rs b/vendor/winit/examples/x11_embed.rs new file mode 100644 index 00000000..9db55e5c --- /dev/null +++ b/vendor/winit/examples/x11_embed.rs @@ -0,0 +1,69 @@ +//! A demonstration of embedding a winit window in an existing X11 application. +use std::error::Error; + +#[cfg(x11_platform)] +fn main() -> Result<(), Box> { + use winit::application::ApplicationHandler; + use winit::event::WindowEvent; + use winit::event_loop::{ActiveEventLoop, EventLoop}; + use winit::platform::x11::WindowAttributesExtX11; + use winit::window::{Window, WindowId}; + + #[path = "util/fill.rs"] + mod fill; + + pub struct XEmbedDemo { + parent_window_id: u32, + window: Option, + } + + impl ApplicationHandler for XEmbedDemo { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window_attributes = Window::default_attributes() + .with_title("An embedded window!") + .with_inner_size(winit::dpi::LogicalSize::new(128.0, 128.0)) + .with_embed_parent_window(self.parent_window_id); + + self.window = Some(event_loop.create_window(window_attributes).unwrap()); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + _window_id: WindowId, + event: WindowEvent, + ) { + let window = self.window.as_ref().unwrap(); + match event { + WindowEvent::CloseRequested => event_loop.exit(), + WindowEvent::RedrawRequested => { + window.pre_present_notify(); + fill::fill_window(window); + }, + _ => (), + } + } + + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + self.window.as_ref().unwrap().request_redraw(); + } + } + + // First argument should be a 32-bit X11 window ID. + let parent_window_id = std::env::args() + .nth(1) + .ok_or("Expected a 32-bit X11 window ID as the first argument.")? + .parse::()?; + + tracing_subscriber::fmt::init(); + let event_loop = EventLoop::new()?; + + let mut app = XEmbedDemo { parent_window_id, window: None }; + event_loop.run_app(&mut app).map_err(Into::into) +} + +#[cfg(not(x11_platform))] +fn main() -> Result<(), Box> { + println!("This example is only supported on X11 platforms."); + Ok(()) +} diff --git a/vendor/winit/src/application.rs b/vendor/winit/src/application.rs new file mode 100644 index 00000000..977a7c79 --- /dev/null +++ b/vendor/winit/src/application.rs @@ -0,0 +1,339 @@ +//! End user application handling. + +use crate::event::{DeviceEvent, DeviceId, StartCause, WindowEvent}; +use crate::event_loop::ActiveEventLoop; +use crate::window::WindowId; + +/// The handler of the application events. +pub trait ApplicationHandler { + /// Emitted when new events arrive from the OS to be processed. + /// + /// This is a useful place to put code that should be done before you start processing + /// events, such as updating frame timing information for benchmarking or checking the + /// [`StartCause`] to see if a timer set by + /// [`ControlFlow::WaitUntil`][crate::event_loop::ControlFlow::WaitUntil] has elapsed. + fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) { + let _ = (event_loop, cause); + } + + /// Emitted when the application has been resumed. + /// + /// For consistency, all platforms emit a `Resumed` event even if they don't themselves have a + /// formal suspend/resume lifecycle. For systems without a formal suspend/resume lifecycle + /// the `Resumed` event is always emitted after the + /// [`NewEvents(StartCause::Init)`][StartCause::Init] event. + /// + /// # Portability + /// + /// It's recommended that applications should only initialize their graphics context and create + /// a window after they have received their first `Resumed` event. Some systems + /// (specifically Android) won't allow applications to create a render surface until they are + /// resumed. + /// + /// Considering that the implementation of [`Suspended`] and `Resumed` events may be internally + /// driven by multiple platform-specific events, and that there may be subtle differences across + /// platforms with how these internal events are delivered, it's recommended that applications + /// be able to gracefully handle redundant (i.e. back-to-back) [`Suspended`] or `Resumed` + /// events. + /// + /// Also see [`Suspended`] notes. + /// + /// ## Android + /// + /// On Android, the `Resumed` event is sent when a new [`SurfaceView`] has been created. This is + /// expected to closely correlate with the [`onResume`] lifecycle event but there may + /// technically be a discrepancy. + /// + /// [`onResume`]: https://developer.android.com/reference/android/app/Activity#onResume() + /// + /// Applications that need to run on Android must wait until they have been `Resumed` + /// before they will be able to create a render surface (such as an `EGLSurface`, + /// [`VkSurfaceKHR`] or [`wgpu::Surface`]) which depend on having a + /// [`SurfaceView`]. Applications must also assume that if they are [`Suspended`], then their + /// render surfaces are invalid and should be dropped. + /// + /// Also see [`Suspended`] notes. + /// + /// [`SurfaceView`]: https://developer.android.com/reference/android/view/SurfaceView + /// [Activity lifecycle]: https://developer.android.com/guide/components/activities/activity-lifecycle + /// [`VkSurfaceKHR`]: https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkSurfaceKHR.html + /// [`wgpu::Surface`]: https://docs.rs/wgpu/latest/wgpu/struct.Surface.html + /// + /// ## iOS + /// + /// On iOS, the `Resumed` event is emitted in response to an [`applicationDidBecomeActive`] + /// callback which means the application is "active" (according to the + /// [iOS application lifecycle]). + /// + /// [`applicationDidBecomeActive`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622956-applicationdidbecomeactive + /// [iOS application lifecycle]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle + /// + /// ## Web + /// + /// On Web, the `Resumed` event is emitted in response to a [`pageshow`] event + /// with the property [`persisted`] being true, which means that the page is being + /// restored from the [`bfcache`] (back/forward cache) - an in-memory cache that + /// stores a complete snapshot of a page (including the JavaScript heap) as the + /// user is navigating away. + /// + /// [`pageshow`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/pageshow_event + /// [`persisted`]: https://developer.mozilla.org/en-US/docs/Web/API/PageTransitionEvent/persisted + /// [`bfcache`]: https://web.dev/bfcache/ + /// [`Suspended`]: Self::suspended + fn resumed(&mut self, event_loop: &ActiveEventLoop); + + /// Emitted when an event is sent from [`EventLoopProxy::send_event`]. + /// + /// [`EventLoopProxy::send_event`]: crate::event_loop::EventLoopProxy::send_event + fn user_event(&mut self, event_loop: &ActiveEventLoop, event: T) { + let _ = (event_loop, event); + } + + /// Emitted when the OS sends an event to a winit window. + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ); + + /// Emitted when the OS sends an event to a device. + fn device_event( + &mut self, + event_loop: &ActiveEventLoop, + device_id: DeviceId, + event: DeviceEvent, + ) { + let _ = (event_loop, device_id, event); + } + + /// Emitted when the event loop is about to block and wait for new events. + /// + /// Most applications shouldn't need to hook into this event since there is no real relationship + /// between how often the event loop needs to wake up and the dispatching of any specific + /// events. + /// + /// High frequency event sources, such as input devices could potentially lead to lots of wake + /// ups and also lots of corresponding `AboutToWait` events. + /// + /// This is not an ideal event to drive application rendering from and instead applications + /// should render in response to [`WindowEvent::RedrawRequested`] events. + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + let _ = event_loop; + } + + /// Emitted when the application has been suspended. + /// + /// # Portability + /// + /// Not all platforms support the notion of suspending applications, and there may be no + /// technical way to guarantee being able to emit a `Suspended` event if the OS has + /// no formal application lifecycle (currently only Android, iOS, and Web do). For this reason, + /// Winit does not currently try to emit pseudo `Suspended` events before the application + /// quits on platforms without an application lifecycle. + /// + /// Considering that the implementation of `Suspended` and [`Resumed`] events may be internally + /// driven by multiple platform-specific events, and that there may be subtle differences across + /// platforms with how these internal events are delivered, it's recommended that applications + /// be able to gracefully handle redundant (i.e. back-to-back) `Suspended` or [`Resumed`] + /// events. + /// + /// Also see [`Resumed`] notes. + /// + /// ## Android + /// + /// On Android, the `Suspended` event is only sent when the application's associated + /// [`SurfaceView`] is destroyed. This is expected to closely correlate with the [`onPause`] + /// lifecycle event but there may technically be a discrepancy. + /// + /// [`onPause`]: https://developer.android.com/reference/android/app/Activity#onPause() + /// + /// Applications that need to run on Android should assume their [`SurfaceView`] has been + /// destroyed, which indirectly invalidates any existing render surfaces that may have been + /// created outside of Winit (such as an `EGLSurface`, [`VkSurfaceKHR`] or [`wgpu::Surface`]). + /// + /// After being `Suspended` on Android applications must drop all render surfaces before + /// the event callback completes, which may be re-created when the application is next + /// [`Resumed`]. + /// + /// [`SurfaceView`]: https://developer.android.com/reference/android/view/SurfaceView + /// [Activity lifecycle]: https://developer.android.com/guide/components/activities/activity-lifecycle + /// [`VkSurfaceKHR`]: https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkSurfaceKHR.html + /// [`wgpu::Surface`]: https://docs.rs/wgpu/latest/wgpu/struct.Surface.html + /// + /// ## iOS + /// + /// On iOS, the `Suspended` event is currently emitted in response to an + /// [`applicationWillResignActive`] callback which means that the application is + /// about to transition from the active to inactive state (according to the + /// [iOS application lifecycle]). + /// + /// [`applicationWillResignActive`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622950-applicationwillresignactive + /// [iOS application lifecycle]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle + /// + /// ## Web + /// + /// On Web, the `Suspended` event is emitted in response to a [`pagehide`] event + /// with the property [`persisted`] being true, which means that the page is being + /// put in the [`bfcache`] (back/forward cache) - an in-memory cache that stores a + /// complete snapshot of a page (including the JavaScript heap) as the user is + /// navigating away. + /// + /// [`pagehide`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/pagehide_event + /// [`persisted`]: https://developer.mozilla.org/en-US/docs/Web/API/PageTransitionEvent/persisted + /// [`bfcache`]: https://web.dev/bfcache/ + /// [`Resumed`]: Self::resumed + fn suspended(&mut self, event_loop: &ActiveEventLoop) { + let _ = event_loop; + } + + /// Emitted when the event loop is being shut down. + /// + /// This is irreversible - if this method is called, it is guaranteed that the event loop + /// will exit right after. + fn exiting(&mut self, event_loop: &ActiveEventLoop) { + let _ = event_loop; + } + + /// Emitted when the application has received a memory warning. + /// + /// ## Platform-specific + /// + /// ### Android + /// + /// On Android, the `MemoryWarning` event is sent when [`onLowMemory`] was called. The + /// application must [release memory] or risk being killed. + /// + /// [`onLowMemory`]: https://developer.android.com/reference/android/app/Application.html#onLowMemory() + /// [release memory]: https://developer.android.com/topic/performance/memory#release + /// + /// ### iOS + /// + /// On iOS, the `MemoryWarning` event is emitted in response to an + /// [`applicationDidReceiveMemoryWarning`] callback. The application must free as much + /// memory as possible or risk being terminated, see [how to respond to memory warnings]. + /// + /// [`applicationDidReceiveMemoryWarning`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623063-applicationdidreceivememorywarni + /// [how to respond to memory warnings]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle/responding_to_memory_warnings + /// + /// ### Others + /// + /// - **macOS / Orbital / Wayland / Web / Windows:** Unsupported. + fn memory_warning(&mut self, event_loop: &ActiveEventLoop) { + let _ = event_loop; + } +} + +impl, T: 'static> ApplicationHandler for &mut A { + #[inline] + fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) { + (**self).new_events(event_loop, cause); + } + + #[inline] + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + (**self).resumed(event_loop); + } + + #[inline] + fn user_event(&mut self, event_loop: &ActiveEventLoop, event: T) { + (**self).user_event(event_loop, event); + } + + #[inline] + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + (**self).window_event(event_loop, window_id, event); + } + + #[inline] + fn device_event( + &mut self, + event_loop: &ActiveEventLoop, + device_id: DeviceId, + event: DeviceEvent, + ) { + (**self).device_event(event_loop, device_id, event); + } + + #[inline] + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + (**self).about_to_wait(event_loop); + } + + #[inline] + fn suspended(&mut self, event_loop: &ActiveEventLoop) { + (**self).suspended(event_loop); + } + + #[inline] + fn exiting(&mut self, event_loop: &ActiveEventLoop) { + (**self).exiting(event_loop); + } + + #[inline] + fn memory_warning(&mut self, event_loop: &ActiveEventLoop) { + (**self).memory_warning(event_loop); + } +} + +impl, T: 'static> ApplicationHandler for Box { + #[inline] + fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) { + (**self).new_events(event_loop, cause); + } + + #[inline] + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + (**self).resumed(event_loop); + } + + #[inline] + fn user_event(&mut self, event_loop: &ActiveEventLoop, event: T) { + (**self).user_event(event_loop, event); + } + + #[inline] + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + (**self).window_event(event_loop, window_id, event); + } + + #[inline] + fn device_event( + &mut self, + event_loop: &ActiveEventLoop, + device_id: DeviceId, + event: DeviceEvent, + ) { + (**self).device_event(event_loop, device_id, event); + } + + #[inline] + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + (**self).about_to_wait(event_loop); + } + + #[inline] + fn suspended(&mut self, event_loop: &ActiveEventLoop) { + (**self).suspended(event_loop); + } + + #[inline] + fn exiting(&mut self, event_loop: &ActiveEventLoop) { + (**self).exiting(event_loop); + } + + #[inline] + fn memory_warning(&mut self, event_loop: &ActiveEventLoop) { + (**self).memory_warning(event_loop); + } +} diff --git a/vendor/winit/src/changelog/mod.rs b/vendor/winit/src/changelog/mod.rs new file mode 100644 index 00000000..a05ea9cc --- /dev/null +++ b/vendor/winit/src/changelog/mod.rs @@ -0,0 +1,77 @@ +//! # Changelog and migrations +//! +//! All notable changes to this project will be documented in this module, +//! along with migration instructions for larger changes. +// Put the current entry at the top of this page, for discoverability. +// See `.cargo/config.toml` for details about `unreleased_changelogs`. +#![cfg_attr(unreleased_changelogs, doc = include_str!("unreleased.md"))] +#![cfg_attr(not(unreleased_changelogs), doc = include_str!("v0.30.md"))] + +#[doc = include_str!("v0.30.md")] +pub mod v0_30 {} + +#[doc = include_str!("v0.29.md")] +pub mod v0_29 {} + +#[doc = include_str!("v0.28.md")] +pub mod v0_28 {} + +#[doc = include_str!("v0.27.md")] +pub mod v0_27 {} + +#[doc = include_str!("v0.26.md")] +pub mod v0_26 {} + +#[doc = include_str!("v0.25.md")] +pub mod v0_25 {} + +#[doc = include_str!("v0.24.md")] +pub mod v0_24 {} + +#[doc = include_str!("v0.23.md")] +pub mod v0_23 {} + +#[doc = include_str!("v0.22.md")] +pub mod v0_22 {} + +#[doc = include_str!("v0.21.md")] +pub mod v0_21 {} + +#[doc = include_str!("v0.20.md")] +pub mod v0_20 {} + +#[doc = include_str!("v0.19.md")] +pub mod v0_19 {} + +#[doc = include_str!("v0.18.md")] +pub mod v0_18 {} + +#[doc = include_str!("v0.17.md")] +pub mod v0_17 {} + +#[doc = include_str!("v0.16.md")] +pub mod v0_16 {} + +#[doc = include_str!("v0.15.md")] +pub mod v0_15 {} + +#[doc = include_str!("v0.14.md")] +pub mod v0_14 {} + +#[doc = include_str!("v0.13.md")] +pub mod v0_13 {} + +#[doc = include_str!("v0.12.md")] +pub mod v0_12 {} + +#[doc = include_str!("v0.11.md")] +pub mod v0_11 {} + +#[doc = include_str!("v0.10.md")] +pub mod v0_10 {} + +#[doc = include_str!("v0.9.md")] +pub mod v0_9 {} + +#[doc = include_str!("v0.8.md")] +pub mod v0_8 {} diff --git a/vendor/winit/src/changelog/unreleased.md b/vendor/winit/src/changelog/unreleased.md new file mode 100644 index 00000000..f3a0f6d2 --- /dev/null +++ b/vendor/winit/src/changelog/unreleased.md @@ -0,0 +1,41 @@ +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +The sections should follow the order `Added`, `Changed`, `Deprecated`, +`Removed`, and `Fixed`. + +Platform specific changed should be added to the end of the section and grouped +by platform name. Common API additions should have `, implemented` at the end +for platforms where the API was initially implemented. See the following example +on how to add them: + +```md +### Added + +- Add `Window::turbo()`, implemented on X11, Wayland, and Web. +- On X11, add `Window::some_rare_api`. +- On X11, add `Window::even_more_rare_api`. +- On Wayland, add `Window::common_api`. +- On Windows, add `Window::some_rare_api`. +``` + +When the change requires non-trivial amount of work for users to comply +with it, the migration guide should be added below the entry, like: + +```md +- Deprecate `Window` creation outside of `EventLoop::run` + + This was done to simply migration in the future. Consider the + following code: + + // Code snippet. + + To migrate it we should do X, Y, and then Z, for example: + + // Code snippet. + +``` + +The migration guide could reference other migration examples in the current +changelog entry. + +## Unreleased diff --git a/vendor/winit/src/changelog/v0.10.md b/vendor/winit/src/changelog/v0.10.md new file mode 100644 index 00000000..9e47c0b1 --- /dev/null +++ b/vendor/winit/src/changelog/v0.10.md @@ -0,0 +1,13 @@ +## 0.10.1 + +_Yanked_ + +## 0.10.0 + +- Add support for `Touch` for emscripten backend. +- Added support for `DroppedFile`, `HoveredFile`, and `HoveredFileCancelled` to X11 backend. +- **Breaking:** `unix::WindowExt` no longer returns pointers for things that aren't actually pointers; `get_xlib_window` now returns `Option` and `get_xlib_screen_id` returns `Option`. Additionally, methods that previously returned `libc::c_void` have been changed to return `std::os::raw::c_void`, which are not interchangeable types, so users wanting the former will need to explicitly cast. +- Added `set_decorations` method to `Window` to allow decorations to be toggled after the window is built. Presently only implemented on X11. +- Raised the minimum supported version of Rust to 1.20 on MacOS due to usage of associated constants in new versions of cocoa and core-graphics. +- Added `modifiers` field to `MouseInput`, `MouseWheel`, and `CursorMoved` events to track the modifiers state (`ModifiersState`). +- Fixed the emscripten backend to return the size of the canvas instead of the size of the window. diff --git a/vendor/winit/src/changelog/v0.11.md b/vendor/winit/src/changelog/v0.11.md new file mode 100644 index 00000000..31fb1d3b --- /dev/null +++ b/vendor/winit/src/changelog/v0.11.md @@ -0,0 +1,27 @@ +## 0.11.3 + +- Added `set_min_dimensions` and `set_max_dimensions` methods to `Window`, and implemented on Windows, X11, Wayland, and OSX. +- On X11, dropping a `Window` actually closes it now, and clicking the window's × button (or otherwise having the WM signal to close it) will result in the window closing. +- Added `WindowBuilderExt` methods for macos: `with_titlebar_transparent`, + `with_title_hidden`, `with_titlebar_buttons_hidden`, + `with_fullsize_content_view`. +- Mapped X11 numpad keycodes (arrows, Home, End, PageUp, PageDown, Insert and Delete) to corresponding virtual keycodes + +## 0.11.2 + +- Impl `Hash`, `PartialEq`, and `Eq` for `events::ModifiersState`. +- Implement `MonitorId::get_hidpi_factor` for MacOS. +- Added method `os::macos::MonitorIdExt::get_nsscreen() -> *mut c_void` that gets a `NSScreen` object matching the monitor ID. +- Send `Awakened` event on Android when event loop is woken up. + +## 0.11.1 + +- Fixed windows not receiving mouse events when click-dragging the mouse outside the client area of a window, on Windows platforms. +- Added method `os::android::EventsLoopExt:set_suspend_callback(Option ()>>)` that allows glutin to register a callback when a suspend event happens + +## 0.11.0 + +- Implement `MonitorId::get_dimensions` for Android. +- Added method `os::macos::WindowBuilderExt::with_movable_by_window_background(bool)` that allows to move a window without a titlebar - `with_decorations(false)` +- Implement `Window::set_fullscreen`, `Window::set_maximized` and `Window::set_decorations` for Wayland. +- Added `Caret` as VirtualKeyCode and support OSX ^-Key with german input. diff --git a/vendor/winit/src/changelog/v0.12.md b/vendor/winit/src/changelog/v0.12.md new file mode 100644 index 00000000..3f9c83d8 --- /dev/null +++ b/vendor/winit/src/changelog/v0.12.md @@ -0,0 +1,8 @@ +## 0.12.0 + +- Added subclass to macos windows so they can be made resizable even with no decorations. +- Dead keys now work properly on X11, no longer resulting in a panic. +- On X11, input method creation first tries to use the value from the user's `XMODIFIERS` environment variable, so application developers should no longer need to manually call `XSetLocaleModifiers`. If that fails, fallbacks are tried, which should prevent input method initialization from ever outright failing. +- Fixed thread safety issues with input methods on X11. +- Add support for `Touch` for win32 backend. +- Fixed `Window::get_inner_size` and friends to return the size in pixels instead of points when using HIDPI displays on OSX. diff --git a/vendor/winit/src/changelog/v0.13.md b/vendor/winit/src/changelog/v0.13.md new file mode 100644 index 00000000..fd45665f --- /dev/null +++ b/vendor/winit/src/changelog/v0.13.md @@ -0,0 +1,20 @@ +## 0.13.1 + +- Ensure necessary `x11-dl` version is used. + +## 0.13.0 + +- Implement `WindowBuilder::with_maximized`, `Window::set_fullscreen`, `Window::set_maximized` and `Window::set_decorations` for MacOS. +- Implement `WindowBuilder::with_maximized`, `Window::set_fullscreen`, `Window::set_maximized` and `Window::set_decorations` for Windows. +- On Windows, `WindowBuilder::with_fullscreen` no longer changing monitor display resolution. +- Overhauled X11 window geometry calculations. `get_position` and `set_position` are more universally accurate across different window managers, and `get_outer_size` actually works now. +- Fixed SIGSEGV/SIGILL crashes on macOS caused by stabilization of the `!` (never) type. +- Implement `WindowEvent::HiDPIFactorChanged` for macOS +- On X11, input methods now work completely out of the box, no longer requiring application developers to manually call `setlocale`. Additionally, when input methods are started, stopped, or restarted on the server end, it's correctly handled. +- Implemented `Refresh` event on Windows. +- Properly calculate the minimum and maximum window size on Windows, including window decorations. +- Map more `MouseCursor` variants to cursor icons on Windows. +- Corrected `get_position` on macOS to return outer frame position, not content area position. +- Corrected `set_position` on macOS to set outer frame position, not content area position. +- Added `get_inner_position` method to `Window`, which gets the position of the window's client area. This is implemented on all applicable platforms (all desktop platforms other than Wayland, where this isn't possible). +- **Breaking:** the `Closed` event has been replaced by `CloseRequested` and `Destroyed`. To migrate, you typically just need to replace all usages of `Closed` with `CloseRequested`; see example programs for more info. The exception is iOS, where `Closed` must be replaced by `Destroyed`. diff --git a/vendor/winit/src/changelog/v0.14.md b/vendor/winit/src/changelog/v0.14.md new file mode 100644 index 00000000..822ae209 --- /dev/null +++ b/vendor/winit/src/changelog/v0.14.md @@ -0,0 +1,21 @@ +## 0.14.0 + +- Created the `Copy`, `Paste` and `Cut` `VirtualKeyCode`s and added support for them on X11 and Wayland +- Fix `.with_decorations(false)` in macOS +- On Mac, `NSWindow` and supporting objects might be alive long after they were `closed` which resulted in apps consuming more heap then needed. Mainly it was affecting multi window applications. Not expecting any user visible change of behaviour after the fix. +- Fix regression of Window platform extensions for macOS where `NSFullSizeContentViewWindowMask` was not being correctly applied to `.fullsize_content_view`. +- Corrected `get_position` on Windows to be relative to the screen rather than to the taskbar. +- Corrected `Moved` event on Windows to use position values equivalent to those returned by `get_position`. It previously supplied client area positions instead of window positions, and would additionally interpret negative values as being very large (around `u16::MAX`). +- Implemented `Moved` event on macOS. +- On X11, the `Moved` event correctly use window positions rather than client area positions. Additionally, a stray `Moved` that unconditionally accompanied `Resized` with the client area position relative to the parent has been eliminated; `Moved` is still received alongside `Resized`, but now only once and always correctly. +- On Windows, implemented all variants of `DeviceEvent` other than `Text`. Mouse `DeviceEvent`s are now received even if the window isn't in the foreground. +- `DeviceId` on Windows is no longer a unit struct, and now contains a `u32`. For `WindowEvent`s, this will always be 0, but on `DeviceEvent`s it will be the handle to that device. `DeviceIdExt::get_persistent_identifier` can be used to acquire a unique identifier for that device that persists across replugs/reboots/etc. +- Corrected `run_forever` on X11 to stop discarding `Awakened` events. +- Various safety and correctness improvements to the X11 backend internals. +- Fixed memory leak on X11 every time the mouse entered the window. +- On X11, drag and drop now works reliably in release mode. +- Added `WindowBuilderExt::with_resize_increments` and `WindowBuilderExt::with_base_size` to X11, allowing for more optional hints to be set. +- Rework of the wayland backend, migrating it to use [Smithay's Client Toolkit](https://github.com/Smithay/client-toolkit). +- Added `WindowBuilder::with_window_icon` and `Window::set_window_icon`, finally making it possible to set the window icon on Windows and X11. The `icon_loading` feature can be enabled to allow for icons to be easily loaded; see example program `window_icon.rs` for usage. +- Windows additionally has `WindowBuilderExt::with_taskbar_icon` and `WindowExt::set_taskbar_icon`. +- On Windows, fix panic when trying to call `set_fullscreen(None)` on a window that has not been fullscreened prior. diff --git a/vendor/winit/src/changelog/v0.15.md b/vendor/winit/src/changelog/v0.15.md new file mode 100644 index 00000000..f5fc4853 --- /dev/null +++ b/vendor/winit/src/changelog/v0.15.md @@ -0,0 +1,42 @@ +## 0.15.1 + +- On X11, the `Moved` event is no longer sent when the window is resized without changing position. +- `MouseCursor` and `CursorState` now implement `Default`. +- `WindowBuilder::with_resizable` implemented for Windows, X11, Wayland, and macOS. +- `Window::set_resizable` implemented for Windows, X11, Wayland, and macOS. +- On X11, if the monitor's width or height in millimeters is reported as 0, the DPI is now 1.0 instead of +inf. +- On X11, the environment variable `WINIT_HIDPI_FACTOR` has been added for overriding DPI factor. +- On X11, enabling transparency no longer causes the window contents to flicker when resizing. +- On X11, `with_override_redirect` now actually enables override redirect. +- macOS now generates `VirtualKeyCode::LAlt` and `VirtualKeyCode::RAlt` instead of `None` for both. +- On macOS, `VirtualKeyCode::RWin` and `VirtualKeyCode::LWin` are no longer switched. +- On macOS, windows without decorations can once again be resized. +- Fixed race conditions when creating an `EventsLoop` on X11, most commonly manifesting as `"[xcb] Unknown sequence number while processing queue"`. +- On macOS, `CursorMoved` and `MouseInput` events are only generated if they occurs within the window's client area. +- On macOS, resizing the window no longer generates a spurious `MouseInput` event. + +## 0.15.0 + +- `Icon::to_cardinals` is no longer public, since it was never supposed to be. +- Wayland: improve diagnostics if initialization fails +- Fix some system event key doesn't work when focused, do not block keyevent forward to system on macOS +- On X11, the scroll wheel position is now correctly reset on i3 and other WMs that have the same quirk. +- On X11, `Window::get_current_monitor` now reliably returns the correct monitor. +- On X11, `Window::hidpi_factor` returns values from XRandR rather than the inaccurate values previously queried from the core protocol. +- On X11, the primary monitor is detected correctly even when using versions of XRandR less than 1.5. +- `MonitorId` now implements `Debug`. +- Fixed bug on macOS where using `with_decorations(false)` would cause `set_decorations(true)` to produce a transparent titlebar with no title. +- Implemented `MonitorId::get_position` on macOS. +- On macOS, `Window::get_current_monitor` now returns accurate values. +- Added `WindowBuilderExt::with_resize_increments` to macOS. +- **Breaking:** On X11, `WindowBuilderExt::with_resize_increments` and `WindowBuilderExt::with_base_size` now take `u32` values rather than `i32`. +- macOS keyboard handling has been overhauled, allowing for the use of dead keys, IME, etc. Right modifier keys are also no longer reported as being left. +- Added the `Window::set_ime_spot(x: i32, y: i32)` method, which is implemented on X11 and macOS. +- **Breaking**: `os::unix::WindowExt::send_xim_spot(x: i16, y: i16)` no longer exists. Switch to the new `Window::set_ime_spot(x: i32, y: i32)`, which has equivalent functionality. +- Fixed detection of `Pause` and `Scroll` keys on Windows. +- On Windows, alt-tabbing while the cursor is grabbed no longer makes it impossible to re-grab the cursor. +- On Windows, using `CursorState::Hide` when the cursor is grabbed now ungrabs the cursor first. +- Implemented `MouseCursor::NoneCursor` on Windows. +- Added `WindowBuilder::with_always_on_top` and `Window::set_always_on_top`. Implemented on Windows, macOS, and X11. +- On X11, `WindowBuilderExt` now has `with_class`, `with_override_redirect`, and `with_x11_window_type` to allow for more control over window creation. `WindowExt` additionally has `set_urgent`. +- More hints are set by default on X11, including `_NET_WM_PID` and `WM_CLIENT_MACHINE`. Note that prior to this, the `WM_CLASS` hint was automatically set to whatever value was passed to `with_title`. It's now set to the executable name to better conform to expectations and the specification; if this is undesirable, you must explicitly use `WindowBuilderExt::with_class`. diff --git a/vendor/winit/src/changelog/v0.16.md b/vendor/winit/src/changelog/v0.16.md new file mode 100644 index 00000000..126ad9f7 --- /dev/null +++ b/vendor/winit/src/changelog/v0.16.md @@ -0,0 +1,32 @@ +## 0.16.2 + +- On Windows, non-resizable windows now have the maximization button disabled. This is consistent with behavior on macOS and popular X11 WMs. +- Corrected incorrect `unreachable!` usage when guessing the DPI factor with no detected monitors. + +## 0.16.1 + +- Added logging through `log`. Logging will become more extensive over time. +- On X11 and Windows, the window's DPI factor is guessed before creating the window. This _greatly_ cuts back on unsightly auto-resizing that would occur immediately after window creation. +- Fixed X11 backend compilation for environments where `c_char` is unsigned. + +## 0.16.0 + +- Windows additionally has `WindowBuilderExt::with_no_redirection_bitmap`. +- **Breaking:** Removed `VirtualKeyCode::LMenu` and `VirtualKeyCode::RMenu`; Windows now generates `VirtualKeyCode::LAlt` and `VirtualKeyCode::RAlt` instead. +- On X11, exiting fullscreen no longer leaves the window in the monitor's top left corner. +- **Breaking:** `Window::hidpi_factor` has been renamed to `Window::get_hidpi_factor` for better consistency. `WindowEvent::HiDPIFactorChanged` has been renamed to `WindowEvent::HiDpiFactorChanged`. DPI factors are always represented as `f64` instead of `f32` now. +- The Windows backend is now DPI aware. `WindowEvent::HiDpiFactorChanged` is implemented, and `MonitorId::get_hidpi_factor` and `Window::hidpi_factor` return accurate values. +- Implemented `WindowEvent::HiDpiFactorChanged` on X11. +- On macOS, `Window::set_cursor_position` is now relative to the client area. +- On macOS, setting the maximum and minimum dimensions now applies to the client area dimensions rather than to the window dimensions. +- On iOS, `MonitorId::get_dimensions` has been implemented and both `MonitorId::get_hidpi_factor` and `Window::get_hidpi_factor` return accurate values. +- On Emscripten, `MonitorId::get_hidpi_factor` now returns the same value as `Window::get_hidpi_factor` (it previously would always return 1.0). +- **Breaking:** The entire API for sizes, positions, etc. has changed. In the majority of cases, winit produces and consumes positions and sizes as `LogicalPosition` and `LogicalSize`, respectively. The notable exception is `MonitorId` methods, which deal in `PhysicalPosition` and `PhysicalSize`. See the documentation for specifics and explanations of the types. Additionally, winit automatically conserves logical size when the DPI factor changes. +- **Breaking:** All deprecated methods have been removed. For `Window::platform_display` and `Window::platform_window`, switch to the appropriate platform-specific `WindowExt` methods. For `Window::get_inner_size_points` and `Window::get_inner_size_pixels`, use the `LogicalSize` returned by `Window::get_inner_size` and convert as needed. +- HiDPI support for Wayland. +- `EventsLoop::get_available_monitors` and `EventsLoop::get_primary_monitor` now have identical counterparts on `Window`, so this information can be acquired without an `EventsLoop` borrow. +- `AvailableMonitorsIter` now implements `Debug`. +- Fixed quirk on macOS where certain keys would generate characters at twice the normal rate when held down. +- On X11, all event loops now share the same `XConnection`. +- **Breaking:** `Window::set_cursor_state` and `CursorState` enum removed in favor of the more composable `Window::grab_cursor` and `Window::hide_cursor`. As a result, grabbing the cursor no longer automatically hides it; you must call both methods to retain the old behavior on Windows and macOS. `Cursor::NoneCursor` has been removed, as it's no longer useful. +- **Breaking:** `Window::set_cursor_position` now returns `Result<(), String>`, thus allowing for `Box` conversion via `?`. diff --git a/vendor/winit/src/changelog/v0.17.md b/vendor/winit/src/changelog/v0.17.md new file mode 100644 index 00000000..0a1abc8b --- /dev/null +++ b/vendor/winit/src/changelog/v0.17.md @@ -0,0 +1,23 @@ +## 0.17.2 + +- On macOS, fix `` so applications receive the event. +- On macOS, fix `` so applications receive the event. +- On Wayland, key press events will now be repeated. + +## 0.17.1 + +- On X11, prevent a compilation failure in release mode for versions of Rust greater than or equal to 1.30. +- Fixed deadlock that broke fullscreen mode on Windows. + +## 0.17.0 + +- Cocoa and core-graphics updates. +- Fixed thread-safety issues in several `Window` functions on Windows. +- On MacOS, the key state for modifiers key events is now properly set. +- On iOS, the view is now set correctly. This makes it possible to render things (instead of being stuck on a black screen), and touch events work again. +- Added NetBSD support. +- **Breaking:** On iOS, `UIView` is now the default root view. `WindowBuilderExt::with_root_view_class` can be used to set the root view objective-c class to `GLKView` (OpenGLES) or `MTKView` (Metal/MoltenVK). +- On iOS, the `UIApplication` is not started until `Window::new` is called. +- Fixed thread unsafety with cursor hiding on macOS. +- On iOS, fixed the size of the `JmpBuf` type used for `setjmp`/`longjmp` calls. Previously this was a buffer overflow on most architectures. +- On Windows, use cached window DPI instead of repeatedly querying the system. This fixes sporadic crashes on Windows 7. diff --git a/vendor/winit/src/changelog/v0.18.md b/vendor/winit/src/changelog/v0.18.md new file mode 100644 index 00000000..e98df5bc --- /dev/null +++ b/vendor/winit/src/changelog/v0.18.md @@ -0,0 +1,52 @@ +## 0.18.1 + +- On macOS, fix `Yen` (JIS) so applications receive the event. +- On X11 with a tiling WM, fixed high CPU usage when moving windows across monitors. +- On X11, fixed panic caused by dropping the window before running the event loop. +- on macOS, added `WindowExt::set_simple_fullscreen` which does not require a separate space +- Introduce `WindowBuilderExt::with_app_id` to allow setting the application ID on Wayland. +- On Windows, catch panics in event loop child thread and forward them to the parent thread. This prevents an invocation of undefined behavior due to unwinding into foreign code. +- On Windows, fix issue where resizing or moving window combined with grabbing the cursor would freeze program. +- On Windows, fix issue where resizing or moving window would eat `Awakened` events. +- On Windows, exiting fullscreen after entering fullscreen with disabled decorations no longer shrinks window. +- On X11, fixed a segfault when using virtual monitors with XRandR. +- Derive `Ord` and `PartialOrd` for `VirtualKeyCode` enum. +- On Windows, fix issue where hovering or dropping a non file item would create a panic. +- On Wayland, fix resizing and DPI calculation when a `wl_output` is removed without sending a `leave` event to the `wl_surface`, such as disconnecting a monitor from a laptop. +- On Wayland, DPI calculation is handled by smithay-client-toolkit. +- On X11, `WindowBuilder::with_min_dimensions` and `WindowBuilder::with_max_dimensions` now correctly account for DPI. +- Added support for generating dummy `DeviceId`s and `WindowId`s to better support unit testing. +- On macOS, fixed unsoundness in drag-and-drop that could result in drops being rejected. +- On macOS, implemented `WindowEvent::Refresh`. +- On macOS, all `MouseCursor` variants are now implemented and the cursor will no longer reset after unfocusing. +- Removed minimum supported Rust version guarantee. + +## 0.18.0 + +- **Breaking:** `image` crate upgraded to 0.20. This is exposed as part of the `icon_loading` API. +- On Wayland, pointer events will now provide the current modifiers state. +- On Wayland, titles will now be displayed in the window header decoration. +- On Wayland, key repetition is now ended when keyboard loses focus. +- On Wayland, windows will now use more stylish and modern client side decorations. +- On Wayland, windows will use server-side decorations when available. +- **Breaking:** Added support for F16-F24 keys (variants were added to the `VirtualKeyCode` enum). +- Fixed graphical glitches when resizing on Wayland. +- On Windows, fix freezes when performing certain actions after a window resize has been triggered. Reintroduces some visual artifacts when resizing. +- Updated window manager hints under X11 to v1.5 of [Extended Window Manager Hints](https://specifications.freedesktop.org/wm-spec/wm-spec-1.5.html#idm140200472629520). +- Added `WindowBuilderExt::with_gtk_theme_variant` to X11-specific `WindowBuilder` functions. +- Fixed UTF8 handling bug in X11 `set_title` function. +- On Windows, `Window::set_cursor` now applies immediately instead of requiring specific events to occur first. +- On Windows, the `HoveredFile` and `HoveredFileCancelled` events are now implemented. +- On Windows, fix `Window::set_maximized`. +- On Windows 10, fix transparency (#260). +- On macOS, fix modifiers during key repeat. +- Implemented the `Debug` trait for `Window`, `EventsLoop`, `EventsLoopProxy` and `WindowBuilder`. +- On X11, now a `Resized` event will always be generated after a DPI change to ensure the window's logical size is consistent with the new DPI. +- Added further clarifications to the DPI docs. +- On Linux, if neither X11 nor Wayland manage to initialize, the corresponding panic now consists of a single line only. +- Add optional `serde` feature with implementations of `Serialize`/`Deserialize` for DPI types and various event types. +- Add `PartialEq`, `Eq`, and `Hash` implementations on public types that could have them but were missing them. +- On X11, drag-and-drop receiving an unsupported drop type can no longer cause the WM to freeze. +- Fix issue whereby the OpenGL context would not appear at startup on macOS Mojave (#1069). +- **Breaking:** Removed `From` impl from `ActivationPolicy` on macOS. +- On macOS, the application can request the user's attention with `WindowExt::request_user_attention`. diff --git a/vendor/winit/src/changelog/v0.19.md b/vendor/winit/src/changelog/v0.19.md new file mode 100644 index 00000000..65aefef8 --- /dev/null +++ b/vendor/winit/src/changelog/v0.19.md @@ -0,0 +1,27 @@ +## 0.19.1 + +- On Wayland, added a `get_wayland_display` function to `EventsLoopExt`. +- On Windows, fix `CursorMoved(0, 0)` getting dispatched on window focus. +- On macOS, fix command key event left and right reverse. +- On FreeBSD, NetBSD, and OpenBSD, fix build of X11 backend. +- On Linux, the numpad's add, subtract and divide keys are now mapped to the `Add`, `Subtract` and `Divide` virtual key codes +- On macOS, the numpad's subtract key has been added to the `Subtract` mapping +- On Wayland, the numpad's home, end, page up and page down keys are now mapped to the `Home`, `End`, `PageUp` and `PageDown` virtual key codes +- On Windows, fix icon not showing up in corner of window. +- On X11, change DPI scaling factor behavior. First, winit tries to read it from "Xft.dpi" XResource, and uses DPI calculation from xrandr dimensions as fallback behavior. + +## 0.19.0 + +- On X11, we will use the faster `XRRGetScreenResourcesCurrent` function instead of `XRRGetScreenResources` when available. +- On macOS, fix keycodes being incorrect when using a non-US keyboard layout. +- On Wayland, fix `with_title()` not setting the windows title +- On Wayland, add `set_wayland_theme()` to control client decoration color theme +- Added serde serialization to `os::unix::XWindowType`. +- **Breaking:** Remove the `icon_loading` feature and the associated `image` dependency. +- On X11, make event loop thread safe by replacing XNextEvent with select(2) and XCheckIfEvent +- On Windows, fix malformed function pointer typecast that could invoke undefined behavior. +- Refactored Windows state/flag-setting code. +- On Windows, hiding the cursor no longer hides the cursor for all Winit windows - just the one `hide_cursor` was called on. +- On Windows, cursor grabs used to get perpetually canceled when the grabbing window lost focus. Now, cursor grabs automatically get re-initialized when the window regains focus and the mouse moves over the client area. +- On Windows, only vertical mouse wheel events were handled. Now, horizontal mouse wheel events are also handled. +- On Windows, ignore the AltGr key when populating the `ModifiersState` type. diff --git a/vendor/winit/src/changelog/v0.20.md b/vendor/winit/src/changelog/v0.20.md new file mode 100644 index 00000000..eb4ba6a1 --- /dev/null +++ b/vendor/winit/src/changelog/v0.20.md @@ -0,0 +1,200 @@ +## 0.20.0 + +- On X11, fix `ModifiersChanged` emitting incorrect modifier change events +- **Breaking**: Overhaul how Winit handles DPI: + - Window functions and events now return `PhysicalSize` instead of `LogicalSize`. + - Functions that take `Size` or `Position` types can now take either `Logical` or `Physical` types. + - `hidpi_factor` has been renamed to `scale_factor`. + - `HiDpiFactorChanged` has been renamed to `ScaleFactorChanged`, and lets you control how the OS + resizes the window in response to the change. + - On X11, deprecate `WINIT_HIDPI_FACTOR` environment variable in favor of `WINIT_X11_SCALE_FACTOR`. + - `Size` and `Position` types are now generic over their exact pixel type. + +## 0.20.0-alpha6 + +- On macOS, fix `set_cursor_visible` hides cursor outside of window. +- On macOS, fix `CursorEntered` and `CursorLeft` events fired at old window size. +- On macOS, fix error when `set_fullscreen` is called during fullscreen transition. +- On all platforms except mobile and WASM, implement `Window::set_minimized`. +- On X11, fix `CursorEntered` event being generated for non-winit windows. +- On macOS, fix crash when starting maximized without decorations. +- On macOS, fix application not terminating on `run_return`. +- On Wayland, fix cursor icon updates on window borders when using CSD. +- On Wayland, under mutter(GNOME Wayland), fix CSD being behind the status bar, when starting window in maximized mode. +- On Windows, theme the title bar according to whether the system theme is "Light" or "Dark". +- Added `WindowEvent::ThemeChanged` variant to handle changes to the system theme. Currently only implemented on Windows. +- **Breaking**: Changes to the `RedrawRequested` event (#1041): + - `RedrawRequested` has been moved from `WindowEvent` to `Event`. + - `EventsCleared` has been renamed to `MainEventsCleared`. + - `RedrawRequested` is now issued only after `MainEventsCleared`. + - `RedrawEventsCleared` is issued after each set of `RedrawRequested` events. +- Implement synthetic window focus key events on Windows. +- **Breaking**: Change `ModifiersState` to a `bitflags` struct. +- On Windows, implement `VirtualKeyCode` translation for `LWin` and `RWin`. +- On Windows, fix closing the last opened window causing `DeviceEvent`s to stop getting emitted. +- On Windows, fix `Window::set_visible` not setting internal flags correctly. This resulted in some weird behavior. +- Add `DeviceEvent::ModifiersChanged`. + - Deprecate `modifiers` fields in other events in favor of `ModifiersChanged`. +- On X11, `WINIT_HIDPI_FACTOR` now dominates `Xft.dpi` when picking DPI factor for output. +- On X11, add special value `randr` for `WINIT_HIDPI_FACTOR` to make winit use self computed DPI factor instead of the one from `Xft.dpi`. + +## 0.20.0-alpha5 + +- On macOS, fix application termination on `ControlFlow::Exit` +- On Windows, fix missing `ReceivedCharacter` events when Alt is held. +- On macOS, stop emitting private corporate characters in `ReceivedCharacter` events. +- On X11, fix misreporting DPI factor at startup. +- On X11, fix events not being reported when using `run_return`. +- On X11, fix key modifiers being incorrectly reported. +- On X11, fix window creation hanging when another window is fullscreen. +- On Windows, fix focusing unfocused windows when switching from fullscreen to windowed. +- On X11, fix reporting incorrect DPI factor when waking from suspend. +- Change `EventLoopClosed` to contain the original event. +- **Breaking**: Add `is_synthetic` field to `WindowEvent` variant `KeyboardInput`, + indicating that the event is generated by winit. +- On X11, generate synthetic key events for keys held when a window gains or loses focus. +- On X11, issue a `CursorMoved` event when a `Touch` event occurs, + as X11 implicitly moves the cursor for such events. + +## 0.20.0-alpha4 + +- Add web support via the 'stdweb' or 'web-sys' features +- On Windows, implemented function to get HINSTANCE +- On macOS, implement `run_return`. +- On iOS, fix inverted parameter in `set_prefers_home_indicator_hidden`. +- On X11, performance is improved when rapidly calling `Window::set_cursor_icon`. +- On iOS, fix improper `msg_send` usage that was UB and/or would break if `!` is stabilized. +- On Windows, unset `maximized` when manually changing the window's position or size. +- On Windows, add touch pressure information for touch events. +- On macOS, differentiate between `CursorIcon::Grab` and `CursorIcon::Grabbing`. +- On Wayland, fix event processing sometimes stalling when using OpenGL with vsync. +- Officially remove the Emscripten backend. +- On Windows, fix handling of surrogate pairs when dispatching `ReceivedCharacter`. +- On macOS 10.15, fix freeze upon exiting exclusive fullscreen mode. +- On iOS, fix panic upon closing the app. +- On X11, allow setting multiple `XWindowType`s. +- On iOS, fix null window on initial `HiDpiFactorChanged` event. +- On Windows, fix fullscreen window shrinking upon getting restored to a normal window. +- On macOS, fix events not being emitted during modal loops, such as when windows are being resized + by the user. +- On Windows, fix hovering the mouse over the active window creating an endless stream of CursorMoved events. +- Always dispatch a `RedrawRequested` event after creating a new window. +- On X11, return dummy monitor data to avoid panicking when no monitors exist. +- On X11, prevent stealing input focus when creating a new window. + Only steal input focus when entering fullscreen mode. +- On Wayland, fixed DeviceEvents for relative mouse movement is not always produced +- On Wayland, add support for set_cursor_visible and set_cursor_grab. +- On Wayland, fixed DeviceEvents for relative mouse movement is not always produced. +- Removed `derivative` crate dependency. +- On Wayland, add support for set_cursor_icon. +- Use `impl Iterator` instead of `AvailableMonitorsIter` consistently. +- On macOS, fix fullscreen state being updated after entering fullscreen instead of before, + resulting in `Window::fullscreen` returning the old state in `Resized` events instead of + reflecting the new fullscreen state +- On X11, fix use-after-free during window creation +- On Windows, disable monitor change keyboard shortcut while in exclusive fullscreen. +- On Windows, ensure that changing a borderless fullscreen window's monitor via keyboard shortcuts keeps the window fullscreen on the new monitor. +- Prevent `EventLoop::new` and `EventLoop::with_user_event` from getting called outside the main thread. + - This is because some platforms cannot run the event loop outside the main thread. Preventing this + reduces the potential for cross-platform compatibility gotchyas. +- On Windows and Linux X11/Wayland, add platform-specific functions for creating an `EventLoop` outside the main thread. +- On Wayland, drop resize events identical to the current window size. +- On Windows, fix window rectangle not getting set correctly on high-DPI systems. + +## 0.20.0-alpha3 + +- On macOS, drop the run closure on exit. +- On Windows, location of `WindowEvent::Touch` are window client coordinates instead of screen coordinates. +- On X11, fix delayed events after window redraw. +- On macOS, add `WindowBuilderExt::with_disallow_hidpi` to have the option to turn off best resolution openGL surface. +- On Windows, screen saver won't start if the window is in fullscreen mode. +- Change all occurrences of the `new_user_event` method to `with_user_event`. +- On macOS, the dock and the menu bar are now hidden in fullscreen mode. +- `Window::set_fullscreen` now takes `Option` where `Fullscreen` + consists of `Fullscreen::Exclusive(VideoMode)` and + `Fullscreen::Borderless(MonitorHandle)` variants. + - Adds support for exclusive fullscreen mode. +- On iOS, add support for hiding the home indicator. +- On iOS, add support for deferring system gestures. +- On iOS, fix a crash that occurred while acquiring a monitor's name. +- On iOS, fix armv7-apple-ios compile target. +- Removed the `T: Clone` requirement from the `Clone` impl of `EventLoopProxy`. +- On iOS, disable overscan compensation for external displays (removes black + bars surrounding the image). +- On Linux, the functions `is_wayland`, `is_x11`, `xlib_xconnection` and `wayland_display` have been moved to a new `EventLoopWindowTargetExtUnix` trait. +- On iOS, add `set_prefers_status_bar_hidden` extension function instead of + hijacking `set_decorations` for this purpose. +- On macOS and iOS, corrected the auto trait impls of `EventLoopProxy`. +- On iOS, add touch pressure information for touch events. +- Implement `raw_window_handle::HasRawWindowHandle` for `Window` type on all supported platforms. +- On macOS, fix the signature of `-[NSView drawRect:]`. +- On iOS, fix the behavior of `ControlFlow::Poll`. It wasn't polling if that was the only mode ever used by the application. +- On iOS, fix DPI sent out by views on creation was `0.0` - now it gives a reasonable number. +- On iOS, RedrawRequested now works for gl/metal backed views. +- On iOS, RedrawRequested is generally ordered after EventsCleared. + +## 0.20.0-alpha2 + +- On X11, non-resizable windows now have maximize explicitly disabled. +- On Windows, support paths longer than MAX_PATH (260 characters) in `WindowEvent::DroppedFile` + and `WindowEvent::HoveredFile`. +- On Mac, implement `DeviceEvent::Button`. +- Change `Event::Suspended(true / false)` to `Event::Suspended` and `Event::Resumed`. +- On X11, fix sanity check which checks that a monitor's reported width and height (in millimeters) are non-zero when calculating the DPI factor. +- Revert the use of invisible surfaces in Wayland, which introduced graphical glitches with OpenGL (#835) +- On X11, implement `_NET_WM_PING` to allow desktop environment to kill unresponsive programs. +- On Windows, when a window is initially invisible, it won't take focus from the existing visible windows. +- On Windows, fix multiple calls to `request_redraw` during `EventsCleared` sending multiple `RedrawRequested events.` +- On Windows, fix edge case where `RedrawRequested` could be dispatched before input events in event loop iteration. +- On Windows, fix timing issue that could cause events to be improperly dispatched after `RedrawRequested` but before `EventsCleared`. +- On macOS, drop unused Metal dependency. +- On Windows, fix the trail effect happening on transparent decorated windows. Borderless (or un-decorated) windows were not affected. +- On Windows, fix `with_maximized` not properly setting window size to entire window. +- On macOS, change `WindowExtMacOS::request_user_attention()` to take an `enum` instead of a `bool`. + +## 0.20.0-alpha1 + +- Changes below are considered **breaking**. +- Change all occurrences of `EventsLoop` to `EventLoop`. +- Previously flat API is now exposed through `event`, `event_loop`, `monitor`, and `window` modules. +- `os` module changes: + - Renamed to `platform`. + - All traits now have platform-specific suffixes. + - Exposes new `desktop` module on Windows, Mac, and Linux. +- Changes to event loop types: + - `EventLoopProxy::wakeup` has been removed in favor of `send_event`. + - **Major:** New `run` method drives winit event loop. + - Returns `!` to ensure API behaves identically across all supported platforms. + - This allows `emscripten` implementation to work without lying about the API. + - `ControlFlow`'s variants have been replaced with `Wait`, `WaitUntil(Instant)`, `Poll`, and `Exit`. + - Is read after `EventsCleared` is processed. + - `Wait` waits until new events are available. + - `WaitUntil` waits until either new events are available or the provided time has been reached. + - `Poll` instantly resumes the event loop. + - `Exit` aborts the event loop. + - Takes a closure that implements `'static + FnMut(Event, &EventLoop, &mut ControlFlow)`. + - `&EventLoop` is provided to allow new `Window`s to be created. + - **Major:** `platform::desktop` module exposes `EventLoopExtDesktop` trait with `run_return` method. + - Behaves identically to `run`, but returns control flow to the calling context and can take non-`'static` closures. + - `EventLoop`'s `poll_events` and `run_forever` methods have been removed in favor of `run` and `run_return`. +- Changes to events: + - Remove `Event::Awakened` in favor of `Event::UserEvent(T)`. + - Can be sent with `EventLoopProxy::send_event`. + - Rename `WindowEvent::Refresh` to `WindowEvent::RedrawRequested`. + - `RedrawRequested` can be sent by the user with the `Window::request_redraw` method. + - `EventLoop`, `EventLoopProxy`, and `Event` are now generic over `T`, for use in `UserEvent`. + - **Major:** Add `NewEvents(StartCause)`, `EventsCleared`, and `LoopDestroyed` variants to `Event`. + - `NewEvents` is emitted when new events are ready to be processed by event loop. + - `StartCause` describes why new events are available, with `ResumeTimeReached`, `Poll`, `WaitCancelled`, and `Init` (sent once at start of loop). + - `EventsCleared` is emitted when all available events have been processed. + - Can be used to perform logic that depends on all events being processed (e.g. an iteration of a game loop). + - `LoopDestroyed` is emitted when the `run` or `run_return` method is about to exit. +- Rename `MonitorId` to `MonitorHandle`. +- Removed `serde` implementations from `ControlFlow`. +- Rename several functions to improve both internal consistency and compliance with Rust API guidelines. +- Remove `WindowBuilder::multitouch` field, since it was only implemented on a few platforms. Multitouch is always enabled now. +- **Breaking:** On macOS, change `ns` identifiers to use snake_case for consistency with iOS's `ui` identifiers. +- Add `MonitorHandle::video_modes` method for retrieving supported video modes for the given monitor. +- On Wayland, the window now exists even if nothing has been drawn. +- On Windows, fix initial dimensions of a fullscreen window. +- On Windows, Fix transparent borderless windows rendering wrong. diff --git a/vendor/winit/src/changelog/v0.21.md b/vendor/winit/src/changelog/v0.21.md new file mode 100644 index 00000000..48f3d354 --- /dev/null +++ b/vendor/winit/src/changelog/v0.21.md @@ -0,0 +1,16 @@ +## 0.21.0 + +- On Windows, fixed "error: linking with `link.exe` failed: exit code: 1120" error on older versions of windows. +- On macOS, fix set_minimized(true) works only with decorations. +- On macOS, add `hide_application` to `EventLoopWindowTarget` via a new `EventLoopWindowTargetExtMacOS` trait. `hide_application` will hide the entire application by calling `-[NSApplication hide: nil]`. +- On macOS, fix not sending ReceivedCharacter event for specific keys combinations. +- On macOS, fix `CursorMoved` event reporting the cursor position using logical coordinates. +- On macOS, fix issue where unbundled applications would sometimes open without being focused. +- On macOS, fix `run_return` does not return unless it receives a message. +- On Windows, fix bug where `RedrawRequested` would only get emitted every other iteration of the event loop. +- On X11, fix deadlock on window state when handling certain window events. +- `WindowBuilder` now implements `Default`. +- **Breaking:** `WindowEvent::CursorMoved` changed to `f64` units, preserving high-precision data supplied by most backends +- On Wayland, fix coordinates in mouse events when scale factor isn't 1 +- On Web, add the ability to provide a custom canvas +- **Breaking:** On Wayland, the `WaylandTheme` struct has been replaced with a `Theme` trait, allowing for extra configuration diff --git a/vendor/winit/src/changelog/v0.22.md b/vendor/winit/src/changelog/v0.22.md new file mode 100644 index 00000000..07bc1ef2 --- /dev/null +++ b/vendor/winit/src/changelog/v0.22.md @@ -0,0 +1,37 @@ +## 0.22.2 + +- Added Clone implementation for 'static events. +- On Windows, fix window intermittently hanging when `ControlFlow` was set to `Poll`. +- On Windows, fix `WindowBuilder::with_maximized` being ignored. +- On Android, minimal platform support. +- On iOS, touch positions are now properly converted to physical pixels. +- On macOS, updated core-* dependencies and cocoa + +## 0.22.1 + +- On X11, fix `ResumeTimeReached` being fired too early. +- On Web, replaced zero timeout for `ControlFlow::Poll` with `requestAnimationFrame` +- On Web, fix a possible panic during event handling +- On macOS, fix `EventLoopProxy` leaking memory for every instance. + +## 0.22.0 + +- On Windows, fix minor timing issue in wait_until_time_or_msg +- On Windows, rework handling of request_redraw() to address panics. +- On macOS, fix `set_simple_screen` to remember frame excluding title bar. +- On Wayland, fix coordinates in touch events when scale factor isn't 1. +- On Wayland, fix color from `close_button_icon_color` not applying. +- Ignore locale if unsupported by X11 backend +- On Wayland, Add HiDPI cursor support +- On Web, add the ability to query "Light" or "Dark" system theme send `ThemeChanged` on change. +- Fix `Event::to_static` returning `None` for user events. +- On Wayland, Hide CSD for fullscreen windows. +- On Windows, ignore spurious mouse move messages. +- **Breaking:** Move `ModifiersChanged` variant from `DeviceEvent` to `WindowEvent`. +- On Windows, add `IconExtWindows` trait which exposes creating an `Icon` from an external file or embedded resource +- Add `BadIcon::OsError` variant for when OS icon functionality fails +- On Windows, fix crash at startup on systems that do not properly support Windows' Dark Mode +- Revert On macOS, fix not sending ReceivedCharacter event for specific keys combinations. +- on macOS, fix incorrect ReceivedCharacter events for some key combinations. +- **Breaking:** Use `i32` instead of `u32` for position type in `WindowEvent::Moved`. +- On macOS, a mouse motion event is now generated before every mouse click. diff --git a/vendor/winit/src/changelog/v0.23.md b/vendor/winit/src/changelog/v0.23.md new file mode 100644 index 00000000..33ea2abd --- /dev/null +++ b/vendor/winit/src/changelog/v0.23.md @@ -0,0 +1,65 @@ +## 0.23.0 + +- On iOS, fixed support for the "Debug View Hierarchy" feature in Xcode. +- On all platforms, `available_monitors` and `primary_monitor` are now on `EventLoopWindowTarget` rather than `EventLoop` to list monitors event in the event loop. +- On Unix, X11 and Wayland are now optional features (enabled by default) +- On X11, fix deadlock when calling `set_fullscreen_inner`. +- On Web, prevent the webpage from scrolling when the user is focused on a winit canvas +- On Web, calling `window.set_cursor_icon` no longer breaks HiDPI scaling +- On Windows, drag and drop is now optional (enabled by default) and can be disabled with `WindowBuilderExtWindows::with_drag_and_drop(false)`. +- On Wayland, fix deadlock when calling to `set_inner_size` from a callback. +- On macOS, add `hide__other_applications` to `EventLoopWindowTarget` via existing `EventLoopWindowTargetExtMacOS` trait. `hide_other_applications` will hide other applications by calling `-[NSApplication hideOtherApplications: nil]`. +- On android added support for `run_return`. +- On MacOS, Fixed fullscreen and dialog support for `run_return`. +- On Windows, fix bug where we'd try to emit `MainEventsCleared` events during nested win32 event loops. +- On Web, use mouse events if pointer events aren't supported. This affects Safari. +- On Windows, `set_ime_position` is now a no-op instead of a runtime crash. +- On Android, `set_fullscreen` is now a no-op instead of a runtime crash. +- On iOS and Android, `set_inner_size` is now a no-op instead of a runtime crash. +- On Android, fix `ControlFlow::Poll` not polling the Android event queue. +- On macOS, add `NSWindow.hasShadow` support. +- On Web, fix vertical mouse wheel scrolling being inverted. +- On Web, implement mouse capturing for click-dragging out of the canvas. +- On Web, fix `ControlFlow::Exit` not properly handled. +- On Web (web-sys only), send `WindowEvent::ScaleFactorChanged` event when `window.devicePixelRatio` is changed. +- **Breaking:** On Web, `set_cursor_position` and `set_cursor_grab` will now always return an error. +- **Breaking:** `PixelDelta` scroll events now return a `PhysicalPosition`. +- On NetBSD, fixed crash due to incorrect detection of the main thread. +- **Breaking:** On X11, `-` key is mapped to the `Minus` virtual key code, instead of `Subtract`. +- On macOS, fix inverted horizontal scroll. +- **Breaking:** `current_monitor` now returns `Option`. +- **Breaking:** `primary_monitor` now returns `Option`. +- On macOS, updated core-* dependencies and cocoa. +- Bump `parking_lot` to 0.11 +- On Android, bump `ndk`, `ndk-sys` and `ndk-glue` to 0.2. Checkout the new ndk-glue main proc attribute. +- On iOS, fixed starting the app in landscape where the view still had portrait dimensions. +- Deprecate the stdweb backend, to be removed in a future release +- **Breaking:** Prefixed virtual key codes `Add`, `Multiply`, `Divide`, `Decimal`, and `Subtract` with `Numpad`. +- Added `Asterisk` and `Plus` virtual key codes. +- On Web (web-sys only), the `Event::LoopDestroyed` event is correctly emitted when leaving the page. +- On Web, the `WindowEvent::Destroyed` event now gets emitted when a `Window` is dropped. +- On Web (web-sys only), the event listeners are now removed when a `Window` is dropped or when the event loop is destroyed. +- On Web, the event handler closure passed to `EventLoop::run` now gets dropped after the event loop is destroyed. +- **Breaking:** On Web, the canvas element associated to a `Window` is no longer removed from the DOM when the `Window` is dropped. +- On Web, `WindowEvent::Resized` is now emitted when `Window::set_inner_size` is called. +- **Breaking:** `Fullscreen` enum now uses `Borderless(Option)` instead of `Borderless(MonitorHandle)` to allow picking the current monitor. +- On MacOS, fix `WindowEvent::Moved` ignoring the scale factor. +- On Wayland, add missing virtual keycodes. +- On Wayland, implement proper `set_cursor_grab`. +- On Wayland, the cursor will use similar icons if the requested one isn't available. +- On Wayland, right clicking on client side decorations will request application menu. +- On Wayland, fix tracking of window size after state changes. +- On Wayland, fix client side decorations not being hidden properly in fullscreen. +- On Wayland, fix incorrect size event when entering fullscreen with client side decorations. +- On Wayland, fix `resizable` attribute not being applied properly on startup. +- On Wayland, fix disabled repeat rate not being handled. +- On Wayland, fix decoration buttons not working after tty switch. +- On Wayland, fix scaling not being applied on output re-enable. +- On Wayland, fix crash when `XCURSOR_SIZE` is `0`. +- On Wayland, fix pointer getting created in some cases without pointer capability. +- On Wayland, on kwin, fix space between window and decorations on startup. +- **Breaking:** On Wayland, `Theme` trait was reworked. +- On Wayland, disable maximize button for non-resizable window. +- On Wayland, added support for `set_ime_position`. +- On Wayland, fix crash on startup since GNOME 3.37.90. +- On X11, fix incorrect modifiers state on startup. diff --git a/vendor/winit/src/changelog/v0.24.md b/vendor/winit/src/changelog/v0.24.md new file mode 100644 index 00000000..fa0a27b2 --- /dev/null +++ b/vendor/winit/src/changelog/v0.24.md @@ -0,0 +1,28 @@ +## 0.24.0 + +- On Windows, fix applications not exiting gracefully due to thread_event_target_callback accessing corrupted memory. +- On Windows, implement `Window::set_ime_position`. +- **Breaking:** On Windows, Renamed `WindowBuilderExtWindows`'s `is_dark_mode` to `theme`. +- **Breaking:** On Windows, renamed `WindowBuilderExtWindows::is_dark_mode` to `theme`. +- On Windows, add `WindowBuilderExtWindows::with_theme` to set a preferred theme. +- On Windows, fix bug causing message boxes to appear delayed. +- On Android, calling `WindowEvent::Focused` now works properly instead of always returning false. +- On Windows, fix Alt-Tab behaviour by removing borderless fullscreen "always on top" flag. +- On Windows, fix bug preventing windows with transparency enabled from having fully-opaque regions. +- **Breaking:** On Windows, include prefix byte in scancodes. +- On Wayland, fix window not being resizeable when using `WindowBuilder::with_min_inner_size`. +- On Unix, fix cross-compiling to wasm32 without enabling X11 or Wayland. +- On Windows, fix use-after-free crash during window destruction. +- On Web, fix `WindowEvent::ReceivedCharacter` never being sent on key input. +- On macOS, fix compilation when targeting aarch64. +- On X11, fix `Window::request_redraw` not waking the event loop. +- On Wayland, the keypad arrow keys are now recognized. +- **Breaking** Rename `desktop::EventLoopExtDesktop` to `run_return::EventLoopExtRunReturn`. +- Added `request_user_attention` method to `Window`. +- **Breaking:** On macOS, removed `WindowExt::request_user_attention`, use `Window::request_user_attention`. +- **Breaking:** On X11, removed `WindowExt::set_urgent`, use `Window::request_user_attention`. +- On Wayland, default font size in CSD increased from 11 to 17. +- On Windows, fix bug causing message boxes to appear delayed. +- On Android, support multi-touch. +- On Wayland, extra mouse buttons are not dropped anymore. +- **Breaking**: `MouseButton::Other` now uses `u16`. diff --git a/vendor/winit/src/changelog/v0.25.md b/vendor/winit/src/changelog/v0.25.md new file mode 100644 index 00000000..00451c08 --- /dev/null +++ b/vendor/winit/src/changelog/v0.25.md @@ -0,0 +1,31 @@ +## 0.25.0 + +- **Breaking:** On macOS, replace `WindowBuilderExtMacOS::with_activation_policy` with `EventLoopExtMacOS::set_activation_policy` +- On macOS, wait with activating the application until the application has initialized. +- On macOS, fix creating new windows when the application has a main menu. +- On Windows, fix fractional deltas for mouse wheel device events. +- On macOS, fix segmentation fault after dropping the main window. +- On Android, `InputEvent::KeyEvent` is partially implemented providing the key scancode. +- Added `is_maximized` method to `Window`. +- On Windows, fix bug where clicking the decoration bar would make the cursor blink. +- On Windows, fix bug causing newly created windows to erroneously display the "wait" (spinning) cursor. +- On macOS, wake up the event loop immediately when a redraw is requested. +- On Windows, change the default window size (1024x768) to match the default on other desktop platforms (800x600). +- On Windows, fix bug causing mouse capture to not be released. +- On Windows, fix fullscreen not preserving minimized/maximized state. +- On Android, unimplemented events are marked as unhandled on the native event loop. +- On Windows, added `WindowBuilderExtWindows::with_menu` to set a custom menu at window creation time. +- On Android, bump `ndk` and `ndk-glue` to 0.3: use predefined constants for event `ident`. +- On macOS, fix objects captured by the event loop closure not being dropped on panic. +- On Windows, fixed `WindowEvent::ThemeChanged` not properly firing and fixed `Window::theme` returning the wrong theme. +- On Web, added support for `DeviceEvent::MouseMotion` to listen for relative mouse movements. +- Added `WindowBuilder::with_position` to allow setting the position of a `Window` on creation. Supported on Windows, macOS and X11. +- Added `Window::drag_window`. Implemented on Windows, macOS, X11 and Wayland. +- On X11, bump `mio` to 0.7. +- On Windows, added `WindowBuilderExtWindows::with_owner_window` to allow creating popup windows. +- On Windows, added `WindowExtWindows::set_enable` to allow creating modal popup windows. +- On macOS, emit `RedrawRequested` events immediately while the window is being resized. +- Implement `Default`, `Hash`, and `Eq` for `LogicalPosition`, `PhysicalPosition`, `LogicalSize`, and `PhysicalSize`. +- On macOS, initialize the Menu Bar with minimal defaults. (Can be prevented using `enable_default_menu_creation`) +- On macOS, change the default behavior for first click when the window was unfocused. Now the window becomes focused and then emits a `MouseInput` event on a "first mouse click". +- Implement mint (math interoperability standard types) conversions (under feature flag `mint`). diff --git a/vendor/winit/src/changelog/v0.26.md b/vendor/winit/src/changelog/v0.26.md new file mode 100644 index 00000000..d33d4a3b --- /dev/null +++ b/vendor/winit/src/changelog/v0.26.md @@ -0,0 +1,36 @@ +## 0.26.1 + +- Fix linking to the `ColorSync` framework on macOS 10.7, and in newer Rust versions. +- On Web, implement cursor grabbing through the pointer lock API. +- On X11, add mappings for numpad comma, numpad enter, numlock and pause. +- On macOS, fix Pinyin IME input by reverting a change that intended to improve IME. +- On Windows, fix a crash with transparent windows on Windows 11. + +## 0.26.0 + +- Update `raw-window-handle` to `v0.4`. This is _not_ a breaking change, we still implement `HasRawWindowHandle` from `v0.3`, see [rust-windowing/raw-window-handle#74](https://github.com/rust-windowing/raw-window-handle/pull/74). Note that you might have to run `cargo update -p raw-window-handle` after upgrading. +- On X11, bump `mio` to 0.8. +- On Android, fixed `WindowExtAndroid::config` initially returning an empty `Configuration`. +- On Android, fixed `Window::scale_factor` and `MonitorHandle::scale_factor` initially always returning 1.0. +- On X11, select an appropriate visual for transparency if is requested +- On Wayland and X11, fix diagonal window resize cursor orientation. +- On macOS, drop the event callback before exiting. +- On Android, implement `Window::request_redraw` +- **Breaking:** On Web, remove the `stdweb` backend. +- Added `Window::focus_window`to bring the window to the front and set input focus. +- On Wayland and X11, implement `is_maximized` method on `Window`. +- On Windows, prevent ghost window from showing up in the taskbar after either several hours of use or restarting `explorer.exe`. +- On macOS, fix issue where `ReceivedCharacter` was not being emitted during some key repeat events. +- On Wayland, load cursor icons `hand2` and `hand1` for `CursorIcon::Hand`. +- **Breaking:** On Wayland, Theme trait and its support types are dropped. +- On Wayland, bump `smithay-client-toolkit` to 0.15.1. +- On Wayland, implement `request_user_attention` with `xdg_activation_v1`. +- On X11, emit missing `WindowEvent::ScaleFactorChanged` when the only monitor gets reconnected. +- On X11, if RANDR based scale factor is higher than 20 reset it to 1 +- On Wayland, add an enabled-by-default feature called `wayland-dlopen` so users can opt out of using `dlopen` to load system libraries. +- **Breaking:** On Android, bump `ndk` and `ndk-glue` to 0.5. +- On Windows, increase wait timer resolution for more accurate timing when using `WaitUntil`. +- On macOS, fix native file dialogs hanging the event loop. +- On Wayland, implement a workaround for wrong configure size when using `xdg_decoration` in `kwin_wayland` +- On macOS, fix an issue that prevented the menu bar from showing in borderless fullscreen mode. +- On X11, EINTR while polling for events no longer causes a panic. Instead it will be treated as a spurious wakeup. diff --git a/vendor/winit/src/changelog/v0.27.md b/vendor/winit/src/changelog/v0.27.md new file mode 100644 index 00000000..5f067f59 --- /dev/null +++ b/vendor/winit/src/changelog/v0.27.md @@ -0,0 +1,107 @@ +## 0.27.5 + +- On Wayland, fix byte offset in `Ime::Preedit` pointing to invalid bytes. + +## 0.27.4 + +- On Windows, emit `ReceivedCharacter` events on system keybindings. +- On Windows, fixed focus event emission on minimize. +- On X11, fixed IME crashing during reload. + +## 0.27.3 + +- On Windows, added `WindowExtWindows::set_undecorated_shadow` and `WindowBuilderExtWindows::with_undecorated_shadow` to draw the drop shadow behind a borderless window. +- On Windows, fixed default window features (ie snap, animations, shake, etc.) when decorations are disabled. +- On Windows, fixed ALT+Space shortcut to open window menu. +- On Wayland, fixed `Ime::Preedit` not being sent on IME reset. +- Fixed unbound version specified for `raw-window-handle` leading to compilation failures. +- Empty `Ime::Preedit` event will be sent before `Ime::Commit` to help clearing preedit. +- On X11, fixed IME context picking by querying for supported styles beforehand. + +## 0.27.2 + +- On macOS, fixed touch phase reporting when scrolling. +- On X11, fix min, max and resize increment hints not persisting for resizable windows (e.g. on DPI change). +- On Windows, respect min/max inner sizes when creating the window. +- For backwards compatibility, `Window` now (additionally) implements the old version (`0.4`) of the `HasRawWindowHandle` trait +- On Windows, added support for `EventLoopWindowTarget::set_device_event_filter`. +- On Wayland, fix user requested `WindowEvent::RedrawRequested` being delayed by a frame. + +## 0.27.1 + +- The minimum supported Rust version was lowered to `1.57.0` and now explicitly tested. +- On X11, fix crash on start due to inability to create an IME context without any preedit. + +## 0.27.0 + +- On Windows, fix hiding a maximized window. +- On Android, `ndk-glue`'s `NativeWindow` lock is now held between `Event::Resumed` and `Event::Suspended`. +- On Web, added `EventLoopExtWebSys` with a `spawn` method to start the event loop without throwing an exception. +- Added `WindowEvent::Occluded(bool)`, currently implemented on macOS and X11. +- On X11, fix events for caps lock key not being sent +- Build docs on `docs.rs` for iOS and Android as well. +- **Breaking:** Removed the `WindowAttributes` struct, since all its functionality is accessible from `WindowBuilder`. +- Added `WindowBuilder::transparent` getter to check if the user set `transparent` attribute. +- On macOS, Fix emitting `Event::LoopDestroyed` on CMD+Q. +- On macOS, fixed an issue where having multiple windows would prevent run_return from ever returning. +- On Wayland, fix bug where the cursor wouldn't hide in GNOME. +- On macOS, Windows, and Wayland, add `set_cursor_hittest` to let the window ignore mouse events. +- On Windows, added `WindowExtWindows::set_skip_taskbar` and `WindowBuilderExtWindows::with_skip_taskbar`. +- On Windows, added `EventLoopBuilderExtWindows::with_msg_hook`. +- On Windows, remove internally unique DC per window. +- On macOS, remove the need to call `set_ime_position` after moving the window. +- Added `Window::is_visible`. +- Added `Window::is_resizable`. +- Added `Window::is_decorated`. +- On X11, fix for repeated event loop iteration when `ControlFlow` was `Wait` +- On X11, fix scale factor calculation when the only monitor is reconnected +- On Wayland, report unaccelerated mouse deltas in `DeviceEvent::MouseMotion`. +- On Web, a focused event is manually generated when a click occurs to emulate behaviour of other backends. +- **Breaking:** Bump `ndk` version to 0.6, ndk-sys to `v0.3`, `ndk-glue` to `0.6`. +- Remove no longer needed `WINIT_LINK_COLORSYNC` environment variable. +- **Breaking:** Rename the `Exit` variant of `ControlFlow` to `ExitWithCode`, which holds a value to control the exit code after running. Add an `Exit` constant which aliases to `ExitWithCode(0)` instead to avoid major breakage. This shouldn't affect most existing programs. +- Add `EventLoopBuilder`, which allows you to create and tweak the settings of an event loop before creating it. +- Deprecated `EventLoop::with_user_event`; use `EventLoopBuilder::with_user_event` instead. +- **Breaking:** Replaced `EventLoopExtMacOS` with `EventLoopBuilderExtMacOS` (which also has renamed methods). +- **Breaking:** Replaced `EventLoopExtWindows` with `EventLoopBuilderExtWindows` (which also has renamed methods). +- **Breaking:** Replaced `EventLoopExtUnix` with `EventLoopBuilderExtUnix` (which also has renamed methods). +- **Breaking:** The platform specific extensions for Windows `winit::platform::windows` have changed. All `HANDLE`-like types e.g. `HWND` and `HMENU` were converted from winapi types or `*mut c_void` to `isize`. This was done to be consistent with the type definitions in windows-sys and to not expose internal dependencies. +- The internal bindings to the [Windows API](https://docs.microsoft.com/en-us/windows/) were changed from the unofficial [winapi](https://github.com/retep998/winapi-rs) bindings to the official Microsoft [windows-sys](https://github.com/microsoft/windows-rs) bindings. +- On Wayland, fix polling during consecutive `EventLoop::run_return` invocations. +- On Windows, fix race issue creating fullscreen windows with `WindowBuilder::with_fullscreen` +- On Android, `virtual_keycode` for `KeyboardInput` events is now filled in where a suitable match is found. +- Added helper methods on `ControlFlow` to set its value. +- On Wayland, fix `TouchPhase::Ended` always reporting the location of the first touch down, unless the compositor + sent a cancel or frame event. +- On iOS, send `RedrawEventsCleared` even if there are no redraw events, consistent with other platforms. +- **Breaking:** Replaced `Window::with_app_id` and `Window::with_class` with `Window::with_name` on `WindowBuilderExtUnix`. +- On Wayland, fallback CSD was replaced with proper one: + - `WindowBuilderExtUnix::with_wayland_csd_theme` to set color theme in builder. + - `WindowExtUnix::wayland_set_csd_theme` to set color theme when creating a window. + - `WINIT_WAYLAND_CSD_THEME` env variable was added, it can be used to set "dark"/"light" theme in apps that don't expose theme setting. + - `wayland-csd-adwaita` feature that enables proper CSD with title rendering using FreeType system library. + - `wayland-csd-adwaita-notitle` feature that enables CSD but without title rendering. +- On Wayland and X11, fix window not resizing with `Window::set_inner_size` after calling `Window:set_resizable(false)`. +- On Windows, fix wrong fullscreen monitors being recognized when handling WM_WINDOWPOSCHANGING messages +- **Breaking:** Added new `WindowEvent::Ime` supported on desktop platforms. +- Added `Window::set_ime_allowed` supported on desktop platforms. +- **Breaking:** IME input on desktop platforms won't be received unless it's explicitly allowed via `Window::set_ime_allowed` and new `WindowEvent::Ime` events are handled. +- On macOS, `WindowEvent::Resized` is now emitted in `frameDidChange` instead of `windowDidResize`. +- **Breaking:** On X11, device events are now ignored for unfocused windows by default, use `EventLoopWindowTarget::set_device_event_filter` to set the filter level. +- Implemented `Default` on `EventLoop<()>`. +- Implemented `Eq` for `Fullscreen`, `Theme`, and `UserAttentionType`. +- **Breaking:** `Window::set_cursor_grab` now accepts `CursorGrabMode` to control grabbing behavior. +- On Wayland, add support for `Window::set_cursor_position`. +- Fix on macOS `WindowBuilder::with_disallow_hidpi`, setting true or false by the user no matter the SO default value. +- `EventLoopBuilder::build` will now panic when the `EventLoop` is being created more than once. +- Added `From` for `WindowId` and `From` for `u64`. +- Added `MonitorHandle::refresh_rate_millihertz` to get monitor's refresh rate. +- **Breaking**, Replaced `VideoMode::refresh_rate` with `VideoMode::refresh_rate_millihertz` providing better precision. +- On Web, add `with_prevent_default` and `with_focusable` to `WindowBuilderExtWebSys` to control whether events should be propagated. +- On Windows, fix focus events being sent to inactive windows. +- **Breaking**, update `raw-window-handle` to `v0.5` and implement `HasRawDisplayHandle` for `Window` and `EventLoopWindowTarget`. +- On X11, add function `register_xlib_error_hook` into `winit::platform::unix` to subscribe for errors coming from Xlib. +- On Android, upgrade `ndk` and `ndk-glue` dependencies to the recently released `0.7.0`. +- All platforms can now be relied on to emit a `Resumed` event. Applications are recommended to lazily initialize graphics state and windows on first resume for portability. +- **Breaking:**: Reverse horizontal scrolling sign in `MouseScrollDelta` to match the direction of vertical scrolling. A positive X value now means moving the content to the right. The meaning of vertical scrolling stays the same: a positive Y value means moving the content down. +- On MacOS, fix deadlock when calling `set_maximized` from event loop. diff --git a/vendor/winit/src/changelog/v0.28.md b/vendor/winit/src/changelog/v0.28.md new file mode 100644 index 00000000..a8b3262f --- /dev/null +++ b/vendor/winit/src/changelog/v0.28.md @@ -0,0 +1,100 @@ +## 0.28.7 + +- Fix window size sometimes being invalid when resizing on macOS 14 Sonoma. + +## 0.28.6 + +- On macOS, fixed memory leak when getting monitor handle. +- On macOS, fix `Backspace` being emitted when clearing preedit with it. + +## 0.28.5 + +- On macOS, fix `key_up` being ignored when `Ime` is disabled. + +## 0.28.4 + +- On macOS, fix empty marked text blocking regular input. +- On macOS, fix potential panic when getting refresh rate. +- On macOS, fix crash when calling `Window::set_ime_position` from another thread. + +## 0.28.3 + +- Fix macOS memory leaks. + +## 0.28.2 + +- Implement `HasRawDisplayHandle` for `EventLoop`. +- On macOS, set resize increments only for live resizes. +- On Wayland, fix rare crash on DPI change +- Web: Added support for `Window::theme`. +- On Wayland, fix rounding issues when doing resize. +- On macOS, fix wrong focused state on startup. +- On Windows, fix crash on setting taskbar when using Visual Studio debugger. +- On macOS, resize simple fullscreen windows on windowDidChangeScreen events. + +## 0.28.1 + +- On Wayland, fix crash when dropping a window in multi-window setup. + +## 0.28.0 + +- On macOS, fixed `Ime::Commit` persisting for all input after interacting with `Ime`. +- On macOS, added `WindowExtMacOS::option_as_alt` and `WindowExtMacOS::set_option_as_alt`. +- On Windows, fix window size for maximized, undecorated windows. +- On Windows and macOS, add `WindowBuilder::with_active`. +- Add `Window::is_minimized`. +- On X11, fix errors handled during `register_xlib_error_hook` invocation bleeding into winit. +- Add `Window::has_focus`. +- On Windows, fix `Window::set_minimized(false)` not working for windows minimized by `Win + D` hotkey. +- **Breaking:** On Web, touch input no longer fires `WindowEvent::Cursor*`, `WindowEvent::MouseInput`, or `DeviceEvent::MouseMotion` like other platforms, but instead it fires `WindowEvent::Touch`. +- **Breaking:** Removed platform specific `WindowBuilder::with_parent` API in favor of `WindowBuilder::with_parent_window`. +- On Windows, retain `WS_MAXIMIZE` window style when un-minimizing a maximized window. +- On Windows, fix left mouse button release event not being sent after `Window::drag_window`. +- On macOS, run most actions on the main thread, which is strictly more correct, but might make multithreaded applications block slightly more. +- On macOS, fix panic when getting current monitor without any monitor attached. +- On Windows and MacOS, add API to enable/disable window buttons (close, minimize, ...etc). +- On Windows, macOS, X11 and Wayland, add `Window::set_theme`. +- **Breaking:** Remove `WindowExtWayland::wayland_set_csd_theme` and `WindowBuilderExtX11::with_gtk_theme_variant`. +- On Windows, revert window background to an empty brush to avoid white flashes when changing scaling. +- **Breaking:** Removed `Window::set_always_on_top` and related APIs in favor of `Window::set_window_level`. +- On Windows, MacOS and X11, add always on bottom APIs. +- On Windows, fix the value in `MouseButton::Other`. +- On macOS, add `WindowExtMacOS::is_document_edited` and `WindowExtMacOS::set_document_edited` APIs. +- **Breaking:** Removed `WindowBuilderExtIOS::with_root_view_class`; instead, you should use `[[view layer] addSublayer: ...]` to add an instance of the desired layer class (e.g. `CAEAGLLayer` or `CAMetalLayer`). See `vulkano-win` or `wgpu` for examples of this. +- On MacOS and Windows, add `Window::set_content_protected`. +- On MacOS, add `EventLoopBuilderExtMacOS::with_activate_ignoring_other_apps`. +- On Windows, fix icons specified on `WindowBuilder` not taking effect for windows created after the first one. +- On Windows and macOS, add `Window::title` to query the current window title. +- On Windows, fix focusing menubar when pressing `Alt`. +- On MacOS, made `accepts_first_mouse` configurable. +- Migrated `WindowBuilderExtUnix::with_resize_increments` to `WindowBuilder`. +- Added `Window::resize_increments`/`Window::set_resize_increments` to update resize increments at runtime for X11/macOS. +- macOS/iOS: Use `objc2` instead of `objc` internally. +- **Breaking:** Bump MSRV from `1.57` to `1.60`. +- **Breaking:** Split the `platform::unix` module into `platform::x11` and `platform::wayland`. The extension types are similarly renamed. +- **Breaking:**: Removed deprecated method `platform::unix::WindowExtUnix::is_ready`. +- Removed `parking_lot` dependency. +- **Breaking:** On macOS, add support for two-finger touchpad magnification and rotation gestures with new events `WindowEvent::TouchpadMagnify` and `WindowEvent::TouchpadRotate`. Also add support for touchpad smart-magnification gesture with a new event `WindowEvent::SmartMagnify`. +- **Breaking:** On web, the `WindowBuilderExtWebSys::with_prevent_default` setting (enabled by default), now additionally prevents scrolling of the webpage in mobile browsers, previously it only disabled scrolling on desktop. +- On Wayland, `wayland-csd-adwaita` now uses `ab_glyph` instead of `crossfont` to render the title for decorations. +- On Wayland, a new `wayland-csd-adwaita-crossfont` feature was added to use `crossfont` instead of `ab_glyph` for decorations. +- On Wayland, if not otherwise specified use upstream automatic CSD theme selection. +- On X11, added `WindowExtX11::with_parent` to create child windows. +- Added support for `WindowBuilder::with_theme` and `Window::theme` to support per-window dark/light/system theme configuration on macos, windows and wayland. +- On macOS, added support for `WindowEvent::ThemeChanged`. +- **Breaking:** Removed `WindowBuilderExtWindows::with_theme` and `WindowBuilderExtWayland::with_wayland_csd_theme` in favour of `WindowBuilder::with_theme`. +- **Breaking:** Removed `WindowExtWindows::theme` in favour of `Window::theme`. +- Enabled `doc_auto_cfg` when generating docs on docs.rs for feature labels. +- **Breaking:** On Android, switched to using [`android-activity`](https://github.com/rib/android-activity) crate as a glue layer instead of [`ndk-glue`](https://github.com/rust-windowing/android-ndk-rs/tree/master/ndk-glue). See [README.md#Android](https://github.com/rust-windowing/winit#Android) for more details. ([#2444](https://github.com/rust-windowing/winit/pull/2444)) +- **Breaking:** Removed support for `raw-window-handle` version `0.4` +- On Wayland, `RedrawRequested` not emitted during resize. +- Add a `set_wait_timeout` function to `ControlFlow` to allow waiting for a `Duration`. +- **Breaking:** Remove the unstable `xlib_xconnection()` function from the private interface. +- Added Orbital support for Redox OS +- On X11, added `drag_resize_window` method. +- Added `Window::set_transparent` to provide a hint about transparency of the window on Wayland and macOS. +- On macOS, fix the mouse buttons other than left/right/middle being reported as middle. +- On Wayland, support fractional scaling via the wp-fractional-scale protocol. +- On web, fix removal of mouse event listeners from the global object upon window destruction. +- Add WindowAttributes getter to WindowBuilder to allow introspection of default values. +- Added `Window::set_ime_purpose` for setting the IME purpose, currently implemented on Wayland only. diff --git a/vendor/winit/src/changelog/v0.29.md b/vendor/winit/src/changelog/v0.29.md new file mode 100644 index 00000000..fc17f170 --- /dev/null +++ b/vendor/winit/src/changelog/v0.29.md @@ -0,0 +1,289 @@ +## 0.29.15 + +- On X11, fix crash due to xsettings query on systems with incomplete xsettings. + +## 0.29.14 + +- On X11/Wayland, fix `text` and `text_with_all_modifiers` not being `None` during compose. +- On Wayland, don't reapply cursor grab when unchanged. +- On X11, fix a bug where some mouse events would be unexpectedly filtered out. + +## 0.29.13 + +- On Web, fix possible crash with `ControlFlow::Wait` and `ControlFlow::WaitUntil`. + +## 0.29.12 + +- On X11, fix use after free during xinput2 handling. +- On X11, filter close to zero values in mouse device events + +## 0.29.11 + +- Fix compatibility with 32-bit platforms without 64-bit atomics. +- On macOS, fix incorrect IME cursor rect origin. +- On Windows, fixed a race condition when sending an event through the loop proxy. +- On X11, fix swapped instance and general class names. +- On X11, don't require XIM to run. +- On X11, fix xkb state not being updated correctly sometimes leading to wrong input. +- On X11, reload dpi on `_XSETTINGS_SETTINGS` update. +- On X11, fix deadlock when adjusting DPI and resizing at the same time. +- On Wayland, disable `Occluded` event handling. +- On Wayland, fix DeviceEvent::Motion not being sent +- On Wayland, fix `Focused(false)` being send when other seats still have window focused. +- On Wayland, fix `Window::set_{min,max}_inner_size` not always applied. +- On Wayland, fix title in CSD not updated from `AboutToWait`. +- On Windows, fix inconsistent resizing behavior with multi-monitor setups when repositioning outside the event loop. +- On Wayland, fix `WAYLAND_SOCKET` not used when detecting platform. +- On Orbital, fix `logical_key` and `text` not reported in `KeyEvent`. +- On Orbital, implement `KeyEventExtModifierSupplement`. +- On Orbital, map keys to `NamedKey` when possible. +- On Orbital, implement `set_cursor_grab`. +- On Orbital, implement `set_cursor_visible`. +- On Orbital, implement `drag_window`. +- On Orbital, implement `drag_resize_window`. +- On Orbital, implement `set_transparent`. +- On Orbital, implement `set_visible`. +- On Orbital, implement `is_visible`. +- On Orbital, implement `set_resizable`. +- On Orbital, implement `is_resizable`. +- On Orbital, implement `set_maximized`. +- On Orbital, implement `is_maximized`. +- On Orbital, implement `set_decorations`. +- On Orbital, implement `is_decorated`. +- On Orbital, implement `set_window_level`. +- On Orbital, emit `DeviceEvent::MouseMotion`. + +## 0.29.10 + +- On Web, account for canvas being focused already before event loop starts. +- On Web, increase cursor position accuracy. + +## 0.29.9 + +- On X11, fix `NotSupported` error not propagated when creating event loop. +- On Wayland, fix resize not issued when scale changes +- On X11 and Wayland, fix arrow up on keypad reported as `ArrowLeft`. +- On macOS, report correct logical key when Ctrl or Cmd is pressed. + +## 0.29.8 + +- On X11, fix IME input lagging behind. +- On X11, fix `ModifiersChanged` not sent from xdotool-like input +- On X11, fix keymap not updated from xmodmap. +- On X11, reduce the amount of time spent fetching screen resources. +- On Wayland, fix `Window::request_inner_size` being overwritten by resize. +- On Wayland, fix `Window::inner_size` not using the correct rounding. + +## 0.29.7 + +- On X11, fix `Xft.dpi` reload during runtime. +- On X11, fix window minimize. + +## 0.29.6 + +- On Web, fix context menu not being disabled by `with_prevent_default(true)`. +- On Wayland, fix `WindowEvent::Destroyed` not being delivered after destroying window. +- Fix `EventLoopExtRunOnDemand::run_on_demand` not working for consequent invocation + +## 0.29.5 + +- On macOS, remove spurious error logging when handling `Fn`. +- On X11, fix an issue where floating point data from the server is + misinterpreted during a drag and drop operation. +- On X11, fix a bug where focusing the window would panic. +- On macOS, fix `refresh_rate_millihertz`. +- On Wayland, disable Client Side Decorations when `wl_subcompositor` is not supported. +- On X11, fix `Xft.dpi` detection from Xresources. +- On Windows, fix consecutive calls to `window.set_fullscreen(Some(Fullscreen::Borderless(None)))` resulting in losing previous window state when eventually exiting fullscreen using `window.set_fullscreen(None)`. +- On Wayland, fix resize being sent on focus change. +- On Windows, fix `set_ime_cursor_area`. + +## 0.29.4 + +- Fix crash when running iOS app on macOS. +- On X11, check common alternative cursor names when loading cursor. +- On X11, reload the DPI after a property change event. +- On Windows, fix so `drag_window` and `drag_resize_window` can be called from another thread. +- On Windows, fix `set_control_flow` in `AboutToWait` not being taken in account. +- On macOS, send a `Resized` event after each `ScaleFactorChanged` event. +- On Wayland, fix `wl_surface` being destroyed before associated objects. +- On macOS, fix assertion when pressing `Fn` key. +- On Windows, add `WindowBuilderExtWindows::with_clip_children` to control `WS_CLIPCHILDREN` style. + +## 0.29.3 + +- On Wayland, apply correct scale to `PhysicalSize` passed in `WindowBuilder::with_inner_size` when possible. +- On Wayland, fix `RedrawRequested` being always sent without decorations and `sctk-adwaita` feature. +- On Wayland, ignore resize requests when the window is fully tiled. +- On Wayland, use `configure_bounds` to constrain `with_inner_size` when compositor wants users to pick size. +- On Windows, fix deadlock when accessing the state during `Cursor{Enter,Leave}`. +- On Windows, add support for `Window::set_transparent`. +- On macOS, fix deadlock when entering a nested event loop from an event handler. +- On macOS, add support for `Window::set_blur`. + +## 0.29.2 + +- **Breaking:** Bump MSRV from `1.60` to `1.65`. +- **Breaking:** Add `Event::MemoryWarning`; implemented on iOS/Android. +- **Breaking:** Bump `ndk` version to `0.8.0`, ndk-sys to `0.5.0`, `android-activity` to `0.5.0`. +- **Breaking:** Change default `ControlFlow` from `Poll` to `Wait`. +- **Breaking:** Move `Event::RedrawRequested` to `WindowEvent::RedrawRequested`. +- **Breaking:** Moved `ControlFlow::Exit` to `EventLoopWindowTarget::exit()` and `EventLoopWindowTarget::exiting()` and removed `ControlFlow::ExitWithCode(_)` entirely. +- **Breaking:** Moved `ControlFlow` to `EventLoopWindowTarget::set_control_flow()` and `EventLoopWindowTarget::control_flow()`. +- **Breaking:** `EventLoop::new` and `EventLoopBuilder::build` now return `Result` +- **Breaking:** `WINIT_UNIX_BACKEND` was removed in favor of standard `WAYLAND_DISPLAY` and `DISPLAY` variables. +- **Breaking:** on Wayland, dispatching user created Wayland queue won't wake up the loop unless winit has event to send back. +- **Breaking:** remove `DeviceEvent::Text`. +- **Breaking:** Remove lifetime parameter from `Event` and `WindowEvent`. +- **Breaking:** Rename `Window::set_inner_size` to `Window::request_inner_size` and indicate if the size was applied immediately. +- **Breaking:** `ActivationTokenDone` event which could be requested with the new `startup_notify` module, see its docs for more. +- **Breaking:** `ScaleFactorChanged` now contains a writer instead of a reference to update inner size. +- **Breaking** `run() -> !` has been replaced by `run() -> Result<(), EventLoopError>` for returning errors without calling `std::process::exit()` ([#2767](https://github.com/rust-windowing/winit/pull/2767)) +- **Breaking** Removed `EventLoopExtRunReturn` / `run_return` in favor of `EventLoopExtPumpEvents` / `pump_events` and `EventLoopExtRunOnDemand` / `run_on_demand` ([#2767](https://github.com/rust-windowing/winit/pull/2767)) +- `RedrawRequested` is no longer guaranteed to be emitted after `MainEventsCleared`, it is now platform-specific when the event is emitted after being requested via `redraw_request()`. + - On Windows, `RedrawRequested` is now driven by `WM_PAINT` messages which are requested via `redraw_request()` +- **Breaking** `LoopDestroyed` renamed to `LoopExiting` ([#2900](https://github.com/rust-windowing/winit/issues/2900)) +- **Breaking** `RedrawEventsCleared` removed ([#2900](https://github.com/rust-windowing/winit/issues/2900)) +- **Breaking** `MainEventsCleared` removed ([#2900](https://github.com/rust-windowing/winit/issues/2900)) +- **Breaking:** Remove all deprecated `modifiers` fields. +- **Breaking:** Rename `DeviceEventFilter` to `DeviceEvents` reversing the behavior of variants. +- **Breaking** Add `AboutToWait` event which is emitted when the event loop is about to block and wait for new events ([#2900](https://github.com/rust-windowing/winit/issues/2900)) +- **Breaking:** Rename `EventLoopWindowTarget::set_device_event_filter` to `listen_device_events`. +- **Breaking:** Rename `Window::set_ime_position` to `Window::set_ime_cursor_area` adding a way to set exclusive zone. +- **Breaking:** `with_x11_visual` now takes the visual ID instead of the bare pointer. +- **Breaking** `MouseButton` now supports `Back` and `Forward` variants, emitted from mouse events on Wayland, X11, Windows, macOS and Web. +- **Breaking:** On Web, `instant` is now replaced by `web_time`. +- **Breaking:** On Web, dropped support for Safari versions below 13.1. +- **Breaking:** On Web, the canvas output bitmap size is no longer adjusted. +- **Breaking:** On Web, the canvas size is not controlled by Winit anymore and external changes to the canvas size will be reported through `WindowEvent::Resized`. +- **Breaking:** Updated `bitflags` crate version to `2`, which changes the API on exposed types. +- **Breaking:** `CursorIcon::Arrow` was removed. +- **Breaking:** `CursorIcon::Hand` is now named `CursorIcon::Pointer`. +- **Breaking:** `CursorIcon` is now used from the `cursor-icon` crate. +- **Breaking:** `WindowExtWebSys::canvas()` now returns an `Option`. +- **Breaking:** Overhaul keyboard input handling. + - Replace `KeyboardInput` with `KeyEvent` and `RawKeyEvent`. + - Change `WindowEvent::KeyboardInput` to contain a `KeyEvent`. + - Change `Event::Key` to contain a `RawKeyEvent`. + - Remove `Event::ReceivedCharacter`. In its place, you should use + `KeyEvent.text` in combination with `WindowEvent::Ime`. + - Replace `VirtualKeyCode` with the `Key` enum. + - Replace `ScanCode` with the `KeyCode` enum. + - Rename `ModifiersState::LOGO` to `SUPER` and `ModifiersState::CTRL` to `CONTROL`. + - Add `PhysicalKey` wrapping `KeyCode` and `NativeKeyCode`. + - Add `KeyCode` to refer to keys (roughly) by their physical location. + - Add `NativeKeyCode` to represent raw `KeyCode`s which Winit doesn't + understand. + - Add `Key` to represent the keys after they've been interpreted by the + active (software) keyboard layout. + - Add `NamedKey` to represent the categorized keys. + - Add `NativeKey` to represent raw `Key`s which Winit doesn't understand. + - Add `KeyLocation` to tell apart `Key`s which usually "mean" the same thing, + but can appear simultaneously in different spots on the same keyboard + layout. + - Add `Window::reset_dead_keys` to enable application-controlled cancellation + of dead key sequences. + - Add `KeyEventExtModifierSupplement` to expose additional (and less + portable) interpretations of a given key-press. + - Add `PhysicalKeyExtScancode`, which lets you convert between scancodes and + `PhysicalKey`. + - `ModifiersChanged` now uses dedicated `Modifiers` struct. +- Removed platform-specific extensions that should be retrieved through `raw-window-handle` trait implementations instead: + - `platform::windows::HINSTANCE`. + - `WindowExtWindows::hinstance`. + - `WindowExtWindows::hwnd`. + - `WindowExtIOS::ui_window`. + - `WindowExtIOS::ui_view_controller`. + - `WindowExtIOS::ui_view`. + - `WindowExtMacOS::ns_window`. + - `WindowExtMacOS::ns_view`. + - `EventLoopWindowTargetExtWayland::wayland_display`. + - `WindowExtWayland::wayland_surface`. + - `WindowExtWayland::wayland_display`. + - `WindowExtX11::xlib_window`. + - `WindowExtX11::xlib_display`. + - `WindowExtX11::xlib_screen_id`. + - `WindowExtX11::xcb_connection`. +- Reexport `raw-window-handle` in `window` module. +- Add `ElementState::is_pressed`. +- Add `Window::pre_present_notify` to notify winit before presenting to the windowing system. +- Add `Window::set_blur` to request a blur behind the window; implemented on Wayland for now. +- Add `Window::show_window_menu` to request a titlebar/system menu; implemented on Wayland/Windows for now. +- Implement `AsFd`/`AsRawFd` for `EventLoop` on X11 and Wayland. +- Implement `PartialOrd` and `Ord` for `MouseButton`. +- Implement `PartialOrd` and `Ord` on types in the `dpi` module. +- Make `WindowBuilder` `Send + Sync`. +- Make iOS `MonitorHandle` and `VideoMode` usable from other threads. +- Make iOS windows usable from other threads. +- On Android, add force data to touch events. +- On Android, added `EventLoopBuilderExtAndroid::handle_volume_keys` to indicate that the application will handle the volume keys manually. +- On Android, fix `DeviceId` to contain device id's. +- On Orbital, fix `ModifiersChanged` not being sent. +- On Wayland, `Window::outer_size` now accounts for **client side** decorations. +- On Wayland, add `Window::drag_resize_window` method. +- On Wayland, remove `WINIT_WAYLAND_CSD_THEME` variable. +- On Wayland, fix `TouchPhase::Canceled` being sent for moved events. +- On Wayland, fix forward compatibility issues. +- On Wayland, fix initial window size not restored for maximized/fullscreened on startup window. +- On Wayland, fix maximized startup not taking full size on GNOME. +- On Wayland, fix maximized window creation and window geometry handling. +- On Wayland, fix window not checking that it actually got initial configure event. +- On Wayland, make double clicking and moving the CSD frame more reliable. +- On Wayland, support `Occluded` event with xdg-shell v6 +- On Wayland, use frame callbacks to throttle `RedrawRequested` events so redraws will align with compositor. +- On Web, `ControlFlow::WaitUntil` now uses the Prioritized Task Scheduling API. `setTimeout()`, with a trick to circumvent throttling to 4ms, is used as a fallback. +- On Web, `EventLoopProxy` now implements `Send`. +- On Web, `Window` now implements `Send` and `Sync`. +- On Web, account for CSS `padding`, `border`, and `margin` when getting or setting the canvas position. +- On Web, add Fullscreen API compatibility for Safari. +- On Web, add `DeviceEvent::Motion`, `DeviceEvent::MouseWheel`, `DeviceEvent::Button` and `DeviceEvent::Key` support. +- On Web, add `EventLoopWindowTargetExtWebSys` and `PollStrategy`, which allows to set different strategies for `ControlFlow::Poll`. By default the Prioritized Task Scheduling API is used, but an option to use `Window.requestIdleCallback` is available as well. Both use `setTimeout()`, with a trick to circumvent throttling to 4ms, as a fallback. +- On Web, add `WindowBuilderExtWebSys::with_append()` to append the canvas element to the web page on creation. +- On Web, allow event loops to be recreated with `spawn`. +- On Web, enable event propagation. +- On Web, fix `ControlFlow::WaitUntil` to never wake up **before** the given time. +- On Web, fix `DeviceEvent::MouseMotion` only being emitted for each canvas instead of the whole window. +- On Web, fix `Window:::set_fullscreen` doing nothing when called outside the event loop but during transient activation. +- On Web, fix pen treated as mouse input. +- On Web, fix pointer button events not being processed when a buttons is already pressed. +- On Web, fix scale factor resize suggestion always overwriting the canvas size. +- On Web, fix some `WindowBuilder` methods doing nothing. +- On Web, fix some `Window` methods using incorrect HTML attributes instead of CSS properties. +- On Web, fix the bfcache by not using the `beforeunload` event and map bfcache loading/unloading to `Suspended`/`Resumed` events. +- On Web, fix touch input not gaining or losing focus. +- On Web, fix touch location to be as accurate as mouse position. +- On Web, handle coalesced pointer events, which increases the resolution of pointer inputs. +- On Web, implement `Window::focus_window()`. +- On Web, implement `Window::set_(min|max)_inner_size()`. +- On Web, implement `WindowEvent::Occluded`. +- On Web, never return a `MonitorHandle`. +- On Web, prevent clicks on the canvas to select text. +- On Web, remove any fullscreen requests from the queue when an external fullscreen activation was detected. +- On Web, remove unnecessary `Window::is_dark_mode()`, which was replaced with `Window::theme()`. +- On Web, respect `EventLoopWindowTarget::listen_device_events()` settings. +- On Web, scale factor and dark mode detection are now more robust. +- On Web, send mouse position on button release as well. +- On Web, take all transient activations on the canvas and window into account to queue a fullscreen request. +- On Web, use `Window.requestAnimationFrame()` to throttle `RedrawRequested` events. +- On Web, use the correct canvas size when calculating the new size during scale factor change, instead of using the output bitmap size. +- On Web: fix `Window::request_redraw` not waking the event loop when called from outside the loop. +- On Web: fix position of touch events to be relative to the canvas. +- On Windows, add `drag_resize_window` method support. +- On Windows, add horizontal MouseWheel `DeviceEvent`. +- On Windows, added `WindowBuilderExtWindows::with_class_name` to customize the internal class name. +- On Windows, fix IME APIs not working when from non event loop thread. +- On Windows, fix `CursorEnter/Left` not being sent when grabbing the mouse. +- On Windows, fix `RedrawRequested` not being delivered when calling `Window::request_redraw` from `RedrawRequested`. +- On Windows, port to `windows-sys` version 0.48.0. +- On X11, add a `with_embedded_parent_window` function to the window builder to allow embedding a window into another window. +- On X11, fix event loop not waking up on `ControlFlow::Poll` and `ControlFlow::WaitUntil`. +- On X11, fix false positive flagging of key repeats when pressing different keys with no release between presses. +- On X11, set `visual_id` in returned `raw-window-handle`. +- On iOS, add ability to change the status bar style. +- On iOS, add force data to touch events when using the Apple Pencil. +- On iOS, always wake the event loop when transitioning from `ControlFlow::Poll` to `ControlFlow::Poll`. +- On iOS, send events `WindowEvent::Occluded(false)`, `WindowEvent::Occluded(true)` when application enters/leaves foreground. +- On macOS, add tabbing APIs on `WindowExtMacOS` and `EventLoopWindowTargetExtMacOS`. +- On macOS, fix assertion when pressing `Globe` key. +- On macOS, fix crash in `window.set_minimized(false)`. +- On macOS, fix crash when dropping `Window`. diff --git a/vendor/winit/src/changelog/v0.30.md b/vendor/winit/src/changelog/v0.30.md new file mode 100644 index 00000000..6b0b47dd --- /dev/null +++ b/vendor/winit/src/changelog/v0.30.md @@ -0,0 +1,413 @@ +## 0.30.13 + +### Added + +- On Wayland, add `Window::set_resize_increments`. + +### Fixed + +- On macOS, fixed crash when dragging non-file content onto window. +- On X11, fix `set_hittest` not working on some window managers. +- On X11, fix debug mode overflow panic in `set_timestamp`. +- On macOS, fix crash in `set_marked_text` when native Pinyin IME sends out-of-bounds `selected_range`. +- On Windows, fix `WM_IME_SETCONTEXT` IME UI flag masking on `lParam`. +- On Android, populate `KeyEvent::text` and `KeyEvent::text_with_all_modifiers` via `Key::to_text()`. + +## 0.30.12 + +### Fixed + +- On macOS, fix crash on macOS 26 by using objc2's `relax-sign-encoding` feature. + +## 0.30.11 + +### Fixed + +- On Windows, fixed crash in should_apps_use_dark_mode() for Windows versions < 17763. +- On Wayland, fixed `pump_events` driven loop deadlocking when loop was not drained before exit. + +## 0.30.10 + +### Added + +- On Windows, add `IconExtWindows::from_resource_name`. +- On Windows, add `CursorGrabMode::Locked`. +- On Wayland, add `WindowExtWayland::xdg_toplevel`. + +### Changed + +- On macOS, no longer need control of the main `NSApplication` class (which means you can now override it yourself). +- On iOS, remove custom application delegates. You are now allowed to override the + application delegate yourself. +- On iOS, no longer act as-if the application successfully open all URLs. Override + `application:didFinishLaunchingWithOptions:` and provide the desired behaviour yourself. + +### Fixed + +- On Windows, fixed ~500 ms pause when clicking the title bar during continuous redraw. +- On macOS, `WindowExtMacOS::set_simple_fullscreen` now honors `WindowExtMacOS::set_borderless_game` +- On X11 and Wayland, fixed pump_events with `Some(Duration::Zero)` blocking with `Wait` polling mode +- On Wayland, fixed a crash when consequently calling `set_cursor_grab` without pointer focus. +- On Wayland, ensure that external event loop is woken-up when using pump_events and integrating via `FD`. +- On Wayland, apply fractional scaling to custom cursors. +- On macOS, fixed `run_app_on_demand` returning without closing open windows. +- On macOS, fixed `VideoMode::refresh_rate_millihertz` for fractional refresh rates. +- On macOS, store monitor handle to avoid panics after going in/out of sleep. +- On macOS, allow certain invalid monitor handles and return `None` instead of panicking. +- On Windows, fixed `Ime::Preedit` cursor offset calculation. + +## 0.30.9 + +### Changed + +- On Wayland, no longer send an explicit clearing `Ime::Preedit` just prior to a new `Ime::Preedit`. + +### Fixed + +- On X11, fix crash with uim. +- On X11, fix modifiers for keys that were sent by the same X11 request. +- On iOS, fix high CPU usage even when using `ControlFlow::Wait`. + +## 0.30.8 + +### Added + +- `ActivationToken::from_raw` and `ActivationToken::into_raw`. +- On X11, add a workaround for disabling IME on GNOME. + +### Fixed + +- On Windows, fixed the event loop not waking on accessibility requests. +- On X11, fixed cursor grab mode state tracking on error. + +## 0.30.7 + +### Fixed + +- On X11, fixed KeyboardInput delivered twice when IME enabled. + +## 0.30.6 + +### Added + +- On macOS, add `WindowExtMacOS::set_borderless_game` and `WindowAttributesExtMacOS::with_borderless_game` + to fully disable the menu bar and dock in Borderless Fullscreen as commonly done in games. +- On X11, the `window` example now understands the `X11_VISUAL_ID` and `X11_SCREEN_ID` env + variables to test the respective modifiers of window creation. +- On Android, the soft keyboard can now be shown using `Window::set_ime_allowed`. +- Add basic iOS IME support. The soft keyboard can now be shown using `Window::set_ime_allowed`. + +### Fixed + +- On macOS, fix `WindowEvent::Moved` sometimes being triggered unnecessarily on resize. +- On macOS, package manifest definitions of `LSUIElement` will no longer be overridden with the + default activation policy, unless explicitly provided during initialization. +- On macOS, fix crash when calling `drag_window()` without a left click present. +- On X11, key events forward to IME anyway, even when it's disabled. +- On Windows, make `ControlFlow::WaitUntil` work more precisely using `CREATE_WAITABLE_TIMER_HIGH_RESOLUTION`. +- On X11, creating windows on screen that is not the first one (e.g. `DISPLAY=:0.1`) works again. +- On X11, creating windows while passing `with_x11_screen(non_default_screen)` works again. +- On X11, fix XInput handling that prevented a new window from getting the focus in some cases. +- On macOS, fix crash when pressing Caps Lock in certain configurations. +- On iOS, fixed `MonitorHandle`'s `PartialEq` and `Hash` implementations. +- On macOS, fixed undocumented cursors (e.g. zoom, resize, help) always appearing to be invalid and falling back to the default cursor. + +## 0.30.5 + +### Added + +- Add `ActiveEventLoop::system_theme()`, returning the current system theme. +- On Web, implement `Error` for `platform::web::CustomCursorError`. +- On Android, add `{Active,}EventLoopExtAndroid::android_app()` to access the app used to create the loop. + +### Fixed + +- On MacOS, fix building with `feature = "rwh_04"`. +- On Web, pen events are now routed through to `WindowEvent::Cursor*`. +- On macOS, fix panic when releasing not available monitor. +- On MacOS, return the system theme in `Window::theme()` if no theme override is set. + +## 0.30.4 + +### Changed + +- `DeviceId::dummy()` and `WindowId::dummy()` are no longer marked `unsafe`. + +### Fixed + +- On Wayland, avoid crashing when compositor is misbehaving. +- On Web, fix `WindowEvent::Resized` not using `requestAnimationFrame` when sending + `WindowEvent::RedrawRequested` and also potentially causing `WindowEvent::RedrawRequested` + to not be de-duplicated. +- Account for different browser engine implementations of pointer movement coordinate space. + +## 0.30.3 + +### Added + +- On Web, add `EventLoopExtWebSys::(set_)poll_strategy()` to allow setting + control flow strategies before starting the event loop. +- On Web, add `WaitUntilStrategy`, which allows to set different strategies for + `ControlFlow::WaitUntil`. By default the Prioritized Task Scheduling API is + used, with a fallback to `setTimeout()` with a trick to circumvent throttling + to 4ms. But an option to use a Web worker to schedule the timer is available + as well, which commonly prevents any throttling when the window is not focused. + +### Changed + +- On macOS, set the window theme on the `NSWindow` instead of application-wide. + +### Fixed + +- On X11, build on arm platforms. +- On macOS, fixed `WindowBuilder::with_theme` not having any effect on the window. + +## 0.30.2 + +### Fixed + +- On Web, fix `EventLoopProxy::send_event()` triggering event loop immediately + when not called from inside the event loop. Now queues a microtask instead. +- On Web, stop overwriting default cursor with `CursorIcon::Default`. +- On Web, prevent crash when using `InnerSizeWriter::request_inner_size()`. +- On macOS, fix not working opacity for entire window. + +## 0.30.1 + +### Added + +- Reexport `raw-window-handle` versions 0.4 and 0.5 as `raw_window_handle_04` and `raw_window_handle_05`. +- Implement `ApplicationHandler` for `&mut` references and heap allocations to something that implements `ApplicationHandler`. + +### Fixed + +- On macOS, fix panic on exit when dropping windows outside the event loop. +- On macOS, fix window dragging glitches when dragging across a monitor boundary with different scale factor. +- On macOS, fix the range in `Ime::Preedit`. +- On macOS, use the system's internal mechanisms for queuing events. +- On macOS, handle events directly instead of queuing when possible. + +## 0.30.0 + +### Added + +- Add `OwnedDisplayHandle` type for allowing safe display handle usage outside of + trivial cases. +- Add `ApplicationHandler` trait which mimics `Event`. +- Add `WindowBuilder::with_cursor` and `Window::set_cursor` which takes a + `CursorIcon` or `CustomCursor`. +- Add `Sync` implementation for `EventLoopProxy`. +- Add `Window::default_attributes` to get default `WindowAttributes`. +- Add `EventLoop::builder` to get `EventLoopBuilder` without export. +- Add `CustomCursor::from_rgba` to allow creating cursor images from RGBA data. +- Add `CustomCursorExtWebSys::from_url` to allow loading cursor images from URLs. +- Add `CustomCursorExtWebSys::from_animation` to allow creating animated + cursors from other `CustomCursor`s. +- Add `{Active,}EventLoop::create_custom_cursor` to load custom cursor image sources. +- Add `ActiveEventLoop::create_window` and `EventLoop::create_window`. +- Add `CustomCursor` which could be set via `Window::set_cursor`, implemented on + Windows, macOS, X11, Wayland, and Web. +- On Web, add to toggle calling `Event.preventDefault()` on `Window`. +- On iOS, add `PinchGesture`, `DoubleTapGesture`, `PanGesture` and `RotationGesture`. +- on iOS, use `UIGestureRecognizerDelegate` for fine grained control of gesture recognizers. +- On macOS, add services menu. +- On Windows, add `with_title_text_color`, and `with_corner_preference` on + `WindowAttributesExtWindows`. +- On Windows, implement resize increments. +- On Windows, add `AnyThread` API to access window handle off the main thread. + +### Changed + +- Bump MSRV from `1.65` to `1.70`. +- On Wayland, bump `sctk-adwaita` to `0.9.0`, which changed system library + crates. This change is a **cascading breaking change**, you must do breaking + change as well, even if you don't expose winit. +- Rename `TouchpadMagnify` to `PinchGesture`. +- Rename `SmartMagnify` to `DoubleTapGesture`. +- Rename `TouchpadRotate` to `RotationGesture`. +- Rename `EventLoopWindowTarget` to `ActiveEventLoop`. +- Rename `platform::x11::XWindowType` to `platform::x11::WindowType`. +- Rename `VideoMode` to `VideoModeHandle` to represent that it doesn't hold + static data. +- Make `Debug` formatting of `WindowId` more concise. +- Move `dpi` types to its own crate, and re-export it from the root crate. +- Replace `log` with `tracing`, use `log` feature on `tracing` to restore old + behavior. +- `EventLoop::with_user_event` now returns `EventLoopBuilder`. +- On Web, return `HandleError::Unavailable` when a window handle is not available. +- On Web, return `RawWindowHandle::WebCanvas` instead of `RawWindowHandle::Web`. +- On Web, remove queuing fullscreen request in absence of transient activation. +- On iOS, return `HandleError::Unavailable` when a window handle is not available. +- On macOS, return `HandleError::Unavailable` when a window handle is not available. +- On Windows, remove `WS_CAPTION`, `WS_BORDER`, and `WS_EX_WINDOWEDGE` styles + for child windows without decorations. +- On Android, bump `ndk` to `0.9.0` and `android-activity` to `0.6.0`, + and remove unused direct dependency on `ndk-sys`. + +### Deprecated + +- Deprecate `EventLoop::run`, use `EventLoop::run_app`. +- Deprecate `EventLoopExtRunOnDemand::run_on_demand`, use `EventLoop::run_app_on_demand`. +- Deprecate `EventLoopExtPumpEvents::pump_events`, use `EventLoopExtPumpEvents::pump_app_events`. + + The new `app` APIs accept a newly added `ApplicationHandler` instead of + `Fn`. The semantics are mostly the same, given that the capture list of the + closure is your new `State`. Consider the following code: + + ```rust,no_run + use winit::event::Event; + use winit::event_loop::EventLoop; + use winit::window::Window; + + struct MyUserEvent; + + let event_loop = EventLoop::::with_user_event().build().unwrap(); + let window = event_loop.create_window(Window::default_attributes()).unwrap(); + let mut counter = 0; + + let _ = event_loop.run(move |event, event_loop| { + match event { + Event::AboutToWait => { + window.request_redraw(); + counter += 1; + } + Event::WindowEvent { window_id, event } => { + // Handle window event. + } + Event::UserEvent(event) => { + // Handle user event. + } + Event::DeviceEvent { device_id, event } => { + // Handle device event. + } + _ => (), + } + }); + ``` + + To migrate this code, you should move all the captured values into some + newtype `State` and implement `ApplicationHandler` for this type. Finally, + we move particular `match event` arms into methods on `ApplicationHandler`, + for example: + + ```rust,no_run + use winit::application::ApplicationHandler; + use winit::event::{Event, WindowEvent, DeviceEvent, DeviceId}; + use winit::event_loop::{EventLoop, ActiveEventLoop}; + use winit::window::{Window, WindowId}; + + struct MyUserEvent; + + struct State { + window: Window, + counter: i32, + } + + impl ApplicationHandler for State { + fn user_event(&mut self, event_loop: &ActiveEventLoop, user_event: MyUserEvent) { + // Handle user event. + } + + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + // Your application got resumed. + } + + fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) { + // Handle window event. + } + + fn device_event(&mut self, event_loop: &ActiveEventLoop, device_id: DeviceId, event: DeviceEvent) { + // Handle device event. + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + self.window.request_redraw(); + self.counter += 1; + } + } + + let event_loop = EventLoop::::with_user_event().build().unwrap(); + #[allow(deprecated)] + let window = event_loop.create_window(Window::default_attributes()).unwrap(); + let mut state = State { window, counter: 0 }; + + let _ = event_loop.run_app(&mut state); + ``` + + Please submit your feedback after migrating in [this issue](https://github.com/rust-windowing/winit/issues/3626). + +- Deprecate `Window::set_cursor_icon`, use `Window::set_cursor`. + +### Removed + +- Remove `Window::new`, use `ActiveEventLoop::create_window` instead. + + You now have to create your windows inside the actively running event loop + (usually the `new_events(cause: StartCause::Init)` or `resumed()` events), + and can no longer do it before the application has properly launched. + This change is done to fix many long-standing issues on iOS and macOS, and + will improve things on Wayland once fully implemented. + + To ease migration, we provide the deprecated `EventLoop::create_window` that + will allow you to bypass this restriction in this release. + + Using the migration example from above, you can change your code as follows: + + ```rust,no_run + use winit::application::ApplicationHandler; + use winit::event::{Event, WindowEvent, DeviceEvent, DeviceId}; + use winit::event_loop::{EventLoop, ActiveEventLoop}; + use winit::window::{Window, WindowId}; + + #[derive(Default)] + struct State { + // Use an `Option` to allow the window to not be available until the + // application is properly running. + window: Option, + counter: i32, + } + + impl ApplicationHandler for State { + // This is a common indicator that you can create a window. + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); + } + fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) { + // `unwrap` is fine, the window will always be available when + // receiving a window event. + let window = self.window.as_ref().unwrap(); + // Handle window event. + } + fn device_event(&mut self, event_loop: &ActiveEventLoop, device_id: DeviceId, event: DeviceEvent) { + // Handle window event. + } + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + if let Some(window) = self.window.as_ref() { + window.request_redraw(); + self.counter += 1; + } + } + } + + let event_loop = EventLoop::new().unwrap(); + let mut state = State::default(); + let _ = event_loop.run_app(&mut state); + ``` + +- Remove `Deref` implementation for `EventLoop` that gave `EventLoopWindowTarget`. +- Remove `WindowBuilder` in favor of `WindowAttributes`. +- Remove Generic parameter `T` from `ActiveEventLoop`. +- Remove `EventLoopBuilder::with_user_event`, use `EventLoop::with_user_event`. +- Remove Redundant `EventLoopError::AlreadyRunning`. +- Remove `WindowAttributes::fullscreen` and expose as field directly. +- On X11, remove `platform::x11::XNotSupported` export. + +### Fixed + +- On Web, fix setting cursor icon overriding cursor visibility. +- On Windows, fix cursor not confined to center of window when grabbed and hidden. +- On macOS, fix sequence of mouse events being out of order when dragging on the trackpad. +- On Wayland, fix decoration glitch on close with some compositors. +- On Android, fix a regression introduced in #2748 to allow volume key events to be received again. +- On Windows, don't return a valid window handle outside of the GUI thread. +- On macOS, don't set the background color when initializing a window with transparency. diff --git a/vendor/winit/src/changelog/v0.8.md b/vendor/winit/src/changelog/v0.8.md new file mode 100644 index 00000000..1963429c --- /dev/null +++ b/vendor/winit/src/changelog/v0.8.md @@ -0,0 +1,33 @@ +## 0.8.3 + +- Fixed issue of calls to `set_inner_size` blocking on Windows. +- Mapped `ISO_Left_Tab` to `VirtualKeyCode::Tab` to make the key work with modifiers +- Fixed the X11 backed on 32bit targets + +## 0.8.2 + +- Uniformize keyboard scancode values across Wayland and X11 (#297). +- Internal rework of the wayland event loop +- Added method `os::linux::WindowExt::is_ready` + +## 0.8.1 + +- Added various methods to `os::linux::EventsLoopExt`, plus some hidden items necessary to make + glutin work. + +## 0.8.0 + +- Added `Window::set_maximized`, `WindowAttributes::maximized` and `WindowBuilder::with_maximized`. +- Added `Window::set_fullscreen`. +- Changed `with_fullscreen` to take a `Option` instead of a `MonitorId`. +- Removed `MonitorId::get_native_identifier()` in favor of platform-specific traits in the `os` + module. +- Changed `get_available_monitors()` and `get_primary_monitor()` to be methods of `EventsLoop` + instead of stand-alone methods. +- Changed `EventsLoop` to be tied to a specific X11 or Wayland connection. +- Added a `os::linux::EventsLoopExt` trait that makes it possible to configure the connection. +- Fixed the emscripten code, which now compiles. +- Changed the X11 fullscreen code to use `xrandr` instead of `xxf86vm`. +- Fixed the Wayland backend to produce `Refresh` event after window creation. +- Changed the `Suspended` event to be outside of `WindowEvent`. +- Fixed the X11 backend sometimes reporting the wrong virtual key (#273). diff --git a/vendor/winit/src/changelog/v0.9.md b/vendor/winit/src/changelog/v0.9.md new file mode 100644 index 00000000..2a9e8cc3 --- /dev/null +++ b/vendor/winit/src/changelog/v0.9.md @@ -0,0 +1,22 @@ +## 0.9.0 + +- Added event `WindowEvent::HiDPIFactorChanged`. +- Added method `MonitorId::get_hidpi_factor`. +- Deprecated `get_inner_size_pixels` and `get_inner_size_points` methods of `Window` in favor of + `get_inner_size`. +- **Breaking:** `EventsLoop` is `!Send` and `!Sync` because of platform-dependant constraints, + but `Window`, `WindowId`, `DeviceId` and `MonitorId` guaranteed to be `Send`. +- `MonitorId::get_position` now returns `(i32, i32)` instead of `(u32, u32)`. +- Rewrite of the wayland backend to use wayland-client-0.11 +- Support for dead keys on wayland for keyboard utf8 input +- Monitor enumeration on Windows is now implemented using `EnumDisplayMonitors` instead of + `EnumDisplayDevices`. This changes the value returned by `MonitorId::get_name()`. +- On Windows added `MonitorIdExt::hmonitor` method +- Impl `Clone` for `EventsLoopProxy` +- `EventsLoop::get_primary_monitor()` on X11 will fallback to any available monitor if no primary is found +- Support for touch event on wayland +- `WindowEvent`s `MouseMoved`, `MouseEntered`, and `MouseLeft` have been renamed to + `CursorMoved`, `CursorEntered`, and `CursorLeft`. +- New `DeviceEvent`s added, `MouseMotion` and `MouseWheel`. +- Send `CursorMoved` event after `CursorEntered` and `Focused` events. +- Add support for `ModifiersState`, `MouseMove`, `MouseInput`, `MouseMotion` for emscripten backend. diff --git a/vendor/winit/src/cursor.rs b/vendor/winit/src/cursor.rs new file mode 100644 index 00000000..7bcac54d --- /dev/null +++ b/vendor/winit/src/cursor.rs @@ -0,0 +1,263 @@ +use core::fmt; +use std::error::Error; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use cursor_icon::CursorIcon; + +use crate::platform_impl::{PlatformCustomCursor, PlatformCustomCursorSource}; + +/// The maximum width and height for a cursor when using [`CustomCursor::from_rgba`]. +pub const MAX_CURSOR_SIZE: u16 = 2048; + +const PIXEL_SIZE: usize = 4; + +/// See [`Window::set_cursor()`][crate::window::Window::set_cursor] for more details. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum Cursor { + Icon(CursorIcon), + Custom(CustomCursor), +} + +impl Default for Cursor { + fn default() -> Self { + Self::Icon(CursorIcon::default()) + } +} + +impl From for Cursor { + fn from(icon: CursorIcon) -> Self { + Self::Icon(icon) + } +} + +impl From for Cursor { + fn from(custom: CustomCursor) -> Self { + Self::Custom(custom) + } +} + +/// Use a custom image as a cursor (mouse pointer). +/// +/// Is guaranteed to be cheap to clone. +/// +/// ## Platform-specific +/// +/// **Web**: Some browsers have limits on cursor sizes usually at 128x128. +/// +/// # Example +/// +/// ```no_run +/// # use winit::event_loop::ActiveEventLoop; +/// # use winit::window::Window; +/// # fn scope(event_loop: &ActiveEventLoop, window: &Window) { +/// use winit::window::CustomCursor; +/// +/// let w = 10; +/// let h = 10; +/// let rgba = vec![255; (w * h * 4) as usize]; +/// +/// #[cfg(not(target_family = "wasm"))] +/// let source = CustomCursor::from_rgba(rgba, w, h, w / 2, h / 2).unwrap(); +/// +/// #[cfg(target_family = "wasm")] +/// let source = { +/// use winit::platform::web::CustomCursorExtWebSys; +/// CustomCursor::from_url(String::from("http://localhost:3000/cursor.png"), 0, 0) +/// }; +/// +/// let custom_cursor = event_loop.create_custom_cursor(source); +/// +/// window.set_cursor(custom_cursor.clone()); +/// # } +/// ``` +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct CustomCursor { + /// Platforms should make sure this is cheap to clone. + pub(crate) inner: PlatformCustomCursor, +} + +impl CustomCursor { + /// Creates a new cursor from an rgba buffer. + /// + /// The alpha channel is assumed to be **not** premultiplied. + pub fn from_rgba( + rgba: impl Into>, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + ) -> Result { + let _span = + tracing::debug_span!("winit::Cursor::from_rgba", width, height, hotspot_x, hotspot_y) + .entered(); + + Ok(CustomCursorSource { + inner: PlatformCustomCursorSource::from_rgba( + rgba.into(), + width, + height, + hotspot_x, + hotspot_y, + )?, + }) + } +} + +/// Source for [`CustomCursor`]. +/// +/// See [`CustomCursor`] for more details. +#[derive(Debug)] +pub struct CustomCursorSource { + pub(crate) inner: PlatformCustomCursorSource, +} + +/// An error produced when using [`CustomCursor::from_rgba`] with invalid arguments. +#[derive(Debug, Clone)] +pub enum BadImage { + /// Produced when the image dimensions are larger than [`MAX_CURSOR_SIZE`]. This doesn't + /// guarantee that the cursor will work, but should avoid many platform and device specific + /// limits. + TooLarge { width: u16, height: u16 }, + /// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be + /// safely interpreted as 32bpp RGBA pixels. + ByteCountNotDivisibleBy4 { byte_count: usize }, + /// Produced when the number of pixels (`rgba.len() / 4`) isn't equal to `width * height`. + /// At least one of your arguments is incorrect. + DimensionsVsPixelCount { width: u16, height: u16, width_x_height: u64, pixel_count: u64 }, + /// Produced when the hotspot is outside the image bounds + HotspotOutOfBounds { width: u16, height: u16, hotspot_x: u16, hotspot_y: u16 }, +} + +impl fmt::Display for BadImage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BadImage::TooLarge { width, height } => write!( + f, + "The specified dimensions ({width:?}x{height:?}) are too large. The maximum is \ + {MAX_CURSOR_SIZE:?}x{MAX_CURSOR_SIZE:?}.", + ), + BadImage::ByteCountNotDivisibleBy4 { byte_count } => write!( + f, + "The length of the `rgba` argument ({byte_count:?}) isn't divisible by 4, making \ + it impossible to interpret as 32bpp RGBA pixels.", + ), + BadImage::DimensionsVsPixelCount { width, height, width_x_height, pixel_count } => { + write!( + f, + "The specified dimensions ({width:?}x{height:?}) don't match the number of \ + pixels supplied by the `rgba` argument ({pixel_count:?}). For those \ + dimensions, the expected pixel count is {width_x_height:?}.", + ) + }, + BadImage::HotspotOutOfBounds { width, height, hotspot_x, hotspot_y } => write!( + f, + "The specified hotspot ({hotspot_x:?}, {hotspot_y:?}) is outside the image bounds \ + ({width:?}x{height:?}).", + ), + } + } +} + +impl Error for BadImage {} + +/// Platforms export this directly as `PlatformCustomCursorSource` if they need to only work with +/// images. +#[allow(dead_code)] +#[derive(Debug)] +pub(crate) struct OnlyCursorImageSource(pub(crate) CursorImage); + +#[allow(dead_code)] +impl OnlyCursorImageSource { + pub(crate) fn from_rgba( + rgba: Vec, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + ) -> Result { + CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y).map(Self) + } +} + +/// Platforms export this directly as `PlatformCustomCursor` if they don't implement caching. +#[allow(dead_code)] +#[derive(Debug, Clone)] +pub(crate) struct OnlyCursorImage(pub(crate) Arc); + +impl Hash for OnlyCursorImage { + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.0).hash(state); + } +} + +impl PartialEq for OnlyCursorImage { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl Eq for OnlyCursorImage {} + +#[derive(Debug)] +#[allow(dead_code)] +pub(crate) struct CursorImage { + pub(crate) rgba: Vec, + pub(crate) width: u16, + pub(crate) height: u16, + pub(crate) hotspot_x: u16, + pub(crate) hotspot_y: u16, +} + +impl CursorImage { + pub(crate) fn from_rgba( + rgba: Vec, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + ) -> Result { + if width > MAX_CURSOR_SIZE || height > MAX_CURSOR_SIZE { + return Err(BadImage::TooLarge { width, height }); + } + + if rgba.len() % PIXEL_SIZE != 0 { + return Err(BadImage::ByteCountNotDivisibleBy4 { byte_count: rgba.len() }); + } + + let pixel_count = (rgba.len() / PIXEL_SIZE) as u64; + let width_x_height = width as u64 * height as u64; + if pixel_count != width_x_height { + return Err(BadImage::DimensionsVsPixelCount { + width, + height, + width_x_height, + pixel_count, + }); + } + + if hotspot_x >= width || hotspot_y >= height { + return Err(BadImage::HotspotOutOfBounds { width, height, hotspot_x, hotspot_y }); + } + + Ok(CursorImage { rgba, width, height, hotspot_x, hotspot_y }) + } +} + +// Platforms that don't support cursors will export this as `PlatformCustomCursor`. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub(crate) struct NoCustomCursor; + +#[allow(dead_code)] +impl NoCustomCursor { + pub(crate) fn from_rgba( + rgba: Vec, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + ) -> Result { + CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y)?; + Ok(Self) + } +} diff --git a/vendor/winit/src/error.rs b/vendor/winit/src/error.rs new file mode 100644 index 00000000..d15bb9e7 --- /dev/null +++ b/vendor/winit/src/error.rs @@ -0,0 +1,131 @@ +use std::{error, fmt}; + +use crate::platform_impl; + +// TODO: Rename +/// An error that may be generated when requesting Winit state +#[derive(Debug)] +pub enum ExternalError { + /// The operation is not supported by the backend. + NotSupported(NotSupportedError), + /// The operation was ignored. + Ignored, + /// The OS cannot perform the operation. + Os(OsError), +} + +/// The error type for when the requested operation is not supported by the backend. +#[derive(Clone)] +pub struct NotSupportedError { + _marker: (), +} + +/// The error type for when the OS cannot perform the requested operation. +#[derive(Debug)] +pub struct OsError { + line: u32, + file: &'static str, + error: platform_impl::OsError, +} + +/// A general error that may occur while running the Winit event loop +#[derive(Debug)] +pub enum EventLoopError { + /// The operation is not supported by the backend. + NotSupported(NotSupportedError), + /// The OS cannot perform the operation. + Os(OsError), + /// The event loop can't be re-created. + RecreationAttempt, + /// Application has exit with an error status. + ExitFailure(i32), +} + +impl From for EventLoopError { + fn from(value: OsError) -> Self { + Self::Os(value) + } +} + +impl NotSupportedError { + #[inline] + #[allow(dead_code)] + pub(crate) fn new() -> NotSupportedError { + NotSupportedError { _marker: () } + } +} + +impl OsError { + #[allow(dead_code)] + pub(crate) fn new(line: u32, file: &'static str, error: platform_impl::OsError) -> OsError { + OsError { line, file, error } + } +} + +#[allow(unused_macros)] +macro_rules! os_error { + ($error:expr) => {{ + crate::error::OsError::new(line!(), file!(), $error) + }}; +} + +impl fmt::Display for OsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.pad(&format!("os error at {}:{}: {}", self.file, self.line, self.error)) + } +} + +impl fmt::Display for ExternalError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + ExternalError::NotSupported(e) => e.fmt(f), + ExternalError::Ignored => write!(f, "Operation was ignored"), + ExternalError::Os(e) => e.fmt(f), + } + } +} + +impl fmt::Debug for NotSupportedError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.debug_struct("NotSupportedError").finish() + } +} + +impl fmt::Display for NotSupportedError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.pad("the requested operation is not supported by Winit") + } +} + +impl fmt::Display for EventLoopError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + EventLoopError::RecreationAttempt => write!(f, "EventLoop can't be recreated"), + EventLoopError::NotSupported(e) => e.fmt(f), + EventLoopError::Os(e) => e.fmt(f), + EventLoopError::ExitFailure(status) => write!(f, "Exit Failure: {status}"), + } + } +} + +impl error::Error for OsError {} +impl error::Error for ExternalError {} +impl error::Error for NotSupportedError {} +impl error::Error for EventLoopError {} + +#[cfg(test)] +#[allow(clippy::redundant_clone)] +mod tests { + use super::*; + + // Eat attributes for testing + #[test] + fn ensure_fmt_does_not_panic() { + let _ = format!("{:?}, {}", NotSupportedError::new(), NotSupportedError::new().clone()); + let _ = format!( + "{:?}, {}", + ExternalError::NotSupported(NotSupportedError::new()), + ExternalError::NotSupported(NotSupportedError::new()) + ); + } +} diff --git a/vendor/winit/src/event.rs b/vendor/winit/src/event.rs new file mode 100644 index 00000000..4e01420a --- /dev/null +++ b/vendor/winit/src/event.rs @@ -0,0 +1,1183 @@ +//! The [`Event`] enum and assorted supporting types. +//! +//! These are sent to the closure given to [`EventLoop::run_app(...)`], where they get +//! processed and used to modify the program state. For more details, see the root-level +//! documentation. +//! +//! Some of these events represent different "parts" of a traditional event-handling loop. You could +//! approximate the basic ordering loop of [`EventLoop::run_app(...)`] like this: +//! +//! ```rust,ignore +//! let mut start_cause = StartCause::Init; +//! +//! while !elwt.exiting() { +//! app.new_events(event_loop, start_cause); +//! +//! for event in (window events, user events, device events) { +//! // This will pick the right method on the application based on the event. +//! app.handle_event(event_loop, event); +//! } +//! +//! for window_id in (redraw windows) { +//! app.window_event(event_loop, window_id, RedrawRequested); +//! } +//! +//! app.about_to_wait(event_loop); +//! start_cause = wait_if_necessary(); +//! } +//! +//! app.exiting(event_loop); +//! ``` +//! +//! This leaves out timing details like [`ControlFlow::WaitUntil`] but hopefully +//! describes what happens in what order. +//! +//! [`EventLoop::run_app(...)`]: crate::event_loop::EventLoop::run_app +//! [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil +use std::path::PathBuf; +use std::sync::{Mutex, Weak}; +#[cfg(not(web_platform))] +use std::time::Instant; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use smol_str::SmolStr; +#[cfg(web_platform)] +use web_time::Instant; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::error::ExternalError; +use crate::event_loop::AsyncRequestSerial; +use crate::keyboard::{self, ModifiersKeyState, ModifiersKeys, ModifiersState}; +use crate::platform_impl; +#[cfg(doc)] +use crate::window::Window; +use crate::window::{ActivationToken, Theme, WindowId}; + +/// Describes a generic event. +/// +/// See the module-level docs for more information on the event loop manages each event. +#[derive(Debug, Clone, PartialEq)] +pub enum Event { + /// See [`ApplicationHandler::new_events`] for details. + /// + /// [`ApplicationHandler::new_events`]: crate::application::ApplicationHandler::new_events + NewEvents(StartCause), + + /// See [`ApplicationHandler::window_event`] for details. + /// + /// [`ApplicationHandler::window_event`]: crate::application::ApplicationHandler::window_event + WindowEvent { window_id: WindowId, event: WindowEvent }, + + /// See [`ApplicationHandler::device_event`] for details. + /// + /// [`ApplicationHandler::device_event`]: crate::application::ApplicationHandler::device_event + DeviceEvent { device_id: DeviceId, event: DeviceEvent }, + + /// See [`ApplicationHandler::user_event`] for details. + /// + /// [`ApplicationHandler::user_event`]: crate::application::ApplicationHandler::user_event + UserEvent(T), + + /// See [`ApplicationHandler::suspended`] for details. + /// + /// [`ApplicationHandler::suspended`]: crate::application::ApplicationHandler::suspended + Suspended, + + /// See [`ApplicationHandler::resumed`] for details. + /// + /// [`ApplicationHandler::resumed`]: crate::application::ApplicationHandler::resumed + Resumed, + + /// See [`ApplicationHandler::about_to_wait`] for details. + /// + /// [`ApplicationHandler::about_to_wait`]: crate::application::ApplicationHandler::about_to_wait + AboutToWait, + + /// See [`ApplicationHandler::exiting`] for details. + /// + /// [`ApplicationHandler::exiting`]: crate::application::ApplicationHandler::exiting + LoopExiting, + + /// See [`ApplicationHandler::memory_warning`] for details. + /// + /// [`ApplicationHandler::memory_warning`]: crate::application::ApplicationHandler::memory_warning + MemoryWarning, +} + +impl Event { + #[allow(clippy::result_large_err)] + pub fn map_nonuser_event(self) -> Result, Event> { + use self::Event::*; + match self { + UserEvent(_) => Err(self), + WindowEvent { window_id, event } => Ok(WindowEvent { window_id, event }), + DeviceEvent { device_id, event } => Ok(DeviceEvent { device_id, event }), + NewEvents(cause) => Ok(NewEvents(cause)), + AboutToWait => Ok(AboutToWait), + LoopExiting => Ok(LoopExiting), + Suspended => Ok(Suspended), + Resumed => Ok(Resumed), + MemoryWarning => Ok(MemoryWarning), + } + } +} + +/// Describes the reason the event loop is resuming. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StartCause { + /// Sent if the time specified by [`ControlFlow::WaitUntil`] has been reached. Contains the + /// moment the timeout was requested and the requested resume time. The actual resume time is + /// guaranteed to be equal to or after the requested resume time. + /// + /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil + ResumeTimeReached { start: Instant, requested_resume: Instant }, + + /// Sent if the OS has new events to send to the window, after a wait was requested. Contains + /// the moment the wait was requested and the resume time, if requested. + WaitCancelled { start: Instant, requested_resume: Option }, + + /// Sent if the event loop is being resumed after the loop's control flow was set to + /// [`ControlFlow::Poll`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + Poll, + + /// Sent once, immediately after `run` is called. Indicates that the loop was just initialized. + Init, +} + +/// Describes an event from a [`Window`]. +#[derive(Debug, Clone, PartialEq)] +pub enum WindowEvent { + /// The activation token was delivered back and now could be used. + #[cfg_attr(not(any(x11_platform, wayland_platform)), allow(rustdoc::broken_intra_doc_links))] + /// Delivered in response to [`request_activation_token`]. + /// + /// [`request_activation_token`]: crate::platform::startup_notify::WindowExtStartupNotify::request_activation_token + ActivationTokenDone { serial: AsyncRequestSerial, token: ActivationToken }, + + /// The size of the window has changed. Contains the client area's new dimensions. + Resized(PhysicalSize), + + /// The position of the window has changed. Contains the window's new position. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Wayland:** Unsupported. + Moved(PhysicalPosition), + + /// The window has been requested to close. + CloseRequested, + + /// The window has been destroyed. + Destroyed, + + /// A file has been dropped into the window. + /// + /// When the user drops multiple files at once, this event will be emitted for each file + /// separately. + DroppedFile(PathBuf), + + /// A file is being hovered over the window. + /// + /// When the user hovers multiple files at once, this event will be emitted for each file + /// separately. + HoveredFile(PathBuf), + + /// A file was hovered, but has exited the window. + /// + /// There will be a single `HoveredFileCancelled` event triggered even if multiple files were + /// hovered. + HoveredFileCancelled, + + /// The window gained or lost focus. + /// + /// The parameter is true if the window has gained focus, and false if it has lost focus. + Focused(bool), + + /// An event from the keyboard has been received. + /// + /// ## Platform-specific + /// - **Windows:** The shift key overrides NumLock. In other words, while shift is held down, + /// numpad keys act as if NumLock wasn't active. When this is used, the OS sends fake key + /// events which are not marked as `is_synthetic`. + KeyboardInput { + device_id: DeviceId, + event: KeyEvent, + + /// If `true`, the event was generated synthetically by winit + /// in one of the following circumstances: + /// + /// * Synthetic key press events are generated for all keys pressed when a window gains + /// focus. Likewise, synthetic key release events are generated for all keys pressed when + /// a window goes out of focus. ***Currently, this is only functional on X11 and + /// Windows*** + /// + /// Otherwise, this value is always `false`. + is_synthetic: bool, + }, + + /// The keyboard modifiers have changed. + ModifiersChanged(Modifiers), + + /// An event from an input method. + /// + /// **Note:** You have to explicitly enable this event using [`Window::set_ime_allowed`]. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Orbital:** Unsupported. + Ime(Ime), + + /// The cursor has moved on the window. + /// + /// ## Platform-specific + /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + CursorMoved { + device_id: DeviceId, + + /// (x,y) coords in pixels relative to the top-left corner of the window. Because the range + /// of this data is limited by the display area and it may have been transformed by + /// the OS to implement effects such as cursor acceleration, it should not be used + /// to implement non-cursor-like interactions such as 3D camera control. + position: PhysicalPosition, + }, + + /// The cursor has entered the window. + /// + /// ## Platform-specific + /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + CursorEntered { device_id: DeviceId }, + + /// The cursor has left the window. + /// + /// ## Platform-specific + /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + CursorLeft { device_id: DeviceId }, + + /// A mouse wheel movement or touchpad scroll occurred. + MouseWheel { device_id: DeviceId, delta: MouseScrollDelta, phase: TouchPhase }, + + /// An mouse button press has been received. + MouseInput { device_id: DeviceId, state: ElementState, button: MouseButton }, + + /// Two-finger pinch gesture, often used for magnification. + /// + /// ## Platform-specific + /// + /// - Only available on **macOS** and **iOS**. + /// - On iOS, not recognized by default. It must be enabled when needed. + PinchGesture { + device_id: DeviceId, + /// Positive values indicate magnification (zooming in) and negative + /// values indicate shrinking (zooming out). + /// + /// This value may be NaN. + delta: f64, + phase: TouchPhase, + }, + + /// N-finger pan gesture + /// + /// ## Platform-specific + /// + /// - Only available on **iOS**. + /// - On iOS, not recognized by default. It must be enabled when needed. + PanGesture { + device_id: DeviceId, + /// Change in pixels of pan gesture from last update. + delta: PhysicalPosition, + phase: TouchPhase, + }, + + /// Double tap gesture. + /// + /// On a Mac, smart magnification is triggered by a double tap with two fingers + /// on the trackpad and is commonly used to zoom on a certain object + /// (e.g. a paragraph of a PDF) or (sort of like a toggle) to reset any zoom. + /// The gesture is also supported in Safari, Pages, etc. + /// + /// The event is general enough that its generating gesture is allowed to vary + /// across platforms. It could also be generated by another device. + /// + /// Unfortunately, neither [Windows](https://support.microsoft.com/en-us/windows/touch-gestures-for-windows-a9d28305-4818-a5df-4e2b-e5590f850741) + /// nor [Wayland](https://wayland.freedesktop.org/libinput/doc/latest/gestures.html) + /// support this gesture or any other gesture with the same effect. + /// + /// ## Platform-specific + /// + /// - Only available on **macOS 10.8** and later, and **iOS**. + /// - On iOS, not recognized by default. It must be enabled when needed. + DoubleTapGesture { device_id: DeviceId }, + + /// Two-finger rotation gesture. + /// + /// Positive delta values indicate rotation counterclockwise and + /// negative delta values indicate rotation clockwise. + /// + /// ## Platform-specific + /// + /// - Only available on **macOS** and **iOS**. + /// - On iOS, not recognized by default. It must be enabled when needed. + RotationGesture { + device_id: DeviceId, + /// change in rotation in degrees + delta: f32, + phase: TouchPhase, + }, + + /// Touchpad pressure event. + /// + /// At the moment, only supported on Apple forcetouch-capable macbooks. + /// The parameters are: pressure level (value between 0 and 1 representing how hard the + /// touchpad is being pressed) and stage (integer representing the click level). + TouchpadPressure { device_id: DeviceId, pressure: f32, stage: i64 }, + + /// Motion on some analog axis. May report data redundant to other, more specific events. + AxisMotion { device_id: DeviceId, axis: AxisId, value: f64 }, + + /// Touch event has been received + /// + /// ## Platform-specific + /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. + /// - **macOS:** Unsupported. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + Touch(Touch), + + /// The window's scale factor has changed. + /// + /// The following user actions can cause DPI changes: + /// + /// * Changing the display's resolution. + /// * Changing the display's scale factor (e.g. in Control Panel on Windows). + /// * Moving the window to a display with a different scale factor. + /// + /// To update the window size, use the provided [`InnerSizeWriter`] handle. By default, the + /// window is resized to the value suggested by the OS, but it can be changed to any value. + /// + /// For more information about DPI in general, see the [`dpi`] crate. + ScaleFactorChanged { + scale_factor: f64, + /// Handle to update inner size during scale changes. + /// + /// See [`InnerSizeWriter`] docs for more details. + inner_size_writer: InnerSizeWriter, + }, + + /// The system window theme has changed. + /// + /// Applications might wish to react to this to change the theme of the content of the window + /// when the system changes the window theme. + /// + /// This only reports a change if the window theme was not overridden by [`Window::set_theme`]. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / X11 / Wayland / Orbital:** Unsupported. + ThemeChanged(Theme), + + /// The window has been occluded (completely hidden from view). + /// + /// This is different to window visibility as it depends on whether the window is closed, + /// minimised, set invisible, or fully occluded by another window. + /// + /// ## Platform-specific + /// + /// ### iOS + /// + /// On iOS, the `Occluded(false)` event is emitted in response to an + /// [`applicationWillEnterForeground`] callback which means the application should start + /// preparing its data. The `Occluded(true)` event is emitted in response to an + /// [`applicationDidEnterBackground`] callback which means the application should free + /// resources (according to the [iOS application lifecycle]). + /// + /// [`applicationWillEnterForeground`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623076-applicationwillenterforeground + /// [`applicationDidEnterBackground`]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622997-applicationdidenterbackground + /// [iOS application lifecycle]: https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle + /// + /// ### Others + /// + /// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. + /// - **Android / Wayland / Windows / Orbital:** Unsupported. + /// + /// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border + /// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + Occluded(bool), + + /// Emitted when a window should be redrawn. + /// + /// This gets triggered in two scenarios: + /// - The OS has performed an operation that's invalidated the window's contents (such as + /// resizing the window). + /// - The application has explicitly requested a redraw via [`Window::request_redraw`]. + /// + /// Winit will aggregate duplicate redraw requests into a single event, to + /// help avoid duplicating rendering work. + RedrawRequested, +} + +/// Identifier of an input device. +/// +/// Whenever you receive an event arising from a particular input device, this event contains a +/// `DeviceId` which identifies its origin. Note that devices may be virtual (representing an +/// on-screen cursor and keyboard focus) or physical. Virtual devices typically aggregate inputs +/// from multiple physical devices. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId(pub(crate) platform_impl::DeviceId); + +impl DeviceId { + /// Returns a dummy id, useful for unit testing. + /// + /// # Notes + /// + /// The only guarantee made about the return value of this function is that + /// it will always be equal to itself and to future values returned by this function. + /// No other guarantees are made. This may be equal to a real `DeviceId`. + pub const fn dummy() -> Self { + DeviceId(platform_impl::DeviceId::dummy()) + } +} + +/// Represents raw hardware events that are not associated with any particular window. +/// +/// Useful for interactions that diverge significantly from a conventional 2D GUI, such as 3D camera +/// or first-person game controls. Many physical actions, such as mouse movement, can produce both +/// device and window events. Because window events typically arise from virtual devices +/// (corresponding to GUI cursors and keyboard focus) the device IDs may not match. +/// +/// Note that these events are delivered regardless of input focus. +#[derive(Clone, Debug, PartialEq)] +pub enum DeviceEvent { + Added, + Removed, + + /// Change in physical position of a pointing device. + /// + /// This represents raw, unfiltered physical motion. Not to be confused with + /// [`WindowEvent::CursorMoved`]. + MouseMotion { + /// (x, y) change in position in unspecified units. + /// + /// Different devices may use different units. + delta: (f64, f64), + }, + + /// Physical scroll event + MouseWheel { + delta: MouseScrollDelta, + }, + + /// Motion on some analog axis. This event will be reported for all arbitrary input devices + /// that winit supports on this platform, including mouse devices. If the device is a mouse + /// device then this will be reported alongside the MouseMotion event. + Motion { + axis: AxisId, + value: f64, + }, + + Button { + button: ButtonId, + state: ElementState, + }, + + Key(RawKeyEvent), +} + +/// Describes a keyboard input as a raw device event. +/// +/// Note that holding down a key may produce repeated `RawKeyEvent`s. The +/// operating system doesn't provide information whether such an event is a +/// repeat or the initial keypress. An application may emulate this by, for +/// example keeping a Map/Set of pressed keys and determining whether a keypress +/// corresponds to an already pressed key. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct RawKeyEvent { + pub physical_key: keyboard::PhysicalKey, + pub state: ElementState, +} + +/// Describes a keyboard input targeting a window. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEvent { + /// Represents the position of a key independent of the currently active layout. + /// + /// It also uniquely identifies the physical key (i.e. it's mostly synonymous with a scancode). + /// The most prevalent use case for this is games. For example the default keys for the player + /// to move around might be the W, A, S, and D keys on a US layout. The position of these keys + /// is more important than their label, so they should map to Z, Q, S, and D on an "AZERTY" + /// layout. (This value is `KeyCode::KeyW` for the Z key on an AZERTY layout.) + /// + /// ## Caveats + /// + /// - Certain niche hardware will shuffle around physical key positions, e.g. a keyboard that + /// implements DVORAK in hardware (or firmware) + /// - Your application will likely have to handle keyboards which are missing keys that your + /// own keyboard has. + /// - Certain `KeyCode`s will move between a couple of different positions depending on what + /// layout the keyboard was manufactured to support. + /// + /// **Because of these caveats, it is important that you provide users with a way to configure + /// most (if not all) keybinds in your application.** + /// + /// ## `Fn` and `FnLock` + /// + /// `Fn` and `FnLock` key events are *exceedingly unlikely* to be emitted by Winit. These keys + /// are usually handled at the hardware or OS level, and aren't surfaced to applications. If + /// you somehow see this in the wild, we'd like to know :) + pub physical_key: keyboard::PhysicalKey, + + // Allowing `broken_intra_doc_links` for `logical_key`, because + // `key_without_modifiers` is not available on all platforms + #[cfg_attr( + not(any(windows_platform, macos_platform, x11_platform, wayland_platform)), + allow(rustdoc::broken_intra_doc_links) + )] + /// This value is affected by all modifiers except Ctrl. + /// + /// This has two use cases: + /// - Allows querying whether the current input is a Dead key. + /// - Allows handling key-bindings on platforms which don't support [`key_without_modifiers`]. + /// + /// If you use this field (or [`key_without_modifiers`] for that matter) for keyboard + /// shortcuts, **it is important that you provide users with a way to configure your + /// application's shortcuts so you don't render your application unusable for users with an + /// incompatible keyboard layout.** + /// + /// ## Platform-specific + /// - **Web:** Dead keys might be reported as the real key instead of `Dead` depending on the + /// browser/OS. + /// + /// [`key_without_modifiers`]: crate::platform::modifier_supplement::KeyEventExtModifierSupplement::key_without_modifiers + pub logical_key: keyboard::Key, + + /// Contains the text produced by this keypress. + /// + /// In most cases this is identical to the content + /// of the `Character` variant of `logical_key`. + /// However, on Windows when a dead key was pressed earlier + /// but cannot be combined with the character from this + /// keypress, the produced text will consist of two characters: + /// the dead-key-character followed by the character resulting + /// from this keypress. + /// + /// An additional difference from `logical_key` is that + /// this field stores the text representation of any key + /// that has such a representation. For example when + /// `logical_key` is `Key::Named(NamedKey::Enter)`, this field is `Some("\r")`. + /// + /// This is `None` if the current keypress cannot + /// be interpreted as text. + /// + /// See also: `text_with_all_modifiers()` + pub text: Option, + + /// Contains the location of this key on the keyboard. + /// + /// Certain keys on the keyboard may appear in more than once place. For example, the "Shift" + /// key appears on the left side of the QWERTY keyboard as well as the right side. However, + /// both keys have the same symbolic value. Another example of this phenomenon is the "1" + /// key, which appears both above the "Q" key and as the "Keypad 1" key. + /// + /// This field allows the user to differentiate between keys like this that have the same + /// symbolic value but different locations on the keyboard. + /// + /// See the [`KeyLocation`] type for more details. + /// + /// [`KeyLocation`]: crate::keyboard::KeyLocation + pub location: keyboard::KeyLocation, + + /// Whether the key is being pressed or released. + /// + /// See the [`ElementState`] type for more details. + pub state: ElementState, + + /// Whether or not this key is a key repeat event. + /// + /// On some systems, holding down a key for some period of time causes that key to be repeated + /// as though it were being pressed and released repeatedly. This field is `true` if and only + /// if this event is the result of one of those repeats. + /// + /// # Example + /// + /// In games, you often want to ignore repeated key events - this can be + /// done by ignoring events where this property is set. + /// + /// ``` + /// use winit::event::{ElementState, KeyEvent, WindowEvent}; + /// use winit::keyboard::{KeyCode, PhysicalKey}; + /// # let window_event = WindowEvent::RedrawRequested; // To make the example compile + /// match window_event { + /// WindowEvent::KeyboardInput { + /// event: + /// KeyEvent { + /// physical_key: PhysicalKey::Code(KeyCode::KeyW), + /// state: ElementState::Pressed, + /// repeat: false, + /// .. + /// }, + /// .. + /// } => { + /// // The physical key `W` was pressed, and it was not a repeat + /// }, + /// _ => {}, // Handle other events + /// } + /// ``` + pub repeat: bool, + + /// Platform-specific key event information. + /// + /// On Windows, Linux and macOS, this type contains the key without modifiers and the text with + /// all modifiers applied. + /// + /// On Android, iOS, Redox and Web, this type is a no-op. + pub(crate) platform_specific: platform_impl::KeyEventExtra, +} + +/// Describes keyboard modifiers event. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct Modifiers { + pub(crate) state: ModifiersState, + + // NOTE: Currently pressed modifiers keys. + // + // The field providing a metadata, it shouldn't be used as a source of truth. + pub(crate) pressed_mods: ModifiersKeys, +} + +impl Modifiers { + /// The state of the modifiers. + pub fn state(&self) -> ModifiersState { + self.state + } + + /// The state of the left shift key. + pub fn lshift_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::LSHIFT) + } + + /// The state of the right shift key. + pub fn rshift_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::RSHIFT) + } + + /// The state of the left alt key. + pub fn lalt_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::LALT) + } + + /// The state of the right alt key. + pub fn ralt_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::RALT) + } + + /// The state of the left control key. + pub fn lcontrol_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::LCONTROL) + } + + /// The state of the right control key. + pub fn rcontrol_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::RCONTROL) + } + + /// The state of the left super key. + pub fn lsuper_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::LSUPER) + } + + /// The state of the right super key. + pub fn rsuper_state(&self) -> ModifiersKeyState { + self.mod_state(ModifiersKeys::RSUPER) + } + + fn mod_state(&self, modifier: ModifiersKeys) -> ModifiersKeyState { + if self.pressed_mods.contains(modifier) { + ModifiersKeyState::Pressed + } else { + ModifiersKeyState::Unknown + } + } +} + +impl From for Modifiers { + fn from(value: ModifiersState) -> Self { + Self { state: value, pressed_mods: Default::default() } + } +} + +/// Describes [input method](https://en.wikipedia.org/wiki/Input_method) events. +/// +/// This is also called a "composition event". +/// +/// Most keypresses using a latin-like keyboard layout simply generate a +/// [`WindowEvent::KeyboardInput`]. However, one couldn't possibly have a key for every single +/// unicode character that the user might want to type +/// - so the solution operating systems employ is to allow the user to type these using _a sequence +/// of keypresses_ instead. +/// +/// A prominent example of this is accents - many keyboard layouts allow you to first click the +/// "accent key", and then the character you want to apply the accent to. In this case, some +/// platforms will generate the following event sequence: +/// +/// ```ignore +/// // Press "`" key +/// Ime::Preedit("`", Some((0, 0))) +/// // Press "E" key +/// Ime::Preedit("", None) // Synthetic event generated by winit to clear preedit. +/// Ime::Commit("é") +/// ``` +/// +/// Additionally, certain input devices are configured to display a candidate box that allow the +/// user to select the desired character interactively. (To properly position this box, you must use +/// [`Window::set_ime_cursor_area`].) +/// +/// An example of a keyboard layout which uses candidate boxes is pinyin. On a latin keyboard the +/// following event sequence could be obtained: +/// +/// ```ignore +/// // Press "A" key +/// Ime::Preedit("a", Some((1, 1))) +/// // Press "B" key +/// Ime::Preedit("a b", Some((3, 3))) +/// // Press left arrow key +/// Ime::Preedit("a b", Some((1, 1))) +/// // Press space key +/// Ime::Preedit("啊b", Some((3, 3))) +/// // Press space key +/// Ime::Preedit("", None) // Synthetic event generated by winit to clear preedit. +/// Ime::Commit("啊不") +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Ime { + /// Notifies when the IME was enabled. + /// + /// After getting this event you could receive [`Preedit`][Self::Preedit] and + /// [`Commit`][Self::Commit] events. You should also start performing IME related requests + /// like [`Window::set_ime_cursor_area`]. + Enabled, + + /// Notifies when a new composing text should be set at the cursor position. + /// + /// The value represents a pair of the preedit string and the cursor begin position and end + /// position. When it's `None`, the cursor should be hidden. When `String` is an empty string + /// this indicates that preedit was cleared. + /// + /// The cursor position is byte-wise indexed. + Preedit(String, Option<(usize, usize)>), + + /// Notifies when text should be inserted into the editor widget. + /// + /// Right before this event winit will send empty [`Self::Preedit`] event. + Commit(String), + + /// Notifies when the IME was disabled. + /// + /// After receiving this event you won't get any more [`Preedit`][Self::Preedit] or + /// [`Commit`][Self::Commit] events until the next [`Enabled`][Self::Enabled] event. You should + /// also stop issuing IME related requests like [`Window::set_ime_cursor_area`] and clear + /// pending preedit text. + Disabled, +} + +/// Describes touch-screen input state. +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum TouchPhase { + Started, + Moved, + Ended, + Cancelled, +} + +/// Represents a touch event +/// +/// Every time the user touches the screen, a new [`TouchPhase::Started`] event with an unique +/// identifier for the finger is generated. When the finger is lifted, an [`TouchPhase::Ended`] +/// event is generated with the same finger id. +/// +/// After a `Started` event has been emitted, there may be zero or more `Move` +/// events when the finger is moved or the touch pressure changes. +/// +/// The finger id may be reused by the system after an `Ended` event. The user +/// should assume that a new `Started` event received with the same id has nothing +/// to do with the old finger and is a new finger. +/// +/// A [`TouchPhase::Cancelled`] event is emitted when the system has canceled tracking this +/// touch, such as when the window loses focus, or on iOS if the user moves the +/// device against their face. +/// +/// ## Platform-specific +/// +/// - **Web:** Doesn't take into account CSS [`border`], [`padding`], or [`transform`]. +/// - **macOS:** Unsupported. +/// +/// [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border +/// [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding +/// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Touch { + pub device_id: DeviceId, + pub phase: TouchPhase, + pub location: PhysicalPosition, + /// Describes how hard the screen was pressed. May be `None` if the platform + /// does not support pressure sensitivity. + /// + /// ## Platform-specific + /// + /// - Only available on **iOS** 9.0+, **Windows** 8+, **Web**, and **Android**. + /// - **Android**: This will never be [None]. If the device doesn't support pressure + /// sensitivity, force will either be 0.0 or 1.0. Also see the + /// [android documentation](https://developer.android.com/reference/android/view/MotionEvent#AXIS_PRESSURE). + pub force: Option, + /// Unique identifier of a finger. + pub id: u64, +} + +/// Describes the force of a touch event +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Force { + /// On iOS, the force is calibrated so that the same number corresponds to + /// roughly the same amount of pressure on the screen regardless of the + /// device. + Calibrated { + /// The force of the touch, where a value of 1.0 represents the force of + /// an average touch (predetermined by the system, not user-specific). + /// + /// The force reported by Apple Pencil is measured along the axis of the + /// pencil. If you want a force perpendicular to the device, you need to + /// calculate this value using the `altitude_angle` value. + force: f64, + /// The maximum possible force for a touch. + /// + /// The value of this field is sufficiently high to provide a wide + /// dynamic range for values of the `force` field. + max_possible_force: f64, + /// The altitude (in radians) of the stylus. + /// + /// A value of 0 radians indicates that the stylus is parallel to the + /// surface. The value of this property is Pi/2 when the stylus is + /// perpendicular to the surface. + altitude_angle: Option, + }, + /// If the platform reports the force as normalized, we have no way of + /// knowing how much pressure 1.0 corresponds to – we know it's the maximum + /// amount of force, but as to how much force, you might either have to + /// press really really hard, or not hard at all, depending on the device. + Normalized(f64), +} + +impl Force { + /// Returns the force normalized to the range between 0.0 and 1.0 inclusive. + /// + /// Instead of normalizing the force, you should prefer to handle + /// [`Force::Calibrated`] so that the amount of force the user has to apply is + /// consistent across devices. + pub fn normalized(&self) -> f64 { + match self { + Force::Calibrated { force, max_possible_force, altitude_angle } => { + let force = match altitude_angle { + Some(altitude_angle) => force / altitude_angle.sin(), + None => *force, + }; + force / max_possible_force + }, + Force::Normalized(force) => *force, + } + } +} + +/// Identifier for a specific analog axis on some device. +pub type AxisId = u32; + +/// Identifier for a specific button on some device. +pub type ButtonId = u32; + +/// Describes the input state of a key. +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum ElementState { + Pressed, + Released, +} + +impl ElementState { + /// True if `self == Pressed`. + pub fn is_pressed(self) -> bool { + self == ElementState::Pressed + } +} + +/// Describes a button of a mouse controller. +/// +/// ## Platform-specific +/// +/// **macOS:** `Back` and `Forward` might not work with all hardware. +/// **Orbital:** `Back` and `Forward` are unsupported due to orbital not supporting them. +#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum MouseButton { + Left, + Right, + Middle, + Back, + Forward, + Other(u16), +} + +/// Describes a difference in the mouse scroll wheel state. +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum MouseScrollDelta { + /// Amount in lines or rows to scroll in the horizontal + /// and vertical directions. + /// + /// Positive values indicate that the content that is being scrolled should move + /// right and down (revealing more content left and up). + LineDelta(f32, f32), + + /// Amount in pixels to scroll in the horizontal and + /// vertical direction. + /// + /// Scroll events are expressed as a `PixelDelta` if + /// supported by the device (eg. a touchpad) and + /// platform. + /// + /// Positive values indicate that the content being scrolled should + /// move right/down. + /// + /// For a 'natural scrolling' touch pad (that acts like a touch screen) + /// this means moving your fingers right and down should give positive values, + /// and move the content right and down (to reveal more things left and up). + PixelDelta(PhysicalPosition), +} + +/// Handle to synchronously change the size of the window from the +/// [`WindowEvent`]. +#[derive(Debug, Clone)] +pub struct InnerSizeWriter { + pub(crate) new_inner_size: Weak>>, +} + +impl InnerSizeWriter { + #[cfg(not(orbital_platform))] + pub(crate) fn new(new_inner_size: Weak>>) -> Self { + Self { new_inner_size } + } + + /// Try to request inner size which will be set synchronously on the window. + pub fn request_inner_size( + &mut self, + new_inner_size: PhysicalSize, + ) -> Result<(), ExternalError> { + if let Some(inner) = self.new_inner_size.upgrade() { + *inner.lock().unwrap() = new_inner_size; + Ok(()) + } else { + Err(ExternalError::Ignored) + } + } +} + +impl PartialEq for InnerSizeWriter { + fn eq(&self, other: &Self) -> bool { + self.new_inner_size.as_ptr() == other.new_inner_size.as_ptr() + } +} + +#[cfg(test)] +mod tests { + use crate::dpi::PhysicalPosition; + use crate::event; + use std::collections::{BTreeSet, HashSet}; + + macro_rules! foreach_event { + ($closure:expr) => {{ + #[allow(unused_mut)] + let mut x = $closure; + let did = event::DeviceId::dummy(); + + #[allow(deprecated)] + { + use crate::event::Event::*; + use crate::event::Ime::Enabled; + use crate::event::WindowEvent::*; + use crate::window::WindowId; + + // Mainline events. + let wid = WindowId::dummy(); + x(UserEvent(())); + x(NewEvents(event::StartCause::Init)); + x(AboutToWait); + x(LoopExiting); + x(Suspended); + x(Resumed); + + // Window events. + let with_window_event = |wev| x(WindowEvent { window_id: wid, event: wev }); + + with_window_event(CloseRequested); + with_window_event(Destroyed); + with_window_event(Focused(true)); + with_window_event(Moved((0, 0).into())); + with_window_event(Resized((0, 0).into())); + with_window_event(DroppedFile("x.txt".into())); + with_window_event(HoveredFile("x.txt".into())); + with_window_event(HoveredFileCancelled); + with_window_event(Ime(Enabled)); + with_window_event(CursorMoved { device_id: did, position: (0, 0).into() }); + with_window_event(ModifiersChanged(event::Modifiers::default())); + with_window_event(CursorEntered { device_id: did }); + with_window_event(CursorLeft { device_id: did }); + with_window_event(MouseWheel { + device_id: did, + delta: event::MouseScrollDelta::LineDelta(0.0, 0.0), + phase: event::TouchPhase::Started, + }); + with_window_event(MouseInput { + device_id: did, + state: event::ElementState::Pressed, + button: event::MouseButton::Other(0), + }); + with_window_event(PinchGesture { + device_id: did, + delta: 0.0, + phase: event::TouchPhase::Started, + }); + with_window_event(DoubleTapGesture { device_id: did }); + with_window_event(RotationGesture { + device_id: did, + delta: 0.0, + phase: event::TouchPhase::Started, + }); + with_window_event(PanGesture { + device_id: did, + delta: PhysicalPosition::::new(0.0, 0.0), + phase: event::TouchPhase::Started, + }); + with_window_event(TouchpadPressure { device_id: did, pressure: 0.0, stage: 0 }); + with_window_event(AxisMotion { device_id: did, axis: 0, value: 0.0 }); + with_window_event(Touch(event::Touch { + device_id: did, + phase: event::TouchPhase::Started, + location: (0.0, 0.0).into(), + id: 0, + force: Some(event::Force::Normalized(0.0)), + })); + with_window_event(ThemeChanged(crate::window::Theme::Light)); + with_window_event(Occluded(true)); + } + + #[allow(deprecated)] + { + use event::DeviceEvent::*; + + let with_device_event = + |dev_ev| x(event::Event::DeviceEvent { device_id: did, event: dev_ev }); + + with_device_event(Added); + with_device_event(Removed); + with_device_event(MouseMotion { delta: (0.0, 0.0).into() }); + with_device_event(MouseWheel { + delta: event::MouseScrollDelta::LineDelta(0.0, 0.0), + }); + with_device_event(Motion { axis: 0, value: 0.0 }); + with_device_event(Button { button: 0, state: event::ElementState::Pressed }); + } + }}; + } + + #[allow(clippy::redundant_clone)] + #[test] + fn test_event_clone() { + foreach_event!(|event: event::Event<()>| { + let event2 = event.clone(); + assert_eq!(event, event2); + }) + } + + #[test] + fn test_map_nonuser_event() { + foreach_event!(|event: event::Event<()>| { + let is_user = matches!(event, event::Event::UserEvent(())); + let event2 = event.map_nonuser_event::<()>(); + if is_user { + assert_eq!(event2, Err(event::Event::UserEvent(()))); + } else { + assert!(event2.is_ok()); + } + }) + } + + #[test] + fn test_force_normalize() { + let force = event::Force::Normalized(0.0); + assert_eq!(force.normalized(), 0.0); + + let force2 = + event::Force::Calibrated { force: 5.0, max_possible_force: 2.5, altitude_angle: None }; + assert_eq!(force2.normalized(), 2.0); + + let force3 = event::Force::Calibrated { + force: 5.0, + max_possible_force: 2.5, + altitude_angle: Some(std::f64::consts::PI / 2.0), + }; + assert_eq!(force3.normalized(), 2.0); + } + + #[allow(clippy::clone_on_copy)] + #[test] + fn ensure_attrs_do_not_panic() { + foreach_event!(|event: event::Event<()>| { + let _ = format!("{event:?}"); + }); + let _ = event::StartCause::Init.clone(); + + let did = crate::event::DeviceId::dummy().clone(); + HashSet::new().insert(did); + let mut set = [did, did, did]; + set.sort_unstable(); + let mut set2 = BTreeSet::new(); + set2.insert(did); + set2.insert(did); + + HashSet::new().insert(event::TouchPhase::Started.clone()); + HashSet::new().insert(event::MouseButton::Left.clone()); + HashSet::new().insert(event::Ime::Enabled); + + let _ = event::Touch { + device_id: did, + phase: event::TouchPhase::Started, + location: (0.0, 0.0).into(), + id: 0, + force: Some(event::Force::Normalized(0.0)), + } + .clone(); + let _ = + event::Force::Calibrated { force: 0.0, max_possible_force: 0.0, altitude_angle: None } + .clone(); + } +} diff --git a/vendor/winit/src/event_loop.rs b/vendor/winit/src/event_loop.rs new file mode 100644 index 00000000..233374be --- /dev/null +++ b/vendor/winit/src/event_loop.rs @@ -0,0 +1,651 @@ +//! The [`EventLoop`] struct and assorted supporting types, including +//! [`ControlFlow`]. +//! +//! If you want to send custom events to the event loop, use +//! [`EventLoop::create_proxy`] to acquire an [`EventLoopProxy`] and call its +//! [`send_event`][EventLoopProxy::send_event] method. +//! +//! See the root-level documentation for information on how to create and use an event loop to +//! handle events. +use std::marker::PhantomData; +#[cfg(any(x11_platform, wayland_platform))] +use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::{error, fmt}; + +#[cfg(not(web_platform))] +use std::time::{Duration, Instant}; +#[cfg(web_platform)] +use web_time::{Duration, Instant}; + +use crate::application::ApplicationHandler; +use crate::error::{EventLoopError, OsError}; +use crate::event::Event; +use crate::monitor::MonitorHandle; +use crate::platform_impl; +use crate::window::{CustomCursor, CustomCursorSource, Theme, Window, WindowAttributes}; + +/// Provides a way to retrieve events from the system and from the windows that were registered to +/// the events loop. +/// +/// An `EventLoop` can be seen more or less as a "context". Calling [`EventLoop::new`] +/// initializes everything that will be required to create windows. For example on Linux creating +/// an event loop opens a connection to the X or Wayland server. +/// +/// To wake up an `EventLoop` from a another thread, see the [`EventLoopProxy`] docs. +/// +/// Note that this cannot be shared across threads (due to platform-dependant logic +/// forbidding it), as such it is neither [`Send`] nor [`Sync`]. If you need cross-thread access, +/// the [`Window`] created from this _can_ be sent to an other thread, and the +/// [`EventLoopProxy`] allows you to wake up an `EventLoop` from another thread. +/// +/// [`Window`]: crate::window::Window +pub struct EventLoop { + pub(crate) event_loop: platform_impl::EventLoop, + pub(crate) _marker: PhantomData<*mut ()>, // Not Send nor Sync +} + +/// Target that associates windows with an [`EventLoop`]. +/// +/// This type exists to allow you to create new windows while Winit executes +/// your callback. +pub struct ActiveEventLoop { + pub(crate) p: platform_impl::ActiveEventLoop, + pub(crate) _marker: PhantomData<*mut ()>, // Not Send nor Sync +} + +/// Object that allows building the event loop. +/// +/// This is used to make specifying options that affect the whole application +/// easier. But note that constructing multiple event loops is not supported. +/// +/// This can be created using [`EventLoop::new`] or [`EventLoop::with_user_event`]. +#[derive(Default)] +pub struct EventLoopBuilder { + pub(crate) platform_specific: platform_impl::PlatformSpecificEventLoopAttributes, + _p: PhantomData, +} + +static EVENT_LOOP_CREATED: AtomicBool = AtomicBool::new(false); + +impl EventLoopBuilder<()> { + /// Start building a new event loop. + #[inline] + #[deprecated = "use `EventLoop::builder` instead"] + pub fn new() -> Self { + EventLoop::builder() + } +} + +impl EventLoopBuilder { + /// Builds a new event loop. + /// + /// ***For cross-platform compatibility, the [`EventLoop`] must be created on the main thread, + /// and only once per application.*** + /// + /// Calling this function will result in display backend initialisation. + /// + /// ## Panics + /// + /// Attempting to create the event loop off the main thread will panic. This + /// restriction isn't strictly necessary on all platforms, but is imposed to + /// eliminate any nasty surprises when porting to platforms that require it. + /// `EventLoopBuilderExt::any_thread` functions are exposed in the relevant + /// [`platform`] module if the target platform supports creating an event + /// loop on any thread. + /// + /// ## Platform-specific + /// + /// - **Wayland/X11:** to prevent running under `Wayland` or `X11` unset `WAYLAND_DISPLAY` or + /// `DISPLAY` respectively when building the event loop. + /// - **Android:** must be configured with an `AndroidApp` from `android_main()` by calling + /// [`.with_android_app(app)`] before calling `.build()`, otherwise it'll panic. + /// + /// [`platform`]: crate::platform + #[cfg_attr( + android_platform, + doc = "[`.with_android_app(app)`]: \ + crate::platform::android::EventLoopBuilderExtAndroid::with_android_app" + )] + #[cfg_attr( + not(android_platform), + doc = "[`.with_android_app(app)`]: #only-available-on-android" + )] + #[inline] + pub fn build(&mut self) -> Result, EventLoopError> { + let _span = tracing::debug_span!("winit::EventLoopBuilder::build").entered(); + + if EVENT_LOOP_CREATED.swap(true, Ordering::Relaxed) { + return Err(EventLoopError::RecreationAttempt); + } + + // Certain platforms accept a mutable reference in their API. + #[allow(clippy::unnecessary_mut_passed)] + Ok(EventLoop { + event_loop: platform_impl::EventLoop::new(&mut self.platform_specific)?, + _marker: PhantomData, + }) + } + + #[cfg(web_platform)] + pub(crate) fn allow_event_loop_recreation() { + EVENT_LOOP_CREATED.store(false, Ordering::Relaxed); + } +} + +impl fmt::Debug for EventLoop { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad("EventLoop { .. }") + } +} + +impl fmt::Debug for ActiveEventLoop { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad("ActiveEventLoop { .. }") + } +} + +/// Set through [`ActiveEventLoop::set_control_flow()`]. +/// +/// Indicates the desired behavior of the event loop after [`Event::AboutToWait`] is emitted. +/// +/// Defaults to [`Wait`]. +/// +/// [`Wait`]: Self::Wait +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum ControlFlow { + /// When the current loop iteration finishes, immediately begin a new iteration regardless of + /// whether or not new events are available to process. + Poll, + + /// When the current loop iteration finishes, suspend the thread until another event arrives. + #[default] + Wait, + + /// When the current loop iteration finishes, suspend the thread until either another event + /// arrives or the given time is reached. + /// + /// Useful for implementing efficient timers. Applications which want to render at the + /// display's native refresh rate should instead use [`Poll`] and the VSync functionality + /// of a graphics API to reduce odds of missed frames. + /// + /// [`Poll`]: Self::Poll + WaitUntil(Instant), +} + +impl ControlFlow { + /// Creates a [`ControlFlow`] that waits until a timeout has expired. + /// + /// In most cases, this is set to [`WaitUntil`]. However, if the timeout overflows, it is + /// instead set to [`Wait`]. + /// + /// [`WaitUntil`]: Self::WaitUntil + /// [`Wait`]: Self::Wait + pub fn wait_duration(timeout: Duration) -> Self { + match Instant::now().checked_add(timeout) { + Some(instant) => Self::WaitUntil(instant), + None => Self::Wait, + } + } +} + +impl EventLoop<()> { + /// Create the event loop. + /// + /// This is an alias of `EventLoop::builder().build()`. + #[inline] + pub fn new() -> Result, EventLoopError> { + Self::builder().build() + } + + /// Start building a new event loop. + /// + /// This returns an [`EventLoopBuilder`], to allow configuring the event loop before creation. + /// + /// To get the actual event loop, call [`build`][EventLoopBuilder::build] on that. + #[inline] + pub fn builder() -> EventLoopBuilder<()> { + Self::with_user_event() + } +} + +impl EventLoop { + /// Start building a new event loop, with the given type as the user event + /// type. + pub fn with_user_event() -> EventLoopBuilder { + EventLoopBuilder { platform_specific: Default::default(), _p: PhantomData } + } + + /// See [`run_app`]. + /// + /// [`run_app`]: Self::run_app + #[inline] + #[deprecated = "use `EventLoop::run_app` instead"] + #[cfg(not(all(web_platform, target_feature = "exception-handling")))] + pub fn run(self, event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &ActiveEventLoop), + { + let _span = tracing::debug_span!("winit::EventLoop::run").entered(); + + self.event_loop.run(event_handler) + } + + /// Run the application with the event loop on the calling thread. + /// + /// See the [`set_control_flow()`] docs on how to change the event loop's behavior. + /// + /// ## Platform-specific + /// + /// - **iOS:** Will never return to the caller and so values not passed to this function will + /// *not* be dropped before the process exits. + /// - **Web:** Will _act_ as if it never returns to the caller by throwing a Javascript + /// exception (that Rust doesn't see) that will also mean that the rest of the function is + /// never executed and any values not passed to this function will *not* be dropped. + /// + /// Web applications are recommended to use + #[cfg_attr( + web_platform, + doc = "[`EventLoopExtWebSys::spawn_app()`][crate::platform::web::EventLoopExtWebSys::spawn_app()]" + )] + #[cfg_attr(not(web_platform), doc = "`EventLoopExtWebSys::spawn()`")] + /// [^1] instead of [`run_app()`] to avoid the need + /// for the Javascript exception trick, and to make it clearer that the event loop runs + /// asynchronously (via the browser's own, internal, event loop) and doesn't block the + /// current thread of execution like it does on other platforms. + /// + /// This function won't be available with `target_feature = "exception-handling"`. + /// + /// [`set_control_flow()`]: ActiveEventLoop::set_control_flow() + /// [`run_app()`]: Self::run_app() + /// [^1]: `EventLoopExtWebSys::spawn_app()` is only available on Web. + #[inline] + #[cfg(not(all(web_platform, target_feature = "exception-handling")))] + pub fn run_app>(self, app: &mut A) -> Result<(), EventLoopError> { + self.event_loop.run(|event, event_loop| dispatch_event_for_app(app, event_loop, event)) + } + + /// Creates an [`EventLoopProxy`] that can be used to dispatch user events + /// to the main event loop, possibly from another thread. + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { event_loop_proxy: self.event_loop.create_proxy() } + } + + /// Gets a persistent reference to the underlying platform display. + /// + /// See the [`OwnedDisplayHandle`] type for more information. + pub fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle { platform: self.event_loop.window_target().p.owned_display_handle() } + } + + /// Change if or when [`DeviceEvent`]s are captured. + /// + /// See [`ActiveEventLoop::listen_device_events`] for details. + /// + /// [`DeviceEvent`]: crate::event::DeviceEvent + pub fn listen_device_events(&self, allowed: DeviceEvents) { + let _span = tracing::debug_span!( + "winit::EventLoop::listen_device_events", + allowed = ?allowed + ) + .entered(); + + self.event_loop.window_target().p.listen_device_events(allowed); + } + + /// Sets the [`ControlFlow`]. + pub fn set_control_flow(&self, control_flow: ControlFlow) { + self.event_loop.window_target().p.set_control_flow(control_flow) + } + + /// Create a window. + /// + /// Creating window without event loop running often leads to improper window creation; + /// use [`ActiveEventLoop::create_window`] instead. + #[deprecated = "use `ActiveEventLoop::create_window` instead"] + #[inline] + pub fn create_window(&self, window_attributes: WindowAttributes) -> Result { + let _span = tracing::debug_span!( + "winit::EventLoop::create_window", + window_attributes = ?window_attributes + ) + .entered(); + + let window = + platform_impl::Window::new(&self.event_loop.window_target().p, window_attributes)?; + Ok(Window { window }) + } + + /// Create custom cursor. + pub fn create_custom_cursor(&self, custom_cursor: CustomCursorSource) -> CustomCursor { + self.event_loop.window_target().p.create_custom_cursor(custom_cursor) + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasDisplayHandle for EventLoop { + fn display_handle(&self) -> Result, rwh_06::HandleError> { + rwh_06::HasDisplayHandle::display_handle(self.event_loop.window_target()) + } +} + +#[cfg(feature = "rwh_05")] +unsafe impl rwh_05::HasRawDisplayHandle for EventLoop { + /// Returns a [`rwh_05::RawDisplayHandle`] for the event loop. + fn raw_display_handle(&self) -> rwh_05::RawDisplayHandle { + rwh_05::HasRawDisplayHandle::raw_display_handle(self.event_loop.window_target()) + } +} + +#[cfg(any(x11_platform, wayland_platform))] +impl AsFd for EventLoop { + /// Get the underlying [EventLoop]'s `fd` which you can register + /// into other event loop, like [`calloop`] or [`mio`]. When doing so, the + /// loop must be polled with the [`pump_app_events`] API. + /// + /// [`calloop`]: https://crates.io/crates/calloop + /// [`mio`]: https://crates.io/crates/mio + /// [`pump_app_events`]: crate::platform::pump_events::EventLoopExtPumpEvents::pump_app_events + fn as_fd(&self) -> BorrowedFd<'_> { + self.event_loop.as_fd() + } +} + +#[cfg(any(x11_platform, wayland_platform))] +impl AsRawFd for EventLoop { + /// Get the underlying [EventLoop]'s raw `fd` which you can register + /// into other event loop, like [`calloop`] or [`mio`]. When doing so, the + /// loop must be polled with the [`pump_app_events`] API. + /// + /// [`calloop`]: https://crates.io/crates/calloop + /// [`mio`]: https://crates.io/crates/mio + /// [`pump_app_events`]: crate::platform::pump_events::EventLoopExtPumpEvents::pump_app_events + fn as_raw_fd(&self) -> RawFd { + self.event_loop.as_raw_fd() + } +} + +impl ActiveEventLoop { + /// Create the window. + /// + /// Possible causes of error include denied permission, incompatible system, and lack of memory. + /// + /// ## Platform-specific + /// + /// - **Web:** The window is created but not inserted into the web page automatically. Please + /// see the web platform module for more information. + #[inline] + pub fn create_window(&self, window_attributes: WindowAttributes) -> Result { + let _span = tracing::debug_span!( + "winit::ActiveEventLoop::create_window", + window_attributes = ?window_attributes + ) + .entered(); + + let window = platform_impl::Window::new(&self.p, window_attributes)?; + Ok(Window { window }) + } + + /// Create custom cursor. + pub fn create_custom_cursor(&self, custom_cursor: CustomCursorSource) -> CustomCursor { + let _span = tracing::debug_span!("winit::ActiveEventLoop::create_custom_cursor",).entered(); + + self.p.create_custom_cursor(custom_cursor) + } + + /// Returns the list of all the monitors available on the system. + #[inline] + pub fn available_monitors(&self) -> impl Iterator { + let _span = tracing::debug_span!("winit::ActiveEventLoop::available_monitors",).entered(); + + #[allow(clippy::useless_conversion)] // false positive on some platforms + self.p.available_monitors().into_iter().map(|inner| MonitorHandle { inner }) + } + + /// Returns the primary monitor of the system. + /// + /// Returns `None` if it can't identify any monitor as a primary one. + /// + /// ## Platform-specific + /// + /// **Wayland / Web:** Always returns `None`. + #[inline] + pub fn primary_monitor(&self) -> Option { + let _span = tracing::debug_span!("winit::ActiveEventLoop::primary_monitor",).entered(); + + self.p.primary_monitor().map(|inner| MonitorHandle { inner }) + } + + /// Change if or when [`DeviceEvent`]s are captured. + /// + /// Since the [`DeviceEvent`] capture can lead to high CPU usage for unfocused windows, winit + /// will ignore them by default for unfocused windows on Linux/BSD. This method allows changing + /// this at runtime to explicitly capture them again. + /// + /// ## Platform-specific + /// + /// - **Wayland / macOS / iOS / Android / Orbital:** Unsupported. + /// + /// [`DeviceEvent`]: crate::event::DeviceEvent + pub fn listen_device_events(&self, allowed: DeviceEvents) { + let _span = tracing::debug_span!( + "winit::ActiveEventLoop::listen_device_events", + allowed = ?allowed + ) + .entered(); + + self.p.listen_device_events(allowed); + } + + /// Returns the current system theme. + /// + /// Returns `None` if it cannot be determined on the current platform. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Wayland / x11 / Orbital:** Unsupported. + pub fn system_theme(&self) -> Option { + self.p.system_theme() + } + + /// Sets the [`ControlFlow`]. + pub fn set_control_flow(&self, control_flow: ControlFlow) { + self.p.set_control_flow(control_flow) + } + + /// Gets the current [`ControlFlow`]. + pub fn control_flow(&self) -> ControlFlow { + self.p.control_flow() + } + + /// This exits the event loop. + /// + /// See [`LoopExiting`][Event::LoopExiting]. + pub fn exit(&self) { + let _span = tracing::debug_span!("winit::ActiveEventLoop::exit",).entered(); + + self.p.exit() + } + + /// Returns if the [`EventLoop`] is about to stop. + /// + /// See [`exit()`][Self::exit]. + pub fn exiting(&self) -> bool { + self.p.exiting() + } + + /// Gets a persistent reference to the underlying platform display. + /// + /// See the [`OwnedDisplayHandle`] type for more information. + pub fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle { platform: self.p.owned_display_handle() } + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasDisplayHandle for ActiveEventLoop { + fn display_handle(&self) -> Result, rwh_06::HandleError> { + let raw = self.p.raw_display_handle_rwh_06()?; + // SAFETY: The display will never be deallocated while the event loop is alive. + Ok(unsafe { rwh_06::DisplayHandle::borrow_raw(raw) }) + } +} + +#[cfg(feature = "rwh_05")] +unsafe impl rwh_05::HasRawDisplayHandle for ActiveEventLoop { + /// Returns a [`rwh_05::RawDisplayHandle`] for the event loop. + fn raw_display_handle(&self) -> rwh_05::RawDisplayHandle { + self.p.raw_display_handle_rwh_05() + } +} + +/// A proxy for the underlying display handle. +/// +/// The purpose of this type is to provide a cheaply cloneable handle to the underlying +/// display handle. This is often used by graphics APIs to connect to the underlying APIs. +/// It is difficult to keep a handle to the [`EventLoop`] type or the [`ActiveEventLoop`] +/// type. In contrast, this type involves no lifetimes and can be persisted for as long as +/// needed. +/// +/// For all platforms, this is one of the following: +/// +/// - A zero-sized type that is likely optimized out. +/// - A reference-counted pointer to the underlying type. +#[derive(Clone)] +pub struct OwnedDisplayHandle { + #[cfg_attr(not(any(feature = "rwh_05", feature = "rwh_06")), allow(dead_code))] + platform: platform_impl::OwnedDisplayHandle, +} + +impl fmt::Debug for OwnedDisplayHandle { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OwnedDisplayHandle").finish_non_exhaustive() + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasDisplayHandle for OwnedDisplayHandle { + #[inline] + fn display_handle(&self) -> Result, rwh_06::HandleError> { + let raw = self.platform.raw_display_handle_rwh_06()?; + + // SAFETY: The underlying display handle should be safe. + let handle = unsafe { rwh_06::DisplayHandle::borrow_raw(raw) }; + + Ok(handle) + } +} + +#[cfg(feature = "rwh_05")] +unsafe impl rwh_05::HasRawDisplayHandle for OwnedDisplayHandle { + #[inline] + fn raw_display_handle(&self) -> rwh_05::RawDisplayHandle { + self.platform.raw_display_handle_rwh_05() + } +} + +/// Used to send custom events to [`EventLoop`]. +pub struct EventLoopProxy { + event_loop_proxy: platform_impl::EventLoopProxy, +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + Self { event_loop_proxy: self.event_loop_proxy.clone() } + } +} + +impl EventLoopProxy { + /// Send an event to the [`EventLoop`] from which this proxy was created. This emits a + /// `UserEvent(event)` event in the event loop, where `event` is the value passed to this + /// function. + /// + /// Returns an `Err` if the associated [`EventLoop`] no longer exists. + /// + /// [`UserEvent(event)`]: Event::UserEvent + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + let _span = tracing::debug_span!("winit::EventLoopProxy::send_event",).entered(); + + self.event_loop_proxy.send_event(event) + } +} + +impl fmt::Debug for EventLoopProxy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad("EventLoopProxy { .. }") + } +} + +/// The error that is returned when an [`EventLoopProxy`] attempts to wake up an [`EventLoop`] that +/// no longer exists. +/// +/// Contains the original event given to [`EventLoopProxy::send_event`]. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct EventLoopClosed(pub T); + +impl fmt::Display for EventLoopClosed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Tried to wake up a closed `EventLoop`") + } +} + +impl error::Error for EventLoopClosed {} + +/// Control when device events are captured. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)] +pub enum DeviceEvents { + /// Report device events regardless of window focus. + Always, + /// Only capture device events while the window is focused. + #[default] + WhenFocused, + /// Never capture device events. + Never, +} + +/// A unique identifier of the winit's async request. +/// +/// This could be used to identify the async request once it's done +/// and a specific action must be taken. +/// +/// One of the handling scenarios could be to maintain a working list +/// containing [`AsyncRequestSerial`] and some closure associated with it. +/// Then once event is arriving the working list is being traversed and a job +/// executed and removed from the list. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AsyncRequestSerial { + serial: usize, +} + +impl AsyncRequestSerial { + // TODO(kchibisov): Remove `cfg` when the clipboard will be added. + #[allow(dead_code)] + pub(crate) fn get() -> Self { + static CURRENT_SERIAL: AtomicUsize = AtomicUsize::new(0); + // NOTE: We rely on wrap around here, while the user may just request + // in the loop usize::MAX times that's issue is considered on them. + let serial = CURRENT_SERIAL.fetch_add(1, Ordering::Relaxed); + Self { serial } + } +} + +/// Shim for various run APIs. +#[inline(always)] +pub(crate) fn dispatch_event_for_app>( + app: &mut A, + event_loop: &ActiveEventLoop, + event: Event, +) { + match event { + Event::NewEvents(cause) => app.new_events(event_loop, cause), + Event::WindowEvent { window_id, event } => app.window_event(event_loop, window_id, event), + Event::DeviceEvent { device_id, event } => app.device_event(event_loop, device_id, event), + Event::UserEvent(event) => app.user_event(event_loop, event), + Event::Suspended => app.suspended(event_loop), + Event::Resumed => app.resumed(event_loop), + Event::AboutToWait => app.about_to_wait(event_loop), + Event::LoopExiting => app.exiting(event_loop), + Event::MemoryWarning => app.memory_warning(event_loop), + } +} diff --git a/vendor/winit/src/icon.rs b/vendor/winit/src/icon.rs new file mode 100644 index 00000000..b013d2f0 --- /dev/null +++ b/vendor/winit/src/icon.rs @@ -0,0 +1,117 @@ +use crate::platform_impl::PlatformIcon; +use std::error::Error; +use std::{fmt, io, mem}; + +#[repr(C)] +#[derive(Debug)] +pub(crate) struct Pixel { + pub(crate) r: u8, + pub(crate) g: u8, + pub(crate) b: u8, + pub(crate) a: u8, +} + +pub(crate) const PIXEL_SIZE: usize = mem::size_of::(); + +#[derive(Debug)] +/// An error produced when using [`Icon::from_rgba`] with invalid arguments. +pub enum BadIcon { + /// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be + /// safely interpreted as 32bpp RGBA pixels. + ByteCountNotDivisibleBy4 { byte_count: usize }, + /// Produced when the number of pixels (`rgba.len() / 4`) isn't equal to `width * height`. + /// At least one of your arguments is incorrect. + DimensionsVsPixelCount { width: u32, height: u32, width_x_height: usize, pixel_count: usize }, + /// Produced when underlying OS functionality failed to create the icon + OsError(io::Error), +} + +impl fmt::Display for BadIcon { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BadIcon::ByteCountNotDivisibleBy4 { byte_count } => write!( + f, + "The length of the `rgba` argument ({byte_count:?}) isn't divisible by 4, making \ + it impossible to interpret as 32bpp RGBA pixels.", + ), + BadIcon::DimensionsVsPixelCount { width, height, width_x_height, pixel_count } => { + write!( + f, + "The specified dimensions ({width:?}x{height:?}) don't match the number of \ + pixels supplied by the `rgba` argument ({pixel_count:?}). For those \ + dimensions, the expected pixel count is {width_x_height:?}.", + ) + }, + BadIcon::OsError(e) => write!(f, "OS error when instantiating the icon: {e:?}"), + } + } +} + +impl Error for BadIcon {} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct RgbaIcon { + pub(crate) rgba: Vec, + pub(crate) width: u32, + pub(crate) height: u32, +} + +/// For platforms which don't have window icons (e.g. web) +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NoIcon; + +#[allow(dead_code)] // These are not used on every platform +mod constructors { + use super::*; + + impl RgbaIcon { + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + if rgba.len() % PIXEL_SIZE != 0 { + return Err(BadIcon::ByteCountNotDivisibleBy4 { byte_count: rgba.len() }); + } + let pixel_count = rgba.len() / PIXEL_SIZE; + if pixel_count != (width * height) as usize { + Err(BadIcon::DimensionsVsPixelCount { + width, + height, + width_x_height: (width * height) as usize, + pixel_count, + }) + } else { + Ok(RgbaIcon { rgba, width, height }) + } + } + } + + impl NoIcon { + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + // Create the rgba icon anyway to validate the input + let _ = RgbaIcon::from_rgba(rgba, width, height)?; + Ok(NoIcon) + } + } +} + +/// An icon used for the window titlebar, taskbar, etc. +#[derive(Clone)] +pub struct Icon { + pub(crate) inner: PlatformIcon, +} + +impl fmt::Debug for Icon { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + fmt::Debug::fmt(&self.inner, formatter) + } +} + +impl Icon { + /// Creates an icon from 32bpp RGBA data. + /// + /// The length of `rgba` must be divisible by 4, and `width * height` must equal + /// `rgba.len() / 4`. Otherwise, this will return a `BadIcon` error. + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + let _span = tracing::debug_span!("winit::Icon::from_rgba", width, height).entered(); + + Ok(Icon { inner: PlatformIcon::from_rgba(rgba, width, height)? }) + } +} diff --git a/vendor/winit/src/keyboard.rs b/vendor/winit/src/keyboard.rs new file mode 100644 index 00000000..7b406f08 --- /dev/null +++ b/vendor/winit/src/keyboard.rs @@ -0,0 +1,1804 @@ +//! Types related to the keyboard. + +// This file contains a substantial portion of the UI Events Specification by the W3C. In +// particular, the variant names within `Key` and `KeyCode` and their documentation are modified +// versions of contents of the aforementioned specification. +// +// The original documents are: +// +// ### For `Key` +// UI Events KeyboardEvent key Values +// https://www.w3.org/TR/2017/CR-uievents-key-20170601/ +// Copyright © 2017 W3C® (MIT, ERCIM, Keio, Beihang). +// +// ### For `KeyCode` +// UI Events KeyboardEvent code Values +// https://www.w3.org/TR/2017/CR-uievents-code-20170601/ +// Copyright © 2017 W3C® (MIT, ERCIM, Keio, Beihang). +// +// These documents were used under the terms of the following license. This W3C license as well as +// the W3C short notice apply to the `Key` and `KeyCode` enums and their variants and the +// documentation attached to their variants. + +// --------- BEGINNING OF W3C LICENSE -------------------------------------------------------------- +// +// License +// +// By obtaining and/or copying this work, you (the licensee) agree that you have read, understood, +// and will comply with the following terms and conditions. +// +// Permission to copy, modify, and distribute this work, with or without modification, for any +// purpose and without fee or royalty is hereby granted, provided that you include the following on +// ALL copies of the work or portions thereof, including modifications: +// +// - The full text of this NOTICE in a location viewable to users of the redistributed or derivative +// work. +// - Any pre-existing intellectual property disclaimers, notices, or terms and conditions. If none +// exist, the W3C Software and Document Short Notice should be included. +// - Notice of any changes or modifications, through a copyright statement on the new code or +// document such as "This software or document includes material copied from or derived from +// [title and URI of the W3C document]. Copyright © [YEAR] W3C® (MIT, ERCIM, Keio, Beihang)." +// +// Disclaimers +// +// THIS WORK IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR +// ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD +// PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. +// +// COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES +// ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT. +// +// The name and trademarks of copyright holders may NOT be used in advertising or publicity +// pertaining to the work without specific, written prior permission. Title to copyright in this +// work will at all times remain with copyright holders. +// +// --------- END OF W3C LICENSE -------------------------------------------------------------------- + +// --------- BEGINNING OF W3C SHORT NOTICE --------------------------------------------------------- +// +// winit: https://github.com/rust-windowing/winit +// +// Copyright © 2021 World Wide Web Consortium, (Massachusetts Institute of Technology, European +// Research Consortium for Informatics and Mathematics, Keio University, Beihang). All Rights +// Reserved. This work is distributed under the W3C® Software License [1] in the hope that it will +// be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// [1] http://www.w3.org/Consortium/Legal/copyright-software +// +// --------- END OF W3C SHORT NOTICE --------------------------------------------------------------- + +use bitflags::bitflags; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +pub use smol_str::SmolStr; + +/// Contains the platform-native physical key identifier +/// +/// The exact values vary from platform to platform (which is part of why this is a per-platform +/// enum), but the values are primarily tied to the key's physical location on the keyboard. +/// +/// This enum is primarily used to store raw keycodes when Winit doesn't map a given native +/// physical key identifier to a meaningful [`KeyCode`] variant. In the presence of identifiers we +/// haven't mapped for you yet, this lets you use use [`KeyCode`] to: +/// +/// - Correctly match key press and release events. +/// - On non-web platforms, support assigning keybinds to virtually any key through a UI. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum NativeKeyCode { + Unidentified, + /// An Android "scancode". + Android(u32), + /// A macOS "scancode". + MacOS(u16), + /// A Windows "scancode". + Windows(u16), + /// An XKB "keycode". + Xkb(u32), +} + +impl std::fmt::Debug for NativeKeyCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use NativeKeyCode::{Android, MacOS, Unidentified, Windows, Xkb}; + let mut debug_tuple; + match self { + Unidentified => { + debug_tuple = f.debug_tuple("Unidentified"); + }, + Android(code) => { + debug_tuple = f.debug_tuple("Android"); + debug_tuple.field(&format_args!("0x{code:04X}")); + }, + MacOS(code) => { + debug_tuple = f.debug_tuple("MacOS"); + debug_tuple.field(&format_args!("0x{code:04X}")); + }, + Windows(code) => { + debug_tuple = f.debug_tuple("Windows"); + debug_tuple.field(&format_args!("0x{code:04X}")); + }, + Xkb(code) => { + debug_tuple = f.debug_tuple("Xkb"); + debug_tuple.field(&format_args!("0x{code:04X}")); + }, + } + debug_tuple.finish() + } +} + +/// Contains the platform-native logical key identifier +/// +/// Exactly what that means differs from platform to platform, but the values are to some degree +/// tied to the currently active keyboard layout. The same key on the same keyboard may also report +/// different values on different platforms, which is one of the reasons this is a per-platform +/// enum. +/// +/// This enum is primarily used to store raw keysym when Winit doesn't map a given native logical +/// key identifier to a meaningful [`Key`] variant. This lets you use [`Key`], and let the user +/// define keybinds which work in the presence of identifiers we haven't mapped for you yet. +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum NativeKey { + Unidentified, + /// An Android "keycode", which is similar to a "virtual-key code" on Windows. + Android(u32), + /// A macOS "scancode". There does not appear to be any direct analogue to either keysyms or + /// "virtual-key" codes in macOS, so we report the scancode instead. + MacOS(u16), + /// A Windows "virtual-key code". + Windows(u16), + /// An XKB "keysym". + Xkb(u32), + /// A "key value string". + Web(SmolStr), +} + +impl std::fmt::Debug for NativeKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use NativeKey::{Android, MacOS, Unidentified, Web, Windows, Xkb}; + let mut debug_tuple; + match self { + Unidentified => { + debug_tuple = f.debug_tuple("Unidentified"); + }, + Android(code) => { + debug_tuple = f.debug_tuple("Android"); + debug_tuple.field(&format_args!("0x{code:04X}")); + }, + MacOS(code) => { + debug_tuple = f.debug_tuple("MacOS"); + debug_tuple.field(&format_args!("0x{code:04X}")); + }, + Windows(code) => { + debug_tuple = f.debug_tuple("Windows"); + debug_tuple.field(&format_args!("0x{code:04X}")); + }, + Xkb(code) => { + debug_tuple = f.debug_tuple("Xkb"); + debug_tuple.field(&format_args!("0x{code:04X}")); + }, + Web(code) => { + debug_tuple = f.debug_tuple("Web"); + debug_tuple.field(code); + }, + } + debug_tuple.finish() + } +} + +impl From for NativeKey { + #[inline] + fn from(code: NativeKeyCode) -> Self { + match code { + NativeKeyCode::Unidentified => NativeKey::Unidentified, + NativeKeyCode::Android(x) => NativeKey::Android(x), + NativeKeyCode::MacOS(x) => NativeKey::MacOS(x), + NativeKeyCode::Windows(x) => NativeKey::Windows(x), + NativeKeyCode::Xkb(x) => NativeKey::Xkb(x), + } + } +} + +impl PartialEq for NativeKeyCode { + #[allow(clippy::cmp_owned)] // uses less code than direct match; target is stack allocated + #[inline] + fn eq(&self, rhs: &NativeKey) -> bool { + NativeKey::from(*self) == *rhs + } +} + +impl PartialEq for NativeKey { + #[inline] + fn eq(&self, rhs: &NativeKeyCode) -> bool { + rhs == self + } +} + +/// Represents the location of a physical key. +/// +/// This type is a superset of [`KeyCode`], including an [`Unidentified`][Self::Unidentified] +/// variant. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum PhysicalKey { + /// A known key code + Code(KeyCode), + /// This variant is used when the key cannot be translated to a [`KeyCode`] + /// + /// The native keycode is provided (if available) so you're able to more reliably match + /// key-press and key-release events by hashing the [`PhysicalKey`]. It is also possible to use + /// this for keybinds for non-standard keys, but such keybinds are tied to a given platform. + Unidentified(NativeKeyCode), +} + +impl From for PhysicalKey { + #[inline] + fn from(code: KeyCode) -> Self { + PhysicalKey::Code(code) + } +} + +impl From for PhysicalKey { + #[inline] + fn from(code: NativeKeyCode) -> Self { + PhysicalKey::Unidentified(code) + } +} + +impl PartialEq for PhysicalKey { + #[inline] + fn eq(&self, rhs: &KeyCode) -> bool { + match self { + PhysicalKey::Code(ref code) => code == rhs, + _ => false, + } + } +} + +impl PartialEq for KeyCode { + #[inline] + fn eq(&self, rhs: &PhysicalKey) -> bool { + rhs == self + } +} + +impl PartialEq for PhysicalKey { + #[inline] + fn eq(&self, rhs: &NativeKeyCode) -> bool { + match self { + PhysicalKey::Unidentified(ref code) => code == rhs, + _ => false, + } + } +} + +impl PartialEq for NativeKeyCode { + #[inline] + fn eq(&self, rhs: &PhysicalKey) -> bool { + rhs == self + } +} + +/// Code representing the location of a physical key +/// +/// This mostly conforms to the UI Events Specification's [`KeyboardEvent.code`] with a few +/// exceptions: +/// - The keys that the specification calls "MetaLeft" and "MetaRight" are named "SuperLeft" and +/// "SuperRight" here. +/// - The key that the specification calls "Super" is reported as `Unidentified` here. +/// +/// [`KeyboardEvent.code`]: https://w3c.github.io/uievents-code/#code-value-tables +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum KeyCode { + /// ` on a US keyboard. This is also called a backtick or grave. + /// This is the 半角/全角/漢字 + /// (hankaku/zenkaku/kanji) key on Japanese keyboards + Backquote, + /// Used for both the US \\ (on the 101-key layout) and also for the key + /// located between the " and Enter keys on row C of the 102-, + /// 104- and 106-key layouts. + /// Labeled # on a UK (102) keyboard. + Backslash, + /// [ on a US keyboard. + BracketLeft, + /// ] on a US keyboard. + BracketRight, + /// , on a US keyboard. + Comma, + /// 0 on a US keyboard. + Digit0, + /// 1 on a US keyboard. + Digit1, + /// 2 on a US keyboard. + Digit2, + /// 3 on a US keyboard. + Digit3, + /// 4 on a US keyboard. + Digit4, + /// 5 on a US keyboard. + Digit5, + /// 6 on a US keyboard. + Digit6, + /// 7 on a US keyboard. + Digit7, + /// 8 on a US keyboard. + Digit8, + /// 9 on a US keyboard. + Digit9, + /// = on a US keyboard. + Equal, + /// Located between the left Shift and Z keys. + /// Labeled \\ on a UK keyboard. + IntlBackslash, + /// Located between the / and right Shift keys. + /// Labeled \\ (ro) on a Japanese keyboard. + IntlRo, + /// Located between the = and Backspace keys. + /// Labeled ¥ (yen) on a Japanese keyboard. \\ on a + /// Russian keyboard. + IntlYen, + /// a on a US keyboard. + /// Labeled q on an AZERTY (e.g., French) keyboard. + KeyA, + /// b on a US keyboard. + KeyB, + /// c on a US keyboard. + KeyC, + /// d on a US keyboard. + KeyD, + /// e on a US keyboard. + KeyE, + /// f on a US keyboard. + KeyF, + /// g on a US keyboard. + KeyG, + /// h on a US keyboard. + KeyH, + /// i on a US keyboard. + KeyI, + /// j on a US keyboard. + KeyJ, + /// k on a US keyboard. + KeyK, + /// l on a US keyboard. + KeyL, + /// m on a US keyboard. + KeyM, + /// n on a US keyboard. + KeyN, + /// o on a US keyboard. + KeyO, + /// p on a US keyboard. + KeyP, + /// q on a US keyboard. + /// Labeled a on an AZERTY (e.g., French) keyboard. + KeyQ, + /// r on a US keyboard. + KeyR, + /// s on a US keyboard. + KeyS, + /// t on a US keyboard. + KeyT, + /// u on a US keyboard. + KeyU, + /// v on a US keyboard. + KeyV, + /// w on a US keyboard. + /// Labeled z on an AZERTY (e.g., French) keyboard. + KeyW, + /// x on a US keyboard. + KeyX, + /// y on a US keyboard. + /// Labeled z on a QWERTZ (e.g., German) keyboard. + KeyY, + /// z on a US keyboard. + /// Labeled w on an AZERTY (e.g., French) keyboard, and y on a + /// QWERTZ (e.g., German) keyboard. + KeyZ, + /// - on a US keyboard. + Minus, + /// . on a US keyboard. + Period, + /// ' on a US keyboard. + Quote, + /// ; on a US keyboard. + Semicolon, + /// / on a US keyboard. + Slash, + /// Alt, Option, or ⌥. + AltLeft, + /// Alt, Option, or ⌥. + /// This is labeled AltGr on many keyboard layouts. + AltRight, + /// Backspace or ⌫. + /// Labeled Delete on Apple keyboards. + Backspace, + /// CapsLock or ⇪ + CapsLock, + /// The application context menu key, which is typically found between the right + /// Super key and the right Control key. + ContextMenu, + /// Control or ⌃ + ControlLeft, + /// Control or ⌃ + ControlRight, + /// Enter or ↵. Labeled Return on Apple keyboards. + Enter, + /// The Windows, ⌘, Command, or other OS symbol key. + SuperLeft, + /// The Windows, ⌘, Command, or other OS symbol key. + SuperRight, + /// Shift or ⇧ + ShiftLeft, + /// Shift or ⇧ + ShiftRight, + ///   (space) + Space, + /// Tab or ⇥ + Tab, + /// Japanese: 変 (henkan) + Convert, + /// Japanese: カタカナ/ひらがな/ローマ字 + /// (katakana/hiragana/romaji) + KanaMode, + /// Korean: HangulMode 한/영 (han/yeong) + /// + /// Japanese (Mac keyboard): か (kana) + Lang1, + /// Korean: Hanja 한 (hanja) + /// + /// Japanese (Mac keyboard): 英 (eisu) + Lang2, + /// Japanese (word-processing keyboard): Katakana + Lang3, + /// Japanese (word-processing keyboard): Hiragana + Lang4, + /// Japanese (word-processing keyboard): Zenkaku/Hankaku + Lang5, + /// Japanese: 無変換 (muhenkan) + NonConvert, + /// ⌦. The forward delete key. + /// Note that on Apple keyboards, the key labelled Delete on the main part of + /// the keyboard is encoded as [`Backspace`]. + /// + /// [`Backspace`]: Self::Backspace + Delete, + /// Page Down, End, or ↘ + End, + /// Help. Not present on standard PC keyboards. + Help, + /// Home or ↖ + Home, + /// Insert or Ins. Not present on Apple keyboards. + Insert, + /// Page Down, PgDn, or ⇟ + PageDown, + /// Page Up, PgUp, or ⇞ + PageUp, + /// ↓ + ArrowDown, + /// ← + ArrowLeft, + /// → + ArrowRight, + /// ↑ + ArrowUp, + /// On the Mac, this is used for the numpad Clear key. + NumLock, + /// 0 Ins on a keyboard. 0 on a phone or remote control + Numpad0, + /// 1 End on a keyboard. 1 or 1 QZ on a phone or remote + /// control + Numpad1, + /// 2 ↓ on a keyboard. 2 ABC on a phone or remote control + Numpad2, + /// 3 PgDn on a keyboard. 3 DEF on a phone or remote control + Numpad3, + /// 4 ← on a keyboard. 4 GHI on a phone or remote control + Numpad4, + /// 5 on a keyboard. 5 JKL on a phone or remote control + Numpad5, + /// 6 → on a keyboard. 6 MNO on a phone or remote control + Numpad6, + /// 7 Home on a keyboard. 7 PQRS or 7 PRS on a phone + /// or remote control + Numpad7, + /// 8 ↑ on a keyboard. 8 TUV on a phone or remote control + Numpad8, + /// 9 PgUp on a keyboard. 9 WXYZ or 9 WXY on a phone + /// or remote control + Numpad9, + /// + + NumpadAdd, + /// Found on the Microsoft Natural Keyboard. + NumpadBackspace, + /// C or A (All Clear). Also for use with numpads that have a + /// Clear key that is separate from the NumLock key. On the Mac, the + /// numpad Clear key is encoded as [`NumLock`]. + /// + /// [`NumLock`]: Self::NumLock + NumpadClear, + /// C (Clear Entry) + NumpadClearEntry, + /// , (thousands separator). For locales where the thousands separator + /// is a "." (e.g., Brazil), this key may generate a .. + NumpadComma, + /// . Del. For locales where the decimal separator is "," (e.g., + /// Brazil), this key may generate a ,. + NumpadDecimal, + /// / + NumpadDivide, + NumpadEnter, + /// = + NumpadEqual, + /// # on a phone or remote control device. This key is typically found + /// below the 9 key and to the right of the 0 key. + NumpadHash, + /// M Add current entry to the value stored in memory. + NumpadMemoryAdd, + /// M Clear the value stored in memory. + NumpadMemoryClear, + /// M Replace the current entry with the value stored in memory. + NumpadMemoryRecall, + /// M Replace the value stored in memory with the current entry. + NumpadMemoryStore, + /// M Subtract current entry from the value stored in memory. + NumpadMemorySubtract, + /// * on a keyboard. For use with numpads that provide mathematical + /// operations (+, - * and /). + /// + /// Use `NumpadStar` for the * key on phones and remote controls. + NumpadMultiply, + /// ( Found on the Microsoft Natural Keyboard. + NumpadParenLeft, + /// ) Found on the Microsoft Natural Keyboard. + NumpadParenRight, + /// * on a phone or remote control device. + /// + /// This key is typically found below the 7 key and to the left of + /// the 0 key. + /// + /// Use "NumpadMultiply" for the * key on + /// numeric keypads. + NumpadStar, + /// - + NumpadSubtract, + /// Esc or ⎋ + Escape, + /// Fn This is typically a hardware key that does not generate a separate code. + Fn, + /// FLock or FnLock. Function Lock key. Found on the Microsoft + /// Natural Keyboard. + FnLock, + /// PrtScr SysRq or Print Screen + PrintScreen, + /// Scroll Lock + ScrollLock, + /// Pause Break + Pause, + /// Some laptops place this key to the left of the ↑ key. + /// + /// This also the "back" button (triangle) on Android. + BrowserBack, + BrowserFavorites, + /// Some laptops place this key to the right of the ↑ key. + BrowserForward, + /// The "home" button on Android. + BrowserHome, + BrowserRefresh, + BrowserSearch, + BrowserStop, + /// Eject or ⏏. This key is placed in the function section on some Apple + /// keyboards. + Eject, + /// Sometimes labelled My Computer on the keyboard + LaunchApp1, + /// Sometimes labelled Calculator on the keyboard + LaunchApp2, + LaunchMail, + MediaPlayPause, + MediaSelect, + MediaStop, + MediaTrackNext, + MediaTrackPrevious, + /// This key is placed in the function section on some Apple keyboards, replacing the + /// Eject key. + Power, + Sleep, + AudioVolumeDown, + AudioVolumeMute, + AudioVolumeUp, + WakeUp, + // Legacy modifier key. Also called "Super" in certain places. + Meta, + // Legacy modifier key. + Hyper, + Turbo, + Abort, + Resume, + Suspend, + /// Found on Sun’s USB keyboard. + Again, + /// Found on Sun’s USB keyboard. + Copy, + /// Found on Sun’s USB keyboard. + Cut, + /// Found on Sun’s USB keyboard. + Find, + /// Found on Sun’s USB keyboard. + Open, + /// Found on Sun’s USB keyboard. + Paste, + /// Found on Sun’s USB keyboard. + Props, + /// Found on Sun’s USB keyboard. + Select, + /// Found on Sun’s USB keyboard. + Undo, + /// Use for dedicated ひらがな key found on some Japanese word processing keyboards. + Hiragana, + /// Use for dedicated カタカナ key found on some Japanese word processing keyboards. + Katakana, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F1, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F2, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F3, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F4, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F5, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F6, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F7, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F8, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F9, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F10, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F11, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F12, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F13, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F14, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F15, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F16, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F17, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F18, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F19, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F20, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F21, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F22, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F23, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F24, + /// General-purpose function key. + F25, + /// General-purpose function key. + F26, + /// General-purpose function key. + F27, + /// General-purpose function key. + F28, + /// General-purpose function key. + F29, + /// General-purpose function key. + F30, + /// General-purpose function key. + F31, + /// General-purpose function key. + F32, + /// General-purpose function key. + F33, + /// General-purpose function key. + F34, + /// General-purpose function key. + F35, +} + +/// A [`Key::Named`] value +/// +/// This mostly conforms to the UI Events Specification's [`KeyboardEvent.key`] with a few +/// exceptions: +/// - The `Super` variant here, is named `Meta` in the aforementioned specification. (There's +/// another key which the specification calls `Super`. That does not exist here.) +/// - The `Space` variant here, can be identified by the character it generates in the +/// specification. +/// +/// [`KeyboardEvent.key`]: https://w3c.github.io/uievents-key/ +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum NamedKey { + /// The `Alt` (Alternative) key. + /// + /// This key enables the alternate modifier function for interpreting concurrent or subsequent + /// keyboard input. This key value is also used for the Apple Option key. + Alt, + /// The Alternate Graphics (AltGr or AltGraph) key. + /// + /// This key is used enable the ISO Level 3 shift modifier (the standard `Shift` key is the + /// level 2 modifier). + AltGraph, + /// The `Caps Lock` (Capital) key. + /// + /// Toggle capital character lock function for interpreting subsequent keyboard input event. + CapsLock, + /// The `Control` or `Ctrl` key. + /// + /// Used to enable control modifier function for interpreting concurrent or subsequent keyboard + /// input. + Control, + /// The Function switch `Fn` key. Activating this key simultaneously with another key changes + /// that key’s value to an alternate character or function. This key is often handled directly + /// in the keyboard hardware and does not usually generate key events. + Fn, + /// The Function-Lock (`FnLock` or `F-Lock`) key. Activating this key switches the mode of the + /// keyboard to changes some keys' values to an alternate character or function. This key is + /// often handled directly in the keyboard hardware and does not usually generate key events. + FnLock, + /// The `NumLock` or Number Lock key. Used to toggle numpad mode function for interpreting + /// subsequent keyboard input. + NumLock, + /// Toggle between scrolling and cursor movement modes. + ScrollLock, + /// Used to enable shift modifier function for interpreting concurrent or subsequent keyboard + /// input. + Shift, + /// The Symbol modifier key (used on some virtual keyboards). + Symbol, + SymbolLock, + // Legacy modifier key. Also called "Super" in certain places. + Meta, + // Legacy modifier key. + Hyper, + /// Used to enable "super" modifier function for interpreting concurrent or subsequent keyboard + /// input. This key value is used for the "Windows Logo" key and the Apple `Command` or `⌘` + /// key. + /// + /// Note: In some contexts (e.g. the Web) this is referred to as the "Meta" key. + Super, + /// The `Enter` or `↵` key. Used to activate current selection or accept current input. This + /// key value is also used for the `Return` (Macintosh numpad) key. This key value is also + /// used for the Android `KEYCODE_DPAD_CENTER`. + Enter, + /// The Horizontal Tabulation `Tab` key. + Tab, + /// Used in text to insert a space between words. Usually located below the character keys. + Space, + /// Navigate or traverse downward. (`KEYCODE_DPAD_DOWN`) + ArrowDown, + /// Navigate or traverse leftward. (`KEYCODE_DPAD_LEFT`) + ArrowLeft, + /// Navigate or traverse rightward. (`KEYCODE_DPAD_RIGHT`) + ArrowRight, + /// Navigate or traverse upward. (`KEYCODE_DPAD_UP`) + ArrowUp, + /// The End key, used with keyboard entry to go to the end of content (`KEYCODE_MOVE_END`). + End, + /// The Home key, used with keyboard entry, to go to start of content (`KEYCODE_MOVE_HOME`). + /// For the mobile phone `Home` key (which goes to the phone’s main screen), use [`GoHome`]. + /// + /// [`GoHome`]: Self::GoHome + Home, + /// Scroll down or display next page of content. + PageDown, + /// Scroll up or display previous page of content. + PageUp, + /// Used to remove the character to the left of the cursor. This key value is also used for + /// the key labeled `Delete` on MacOS keyboards. + Backspace, + /// Remove the currently selected input. + Clear, + /// Copy the current selection. (`APPCOMMAND_COPY`) + Copy, + /// The Cursor Select key. + CrSel, + /// Cut the current selection. (`APPCOMMAND_CUT`) + Cut, + /// Used to delete the character to the right of the cursor. This key value is also used for + /// the key labeled `Delete` on MacOS keyboards when `Fn` is active. + Delete, + /// The Erase to End of Field key. This key deletes all characters from the current cursor + /// position to the end of the current field. + EraseEof, + /// The Extend Selection (Exsel) key. + ExSel, + /// Toggle between text modes for insertion or overtyping. + /// (`KEYCODE_INSERT`) + Insert, + /// The Paste key. (`APPCOMMAND_PASTE`) + Paste, + /// Redo the last action. (`APPCOMMAND_REDO`) + Redo, + /// Undo the last action. (`APPCOMMAND_UNDO`) + Undo, + /// The Accept (Commit, OK) key. Accept current option or input method sequence conversion. + Accept, + /// Redo or repeat an action. + Again, + /// The Attention (Attn) key. + Attn, + Cancel, + /// Show the application’s context menu. + /// This key is commonly found between the right `Super` key and the right `Control` key. + ContextMenu, + /// The `Esc` key. This key was originally used to initiate an escape sequence, but is + /// now more generally used to exit or "escape" the current context, such as closing a dialog + /// or exiting full screen mode. + Escape, + Execute, + /// Open the Find dialog. (`APPCOMMAND_FIND`) + Find, + /// Open a help dialog or toggle display of help information. (`APPCOMMAND_HELP`, + /// `KEYCODE_HELP`) + Help, + /// Pause the current state or application (as appropriate). + /// + /// Note: Do not use this value for the `Pause` button on media controllers. Use `"MediaPause"` + /// instead. + Pause, + /// Play or resume the current state or application (as appropriate). + /// + /// Note: Do not use this value for the `Play` button on media controllers. Use `"MediaPlay"` + /// instead. + Play, + /// The properties (Props) key. + Props, + Select, + /// The ZoomIn key. (`KEYCODE_ZOOM_IN`) + ZoomIn, + /// The ZoomOut key. (`KEYCODE_ZOOM_OUT`) + ZoomOut, + /// The Brightness Down key. Typically controls the display brightness. + /// (`KEYCODE_BRIGHTNESS_DOWN`) + BrightnessDown, + /// The Brightness Up key. Typically controls the display brightness. (`KEYCODE_BRIGHTNESS_UP`) + BrightnessUp, + /// Toggle removable media to eject (open) and insert (close) state. (`KEYCODE_MEDIA_EJECT`) + Eject, + LogOff, + /// Toggle power state. (`KEYCODE_POWER`) + /// Note: Note: Some devices might not expose this key to the operating environment. + Power, + /// The `PowerOff` key. Sometime called `PowerDown`. + PowerOff, + /// Initiate print-screen function. + PrintScreen, + /// The Hibernate key. This key saves the current state of the computer to disk so that it can + /// be restored. The computer will then shutdown. + Hibernate, + /// The Standby key. This key turns off the display and places the computer into a low-power + /// mode without completely shutting down. It is sometimes labelled `Suspend` or `Sleep` key. + /// (`KEYCODE_SLEEP`) + Standby, + /// The WakeUp key. (`KEYCODE_WAKEUP`) + WakeUp, + /// Initiate the multi-candidate mode. + AllCandidates, + Alphanumeric, + /// Initiate the Code Input mode to allow characters to be entered by + /// their code points. + CodeInput, + /// The Compose key, also known as "Multi_key" on the X Window System. This key acts in a + /// manner similar to a dead key, triggering a mode where subsequent key presses are combined + /// to produce a different character. + Compose, + /// Convert the current input method sequence. + Convert, + /// The Final Mode `Final` key used on some Asian keyboards, to enable the final mode for IMEs. + FinalMode, + /// Switch to the first character group. (ISO/IEC 9995) + GroupFirst, + /// Switch to the last character group. (ISO/IEC 9995) + GroupLast, + /// Switch to the next character group. (ISO/IEC 9995) + GroupNext, + /// Switch to the previous character group. (ISO/IEC 9995) + GroupPrevious, + /// Toggle between or cycle through input modes of IMEs. + ModeChange, + NextCandidate, + /// Accept current input method sequence without + /// conversion in IMEs. + NonConvert, + PreviousCandidate, + Process, + SingleCandidate, + /// Toggle between Hangul and English modes. + HangulMode, + HanjaMode, + JunjaMode, + /// The Eisu key. This key may close the IME, but its purpose is defined by the current IME. + /// (`KEYCODE_EISU`) + Eisu, + /// The (Half-Width) Characters key. + Hankaku, + /// The Hiragana (Japanese Kana characters) key. + Hiragana, + /// The Hiragana/Katakana toggle key. (`KEYCODE_KATAKANA_HIRAGANA`) + HiraganaKatakana, + /// The Kana Mode (Kana Lock) key. This key is used to enter hiragana mode (typically from + /// romaji mode). + KanaMode, + /// The Kanji (Japanese name for ideographic characters of Chinese origin) Mode key. This key + /// is typically used to switch to a hiragana keyboard for the purpose of converting input + /// into kanji. (`KEYCODE_KANA`) + KanjiMode, + /// The Katakana (Japanese Kana characters) key. + Katakana, + /// The Roman characters function key. + Romaji, + /// The Zenkaku (Full-Width) Characters key. + Zenkaku, + /// The Zenkaku/Hankaku (full-width/half-width) toggle key. (`KEYCODE_ZENKAKU_HANKAKU`) + ZenkakuHankaku, + /// General purpose virtual function key, as index 1. + Soft1, + /// General purpose virtual function key, as index 2. + Soft2, + /// General purpose virtual function key, as index 3. + Soft3, + /// General purpose virtual function key, as index 4. + Soft4, + /// Select next (numerically or logically) lower channel. (`APPCOMMAND_MEDIA_CHANNEL_DOWN`, + /// `KEYCODE_CHANNEL_DOWN`) + ChannelDown, + /// Select next (numerically or logically) higher channel. (`APPCOMMAND_MEDIA_CHANNEL_UP`, + /// `KEYCODE_CHANNEL_UP`) + ChannelUp, + /// Close the current document or message (Note: This doesn’t close the application). + /// (`APPCOMMAND_CLOSE`) + Close, + /// Open an editor to forward the current message. (`APPCOMMAND_FORWARD_MAIL`) + MailForward, + /// Open an editor to reply to the current message. (`APPCOMMAND_REPLY_TO_MAIL`) + MailReply, + /// Send the current message. (`APPCOMMAND_SEND_MAIL`) + MailSend, + /// Close the current media, for example to close a CD or DVD tray. (`KEYCODE_MEDIA_CLOSE`) + MediaClose, + /// Initiate or continue forward playback at faster than normal speed, or increase speed if + /// already fast forwarding. (`APPCOMMAND_MEDIA_FAST_FORWARD`, `KEYCODE_MEDIA_FAST_FORWARD`) + MediaFastForward, + /// Pause the currently playing media. (`APPCOMMAND_MEDIA_PAUSE`, `KEYCODE_MEDIA_PAUSE`) + /// + /// Note: Media controller devices should use this value rather than `"Pause"` for their pause + /// keys. + MediaPause, + /// Initiate or continue media playback at normal speed, if not currently playing at normal + /// speed. (`APPCOMMAND_MEDIA_PLAY`, `KEYCODE_MEDIA_PLAY`) + MediaPlay, + /// Toggle media between play and pause states. (`APPCOMMAND_MEDIA_PLAY_PAUSE`, + /// `KEYCODE_MEDIA_PLAY_PAUSE`) + MediaPlayPause, + /// Initiate or resume recording of currently selected media. (`APPCOMMAND_MEDIA_RECORD`, + /// `KEYCODE_MEDIA_RECORD`) + MediaRecord, + /// Initiate or continue reverse playback at faster than normal speed, or increase speed if + /// already rewinding. (`APPCOMMAND_MEDIA_REWIND`, `KEYCODE_MEDIA_REWIND`) + MediaRewind, + /// Stop media playing, pausing, forwarding, rewinding, or recording, if not already stopped. + /// (`APPCOMMAND_MEDIA_STOP`, `KEYCODE_MEDIA_STOP`) + MediaStop, + /// Seek to next media or program track. (`APPCOMMAND_MEDIA_NEXTTRACK`, `KEYCODE_MEDIA_NEXT`) + MediaTrackNext, + /// Seek to previous media or program track. (`APPCOMMAND_MEDIA_PREVIOUSTRACK`, + /// `KEYCODE_MEDIA_PREVIOUS`) + MediaTrackPrevious, + /// Open a new document or message. (`APPCOMMAND_NEW`) + New, + /// Open an existing document or message. (`APPCOMMAND_OPEN`) + Open, + /// Print the current document or message. (`APPCOMMAND_PRINT`) + Print, + /// Save the current document or message. (`APPCOMMAND_SAVE`) + Save, + /// Spellcheck the current document or selection. (`APPCOMMAND_SPELL_CHECK`) + SpellCheck, + /// The `11` key found on media numpads that + /// have buttons from `1` ... `12`. + Key11, + /// The `12` key found on media numpads that + /// have buttons from `1` ... `12`. + Key12, + /// Adjust audio balance leftward. (`VK_AUDIO_BALANCE_LEFT`) + AudioBalanceLeft, + /// Adjust audio balance rightward. (`VK_AUDIO_BALANCE_RIGHT`) + AudioBalanceRight, + /// Decrease audio bass boost or cycle down through bass boost states. (`APPCOMMAND_BASS_DOWN`, + /// `VK_BASS_BOOST_DOWN`) + AudioBassBoostDown, + /// Toggle bass boost on/off. (`APPCOMMAND_BASS_BOOST`) + AudioBassBoostToggle, + /// Increase audio bass boost or cycle up through bass boost states. (`APPCOMMAND_BASS_UP`, + /// `VK_BASS_BOOST_UP`) + AudioBassBoostUp, + /// Adjust audio fader towards front. (`VK_FADER_FRONT`) + AudioFaderFront, + /// Adjust audio fader towards rear. (`VK_FADER_REAR`) + AudioFaderRear, + /// Advance surround audio mode to next available mode. (`VK_SURROUND_MODE_NEXT`) + AudioSurroundModeNext, + /// Decrease treble. (`APPCOMMAND_TREBLE_DOWN`) + AudioTrebleDown, + /// Increase treble. (`APPCOMMAND_TREBLE_UP`) + AudioTrebleUp, + /// Decrease audio volume. (`APPCOMMAND_VOLUME_DOWN`, `KEYCODE_VOLUME_DOWN`) + AudioVolumeDown, + /// Increase audio volume. (`APPCOMMAND_VOLUME_UP`, `KEYCODE_VOLUME_UP`) + AudioVolumeUp, + /// Toggle between muted state and prior volume level. (`APPCOMMAND_VOLUME_MUTE`, + /// `KEYCODE_VOLUME_MUTE`) + AudioVolumeMute, + /// Toggle the microphone on/off. (`APPCOMMAND_MIC_ON_OFF_TOGGLE`) + MicrophoneToggle, + /// Decrease microphone volume. (`APPCOMMAND_MICROPHONE_VOLUME_DOWN`) + MicrophoneVolumeDown, + /// Increase microphone volume. (`APPCOMMAND_MICROPHONE_VOLUME_UP`) + MicrophoneVolumeUp, + /// Mute the microphone. (`APPCOMMAND_MICROPHONE_VOLUME_MUTE`, `KEYCODE_MUTE`) + MicrophoneVolumeMute, + /// Show correction list when a word is incorrectly identified. (`APPCOMMAND_CORRECTION_LIST`) + SpeechCorrectionList, + /// Toggle between dictation mode and command/control mode. + /// (`APPCOMMAND_DICTATE_OR_COMMAND_CONTROL_TOGGLE`) + SpeechInputToggle, + /// The first generic "LaunchApplication" key. This is commonly associated with launching "My + /// Computer", and may have a computer symbol on the key. (`APPCOMMAND_LAUNCH_APP1`) + LaunchApplication1, + /// The second generic "LaunchApplication" key. This is commonly associated with launching + /// "Calculator", and may have a calculator symbol on the key. (`APPCOMMAND_LAUNCH_APP2`, + /// `KEYCODE_CALCULATOR`) + LaunchApplication2, + /// The "Calendar" key. (`KEYCODE_CALENDAR`) + LaunchCalendar, + /// The "Contacts" key. (`KEYCODE_CONTACTS`) + LaunchContacts, + /// The "Mail" key. (`APPCOMMAND_LAUNCH_MAIL`) + LaunchMail, + /// The "Media Player" key. (`APPCOMMAND_LAUNCH_MEDIA_SELECT`) + LaunchMediaPlayer, + LaunchMusicPlayer, + LaunchPhone, + LaunchScreenSaver, + LaunchSpreadsheet, + LaunchWebBrowser, + LaunchWebCam, + LaunchWordProcessor, + /// Navigate to previous content or page in current history. (`APPCOMMAND_BROWSER_BACKWARD`) + BrowserBack, + /// Open the list of browser favorites. (`APPCOMMAND_BROWSER_FAVORITES`) + BrowserFavorites, + /// Navigate to next content or page in current history. (`APPCOMMAND_BROWSER_FORWARD`) + BrowserForward, + /// Go to the user’s preferred home page. (`APPCOMMAND_BROWSER_HOME`) + BrowserHome, + /// Refresh the current page or content. (`APPCOMMAND_BROWSER_REFRESH`) + BrowserRefresh, + /// Call up the user’s preferred search page. (`APPCOMMAND_BROWSER_SEARCH`) + BrowserSearch, + /// Stop loading the current page or content. (`APPCOMMAND_BROWSER_STOP`) + BrowserStop, + /// The Application switch key, which provides a list of recent apps to switch between. + /// (`KEYCODE_APP_SWITCH`) + AppSwitch, + /// The Call key. (`KEYCODE_CALL`) + Call, + /// The Camera key. (`KEYCODE_CAMERA`) + Camera, + /// The Camera focus key. (`KEYCODE_FOCUS`) + CameraFocus, + /// The End Call key. (`KEYCODE_ENDCALL`) + EndCall, + /// The Back key. (`KEYCODE_BACK`) + GoBack, + /// The Home key, which goes to the phone’s main screen. (`KEYCODE_HOME`) + GoHome, + /// The Headset Hook key. (`KEYCODE_HEADSETHOOK`) + HeadsetHook, + LastNumberRedial, + /// The Notification key. (`KEYCODE_NOTIFICATION`) + Notification, + /// Toggle between manner mode state: silent, vibrate, ring, ... (`KEYCODE_MANNER_MODE`) + MannerMode, + VoiceDial, + /// Switch to viewing TV. (`KEYCODE_TV`) + TV, + /// TV 3D Mode. (`KEYCODE_3D_MODE`) + TV3DMode, + /// Toggle between antenna and cable input. (`KEYCODE_TV_ANTENNA_CABLE`) + TVAntennaCable, + /// Audio description. (`KEYCODE_TV_AUDIO_DESCRIPTION`) + TVAudioDescription, + /// Audio description mixing volume down. (`KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN`) + TVAudioDescriptionMixDown, + /// Audio description mixing volume up. (`KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP`) + TVAudioDescriptionMixUp, + /// Contents menu. (`KEYCODE_TV_CONTENTS_MENU`) + TVContentsMenu, + /// Contents menu. (`KEYCODE_TV_DATA_SERVICE`) + TVDataService, + /// Switch the input mode on an external TV. (`KEYCODE_TV_INPUT`) + TVInput, + /// Switch to component input #1. (`KEYCODE_TV_INPUT_COMPONENT_1`) + TVInputComponent1, + /// Switch to component input #2. (`KEYCODE_TV_INPUT_COMPONENT_2`) + TVInputComponent2, + /// Switch to composite input #1. (`KEYCODE_TV_INPUT_COMPOSITE_1`) + TVInputComposite1, + /// Switch to composite input #2. (`KEYCODE_TV_INPUT_COMPOSITE_2`) + TVInputComposite2, + /// Switch to HDMI input #1. (`KEYCODE_TV_INPUT_HDMI_1`) + TVInputHDMI1, + /// Switch to HDMI input #2. (`KEYCODE_TV_INPUT_HDMI_2`) + TVInputHDMI2, + /// Switch to HDMI input #3. (`KEYCODE_TV_INPUT_HDMI_3`) + TVInputHDMI3, + /// Switch to HDMI input #4. (`KEYCODE_TV_INPUT_HDMI_4`) + TVInputHDMI4, + /// Switch to VGA input #1. (`KEYCODE_TV_INPUT_VGA_1`) + TVInputVGA1, + /// Media context menu. (`KEYCODE_TV_MEDIA_CONTEXT_MENU`) + TVMediaContext, + /// Toggle network. (`KEYCODE_TV_NETWORK`) + TVNetwork, + /// Number entry. (`KEYCODE_TV_NUMBER_ENTRY`) + TVNumberEntry, + /// Toggle the power on an external TV. (`KEYCODE_TV_POWER`) + TVPower, + /// Radio. (`KEYCODE_TV_RADIO_SERVICE`) + TVRadioService, + /// Satellite. (`KEYCODE_TV_SATELLITE`) + TVSatellite, + /// Broadcast Satellite. (`KEYCODE_TV_SATELLITE_BS`) + TVSatelliteBS, + /// Communication Satellite. (`KEYCODE_TV_SATELLITE_CS`) + TVSatelliteCS, + /// Toggle between available satellites. (`KEYCODE_TV_SATELLITE_SERVICE`) + TVSatelliteToggle, + /// Analog Terrestrial. (`KEYCODE_TV_TERRESTRIAL_ANALOG`) + TVTerrestrialAnalog, + /// Digital Terrestrial. (`KEYCODE_TV_TERRESTRIAL_DIGITAL`) + TVTerrestrialDigital, + /// Timer programming. (`KEYCODE_TV_TIMER_PROGRAMMING`) + TVTimer, + /// Switch the input mode on an external AVR (audio/video receiver). (`KEYCODE_AVR_INPUT`) + AVRInput, + /// Toggle the power on an external AVR (audio/video receiver). (`KEYCODE_AVR_POWER`) + AVRPower, + /// General purpose color-coded media function key, as index 0 (red). (`VK_COLORED_KEY_0`, + /// `KEYCODE_PROG_RED`) + ColorF0Red, + /// General purpose color-coded media function key, as index 1 (green). (`VK_COLORED_KEY_1`, + /// `KEYCODE_PROG_GREEN`) + ColorF1Green, + /// General purpose color-coded media function key, as index 2 (yellow). (`VK_COLORED_KEY_2`, + /// `KEYCODE_PROG_YELLOW`) + ColorF2Yellow, + /// General purpose color-coded media function key, as index 3 (blue). (`VK_COLORED_KEY_3`, + /// `KEYCODE_PROG_BLUE`) + ColorF3Blue, + /// General purpose color-coded media function key, as index 4 (grey). (`VK_COLORED_KEY_4`) + ColorF4Grey, + /// General purpose color-coded media function key, as index 5 (brown). (`VK_COLORED_KEY_5`) + ColorF5Brown, + /// Toggle the display of Closed Captions. (`VK_CC`, `KEYCODE_CAPTIONS`) + ClosedCaptionToggle, + /// Adjust brightness of device, by toggling between or cycling through states. (`VK_DIMMER`) + Dimmer, + /// Swap video sources. (`VK_DISPLAY_SWAP`) + DisplaySwap, + /// Select Digital Video Recorder. (`KEYCODE_DVR`) + DVR, + /// Exit the current application. (`VK_EXIT`) + Exit, + /// Clear program or content stored as favorite 0. (`VK_CLEAR_FAVORITE_0`) + FavoriteClear0, + /// Clear program or content stored as favorite 1. (`VK_CLEAR_FAVORITE_1`) + FavoriteClear1, + /// Clear program or content stored as favorite 2. (`VK_CLEAR_FAVORITE_2`) + FavoriteClear2, + /// Clear program or content stored as favorite 3. (`VK_CLEAR_FAVORITE_3`) + FavoriteClear3, + /// Select (recall) program or content stored as favorite 0. (`VK_RECALL_FAVORITE_0`) + FavoriteRecall0, + /// Select (recall) program or content stored as favorite 1. (`VK_RECALL_FAVORITE_1`) + FavoriteRecall1, + /// Select (recall) program or content stored as favorite 2. (`VK_RECALL_FAVORITE_2`) + FavoriteRecall2, + /// Select (recall) program or content stored as favorite 3. (`VK_RECALL_FAVORITE_3`) + FavoriteRecall3, + /// Store current program or content as favorite 0. (`VK_STORE_FAVORITE_0`) + FavoriteStore0, + /// Store current program or content as favorite 1. (`VK_STORE_FAVORITE_1`) + FavoriteStore1, + /// Store current program or content as favorite 2. (`VK_STORE_FAVORITE_2`) + FavoriteStore2, + /// Store current program or content as favorite 3. (`VK_STORE_FAVORITE_3`) + FavoriteStore3, + /// Toggle display of program or content guide. (`VK_GUIDE`, `KEYCODE_GUIDE`) + Guide, + /// If guide is active and displayed, then display next day’s content. (`VK_NEXT_DAY`) + GuideNextDay, + /// If guide is active and displayed, then display previous day’s content. (`VK_PREV_DAY`) + GuidePreviousDay, + /// Toggle display of information about currently selected context or media. (`VK_INFO`, + /// `KEYCODE_INFO`) + Info, + /// Toggle instant replay. (`VK_INSTANT_REPLAY`) + InstantReplay, + /// Launch linked content, if available and appropriate. (`VK_LINK`) + Link, + /// List the current program. (`VK_LIST`) + ListProgram, + /// Toggle display listing of currently available live content or programs. (`VK_LIVE`) + LiveContent, + /// Lock or unlock current content or program. (`VK_LOCK`) + Lock, + /// Show a list of media applications: audio/video players and image viewers. (`VK_APPS`) + /// + /// Note: Do not confuse this key value with the Windows' `VK_APPS` / `VK_CONTEXT_MENU` key, + /// which is encoded as `"ContextMenu"`. + MediaApps, + /// Audio track key. (`KEYCODE_MEDIA_AUDIO_TRACK`) + MediaAudioTrack, + /// Select previously selected channel or media. (`VK_LAST`, `KEYCODE_LAST_CHANNEL`) + MediaLast, + /// Skip backward to next content or program. (`KEYCODE_MEDIA_SKIP_BACKWARD`) + MediaSkipBackward, + /// Skip forward to next content or program. (`VK_SKIP`, `KEYCODE_MEDIA_SKIP_FORWARD`) + MediaSkipForward, + /// Step backward to next content or program. (`KEYCODE_MEDIA_STEP_BACKWARD`) + MediaStepBackward, + /// Step forward to next content or program. (`KEYCODE_MEDIA_STEP_FORWARD`) + MediaStepForward, + /// Media top menu. (`KEYCODE_MEDIA_TOP_MENU`) + MediaTopMenu, + /// Navigate in. (`KEYCODE_NAVIGATE_IN`) + NavigateIn, + /// Navigate to next key. (`KEYCODE_NAVIGATE_NEXT`) + NavigateNext, + /// Navigate out. (`KEYCODE_NAVIGATE_OUT`) + NavigateOut, + /// Navigate to previous key. (`KEYCODE_NAVIGATE_PREVIOUS`) + NavigatePrevious, + /// Cycle to next favorite channel (in favorites list). (`VK_NEXT_FAVORITE_CHANNEL`) + NextFavoriteChannel, + /// Cycle to next user profile (if there are multiple user profiles). (`VK_USER`) + NextUserProfile, + /// Access on-demand content or programs. (`VK_ON_DEMAND`) + OnDemand, + /// Pairing key to pair devices. (`KEYCODE_PAIRING`) + Pairing, + /// Move picture-in-picture window down. (`VK_PINP_DOWN`) + PinPDown, + /// Move picture-in-picture window. (`VK_PINP_MOVE`) + PinPMove, + /// Toggle display of picture-in-picture window. (`VK_PINP_TOGGLE`) + PinPToggle, + /// Move picture-in-picture window up. (`VK_PINP_UP`) + PinPUp, + /// Decrease media playback speed. (`VK_PLAY_SPEED_DOWN`) + PlaySpeedDown, + /// Reset playback to normal speed. (`VK_PLAY_SPEED_RESET`) + PlaySpeedReset, + /// Increase media playback speed. (`VK_PLAY_SPEED_UP`) + PlaySpeedUp, + /// Toggle random media or content shuffle mode. (`VK_RANDOM_TOGGLE`) + RandomToggle, + /// Not a physical key, but this key code is sent when the remote control battery is low. + /// (`VK_RC_LOW_BATTERY`) + RcLowBattery, + /// Toggle or cycle between media recording speeds. (`VK_RECORD_SPEED_NEXT`) + RecordSpeedNext, + /// Toggle RF (radio frequency) input bypass mode (pass RF input directly to the RF output). + /// (`VK_RF_BYPASS`) + RfBypass, + /// Toggle scan channels mode. (`VK_SCAN_CHANNELS_TOGGLE`) + ScanChannelsToggle, + /// Advance display screen mode to next available mode. (`VK_SCREEN_MODE_NEXT`) + ScreenModeNext, + /// Toggle display of device settings screen. (`VK_SETTINGS`, `KEYCODE_SETTINGS`) + Settings, + /// Toggle split screen mode. (`VK_SPLIT_SCREEN_TOGGLE`) + SplitScreenToggle, + /// Switch the input mode on an external STB (set top box). (`KEYCODE_STB_INPUT`) + STBInput, + /// Toggle the power on an external STB (set top box). (`KEYCODE_STB_POWER`) + STBPower, + /// Toggle display of subtitles, if available. (`VK_SUBTITLE`) + Subtitle, + /// Toggle display of teletext, if available (`VK_TELETEXT`, `KEYCODE_TV_TELETEXT`). + Teletext, + /// Advance video mode to next available mode. (`VK_VIDEO_MODE_NEXT`) + VideoModeNext, + /// Cause device to identify itself in some manner, e.g., audibly or visibly. (`VK_WINK`) + Wink, + /// Toggle between full-screen and scaled content, or alter magnification level. (`VK_ZOOM`, + /// `KEYCODE_TV_ZOOM_MODE`) + ZoomToggle, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F1, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F2, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F3, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F4, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F5, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F6, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F7, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F8, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F9, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F10, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F11, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F12, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F13, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F14, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F15, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F16, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F17, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F18, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F19, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F20, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F21, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F22, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F23, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F24, + /// General-purpose function key. + F25, + /// General-purpose function key. + F26, + /// General-purpose function key. + F27, + /// General-purpose function key. + F28, + /// General-purpose function key. + F29, + /// General-purpose function key. + F30, + /// General-purpose function key. + F31, + /// General-purpose function key. + F32, + /// General-purpose function key. + F33, + /// General-purpose function key. + F34, + /// General-purpose function key. + F35, +} + +/// Key represents the meaning of a keypress. +/// +/// This is a superset of the UI Events Specification's [`KeyboardEvent.key`] with +/// additions: +/// - All simple variants are wrapped under the `Named` variant +/// - The `Unidentified` variant here, can still identify a key through it's `NativeKeyCode`. +/// - The `Dead` variant here, can specify the character which is inserted when pressing the +/// dead-key twice. +/// +/// [`KeyboardEvent.key`]: https://w3c.github.io/uievents-key/ +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Key { + /// A simple (unparameterised) action + Named(NamedKey), + + /// A key string that corresponds to the character typed by the user, taking into account the + /// user’s current locale setting, and any system-level keyboard mapping overrides that are in + /// effect. + Character(Str), + + /// This variant is used when the key cannot be translated to any other variant. + /// + /// The native key is provided (if available) in order to allow the user to specify keybindings + /// for keys which are not defined by this API, mainly through some sort of UI. + Unidentified(NativeKey), + + /// Contains the text representation of the dead-key when available. + /// + /// ## Platform-specific + /// - **Web:** Always contains `None` + Dead(Option), +} + +impl From for Key { + #[inline] + fn from(action: NamedKey) -> Self { + Key::Named(action) + } +} + +impl From for Key { + #[inline] + fn from(code: NativeKey) -> Self { + Key::Unidentified(code) + } +} + +impl PartialEq for Key { + #[inline] + fn eq(&self, rhs: &NamedKey) -> bool { + match self { + Key::Named(ref a) => a == rhs, + _ => false, + } + } +} + +impl> PartialEq for Key { + #[inline] + fn eq(&self, rhs: &str) -> bool { + match self { + Key::Character(ref s) => s == rhs, + _ => false, + } + } +} + +impl> PartialEq<&str> for Key { + #[inline] + fn eq(&self, rhs: &&str) -> bool { + self == *rhs + } +} + +impl PartialEq for Key { + #[inline] + fn eq(&self, rhs: &NativeKey) -> bool { + match self { + Key::Unidentified(ref code) => code == rhs, + _ => false, + } + } +} + +impl PartialEq> for NativeKey { + #[inline] + fn eq(&self, rhs: &Key) -> bool { + rhs == self + } +} + +impl Key { + /// Convert `Key::Character(SmolStr)` to `Key::Character(&str)` so you can more easily match on + /// `Key`. All other variants remain unchanged. + pub fn as_ref(&self) -> Key<&str> { + match self { + Key::Named(a) => Key::Named(*a), + Key::Character(ch) => Key::Character(ch.as_str()), + Key::Dead(d) => Key::Dead(*d), + Key::Unidentified(u) => Key::Unidentified(u.clone()), + } + } +} + +impl NamedKey { + /// Convert an action to its approximate textual equivalent. + /// + /// # Examples + /// + /// ``` + /// use winit::keyboard::NamedKey; + /// + /// assert_eq!(NamedKey::Enter.to_text(), Some("\r")); + /// assert_eq!(NamedKey::F20.to_text(), None); + /// ``` + pub fn to_text(&self) -> Option<&str> { + match self { + NamedKey::Enter => Some("\r"), + NamedKey::Backspace => Some("\x08"), + NamedKey::Tab => Some("\t"), + NamedKey::Space => Some(" "), + NamedKey::Escape => Some("\x1b"), + _ => None, + } + } +} + +impl Key { + /// Convert a key to its approximate textual equivalent. + /// + /// # Examples + /// + /// ``` + /// use winit::keyboard::{Key, NamedKey}; + /// + /// assert_eq!(Key::Character("a".into()).to_text(), Some("a")); + /// assert_eq!(Key::Named(NamedKey::Enter).to_text(), Some("\r")); + /// assert_eq!(Key::Named(NamedKey::F20).to_text(), None); + /// ``` + pub fn to_text(&self) -> Option<&str> { + match self { + Key::Named(action) => action.to_text(), + Key::Character(ch) => Some(ch.as_str()), + _ => None, + } + } +} + +/// The location of the key on the keyboard. +/// +/// Certain physical keys on the keyboard can have the same value, but are in different locations. +/// For instance, the Shift key can be on the left or right side of the keyboard, or the number +/// keys can be above the letters or on the numpad. This enum allows the user to differentiate +/// them. +/// +/// See the documentation for the [`location`] field on the [`KeyEvent`] struct for more +/// information. +/// +/// [`location`]: ../event/struct.KeyEvent.html#structfield.location +/// [`KeyEvent`]: crate::event::KeyEvent +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum KeyLocation { + /// The key is in its "normal" location on the keyboard. + /// + /// For instance, the "1" key above the "Q" key on a QWERTY keyboard will use this location. + /// This invariant is also returned when the location of the key cannot be identified. + /// + /// ![Standard 1 key](https://raw.githubusercontent.com/rust-windowing/winit/master/docs/res/keyboard_standard_1_key.svg) + /// + /// + /// For image attribution, see the + /// + /// ATTRIBUTION.md + /// + /// file. + /// + Standard, + + /// The key is on the left side of the keyboard. + /// + /// For instance, the left Shift key below the Caps Lock key on a QWERTY keyboard will use this + /// location. + /// + /// ![Left Shift key](https://raw.githubusercontent.com/rust-windowing/winit/master/docs/res/keyboard_left_shift_key.svg) + /// + /// + /// For image attribution, see the + /// + /// ATTRIBUTION.md + /// + /// file. + /// + Left, + + /// The key is on the right side of the keyboard. + /// + /// For instance, the right Shift key below the Enter key on a QWERTY keyboard will use this + /// location. + /// + /// ![Right Shift key](https://raw.githubusercontent.com/rust-windowing/winit/master/docs/res/keyboard_right_shift_key.svg) + /// + /// + /// For image attribution, see the + /// + /// ATTRIBUTION.md + /// + /// file. + /// + Right, + + /// The key is on the numpad. + /// + /// For instance, the "1" key on the numpad will use this location. + /// + /// ![Numpad 1 key](https://raw.githubusercontent.com/rust-windowing/winit/master/docs/res/keyboard_numpad_1_key.svg) + /// + /// + /// For image attribution, see the + /// + /// ATTRIBUTION.md + /// + /// file. + /// + Numpad, +} + +bitflags! { + /// Represents the current state of the keyboard modifiers + /// + /// Each flag represents a modifier and is set if this modifier is active. + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct ModifiersState: u32 { + /// The "shift" key. + const SHIFT = 0b100; + /// The "control" key. + const CONTROL = 0b100 << 3; + /// The "alt" key. + const ALT = 0b100 << 6; + /// This is the "windows" key on PC and "command" key on Mac. + const SUPER = 0b100 << 9; + } +} + +impl ModifiersState { + /// Returns `true` if the shift key is pressed. + pub fn shift_key(&self) -> bool { + self.intersects(Self::SHIFT) + } + + /// Returns `true` if the control key is pressed. + pub fn control_key(&self) -> bool { + self.intersects(Self::CONTROL) + } + + /// Returns `true` if the alt key is pressed. + pub fn alt_key(&self) -> bool { + self.intersects(Self::ALT) + } + + /// Returns `true` if the super key is pressed. + pub fn super_key(&self) -> bool { + self.intersects(Self::SUPER) + } +} + +/// The state of the particular modifiers key. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum ModifiersKeyState { + /// The particular key is pressed. + Pressed, + /// The state of the key is unknown. + #[default] + Unknown, +} + +// NOTE: the exact modifier key is not used to represent modifiers state in the +// first place due to a fact that modifiers state could be changed without any +// key being pressed and on some platforms like Wayland/X11 which key resulted +// in modifiers change is hidden, also, not that it really matters. +// +// The reason this API is even exposed is mostly to provide a way for users +// to treat modifiers differently based on their position, which is required +// on macOS due to their AltGr/Option situation. +bitflags! { + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub(crate) struct ModifiersKeys: u8 { + const LSHIFT = 0b0000_0001; + const RSHIFT = 0b0000_0010; + const LCONTROL = 0b0000_0100; + const RCONTROL = 0b0000_1000; + const LALT = 0b0001_0000; + const RALT = 0b0010_0000; + const LSUPER = 0b0100_0000; + const RSUPER = 0b1000_0000; + } +} + +#[cfg(feature = "serde")] +mod modifiers_serde { + use super::ModifiersState; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + #[derive(Default, Serialize, Deserialize)] + #[serde(default)] + #[serde(rename = "ModifiersState")] + pub struct ModifiersStateSerialize { + pub shift_key: bool, + pub control_key: bool, + pub alt_key: bool, + pub super_key: bool, + } + + impl Serialize for ModifiersState { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let s = ModifiersStateSerialize { + shift_key: self.shift_key(), + control_key: self.control_key(), + alt_key: self.alt_key(), + super_key: self.super_key(), + }; + s.serialize(serializer) + } + } + + impl<'de> Deserialize<'de> for ModifiersState { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let ModifiersStateSerialize { shift_key, control_key, alt_key, super_key } = + ModifiersStateSerialize::deserialize(deserializer)?; + let mut m = ModifiersState::empty(); + m.set(ModifiersState::SHIFT, shift_key); + m.set(ModifiersState::CONTROL, control_key); + m.set(ModifiersState::ALT, alt_key); + m.set(ModifiersState::SUPER, super_key); + Ok(m) + } + } +} diff --git a/vendor/winit/src/lib.rs b/vendor/winit/src/lib.rs new file mode 100644 index 00000000..41a55b23 --- /dev/null +++ b/vendor/winit/src/lib.rs @@ -0,0 +1,217 @@ +//! Winit is a cross-platform window creation and event loop management library. +//! +//! # Building windows +//! +//! Before you can create a [`Window`], you first need to build an [`EventLoop`]. This is done with +//! the [`EventLoop::new()`] function. +//! +//! ```no_run +//! use winit::event_loop::EventLoop; +//! +//! # // Intentionally use `fn main` for clarity +//! fn main() { +//! let event_loop = EventLoop::new().unwrap(); +//! // ... +//! } +//! ``` +//! +//! Then you create a [`Window`] with [`create_window`]. +//! +//! # Event handling +//! +//! Once a [`Window`] has been created, it will generate different *events*. A [`Window`] object can +//! generate [`WindowEvent`]s when certain input events occur, such as a cursor moving over the +//! window or a key getting pressed while the window is focused. Devices can generate +//! [`DeviceEvent`]s, which contain unfiltered event data that isn't specific to a certain window. +//! Some user activity, like mouse movement, can generate both a [`WindowEvent`] *and* a +//! [`DeviceEvent`]. You can also create and handle your own custom [`Event::UserEvent`]s, if +//! desired. +//! +//! You can retrieve events by calling [`EventLoop::run_app()`]. This function will +//! dispatch events for every [`Window`] that was created with that particular [`EventLoop`], and +//! will run until [`exit()`] is used, at which point [`Event::LoopExiting`]. +//! +//! Winit no longer uses a `EventLoop::poll_events() -> impl Iterator`-based event loop +//! model, since that can't be implemented properly on some platforms (e.g web, iOS) and works +//! poorly on most other platforms. However, this model can be re-implemented to an extent with +#![cfg_attr( + any(windows_platform, macos_platform, android_platform, x11_platform, wayland_platform), + doc = "[`EventLoopExtPumpEvents::pump_app_events()`][platform::pump_events::EventLoopExtPumpEvents::pump_app_events()]" +)] +#![cfg_attr( + not(any(windows_platform, macos_platform, android_platform, x11_platform, wayland_platform)), + doc = "`EventLoopExtPumpEvents::pump_app_events()`" +)] +//! [^1]. See that method's documentation for more reasons about why +//! it's discouraged beyond compatibility reasons. +//! +//! +//! ```no_run +//! use winit::application::ApplicationHandler; +//! use winit::event::WindowEvent; +//! use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; +//! use winit::window::{Window, WindowId}; +//! +//! #[derive(Default)] +//! struct App { +//! window: Option, +//! } +//! +//! impl ApplicationHandler for App { +//! fn resumed(&mut self, event_loop: &ActiveEventLoop) { +//! self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); +//! } +//! +//! fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) { +//! match event { +//! WindowEvent::CloseRequested => { +//! println!("The close button was pressed; stopping"); +//! event_loop.exit(); +//! }, +//! WindowEvent::RedrawRequested => { +//! // Redraw the application. +//! // +//! // It's preferable for applications that do not render continuously to render in +//! // this event rather than in AboutToWait, since rendering in here allows +//! // the program to gracefully handle redraws requested by the OS. +//! +//! // Draw. +//! +//! // Queue a RedrawRequested event. +//! // +//! // You only need to call this if you've determined that you need to redraw in +//! // applications which do not always need to. Applications that redraw continuously +//! // can render here instead. +//! self.window.as_ref().unwrap().request_redraw(); +//! } +//! _ => (), +//! } +//! } +//! } +//! +//! # // Intentionally use `fn main` for clarity +//! fn main() { +//! let event_loop = EventLoop::new().unwrap(); +//! +//! // ControlFlow::Poll continuously runs the event loop, even if the OS hasn't +//! // dispatched any events. This is ideal for games and similar applications. +//! event_loop.set_control_flow(ControlFlow::Poll); +//! +//! // ControlFlow::Wait pauses the event loop if no events are available to process. +//! // This is ideal for non-game applications that only update in response to user +//! // input, and uses significantly less power/CPU time than ControlFlow::Poll. +//! event_loop.set_control_flow(ControlFlow::Wait); +//! +//! let mut app = App::default(); +//! event_loop.run_app(&mut app); +//! } +//! ``` +//! +//! [`WindowEvent`] has a [`WindowId`] member. In multi-window environments, it should be +//! compared to the value returned by [`Window::id()`] to determine which [`Window`] +//! dispatched the event. +//! +//! # Drawing on the window +//! +//! Winit doesn't directly provide any methods for drawing on a [`Window`]. However, it allows you +//! to retrieve the raw handle of the window and display (see the [`platform`] module and/or the +//! [`raw_window_handle`] and [`raw_display_handle`] methods), which in turn allows +//! you to create an OpenGL/Vulkan/DirectX/Metal/etc. context that can be used to render graphics. +//! +//! Note that many platforms will display garbage data in the window's client area if the +//! application doesn't render anything to the window by the time the desktop compositor is ready to +//! display the window to the user. If you notice this happening, you should create the window with +//! [`visible` set to `false`][crate::window::WindowAttributes::with_visible] and explicitly make +//! the window visible only once you're ready to render into it. +//! +//! # UI scaling +//! +//! UI scaling is important, go read the docs for the [`dpi`] crate for an +//! introduction. +//! +//! All of Winit's functions return physical types, but can take either logical or physical +//! coordinates as input, allowing you to use the most convenient coordinate system for your +//! particular application. +//! +//! Winit will dispatch a [`ScaleFactorChanged`] event whenever a window's scale factor has changed. +//! This can happen if the user drags their window from a standard-resolution monitor to a high-DPI +//! monitor or if the user changes their DPI settings. This allows you to rescale your application's +//! UI elements and adjust how the platform changes the window's size to reflect the new scale +//! factor. If a window hasn't received a [`ScaleFactorChanged`] event, its scale factor +//! can be found by calling [`window.scale_factor()`]. +//! +//! [`ScaleFactorChanged`]: event::WindowEvent::ScaleFactorChanged +//! [`window.scale_factor()`]: window::Window::scale_factor +//! +//! # Cargo Features +//! +//! Winit provides the following Cargo features: +//! +//! * `x11` (enabled by default): On Unix platforms, enables the X11 backend. +//! * `wayland` (enabled by default): On Unix platforms, enables the Wayland backend. +//! * `rwh_04`: Implement `raw-window-handle v0.4` traits. +//! * `rwh_05`: Implement `raw-window-handle v0.5` traits. +//! * `rwh_06`: Implement `raw-window-handle v0.6` traits. +//! * `serde`: Enables serialization/deserialization of certain types with [Serde](https://crates.io/crates/serde). +//! * `mint`: Enables mint (math interoperability standard types) conversions. +//! +//! See the [`platform`] module for documentation on platform-specific cargo +//! features. +//! +//! [`EventLoop`]: event_loop::EventLoop +//! [`EventLoop::new()`]: event_loop::EventLoop::new +//! [`EventLoop::run_app()`]: event_loop::EventLoop::run_app +//! [`exit()`]: event_loop::ActiveEventLoop::exit +//! [`Window`]: window::Window +//! [`WindowId`]: window::WindowId +//! [`WindowAttributes`]: window::WindowAttributes +//! [`create_window`]: event_loop::ActiveEventLoop::create_window +//! [`Window::id()`]: window::Window::id +//! [`WindowEvent`]: event::WindowEvent +//! [`DeviceEvent`]: event::DeviceEvent +//! [`Event::UserEvent`]: event::Event::UserEvent +//! [`Event::LoopExiting`]: event::Event::LoopExiting +//! [`raw_window_handle`]: ./window/struct.Window.html#method.raw_window_handle +//! [`raw_display_handle`]: ./window/struct.Window.html#method.raw_display_handle +//! [^1]: `EventLoopExtPumpEvents::pump_app_events()` is only available on Windows, macOS, Android, X11 and Wayland. + +#![deny(rust_2018_idioms)] +#![deny(rustdoc::broken_intra_doc_links)] +#![deny(clippy::all)] +#![deny(unsafe_op_in_unsafe_fn)] +#![cfg_attr(clippy, deny(warnings))] +// Doc feature labels can be tested locally by running RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly +// doc +#![cfg_attr(docsrs, feature(doc_cfg), doc(auto_cfg(hide(doc, docsrs))))] +#![allow(clippy::missing_safety_doc)] +#![warn(clippy::uninlined_format_args)] +// TODO: wasm-binding needs to be updated for that to be resolved, for now just silence it. +#![cfg_attr(web_platform, allow(unknown_lints, renamed_and_removed_lints, wasm_c_abi))] + +#[cfg(feature = "rwh_04")] +pub use rwh_04 as raw_window_handle_04; +#[cfg(feature = "rwh_05")] +pub use rwh_05 as raw_window_handle_05; +#[cfg(feature = "rwh_06")] +pub use rwh_06 as raw_window_handle; + +// Re-export DPI types so that users don't have to put it in Cargo.toml. +#[doc(inline)] +pub use dpi; + +pub mod application; +#[cfg(any(doc, doctest, test))] +pub mod changelog; +#[macro_use] +pub mod error; +mod cursor; +pub mod event; +pub mod event_loop; +mod icon; +pub mod keyboard; +pub mod monitor; +mod platform_impl; +mod utils; +pub mod window; + +pub mod platform; diff --git a/vendor/winit/src/monitor.rs b/vendor/winit/src/monitor.rs new file mode 100644 index 00000000..ed987e25 --- /dev/null +++ b/vendor/winit/src/monitor.rs @@ -0,0 +1,167 @@ +//! Types useful for interacting with a user's monitors. +//! +//! If you want to get basic information about a monitor, you can use the +//! [`MonitorHandle`] type. This is retrieved from one of the following +//! methods, which return an iterator of [`MonitorHandle`]: +//! - [`ActiveEventLoop::available_monitors`][crate::event_loop::ActiveEventLoop::available_monitors]. +//! - [`Window::available_monitors`][crate::window::Window::available_monitors]. +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::platform_impl; + +/// Deprecated! Use `VideoModeHandle` instead. +#[deprecated = "Renamed to `VideoModeHandle`"] +pub type VideoMode = VideoModeHandle; + +/// Describes a fullscreen video mode of a monitor. +/// +/// Can be acquired with [`MonitorHandle::video_modes`]. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct VideoModeHandle { + pub(crate) video_mode: platform_impl::VideoModeHandle, +} + +impl std::fmt::Debug for VideoModeHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.video_mode.fmt(f) + } +} + +impl PartialOrd for VideoModeHandle { + fn partial_cmp(&self, other: &VideoModeHandle) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for VideoModeHandle { + fn cmp(&self, other: &VideoModeHandle) -> std::cmp::Ordering { + self.monitor().cmp(&other.monitor()).then( + self.size() + .cmp(&other.size()) + .then( + self.refresh_rate_millihertz() + .cmp(&other.refresh_rate_millihertz()) + .then(self.bit_depth().cmp(&other.bit_depth())), + ) + .reverse(), + ) + } +} + +impl VideoModeHandle { + /// Returns the resolution of this video mode. + #[inline] + pub fn size(&self) -> PhysicalSize { + self.video_mode.size() + } + + /// Returns the bit depth of this video mode, as in how many bits you have + /// available per color. This is generally 24 bits or 32 bits on modern + /// systems, depending on whether the alpha channel is counted or not. + /// + /// ## Platform-specific + /// + /// - **Wayland / Orbital:** Always returns 32. + /// - **iOS:** Always returns 32. + #[inline] + pub fn bit_depth(&self) -> u16 { + self.video_mode.bit_depth() + } + + /// Returns the refresh rate of this video mode in mHz. + #[inline] + pub fn refresh_rate_millihertz(&self) -> u32 { + self.video_mode.refresh_rate_millihertz() + } + + /// Returns the monitor that this video mode is valid for. Each monitor has + /// a separate set of valid video modes. + #[inline] + pub fn monitor(&self) -> MonitorHandle { + MonitorHandle { inner: self.video_mode.monitor() } + } +} + +impl std::fmt::Display for VideoModeHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}x{} @ {} mHz ({} bpp)", + self.size().width, + self.size().height, + self.refresh_rate_millihertz(), + self.bit_depth() + ) + } +} + +/// Handle to a monitor. +/// +/// Allows you to retrieve information about a given monitor and can be used in [`Window`] creation. +/// +/// [`Window`]: crate::window::Window +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct MonitorHandle { + pub(crate) inner: platform_impl::MonitorHandle, +} + +impl MonitorHandle { + /// Returns a human-readable name of the monitor. + /// + /// Returns `None` if the monitor doesn't exist anymore. + #[inline] + pub fn name(&self) -> Option { + self.inner.name() + } + + /// Returns the monitor's resolution. + #[inline] + pub fn size(&self) -> PhysicalSize { + self.inner.size() + } + + /// Returns the top-left corner position of the monitor relative to the larger full + /// screen area. + #[inline] + pub fn position(&self) -> PhysicalPosition { + self.inner.position() + } + + /// The monitor refresh rate used by the system. + /// + /// Return `Some` if succeed, or `None` if failed, which usually happens when the monitor + /// the window is on is removed. + /// + /// When using exclusive fullscreen, the refresh rate of the [`VideoModeHandle`] that was + /// used to enter fullscreen should be used instead. + #[inline] + pub fn refresh_rate_millihertz(&self) -> Option { + self.inner.refresh_rate_millihertz() + } + + /// Returns the scale factor of the underlying monitor. To map logical pixels to physical + /// pixels and vice versa, use [`Window::scale_factor`]. + /// + /// See the [`dpi`] module for more information. + /// + /// ## Platform-specific + /// + /// - **X11:** Can be overridden using the `WINIT_X11_SCALE_FACTOR` environment variable. + /// - **Wayland:** May differ from [`Window::scale_factor`]. + /// - **Android:** Always returns 1.0. + /// + /// [`Window::scale_factor`]: crate::window::Window::scale_factor + #[inline] + pub fn scale_factor(&self) -> f64 { + self.inner.scale_factor() + } + + /// Returns all fullscreen video modes supported by this monitor. + /// + /// ## Platform-specific + /// + /// - **Web:** Always returns an empty iterator + #[inline] + pub fn video_modes(&self) -> impl Iterator { + self.inner.video_modes().map(|video_mode| VideoModeHandle { video_mode }) + } +} diff --git a/vendor/winit/src/platform/android.rs b/vendor/winit/src/platform/android.rs new file mode 100644 index 00000000..b76159c0 --- /dev/null +++ b/vendor/winit/src/platform/android.rs @@ -0,0 +1,188 @@ +//! # Android +//! +//! The Android backend builds on (and exposes types from) the [`ndk`](https://docs.rs/ndk/) crate. +//! +//! Native Android applications need some form of "glue" crate that is responsible +//! for defining the main entry point for your Rust application as well as tracking +//! various life-cycle events and synchronizing with the main JVM thread. +//! +//! Winit uses the [android-activity](https://docs.rs/android-activity/) as a +//! glue crate (prior to `0.28` it used +//! [ndk-glue](https://github.com/rust-windowing/android-ndk-rs/tree/master/ndk-glue)). +//! +//! The version of the glue crate that your application depends on _must_ match the +//! version that Winit depends on because the glue crate is responsible for your +//! application's main entry point. If Cargo resolves multiple versions, they will +//! clash. +//! +//! `winit` glue compatibility table: +//! +//! | winit | ndk-glue | +//! | :---: | :--------------------------: | +//! | 0.30 | `android-activity = "0.6"` | +//! | 0.29 | `android-activity = "0.5"` | +//! | 0.28 | `android-activity = "0.4"` | +//! | 0.27 | `ndk-glue = "0.7"` | +//! | 0.26 | `ndk-glue = "0.5"` | +//! | 0.25 | `ndk-glue = "0.3"` | +//! | 0.24 | `ndk-glue = "0.2"` | +//! +//! The recommended way to avoid a conflict with the glue version is to avoid explicitly +//! depending on the `android-activity` crate, and instead consume the API that +//! is re-exported by Winit under `winit::platform::android::activity::*` +//! +//! Running on an Android device needs a dynamic system library. Add this to Cargo.toml: +//! +//! ```toml +//! [lib] +//! name = "main" +//! crate-type = ["cdylib"] +//! ``` +//! +//! All Android applications are based on an `Activity` subclass, and the +//! `android-activity` crate is designed to support different choices for this base +//! class. Your application _must_ specify the base class it needs via a feature flag: +//! +//! | Base Class | Feature Flag | Notes | +//! | :--------------: | :---------------: | :-----: | +//! | `NativeActivity` | `android-native-activity` | Built-in to Android - it is possible to use without compiling any Java or Kotlin code. Java or Kotlin code may be needed to subclass `NativeActivity` to access some platform features. It does not derive from the [`AndroidAppCompat`] base class.| +//! | [`GameActivity`] | `android-game-activity` | Derives from [`AndroidAppCompat`], a defacto standard `Activity` base class that helps support a wider range of Android versions. Requires a build system that can compile Java or Kotlin and fetch Android dependencies from a [Maven repository][agdk_jetpack] (or link with an embedded [release][agdk_releases] of [`GameActivity`]) | +//! +//! [`GameActivity`]: https://developer.android.com/games/agdk/game-activity +//! [`GameTextInput`]: https://developer.android.com/games/agdk/add-support-for-text-input +//! [`AndroidAppCompat`]: https://developer.android.com/reference/androidx/appcompat/app/AppCompatActivity +//! [agdk_jetpack]: https://developer.android.com/jetpack/androidx/releases/games +//! [agdk_releases]: https://developer.android.com/games/agdk/download#agdk-libraries +//! [Gradle]: https://developer.android.com/studio/build +//! +//! For more details, refer to these `android-activity` [example applications](https://github.com/rust-mobile/android-activity/tree/main/examples). +//! +//! ## Converting from `ndk-glue` to `android-activity` +//! +//! If your application is currently based on `NativeActivity` via the `ndk-glue` crate and building +//! with `cargo apk`, then the minimal changes would be: +//! 1. Remove `ndk-glue` from your `Cargo.toml` +//! 2. Enable the `"android-native-activity"` feature for Winit: `winit = { version = "0.30.13", +//! features = [ "android-native-activity" ] }` +//! 3. Add an `android_main` entrypoint (as above), instead of using the '`[ndk_glue::main]` proc +//! macro from `ndk-macros` (optionally add a dependency on `android_logger` and initialize +//! logging as above). +//! 4. Pass a clone of the `AndroidApp` that your application receives to Winit when building your +//! event loop (as shown above). + +use crate::event_loop::{ActiveEventLoop, EventLoop, EventLoopBuilder}; +use crate::window::{Window, WindowAttributes}; + +use self::activity::{AndroidApp, ConfigurationRef, Rect}; + +/// Additional methods on [`EventLoop`] that are specific to Android. +pub trait EventLoopExtAndroid { + /// Get the [`AndroidApp`] which was used to create this event loop. + fn android_app(&self) -> &AndroidApp; +} + +impl EventLoopExtAndroid for EventLoop { + fn android_app(&self) -> &AndroidApp { + &self.event_loop.android_app + } +} + +/// Additional methods on [`ActiveEventLoop`] that are specific to Android. +pub trait ActiveEventLoopExtAndroid { + /// Get the [`AndroidApp`] which was used to create this event loop. + fn android_app(&self) -> &AndroidApp; +} + +/// Additional methods on [`Window`] that are specific to Android. +pub trait WindowExtAndroid { + fn content_rect(&self) -> Rect; + + fn config(&self) -> ConfigurationRef; +} + +impl WindowExtAndroid for Window { + fn content_rect(&self) -> Rect { + self.window.content_rect() + } + + fn config(&self) -> ConfigurationRef { + self.window.config() + } +} + +impl ActiveEventLoopExtAndroid for ActiveEventLoop { + fn android_app(&self) -> &AndroidApp { + &self.p.app + } +} + +/// Additional methods on [`WindowAttributes`] that are specific to Android. +pub trait WindowAttributesExtAndroid {} + +impl WindowAttributesExtAndroid for WindowAttributes {} + +pub trait EventLoopBuilderExtAndroid { + /// Associates the [`AndroidApp`] that was passed to `android_main()` with the event loop + /// + /// This must be called on Android since the [`AndroidApp`] is not global state. + fn with_android_app(&mut self, app: AndroidApp) -> &mut Self; + + /// Calling this will mark the volume keys to be manually handled by the application + /// + /// Default is to let the operating system handle the volume keys + fn handle_volume_keys(&mut self) -> &mut Self; +} + +impl EventLoopBuilderExtAndroid for EventLoopBuilder { + fn with_android_app(&mut self, app: AndroidApp) -> &mut Self { + self.platform_specific.android_app = Some(app); + self + } + + fn handle_volume_keys(&mut self) -> &mut Self { + self.platform_specific.ignore_volume_keys = false; + self + } +} + +/// Re-export of the `android_activity` API +/// +/// Winit re-exports the `android_activity` API for convenience so that most +/// applications can rely on the Winit crate to resolve the required version of +/// `android_activity` and avoid any chance of a conflict between Winit and the +/// application crate. +/// +/// Unlike most libraries there can only be a single implementation +/// of the `android_activity` glue crate linked with an application because +/// it is responsible for the application's `android_main()` entry point. +/// +/// Since Winit depends on a specific version of `android_activity` the simplest +/// way to avoid creating a conflict is for applications to avoid explicitly +/// depending on the `android_activity` crate, and instead consume the API that +/// is re-exported by Winit. +/// +/// For compatibility applications should then import the [`AndroidApp`] type for +/// their `android_main(app: AndroidApp)` function like: +/// ```rust +/// #[cfg(target_os = "android")] +/// use winit::platform::android::activity::AndroidApp; +/// ``` +pub mod activity { + // We enable the `"native-activity"` feature just so that we can build the + // docs, but it'll be very confusing for users to see the docs with that + // feature enabled, so we avoid inlining it so that they're forced to view + // it on the crate's own docs.rs page. + #[doc(no_inline)] + #[cfg(android_platform)] + pub use android_activity::*; + + #[cfg(not(android_platform))] + #[doc(hidden)] + pub struct Rect; + #[cfg(not(android_platform))] + #[doc(hidden)] + pub struct ConfigurationRef; + #[cfg(not(android_platform))] + #[doc(hidden)] + pub struct AndroidApp; +} diff --git a/vendor/winit/src/platform/ios.rs b/vendor/winit/src/platform/ios.rs new file mode 100644 index 00000000..6e628768 --- /dev/null +++ b/vendor/winit/src/platform/ios.rs @@ -0,0 +1,435 @@ +//! # iOS / UIKit +//! +//! Winit has an OS requirement of iOS 8 or higher, and is regularly tested on +//! iOS 9.3. +//! +//! ## Window initialization +//! +//! iOS's main `UIApplicationMain` does some init work that's required by all +//! UI-related code (see issue [#1705]). It is best to create your windows +//! inside [`ApplicationHandler::resumed`]. +//! +//! [#1705]: https://github.com/rust-windowing/winit/issues/1705 +//! [`ApplicationHandler::resumed`]: crate::application::ApplicationHandler::resumed +//! +//! ## Building app +//! +//! To build ios app you will need rustc built for this targets: +//! +//! - armv7-apple-ios +//! - armv7s-apple-ios +//! - i386-apple-ios +//! - aarch64-apple-ios +//! - x86_64-apple-ios +//! +//! Then +//! +//! ``` +//! cargo build --target=... +//! ``` +//! The simplest way to integrate your app into xcode environment is to build it +//! as a static library. Wrap your main function and export it. +//! +//! ```rust, ignore +//! #[no_mangle] +//! pub extern fn start_winit_app() { +//! start_inner() +//! } +//! +//! fn start_inner() { +//! ... +//! } +//! ``` +//! +//! Compile project and then drag resulting .a into Xcode project. Add winit.h to xcode. +//! +//! ```ignore +//! void start_winit_app(); +//! ``` +//! +//! Use start_winit_app inside your xcode's main function. +//! +//! +//! ## App lifecycle and events +//! +//! iOS environment is very different from other platforms and you must be very +//! careful with it's events. Familiarize yourself with +//! [app lifecycle](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplicationDelegate_Protocol/). +//! +//! This is how those event are represented in winit: +//! +//! - applicationDidBecomeActive is Resumed +//! - applicationWillResignActive is Suspended +//! - applicationWillTerminate is LoopExiting +//! +//! Keep in mind that after LoopExiting event is received every attempt to draw with +//! opengl will result in segfault. +//! +//! Also note that app may not receive the LoopExiting event if suspended; it might be SIGKILL'ed. +//! +//! ## Custom `UIApplicationDelegate` +//! +//! Winit usually handles everything related to the lifecycle events of the application. Sometimes, +//! though, you might want to access some of the more niche stuff that [the application +//! delegate][app-delegate] provides. This functionality is not exposed directly in Winit, since it +//! would increase the API surface by quite a lot. Instead, Winit guarantees that it will not +//! register an application delegate, so you can set up a custom one in a nib file instead. +//! +//! [app-delegate]: https://developer.apple.com/documentation/uikit/uiapplicationdelegate?language=objc + +use std::os::raw::c_void; + +use crate::event_loop::EventLoop; +use crate::monitor::{MonitorHandle, VideoModeHandle}; +use crate::window::{Window, WindowAttributes}; + +/// Additional methods on [`EventLoop`] that are specific to iOS. +pub trait EventLoopExtIOS { + /// Returns the [`Idiom`] (phone/tablet/tv/etc) for the current device. + fn idiom(&self) -> Idiom; +} + +impl EventLoopExtIOS for EventLoop { + fn idiom(&self) -> Idiom { + self.event_loop.idiom() + } +} + +/// Additional methods on [`Window`] that are specific to iOS. +pub trait WindowExtIOS { + /// Sets the [`contentScaleFactor`] of the underlying [`UIWindow`] to `scale_factor`. + /// + /// The default value is device dependent, and it's recommended GLES or Metal applications set + /// this to [`MonitorHandle::scale_factor()`]. + /// + /// [`UIWindow`]: https://developer.apple.com/documentation/uikit/uiwindow?language=objc + /// [`contentScaleFactor`]: https://developer.apple.com/documentation/uikit/uiview/1622657-contentscalefactor?language=objc + fn set_scale_factor(&self, scale_factor: f64); + + /// Sets the valid orientations for the [`Window`]. + /// + /// The default value is [`ValidOrientations::LandscapeAndPortrait`]. + /// + /// This changes the value returned by + /// [`-[UIViewController supportedInterfaceOrientations]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621435-supportedinterfaceorientations?language=objc), + /// and then calls + /// [`-[UIViewController attemptRotationToDeviceOrientation]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621400-attemptrotationtodeviceorientati?language=objc). + fn set_valid_orientations(&self, valid_orientations: ValidOrientations); + + /// Sets whether the [`Window`] prefers the home indicator hidden. + /// + /// The default is to prefer showing the home indicator. + /// + /// This changes the value returned by + /// [`-[UIViewController prefersHomeIndicatorAutoHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887510-prefershomeindicatorautohidden?language=objc), + /// and then calls + /// [`-[UIViewController setNeedsUpdateOfHomeIndicatorAutoHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887509-setneedsupdateofhomeindicatoraut?language=objc). + /// + /// This only has an effect on iOS 11.0+. + fn set_prefers_home_indicator_hidden(&self, hidden: bool); + + /// Sets the screen edges for which the system gestures will take a lower priority than the + /// application's touch handling. + /// + /// This changes the value returned by + /// [`-[UIViewController preferredScreenEdgesDeferringSystemGestures]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887512-preferredscreenedgesdeferringsys?language=objc), + /// and then calls + /// [`-[UIViewController setNeedsUpdateOfScreenEdgesDeferringSystemGestures]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887507-setneedsupdateofscreenedgesdefer?language=objc). + /// + /// This only has an effect on iOS 11.0+. + fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge); + + /// Sets whether the [`Window`] prefers the status bar hidden. + /// + /// The default is to prefer showing the status bar. + /// + /// This sets the value of the + /// [`prefersStatusBarHidden`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621440-prefersstatusbarhidden?language=objc) + /// property. + /// + /// [`setNeedsStatusBarAppearanceUpdate()`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621354-setneedsstatusbarappearanceupdat?language=objc) + /// is also called for you. + fn set_prefers_status_bar_hidden(&self, hidden: bool); + + /// Sets the preferred status bar style for the [`Window`]. + /// + /// The default is system-defined. + /// + /// This sets the value of the + /// [`preferredStatusBarStyle`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621416-preferredstatusbarstyle?language=objc) + /// property. + /// + /// [`setNeedsStatusBarAppearanceUpdate()`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621354-setneedsstatusbarappearanceupdat?language=objc) + /// is also called for you. + fn set_preferred_status_bar_style(&self, status_bar_style: StatusBarStyle); + + /// Sets whether the [`Window`] should recognize pinch gestures. + /// + /// The default is to not recognize gestures. + fn recognize_pinch_gesture(&self, should_recognize: bool); + + /// Sets whether the [`Window`] should recognize pan gestures. + /// + /// The default is to not recognize gestures. + /// Installs [`UIPanGestureRecognizer`](https://developer.apple.com/documentation/uikit/uipangesturerecognizer) onto view + /// + /// Set the minimum number of touches required: [`minimumNumberOfTouches`](https://developer.apple.com/documentation/uikit/uipangesturerecognizer/1621208-minimumnumberoftouches) + /// + /// Set the maximum number of touches recognized: [`maximumNumberOfTouches`](https://developer.apple.com/documentation/uikit/uipangesturerecognizer/1621208-maximumnumberoftouches) + fn recognize_pan_gesture( + &self, + should_recognize: bool, + minimum_number_of_touches: u8, + maximum_number_of_touches: u8, + ); + + /// Sets whether the [`Window`] should recognize double tap gestures. + /// + /// The default is to not recognize gestures. + fn recognize_doubletap_gesture(&self, should_recognize: bool); + + /// Sets whether the [`Window`] should recognize rotation gestures. + /// + /// The default is to not recognize gestures. + fn recognize_rotation_gesture(&self, should_recognize: bool); +} + +impl WindowExtIOS for Window { + #[inline] + fn set_scale_factor(&self, scale_factor: f64) { + self.window.maybe_queue_on_main(move |w| w.set_scale_factor(scale_factor)) + } + + #[inline] + fn set_valid_orientations(&self, valid_orientations: ValidOrientations) { + self.window.maybe_queue_on_main(move |w| w.set_valid_orientations(valid_orientations)) + } + + #[inline] + fn set_prefers_home_indicator_hidden(&self, hidden: bool) { + self.window.maybe_queue_on_main(move |w| w.set_prefers_home_indicator_hidden(hidden)) + } + + #[inline] + fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge) { + self.window.maybe_queue_on_main(move |w| { + w.set_preferred_screen_edges_deferring_system_gestures(edges) + }) + } + + #[inline] + fn set_prefers_status_bar_hidden(&self, hidden: bool) { + self.window.maybe_queue_on_main(move |w| w.set_prefers_status_bar_hidden(hidden)) + } + + #[inline] + fn set_preferred_status_bar_style(&self, status_bar_style: StatusBarStyle) { + self.window.maybe_queue_on_main(move |w| w.set_preferred_status_bar_style(status_bar_style)) + } + + #[inline] + fn recognize_pinch_gesture(&self, should_recognize: bool) { + self.window.maybe_queue_on_main(move |w| w.recognize_pinch_gesture(should_recognize)); + } + + #[inline] + fn recognize_pan_gesture( + &self, + should_recognize: bool, + minimum_number_of_touches: u8, + maximum_number_of_touches: u8, + ) { + self.window.maybe_queue_on_main(move |w| { + w.recognize_pan_gesture( + should_recognize, + minimum_number_of_touches, + maximum_number_of_touches, + ) + }); + } + + #[inline] + fn recognize_doubletap_gesture(&self, should_recognize: bool) { + self.window.maybe_queue_on_main(move |w| w.recognize_doubletap_gesture(should_recognize)); + } + + #[inline] + fn recognize_rotation_gesture(&self, should_recognize: bool) { + self.window.maybe_queue_on_main(move |w| w.recognize_rotation_gesture(should_recognize)); + } +} + +/// Additional methods on [`WindowAttributes`] that are specific to iOS. +pub trait WindowAttributesExtIOS { + /// Sets the [`contentScaleFactor`] of the underlying [`UIWindow`] to `scale_factor`. + /// + /// The default value is device dependent, and it's recommended GLES or Metal applications set + /// this to [`MonitorHandle::scale_factor()`]. + /// + /// [`UIWindow`]: https://developer.apple.com/documentation/uikit/uiwindow?language=objc + /// [`contentScaleFactor`]: https://developer.apple.com/documentation/uikit/uiview/1622657-contentscalefactor?language=objc + fn with_scale_factor(self, scale_factor: f64) -> Self; + + /// Sets the valid orientations for the [`Window`]. + /// + /// The default value is [`ValidOrientations::LandscapeAndPortrait`]. + /// + /// This sets the initial value returned by + /// [`-[UIViewController supportedInterfaceOrientations]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621435-supportedinterfaceorientations?language=objc). + fn with_valid_orientations(self, valid_orientations: ValidOrientations) -> Self; + + /// Sets whether the [`Window`] prefers the home indicator hidden. + /// + /// The default is to prefer showing the home indicator. + /// + /// This sets the initial value returned by + /// [`-[UIViewController prefersHomeIndicatorAutoHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887510-prefershomeindicatorautohidden?language=objc). + /// + /// This only has an effect on iOS 11.0+. + fn with_prefers_home_indicator_hidden(self, hidden: bool) -> Self; + + /// Sets the screen edges for which the system gestures will take a lower priority than the + /// application's touch handling. + /// + /// This sets the initial value returned by + /// [`-[UIViewController preferredScreenEdgesDeferringSystemGestures]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/2887512-preferredscreenedgesdeferringsys?language=objc). + /// + /// This only has an effect on iOS 11.0+. + fn with_preferred_screen_edges_deferring_system_gestures(self, edges: ScreenEdge) -> Self; + + /// Sets whether the [`Window`] prefers the status bar hidden. + /// + /// The default is to prefer showing the status bar. + /// + /// This sets the initial value returned by + /// [`-[UIViewController prefersStatusBarHidden]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621440-prefersstatusbarhidden?language=objc). + fn with_prefers_status_bar_hidden(self, hidden: bool) -> Self; + + /// Sets the style of the [`Window`]'s status bar. + /// + /// The default is system-defined. + /// + /// This sets the initial value returned by + /// [`-[UIViewController preferredStatusBarStyle]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621416-preferredstatusbarstyle?language=objc), + fn with_preferred_status_bar_style(self, status_bar_style: StatusBarStyle) -> Self; +} + +impl WindowAttributesExtIOS for WindowAttributes { + #[inline] + fn with_scale_factor(mut self, scale_factor: f64) -> Self { + self.platform_specific.scale_factor = Some(scale_factor); + self + } + + #[inline] + fn with_valid_orientations(mut self, valid_orientations: ValidOrientations) -> Self { + self.platform_specific.valid_orientations = valid_orientations; + self + } + + #[inline] + fn with_prefers_home_indicator_hidden(mut self, hidden: bool) -> Self { + self.platform_specific.prefers_home_indicator_hidden = hidden; + self + } + + #[inline] + fn with_preferred_screen_edges_deferring_system_gestures(mut self, edges: ScreenEdge) -> Self { + self.platform_specific.preferred_screen_edges_deferring_system_gestures = edges; + self + } + + #[inline] + fn with_prefers_status_bar_hidden(mut self, hidden: bool) -> Self { + self.platform_specific.prefers_status_bar_hidden = hidden; + self + } + + #[inline] + fn with_preferred_status_bar_style(mut self, status_bar_style: StatusBarStyle) -> Self { + self.platform_specific.preferred_status_bar_style = status_bar_style; + self + } +} + +/// Additional methods on [`MonitorHandle`] that are specific to iOS. +pub trait MonitorHandleExtIOS { + /// Returns a pointer to the [`UIScreen`] that is used by this monitor. + /// + /// [`UIScreen`]: https://developer.apple.com/documentation/uikit/uiscreen?language=objc + fn ui_screen(&self) -> *mut c_void; + + /// Returns the preferred [`VideoModeHandle`] for this monitor. + /// + /// This translates to a call to [`-[UIScreen preferredMode]`](https://developer.apple.com/documentation/uikit/uiscreen/1617823-preferredmode?language=objc). + fn preferred_video_mode(&self) -> VideoModeHandle; +} + +impl MonitorHandleExtIOS for MonitorHandle { + #[inline] + fn ui_screen(&self) -> *mut c_void { + // SAFETY: The marker is only used to get the pointer of the screen + let mtm = unsafe { objc2_foundation::MainThreadMarker::new_unchecked() }; + objc2::rc::Retained::as_ptr(self.inner.ui_screen(mtm)) as *mut c_void + } + + #[inline] + fn preferred_video_mode(&self) -> VideoModeHandle { + VideoModeHandle { video_mode: self.inner.preferred_video_mode() } + } +} + +/// Valid orientations for a particular [`Window`]. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ValidOrientations { + /// Excludes `PortraitUpsideDown` on iphone + #[default] + LandscapeAndPortrait, + + Landscape, + + /// Excludes `PortraitUpsideDown` on iphone + Portrait, +} + +/// The device [idiom]. +/// +/// [idiom]: https://developer.apple.com/documentation/uikit/uidevice/1620037-userinterfaceidiom?language=objc +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Idiom { + Unspecified, + + /// iPhone and iPod touch. + Phone, + + /// iPad. + Pad, + + /// tvOS and Apple TV. + TV, + CarPlay, +} + +bitflags::bitflags! { + /// The [edges] of a screen. + /// + /// [edges]: https://developer.apple.com/documentation/uikit/uirectedge?language=objc + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct ScreenEdge: u8 { + const NONE = 0; + const TOP = 1 << 0; + const LEFT = 1 << 1; + const BOTTOM = 1 << 2; + const RIGHT = 1 << 3; + const ALL = ScreenEdge::TOP.bits() | ScreenEdge::LEFT.bits() + | ScreenEdge::BOTTOM.bits() | ScreenEdge::RIGHT.bits(); + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum StatusBarStyle { + #[default] + Default, + LightContent, + DarkContent, +} diff --git a/vendor/winit/src/platform/macos.rs b/vendor/winit/src/platform/macos.rs new file mode 100644 index 00000000..30623b01 --- /dev/null +++ b/vendor/winit/src/platform/macos.rs @@ -0,0 +1,531 @@ +//! # macOS / AppKit +//! +//! Winit has an OS requirement of macOS 10.11 or higher (same as Rust +//! itself), and is regularly tested on macOS 10.14. +//! +//! ## Window initialization +//! +//! A lot of functionality expects the application to be ready before you +//! start doing anything; this includes creating windows, fetching monitors, +//! drawing, and so on, see issues [#2238], [#2051] and [#2087]. +//! +//! If you encounter problems, you should try doing your initialization inside +//! [`ApplicationHandler::resumed`]. +//! +//! [#2238]: https://github.com/rust-windowing/winit/issues/2238 +//! [#2051]: https://github.com/rust-windowing/winit/issues/2051 +//! [#2087]: https://github.com/rust-windowing/winit/issues/2087 +//! [`ApplicationHandler::resumed`]: crate::application::ApplicationHandler::resumed +//! +//! ## Custom `NSApplicationDelegate` +//! +//! Winit usually handles everything related to the lifecycle events of the application. Sometimes, +//! though, you might want to do more niche stuff, such as [handle when the user re-activates the +//! application][reopen]. Such functionality is not exposed directly in Winit, since it would +//! increase the API surface by quite a lot. +//! +//! [reopen]: https://developer.apple.com/documentation/appkit/nsapplicationdelegate/1428638-applicationshouldhandlereopen?language=objc +//! +//! Instead, Winit guarantees that it will not register an application delegate, so the solution is +//! to register your own application delegate, as outlined in the following example (see +//! `objc2-app-kit` for more detailed information). +#![cfg_attr(target_os = "macos", doc = "```")] +#![cfg_attr(not(target_os = "macos"), doc = "```ignore")] +//! use objc2::rc::Retained; +//! use objc2::runtime::ProtocolObject; +//! use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass}; +//! use objc2_app_kit::{NSApplication, NSApplicationDelegate}; +//! use objc2_foundation::{NSArray, NSURL, MainThreadMarker, NSObject, NSObjectProtocol}; +//! use winit::event_loop::EventLoop; +//! +//! declare_class!( +//! struct AppDelegate; +//! +//! unsafe impl ClassType for AppDelegate { +//! type Super = NSObject; +//! type Mutability = mutability::MainThreadOnly; +//! const NAME: &'static str = "MyAppDelegate"; +//! } +//! +//! impl DeclaredClass for AppDelegate {} +//! +//! unsafe impl NSObjectProtocol for AppDelegate {} +//! +//! unsafe impl NSApplicationDelegate for AppDelegate { +//! #[method(application:openURLs:)] +//! fn application_openURLs(&self, application: &NSApplication, urls: &NSArray) { +//! // Note: To specifically get `application:openURLs:` to work, you _might_ +//! // have to bundle your application. This is not done in this example. +//! println!("open urls: {application:?}, {urls:?}"); +//! } +//! } +//! ); +//! +//! impl AppDelegate { +//! fn new(mtm: MainThreadMarker) -> Retained { +//! unsafe { msg_send_id![super(mtm.alloc().set_ivars(())), init] } +//! } +//! } +//! +//! fn main() -> Result<(), Box> { +//! let event_loop = EventLoop::new()?; +//! +//! let mtm = MainThreadMarker::new().unwrap(); +//! let delegate = AppDelegate::new(mtm); +//! // Important: Call `sharedApplication` after `EventLoop::new`, +//! // doing it before is not yet supported. +//! let app = NSApplication::sharedApplication(mtm); +//! app.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); +//! +//! // event_loop.run_app(&mut my_app); +//! Ok(()) +//! } +//! ``` + +use std::os::raw::c_void; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::event_loop::{ActiveEventLoop, EventLoopBuilder}; +use crate::monitor::MonitorHandle; +use crate::window::{Window, WindowAttributes}; + +/// Additional methods on [`Window`] that are specific to MacOS. +pub trait WindowExtMacOS { + /// Returns whether or not the window is in simple fullscreen mode. + fn simple_fullscreen(&self) -> bool; + + /// Toggles a fullscreen mode that doesn't require a new macOS space. + /// Returns a boolean indicating whether the transition was successful (this + /// won't work if the window was already in the native fullscreen). + /// + /// This is how fullscreen used to work on macOS in versions before Lion. + /// And allows the user to have a fullscreen window without using another + /// space or taking control over the entire monitor. + fn set_simple_fullscreen(&self, fullscreen: bool) -> bool; + + /// Returns whether or not the window has shadow. + fn has_shadow(&self) -> bool; + + /// Sets whether or not the window has shadow. + fn set_has_shadow(&self, has_shadow: bool); + + /// Group windows together by using the same tabbing identifier. + /// + /// + fn set_tabbing_identifier(&self, identifier: &str); + + /// Returns the window's tabbing identifier. + fn tabbing_identifier(&self) -> String; + + /// Select next tab. + fn select_next_tab(&self); + + /// Select previous tab. + fn select_previous_tab(&self); + + /// Select the tab with the given index. + /// + /// Will no-op when the index is out of bounds. + fn select_tab_at_index(&self, index: usize); + + /// Get the number of tabs in the window tab group. + fn num_tabs(&self) -> usize; + + /// Get the window's edit state. + /// + /// # Examples + /// + /// ```ignore + /// WindowEvent::CloseRequested => { + /// if window.is_document_edited() { + /// // Show the user a save pop-up or similar + /// } else { + /// // Close the window + /// drop(window); + /// } + /// } + /// ``` + fn is_document_edited(&self) -> bool; + + /// Put the window in a state which indicates a file save is required. + fn set_document_edited(&self, edited: bool); + + /// Set option as alt behavior as described in [`OptionAsAlt`]. + /// + /// This will ignore diacritical marks and accent characters from + /// being processed as received characters. Instead, the input + /// device's raw character will be placed in event queues with the + /// Alt modifier set. + fn set_option_as_alt(&self, option_as_alt: OptionAsAlt); + + /// Getter for the [`WindowExtMacOS::set_option_as_alt`]. + fn option_as_alt(&self) -> OptionAsAlt; + + /// Disable the Menu Bar and Dock in Simple or Borderless Fullscreen mode. Useful for games. + /// The effect is applied when [`WindowExtMacOS::set_simple_fullscreen`] or + /// [`Window::set_fullscreen`] is called. + fn set_borderless_game(&self, borderless_game: bool); + + /// Getter for the [`WindowExtMacOS::set_borderless_game`]. + fn is_borderless_game(&self) -> bool; +} + +impl WindowExtMacOS for Window { + #[inline] + fn simple_fullscreen(&self) -> bool { + self.window.maybe_wait_on_main(|w| w.simple_fullscreen()) + } + + #[inline] + fn set_simple_fullscreen(&self, fullscreen: bool) -> bool { + self.window.maybe_wait_on_main(move |w| w.set_simple_fullscreen(fullscreen)) + } + + #[inline] + fn has_shadow(&self) -> bool { + self.window.maybe_wait_on_main(|w| w.has_shadow()) + } + + #[inline] + fn set_has_shadow(&self, has_shadow: bool) { + self.window.maybe_queue_on_main(move |w| w.set_has_shadow(has_shadow)) + } + + #[inline] + fn set_tabbing_identifier(&self, identifier: &str) { + self.window.maybe_wait_on_main(|w| w.set_tabbing_identifier(identifier)) + } + + #[inline] + fn tabbing_identifier(&self) -> String { + self.window.maybe_wait_on_main(|w| w.tabbing_identifier()) + } + + #[inline] + fn select_next_tab(&self) { + self.window.maybe_queue_on_main(|w| w.select_next_tab()) + } + + #[inline] + fn select_previous_tab(&self) { + self.window.maybe_queue_on_main(|w| w.select_previous_tab()) + } + + #[inline] + fn select_tab_at_index(&self, index: usize) { + self.window.maybe_queue_on_main(move |w| w.select_tab_at_index(index)) + } + + #[inline] + fn num_tabs(&self) -> usize { + self.window.maybe_wait_on_main(|w| w.num_tabs()) + } + + #[inline] + fn is_document_edited(&self) -> bool { + self.window.maybe_wait_on_main(|w| w.is_document_edited()) + } + + #[inline] + fn set_document_edited(&self, edited: bool) { + self.window.maybe_queue_on_main(move |w| w.set_document_edited(edited)) + } + + #[inline] + fn set_option_as_alt(&self, option_as_alt: OptionAsAlt) { + self.window.maybe_queue_on_main(move |w| w.set_option_as_alt(option_as_alt)) + } + + #[inline] + fn option_as_alt(&self) -> OptionAsAlt { + self.window.maybe_wait_on_main(|w| w.option_as_alt()) + } + + #[inline] + fn set_borderless_game(&self, borderless_game: bool) { + self.window.maybe_wait_on_main(|w| w.set_borderless_game(borderless_game)) + } + + #[inline] + fn is_borderless_game(&self) -> bool { + self.window.maybe_wait_on_main(|w| w.is_borderless_game()) + } +} + +/// Corresponds to `NSApplicationActivationPolicy`. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ActivationPolicy { + /// Corresponds to `NSApplicationActivationPolicyRegular`. + #[default] + Regular, + + /// Corresponds to `NSApplicationActivationPolicyAccessory`. + Accessory, + + /// Corresponds to `NSApplicationActivationPolicyProhibited`. + Prohibited, +} + +/// Additional methods on [`WindowAttributes`] that are specific to MacOS. +/// +/// **Note:** Properties dealing with the titlebar will be overwritten by the +/// [`WindowAttributes::with_decorations`] method: +/// - `with_titlebar_transparent` +/// - `with_title_hidden` +/// - `with_titlebar_hidden` +/// - `with_titlebar_buttons_hidden` +/// - `with_fullsize_content_view` +pub trait WindowAttributesExtMacOS { + /// Enables click-and-drag behavior for the entire window, not just the titlebar. + fn with_movable_by_window_background(self, movable_by_window_background: bool) -> Self; + /// Makes the titlebar transparent and allows the content to appear behind it. + fn with_titlebar_transparent(self, titlebar_transparent: bool) -> Self; + /// Hides the window title. + fn with_title_hidden(self, title_hidden: bool) -> Self; + /// Hides the window titlebar. + fn with_titlebar_hidden(self, titlebar_hidden: bool) -> Self; + /// Hides the window titlebar buttons. + fn with_titlebar_buttons_hidden(self, titlebar_buttons_hidden: bool) -> Self; + /// Makes the window content appear behind the titlebar. + fn with_fullsize_content_view(self, fullsize_content_view: bool) -> Self; + fn with_disallow_hidpi(self, disallow_hidpi: bool) -> Self; + fn with_has_shadow(self, has_shadow: bool) -> Self; + /// Window accepts click-through mouse events. + fn with_accepts_first_mouse(self, accepts_first_mouse: bool) -> Self; + /// Defines the window tabbing identifier. + /// + /// + fn with_tabbing_identifier(self, identifier: &str) -> Self; + /// Set how the Option keys are interpreted. + /// + /// See [`WindowExtMacOS::set_option_as_alt`] for details on what this means if set. + fn with_option_as_alt(self, option_as_alt: OptionAsAlt) -> Self; + /// See [`WindowExtMacOS::set_borderless_game`] for details on what this means if set. + fn with_borderless_game(self, borderless_game: bool) -> Self; +} + +impl WindowAttributesExtMacOS for WindowAttributes { + #[inline] + fn with_movable_by_window_background(mut self, movable_by_window_background: bool) -> Self { + self.platform_specific.movable_by_window_background = movable_by_window_background; + self + } + + #[inline] + fn with_titlebar_transparent(mut self, titlebar_transparent: bool) -> Self { + self.platform_specific.titlebar_transparent = titlebar_transparent; + self + } + + #[inline] + fn with_titlebar_hidden(mut self, titlebar_hidden: bool) -> Self { + self.platform_specific.titlebar_hidden = titlebar_hidden; + self + } + + #[inline] + fn with_titlebar_buttons_hidden(mut self, titlebar_buttons_hidden: bool) -> Self { + self.platform_specific.titlebar_buttons_hidden = titlebar_buttons_hidden; + self + } + + #[inline] + fn with_title_hidden(mut self, title_hidden: bool) -> Self { + self.platform_specific.title_hidden = title_hidden; + self + } + + #[inline] + fn with_fullsize_content_view(mut self, fullsize_content_view: bool) -> Self { + self.platform_specific.fullsize_content_view = fullsize_content_view; + self + } + + #[inline] + fn with_disallow_hidpi(mut self, disallow_hidpi: bool) -> Self { + self.platform_specific.disallow_hidpi = disallow_hidpi; + self + } + + #[inline] + fn with_has_shadow(mut self, has_shadow: bool) -> Self { + self.platform_specific.has_shadow = has_shadow; + self + } + + #[inline] + fn with_accepts_first_mouse(mut self, accepts_first_mouse: bool) -> Self { + self.platform_specific.accepts_first_mouse = accepts_first_mouse; + self + } + + #[inline] + fn with_tabbing_identifier(mut self, tabbing_identifier: &str) -> Self { + self.platform_specific.tabbing_identifier.replace(tabbing_identifier.to_string()); + self + } + + #[inline] + fn with_option_as_alt(mut self, option_as_alt: OptionAsAlt) -> Self { + self.platform_specific.option_as_alt = option_as_alt; + self + } + + #[inline] + fn with_borderless_game(mut self, borderless_game: bool) -> Self { + self.platform_specific.borderless_game = borderless_game; + self + } +} + +pub trait EventLoopBuilderExtMacOS { + /// Sets the activation policy for the application. If used, this will override + /// any relevant settings provided in the package manifest. + /// For instance, `with_activation_policy(ActivationPolicy::Regular)` will prevent + /// the application from running as an "agent", even if LSUIElement is set to true. + /// + /// If unused, the Winit will honor the package manifest. + /// + /// # Example + /// + /// Set the activation policy to "accessory". + /// + /// ``` + /// use winit::event_loop::EventLoopBuilder; + /// #[cfg(target_os = "macos")] + /// use winit::platform::macos::{ActivationPolicy, EventLoopBuilderExtMacOS}; + /// + /// let mut builder = EventLoopBuilder::new(); + /// #[cfg(target_os = "macos")] + /// builder.with_activation_policy(ActivationPolicy::Accessory); + /// # if false { // We can't test this part + /// let event_loop = builder.build(); + /// # } + /// ``` + fn with_activation_policy(&mut self, activation_policy: ActivationPolicy) -> &mut Self; + + /// Used to control whether a default menubar menu is created. + /// + /// Menu creation is enabled by default. + /// + /// # Example + /// + /// Disable creating a default menubar. + /// + /// ``` + /// use winit::event_loop::EventLoopBuilder; + /// #[cfg(target_os = "macos")] + /// use winit::platform::macos::EventLoopBuilderExtMacOS; + /// + /// let mut builder = EventLoopBuilder::new(); + /// #[cfg(target_os = "macos")] + /// builder.with_default_menu(false); + /// # if false { // We can't test this part + /// let event_loop = builder.build(); + /// # } + /// ``` + fn with_default_menu(&mut self, enable: bool) -> &mut Self; + + /// Used to prevent the application from automatically activating when launched if + /// another application is already active. + /// + /// The default behavior is to ignore other applications and activate when launched. + fn with_activate_ignoring_other_apps(&mut self, ignore: bool) -> &mut Self; +} + +impl EventLoopBuilderExtMacOS for EventLoopBuilder { + #[inline] + fn with_activation_policy(&mut self, activation_policy: ActivationPolicy) -> &mut Self { + self.platform_specific.activation_policy = Some(activation_policy); + self + } + + #[inline] + fn with_default_menu(&mut self, enable: bool) -> &mut Self { + self.platform_specific.default_menu = enable; + self + } + + #[inline] + fn with_activate_ignoring_other_apps(&mut self, ignore: bool) -> &mut Self { + self.platform_specific.activate_ignoring_other_apps = ignore; + self + } +} + +/// Additional methods on [`MonitorHandle`] that are specific to MacOS. +pub trait MonitorHandleExtMacOS { + /// Returns the identifier of the monitor for Cocoa. + fn native_id(&self) -> u32; + /// Returns a pointer to the NSScreen representing this monitor. + fn ns_screen(&self) -> Option<*mut c_void>; +} + +impl MonitorHandleExtMacOS for MonitorHandle { + #[inline] + fn native_id(&self) -> u32 { + self.inner.native_identifier() + } + + fn ns_screen(&self) -> Option<*mut c_void> { + // SAFETY: We only use the marker to get a pointer + let mtm = unsafe { objc2_foundation::MainThreadMarker::new_unchecked() }; + self.inner.ns_screen(mtm).map(|s| objc2::rc::Retained::as_ptr(&s) as _) + } +} + +/// Additional methods on [`ActiveEventLoop`] that are specific to macOS. +pub trait ActiveEventLoopExtMacOS { + /// Hide the entire application. In most applications this is typically triggered with + /// Command-H. + fn hide_application(&self); + /// Hide the other applications. In most applications this is typically triggered with + /// Command+Option-H. + fn hide_other_applications(&self); + /// Set whether the system can automatically organize windows into tabs. + /// + /// + fn set_allows_automatic_window_tabbing(&self, enabled: bool); + /// Returns whether the system can automatically organize windows into tabs. + fn allows_automatic_window_tabbing(&self) -> bool; +} + +impl ActiveEventLoopExtMacOS for ActiveEventLoop { + fn hide_application(&self) { + self.p.hide_application() + } + + fn hide_other_applications(&self) { + self.p.hide_other_applications() + } + + fn set_allows_automatic_window_tabbing(&self, enabled: bool) { + self.p.set_allows_automatic_window_tabbing(enabled); + } + + fn allows_automatic_window_tabbing(&self) -> bool { + self.p.allows_automatic_window_tabbing() + } +} + +/// Option as alt behavior. +/// +/// The default is `None`. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum OptionAsAlt { + /// The left `Option` key is treated as `Alt`. + OnlyLeft, + + /// The right `Option` key is treated as `Alt`. + OnlyRight, + + /// Both `Option` keys are treated as `Alt`. + Both, + + /// No special handling is applied for `Option` key. + #[default] + None, +} diff --git a/vendor/winit/src/platform/mod.rs b/vendor/winit/src/platform/mod.rs new file mode 100644 index 00000000..4f59303e --- /dev/null +++ b/vendor/winit/src/platform/mod.rs @@ -0,0 +1,55 @@ +//! Contains traits with platform-specific methods in them. +//! +//! Only the modules corresponding to the platform you're compiling to will be available. + +#[cfg(any(android_platform, docsrs))] +pub mod android; +#[cfg(any(ios_platform, docsrs))] +pub mod ios; +#[cfg(any(macos_platform, docsrs))] +pub mod macos; +#[cfg(any(orbital_platform, docsrs))] +pub mod orbital; +#[cfg(any(x11_platform, wayland_platform, docsrs))] +pub mod startup_notify; +#[cfg(any(wayland_platform, docsrs))] +pub mod wayland; +#[cfg(any(web_platform, docsrs))] +pub mod web; +#[cfg(any(windows_platform, docsrs))] +pub mod windows; +#[cfg(any(x11_platform, docsrs))] +pub mod x11; + +#[cfg(any( + windows_platform, + macos_platform, + android_platform, + x11_platform, + wayland_platform, + docsrs, +))] +pub mod run_on_demand; + +#[cfg(any( + windows_platform, + macos_platform, + android_platform, + x11_platform, + wayland_platform, + docsrs, +))] +pub mod pump_events; + +#[cfg(any( + windows_platform, + macos_platform, + x11_platform, + wayland_platform, + orbital_platform, + docsrs +))] +pub mod modifier_supplement; + +#[cfg(any(windows_platform, macos_platform, x11_platform, wayland_platform, docsrs))] +pub mod scancode; diff --git a/vendor/winit/src/platform/modifier_supplement.rs b/vendor/winit/src/platform/modifier_supplement.rs new file mode 100644 index 00000000..b1db7345 --- /dev/null +++ b/vendor/winit/src/platform/modifier_supplement.rs @@ -0,0 +1,35 @@ +use crate::event::KeyEvent; +use crate::keyboard::Key; + +/// Additional methods for the `KeyEvent` which cannot be implemented on all +/// platforms. +pub trait KeyEventExtModifierSupplement { + /// Identical to `KeyEvent::text` but this is affected by Ctrl. + /// + /// For example, pressing Ctrl+a produces `Some("\x01")`. + fn text_with_all_modifiers(&self) -> Option<&str>; + + /// This value ignores all modifiers including, + /// but not limited to Shift, Caps Lock, + /// and Ctrl. In most cases this means that the + /// unicode character in the resulting string is lowercase. + /// + /// This is useful for key-bindings / shortcut key combinations. + /// + /// In case `logical_key` reports `Dead`, this will still report the + /// key as `Character` according to the current keyboard layout. This value + /// cannot be `Dead`. + fn key_without_modifiers(&self) -> Key; +} + +impl KeyEventExtModifierSupplement for KeyEvent { + #[inline] + fn text_with_all_modifiers(&self) -> Option<&str> { + self.platform_specific.text_with_all_modifiers.as_ref().map(|s| s.as_str()) + } + + #[inline] + fn key_without_modifiers(&self) -> Key { + self.platform_specific.key_without_modifiers.clone() + } +} diff --git a/vendor/winit/src/platform/orbital.rs b/vendor/winit/src/platform/orbital.rs new file mode 100644 index 00000000..d8093503 --- /dev/null +++ b/vendor/winit/src/platform/orbital.rs @@ -0,0 +1,6 @@ +//! # Orbital / Redox OS +//! +//! Redox OS has some functionality not yet present that will be implemented +//! when its orbital display server provides it. + +// There are no Orbital specific traits yet. diff --git a/vendor/winit/src/platform/pump_events.rs b/vendor/winit/src/platform/pump_events.rs new file mode 100644 index 00000000..44766353 --- /dev/null +++ b/vendor/winit/src/platform/pump_events.rs @@ -0,0 +1,143 @@ +use std::time::Duration; + +use crate::application::ApplicationHandler; +use crate::event::Event; +use crate::event_loop::{self, ActiveEventLoop, EventLoop}; + +/// Additional methods on [`EventLoop`] for pumping events within an external event loop +pub trait EventLoopExtPumpEvents { + /// A type provided by the user that can be passed through [`Event::UserEvent`]. + type UserEvent: 'static; + + /// Pump the `EventLoop` to check for and dispatch pending events. + /// + /// This API is designed to enable applications to integrate Winit into an + /// external event loop, for platforms that can support this. + /// + /// The given `timeout` limits how long it may block waiting for new events. + /// + /// Passing a `timeout` of `Some(Duration::ZERO)` would ensure your external + /// event loop is never blocked but you would likely need to consider how + /// to throttle your own external loop. + /// + /// Passing a `timeout` of `None` means that it may wait indefinitely for new + /// events before returning control back to the external loop. + /// + /// **Note:** This is not a portable API, and its usage involves a number of + /// caveats and trade offs that should be considered before using this API! + /// + /// You almost certainly shouldn't use this API, unless you absolutely know it's + /// the only practical option you have. + /// + /// ## Synchronous events + /// + /// Some events _must_ only be handled synchronously via the closure that + /// is passed to Winit so that the handler will also be synchronized with + /// the window system and operating system. + /// + /// This is because some events are driven by a window system callback + /// where the window systems expects the application to have handled the + /// event before returning. + /// + /// **These events can not be buffered and handled outside of the closure + /// passed to Winit.** + /// + /// As a general rule it is not recommended to ever buffer events to handle + /// them outside of the closure passed to Winit since it's difficult to + /// provide guarantees about which events are safe to buffer across all + /// operating systems. + /// + /// Notable events that will certainly create portability problems if + /// buffered and handled outside of Winit include: + /// - `RedrawRequested` events, used to schedule rendering. + /// + /// macOS for example uses a `drawRect` callback to drive rendering + /// within applications and expects rendering to be finished before + /// the `drawRect` callback returns. + /// + /// For portability it's strongly recommended that applications should + /// keep their rendering inside the closure provided to Winit. + /// - Any lifecycle events, such as `Suspended` / `Resumed`. + /// + /// The handling of these events needs to be synchronized with the + /// operating system and it would never be appropriate to buffer a + /// notification that your application has been suspended or resumed and + /// then handled that later since there would always be a chance that + /// other lifecycle events occur while the event is buffered. + /// + /// ## Supported Platforms + /// + /// - Windows + /// - Linux + /// - MacOS + /// - Android + /// + /// ## Unsupported Platforms + /// + /// - **Web:** This API is fundamentally incompatible with the event-based way in which Web + /// browsers work because it's not possible to have a long-running external loop that would + /// block the browser and there is nothing that can be polled to ask for new new events. + /// Events are delivered via callbacks based on an event loop that is internal to the browser + /// itself. + /// - **iOS:** It's not possible to stop and start an `NSApplication` repeatedly on iOS so + /// there's no way to support the same approach to polling as on MacOS. + /// + /// ## Platform-specific + /// + /// - **Windows**: The implementation will use `PeekMessage` when checking for window messages + /// to avoid blocking your external event loop. + /// + /// - **MacOS**: The implementation works in terms of stopping the global application whenever + /// the application `RunLoop` indicates that it is preparing to block and wait for new events. + /// + /// This is very different to the polling APIs that are available on other + /// platforms (the lower level polling primitives on MacOS are private + /// implementation details for `NSApplication` which aren't accessible to + /// application developers) + /// + /// It's likely this will be less efficient than polling on other OSs and + /// it also means the `NSApplication` is stopped while outside of the Winit + /// event loop - and that's observable (for example to crates like `rfd`) + /// because the `NSApplication` is global state. + /// + /// If you render outside of Winit you are likely to see window resizing artifacts + /// since MacOS expects applications to render synchronously during any `drawRect` + /// callback. + fn pump_app_events>( + &mut self, + timeout: Option, + app: &mut A, + ) -> PumpStatus { + #[allow(deprecated)] + self.pump_events(timeout, |event, event_loop| { + event_loop::dispatch_event_for_app(app, event_loop, event) + }) + } + + /// See [`pump_app_events`]. + /// + /// [`pump_app_events`]: Self::pump_app_events + #[deprecated = "use EventLoopExtPumpEvents::pump_app_events"] + fn pump_events(&mut self, timeout: Option, event_handler: F) -> PumpStatus + where + F: FnMut(Event, &ActiveEventLoop); +} + +impl EventLoopExtPumpEvents for EventLoop { + type UserEvent = T; + + fn pump_events(&mut self, timeout: Option, event_handler: F) -> PumpStatus + where + F: FnMut(Event, &ActiveEventLoop), + { + self.event_loop.pump_events(timeout, event_handler) + } +} + +/// The return status for `pump_events` +pub enum PumpStatus { + /// Continue running external loop. + Continue, + /// Exit external loop. + Exit(i32), +} diff --git a/vendor/winit/src/platform/run_on_demand.rs b/vendor/winit/src/platform/run_on_demand.rs new file mode 100644 index 00000000..0bb96e92 --- /dev/null +++ b/vendor/winit/src/platform/run_on_demand.rs @@ -0,0 +1,111 @@ +use crate::application::ApplicationHandler; +use crate::error::EventLoopError; +use crate::event::Event; +use crate::event_loop::{self, ActiveEventLoop, EventLoop}; + +#[cfg(doc)] +use crate::{platform::pump_events::EventLoopExtPumpEvents, window::Window}; + +/// Additional methods on [`EventLoop`] to return control flow to the caller. +pub trait EventLoopExtRunOnDemand { + /// A type provided by the user that can be passed through [`Event::UserEvent`]. + type UserEvent: 'static; + + /// See [`run_app_on_demand`]. + /// + /// [`run_app_on_demand`]: Self::run_app_on_demand + #[deprecated = "use EventLoopExtRunOnDemand::run_app_on_demand"] + fn run_on_demand(&mut self, event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &ActiveEventLoop); + + /// Run the application with the event loop on the calling thread. + /// + /// Unlike [`EventLoop::run_app`], this function accepts non-`'static` (i.e. non-`move`) + /// closures and it is possible to return control back to the caller without + /// consuming the `EventLoop` (by using [`exit()`]) and + /// so the event loop can be re-run after it has exit. + /// + /// It's expected that each run of the loop will be for orthogonal instantiations of your + /// Winit application, but internally each instantiation may re-use some common window + /// system resources, such as a display server connection. + /// + /// This API is not designed to run an event loop in bursts that you can exit from and return + /// to while maintaining the full state of your application. (If you need something like this + /// you can look at the [`EventLoopExtPumpEvents::pump_app_events()`] API) + /// + /// Each time `run_app_on_demand` is called the startup sequence of `init`, followed by + /// `resume` is being preserved. + /// + /// See the [`set_control_flow()`] docs on how to change the event loop's behavior. + /// + /// # Caveats + /// - This extension isn't available on all platforms, since it's not always possible to return + /// to the caller (specifically this is impossible on iOS and Web - though with the Web + /// backend it is possible to use `EventLoopExtWebSys::spawn()` + #[cfg_attr(not(web_platform), doc = "[^1]")] + /// more than once instead). + /// - No [`Window`] state can be carried between separate runs of the event loop. + /// + /// You are strongly encouraged to use [`EventLoop::run_app()`] for portability, unless you + /// specifically need the ability to re-run a single event loop more than once + /// + /// # Supported Platforms + /// - Windows + /// - Linux + /// - macOS + /// - Android + /// + /// # Unsupported Platforms + /// - **Web:** This API is fundamentally incompatible with the event-based way in which Web + /// browsers work because it's not possible to have a long-running external loop that would + /// block the browser and there is nothing that can be polled to ask for new events. Events + /// are delivered via callbacks based on an event loop that is internal to the browser itself. + /// - **iOS:** It's not possible to stop and start an `UIApplication` repeatedly on iOS. + #[cfg_attr(not(web_platform), doc = "[^1]: `spawn()` is only available on `wasm` platforms.")] + /// + #[rustfmt::skip] + /// [`exit()`]: ActiveEventLoop::exit() + /// [`set_control_flow()`]: ActiveEventLoop::set_control_flow() + fn run_app_on_demand>( + &mut self, + app: &mut A, + ) -> Result<(), EventLoopError> { + #[allow(deprecated)] + self.run_on_demand(|event, event_loop| { + event_loop::dispatch_event_for_app(app, event_loop, event) + }) + } +} + +impl EventLoopExtRunOnDemand for EventLoop { + type UserEvent = T; + + fn run_on_demand(&mut self, event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &ActiveEventLoop), + { + self.event_loop.window_target().clear_exit(); + self.event_loop.run_on_demand(event_handler) + } +} + +impl ActiveEventLoop { + /// Clear exit status. + pub(crate) fn clear_exit(&self) { + self.p.clear_exit() + } +} + +/// ```compile_fail +/// use winit::event_loop::EventLoop; +/// use winit::platform::run_on_demand::EventLoopExtRunOnDemand; +/// +/// let mut event_loop = EventLoop::new().unwrap(); +/// event_loop.run_on_demand(|_, _| { +/// // Attempt to run the event loop re-entrantly; this must fail. +/// event_loop.run_on_demand(|_, _| {}); +/// }); +/// ``` +#[allow(dead_code)] +fn test_run_on_demand_cannot_access_event_loop() {} diff --git a/vendor/winit/src/platform/scancode.rs b/vendor/winit/src/platform/scancode.rs new file mode 100644 index 00000000..0d783135 --- /dev/null +++ b/vendor/winit/src/platform/scancode.rs @@ -0,0 +1,50 @@ +use crate::keyboard::{KeyCode, PhysicalKey}; + +// TODO: Describe what this value contains for each platform + +/// Additional methods for the [`PhysicalKey`] type that allow the user to access the +/// platform-specific scancode. +/// +/// [`PhysicalKey`]: crate::keyboard::PhysicalKey +pub trait PhysicalKeyExtScancode { + /// The raw value of the platform-specific physical key identifier. + /// + /// Returns `Some(key_id)` if the conversion was successful; returns `None` otherwise. + /// + /// ## Platform-specific + /// - **Windows:** A 16bit extended scancode + /// - **Wayland/X11**: A 32-bit linux scancode, which is X11/Wayland keycode subtracted by 8. + fn to_scancode(self) -> Option; + + /// Constructs a `PhysicalKey` from a platform-specific physical key identifier. + /// + /// Note that this conversion may be lossy, i.e. converting the returned `PhysicalKey` back + /// using `to_scancode` might not yield the original value. + /// + /// ## Platform-specific + /// - **Wayland/X11**: A 32-bit linux scancode. When building from X11/Wayland keycode subtract + /// `8` to get the value you wanted. + fn from_scancode(scancode: u32) -> PhysicalKey; +} + +impl PhysicalKeyExtScancode for PhysicalKey { + fn to_scancode(self) -> Option { + crate::platform_impl::physicalkey_to_scancode(self) + } + + fn from_scancode(scancode: u32) -> PhysicalKey { + crate::platform_impl::scancode_to_physicalkey(scancode) + } +} + +impl PhysicalKeyExtScancode for KeyCode { + #[inline] + fn to_scancode(self) -> Option { + ::to_scancode(PhysicalKey::Code(self)) + } + + #[inline] + fn from_scancode(scancode: u32) -> PhysicalKey { + ::from_scancode(scancode) + } +} diff --git a/vendor/winit/src/platform/startup_notify.rs b/vendor/winit/src/platform/startup_notify.rs new file mode 100644 index 00000000..6fab284e --- /dev/null +++ b/vendor/winit/src/platform/startup_notify.rs @@ -0,0 +1,99 @@ +//! Window startup notification to handle window raising. +//! +//! The [`ActivationToken`] is essential to ensure that your newly +//! created window will obtain the focus, otherwise the user could +//! be requered to click on the window. +//! +//! Such token is usually delivered via the environment variable and +//! could be read from it with the [`EventLoopExtStartupNotify::read_token_from_env`]. +//! +//! Such token must also be reset after reading it from your environment with +//! [`reset_activation_token_env`] otherwise child processes could inherit it. +//! +//! When starting a new child process with a newly obtained [`ActivationToken`] from +//! [`WindowExtStartupNotify::request_activation_token`] the [`set_activation_token_env`] +//! must be used to propagate it to the child +//! +//! To ensure the delivery of such token by other processes to you, the user should +//! set `StartupNotify=true` inside the `.desktop` file of their application. +//! +//! The specification could be found [`here`]. +//! +//! [`here`]: https://specifications.freedesktop.org/startup-notification-spec/startup-notification-latest.txt + +use std::env; + +use crate::error::NotSupportedError; +use crate::event_loop::{ActiveEventLoop, AsyncRequestSerial}; +use crate::window::{ActivationToken, Window, WindowAttributes}; + +/// The variable which is used mostly on X11. +const X11_VAR: &str = "DESKTOP_STARTUP_ID"; + +/// The variable which is used mostly on Wayland. +const WAYLAND_VAR: &str = "XDG_ACTIVATION_TOKEN"; + +pub trait EventLoopExtStartupNotify { + /// Read the token from the environment. + /// + /// It's recommended **to unset** this environment variable for child processes. + fn read_token_from_env(&self) -> Option; +} + +pub trait WindowExtStartupNotify { + /// Request a new activation token. + /// + /// The token will be delivered inside + fn request_activation_token(&self) -> Result; +} + +pub trait WindowAttributesExtStartupNotify { + /// Use this [`ActivationToken`] during window creation. + /// + /// Not using such a token upon a window could make your window not gaining + /// focus until the user clicks on the window. + fn with_activation_token(self, token: ActivationToken) -> Self; +} + +impl EventLoopExtStartupNotify for ActiveEventLoop { + fn read_token_from_env(&self) -> Option { + match self.p { + #[cfg(wayland_platform)] + crate::platform_impl::ActiveEventLoop::Wayland(_) => env::var(WAYLAND_VAR), + #[cfg(x11_platform)] + crate::platform_impl::ActiveEventLoop::X(_) => env::var(X11_VAR), + } + .ok() + .map(ActivationToken::from_raw) + } +} + +impl WindowExtStartupNotify for Window { + fn request_activation_token(&self) -> Result { + self.window.request_activation_token() + } +} + +impl WindowAttributesExtStartupNotify for WindowAttributes { + fn with_activation_token(mut self, token: ActivationToken) -> Self { + self.platform_specific.activation_token = Some(token); + self + } +} + +/// Remove the activation environment variables from the current process. +/// +/// This is wise to do before running child processes, +/// which may not to support the activation token. +pub fn reset_activation_token_env() { + env::remove_var(X11_VAR); + env::remove_var(WAYLAND_VAR); +} + +/// Set environment variables responsible for activation token. +/// +/// This could be used before running daemon processes. +pub fn set_activation_token_env(token: ActivationToken) { + env::set_var(X11_VAR, &token.token); + env::set_var(WAYLAND_VAR, token.token); +} diff --git a/vendor/winit/src/platform/wayland.rs b/vendor/winit/src/platform/wayland.rs new file mode 100644 index 00000000..9c1a8e43 --- /dev/null +++ b/vendor/winit/src/platform/wayland.rs @@ -0,0 +1,131 @@ +//! # Wayland +//! +//! **Note:** Windows don't appear on Wayland until you draw/present to them. +//! +//! By default, Winit loads system libraries using `dlopen`. This can be +//! disabled by disabling the `"wayland-dlopen"` cargo feature. +//! +//! ## Client-side decorations +//! +//! Winit provides client-side decorations by default, but the behaviour can +//! be controlled with the following feature flags: +//! +//! * `wayland-csd-adwaita` (default). +//! * `wayland-csd-adwaita-crossfont`. +//! * `wayland-csd-adwaita-notitle`. + +use std::ffi::c_void; +use std::ptr::NonNull; + +use crate::event_loop::{ActiveEventLoop, EventLoop, EventLoopBuilder}; +use crate::monitor::MonitorHandle; +use crate::window::{Window, WindowAttributes}; + +pub use crate::window::Theme; + +/// Additional methods on [`ActiveEventLoop`] that are specific to Wayland. +pub trait ActiveEventLoopExtWayland { + /// True if the [`ActiveEventLoop`] uses Wayland. + fn is_wayland(&self) -> bool; +} + +impl ActiveEventLoopExtWayland for ActiveEventLoop { + #[inline] + fn is_wayland(&self) -> bool { + self.p.is_wayland() + } +} + +/// Additional methods on [`EventLoop`] that are specific to Wayland. +pub trait EventLoopExtWayland { + /// True if the [`EventLoop`] uses Wayland. + fn is_wayland(&self) -> bool; +} + +impl EventLoopExtWayland for EventLoop { + #[inline] + fn is_wayland(&self) -> bool { + self.event_loop.is_wayland() + } +} + +/// Additional methods on [`EventLoopBuilder`] that are specific to Wayland. +pub trait EventLoopBuilderExtWayland { + /// Force using Wayland. + fn with_wayland(&mut self) -> &mut Self; + + /// Whether to allow the event loop to be created off of the main thread. + /// + /// By default, the window is only allowed to be created on the main + /// thread, to make platform compatibility easier. + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self; +} + +impl EventLoopBuilderExtWayland for EventLoopBuilder { + #[inline] + fn with_wayland(&mut self) -> &mut Self { + self.platform_specific.forced_backend = Some(crate::platform_impl::Backend::Wayland); + self + } + + #[inline] + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self { + self.platform_specific.any_thread = any_thread; + self + } +} + +/// Additional methods on [`Window`] that are specific to Wayland. +/// +/// [`Window`]: crate::window::Window +pub trait WindowExtWayland { + /// Returns `xdg_toplevel` of the window or [`None`] if the window is X11 window. + fn xdg_toplevel(&self) -> Option>; +} + +impl WindowExtWayland for Window { + #[inline] + fn xdg_toplevel(&self) -> Option> { + #[allow(clippy::single_match)] + match &self.window { + #[cfg(x11_platform)] + crate::platform_impl::Window::X(_) => None, + #[cfg(wayland_platform)] + crate::platform_impl::Window::Wayland(window) => window.xdg_toplevel(), + } + } +} + +/// Additional methods on [`WindowAttributes`] that are specific to Wayland. +pub trait WindowAttributesExtWayland { + /// Build window with the given name. + /// + /// The `general` name sets an application ID, which should match the `.desktop` + /// file distributed with your program. The `instance` is a `no-op`. + /// + /// For details about application ID conventions, see the + /// [Desktop Entry Spec](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#desktop-file-id) + fn with_name(self, general: impl Into, instance: impl Into) -> Self; +} + +impl WindowAttributesExtWayland for WindowAttributes { + #[inline] + fn with_name(mut self, general: impl Into, instance: impl Into) -> Self { + self.platform_specific.name = + Some(crate::platform_impl::ApplicationName::new(general.into(), instance.into())); + self + } +} + +/// Additional methods on `MonitorHandle` that are specific to Wayland. +pub trait MonitorHandleExtWayland { + /// Returns the inner identifier of the monitor. + fn native_id(&self) -> u32; +} + +impl MonitorHandleExtWayland for MonitorHandle { + #[inline] + fn native_id(&self) -> u32 { + self.inner.native_identifier() + } +} diff --git a/vendor/winit/src/platform/web.rs b/vendor/winit/src/platform/web.rs new file mode 100644 index 00000000..f257ca41 --- /dev/null +++ b/vendor/winit/src/platform/web.rs @@ -0,0 +1,465 @@ +//! # Web +//! +//! The officially supported browsers are Chrome, Firefox and Safari 13.1+, +//! though forks of these should work fine. +//! +//! Winit supports compiling to the `wasm32-unknown-unknown` target with +//! `web-sys`. +//! +//! On the web platform, a Winit window is backed by a `` element. You +//! can either [provide Winit with a `` element][with_canvas], or +//! [let Winit create a `` element which you can then retrieve][get] +//! and insert it into the DOM yourself. +//! +//! Currently, there is no example code using Winit on Web, see [#3473]. For +//! information on using Rust on WebAssembly, check out the [Rust and +//! WebAssembly book]. +//! +//! [with_canvas]: WindowAttributesExtWebSys::with_canvas +//! [get]: WindowExtWebSys::canvas +//! [#3473]: https://github.com/rust-windowing/winit/issues/3473 +//! [Rust and WebAssembly book]: https://rustwasm.github.io/book/ +//! +//! ## CSS properties +//! +//! It is recommended **not** to apply certain CSS properties to the canvas: +//! - [`transform`](https://developer.mozilla.org/en-US/docs/Web/CSS/transform) +//! - [`border`](https://developer.mozilla.org/en-US/docs/Web/CSS/border) +//! - [`padding`](https://developer.mozilla.org/en-US/docs/Web/CSS/padding) +//! +//! The following APIs can't take them into account and will therefore provide inaccurate results: +//! - [`WindowEvent::Resized`] and [`Window::(set_)inner_size()`] +//! - [`WindowEvent::Occluded`] +//! - [`WindowEvent::CursorMoved`], [`WindowEvent::CursorEntered`], [`WindowEvent::CursorLeft`], and +//! [`WindowEvent::Touch`]. +//! - [`Window::set_outer_position()`] +//! +//! [`WindowEvent::Resized`]: crate::event::WindowEvent::Resized +//! [`Window::(set_)inner_size()`]: crate::window::Window::inner_size +//! [`WindowEvent::Occluded`]: crate::event::WindowEvent::Occluded +//! [`WindowEvent::CursorMoved`]: crate::event::WindowEvent::CursorMoved +//! [`WindowEvent::CursorEntered`]: crate::event::WindowEvent::CursorEntered +//! [`WindowEvent::CursorLeft`]: crate::event::WindowEvent::CursorLeft +//! [`WindowEvent::Touch`]: crate::event::WindowEvent::Touch +//! [`Window::set_outer_position()`]: crate::window::Window::set_outer_position + +use std::error::Error; +use std::fmt::{self, Display, Formatter}; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::Duration; + +#[cfg(web_platform)] +use web_sys::HtmlCanvasElement; + +use crate::application::ApplicationHandler; +use crate::cursor::CustomCursorSource; +use crate::event::Event; +use crate::event_loop::{self, ActiveEventLoop, EventLoop}; +#[cfg(web_platform)] +use crate::platform_impl::CustomCursorFuture as PlatformCustomCursorFuture; +use crate::platform_impl::PlatformCustomCursorSource; +use crate::window::{CustomCursor, Window, WindowAttributes}; + +#[cfg(not(web_platform))] +#[doc(hidden)] +pub struct HtmlCanvasElement; + +pub trait WindowExtWebSys { + /// Only returns the canvas if called from inside the window context (the + /// main thread). + fn canvas(&self) -> Option; + + /// Returns [`true`] if calling `event.preventDefault()` is enabled. + /// + /// See [`Window::set_prevent_default()`] for more details. + fn prevent_default(&self) -> bool; + + /// Sets whether `event.preventDefault()` should be called on events on the + /// canvas that have side effects. + /// + /// For example, by default using the mouse wheel would cause the page to scroll, enabling this + /// would prevent that. + /// + /// Some events are impossible to prevent. E.g. Firefox allows to access the native browser + /// context menu with Shift+Rightclick. + fn set_prevent_default(&self, prevent_default: bool); +} + +impl WindowExtWebSys for Window { + #[inline] + fn canvas(&self) -> Option { + self.window.canvas() + } + + fn prevent_default(&self) -> bool { + self.window.prevent_default() + } + + fn set_prevent_default(&self, prevent_default: bool) { + self.window.set_prevent_default(prevent_default) + } +} + +pub trait WindowAttributesExtWebSys { + /// Pass an [`HtmlCanvasElement`] to be used for this [`Window`]. If [`None`], + /// [`WindowAttributes::default()`] will create one. + /// + /// In any case, the canvas won't be automatically inserted into the web page. + /// + /// [`None`] by default. + #[cfg_attr(not(web_platform), doc = "", doc = "[`HtmlCanvasElement`]: #only-available-on-wasm")] + fn with_canvas(self, canvas: Option) -> Self; + + /// Sets whether `event.preventDefault()` should be called on events on the + /// canvas that have side effects. + /// + /// See [`Window::set_prevent_default()`] for more details. + /// + /// Enabled by default. + fn with_prevent_default(self, prevent_default: bool) -> Self; + + /// Whether the canvas should be focusable using the tab key. This is necessary to capture + /// canvas keyboard events. + /// + /// Enabled by default. + fn with_focusable(self, focusable: bool) -> Self; + + /// On window creation, append the canvas element to the web page if it isn't already. + /// + /// Disabled by default. + fn with_append(self, append: bool) -> Self; +} + +impl WindowAttributesExtWebSys for WindowAttributes { + fn with_canvas(mut self, canvas: Option) -> Self { + self.platform_specific.set_canvas(canvas); + self + } + + fn with_prevent_default(mut self, prevent_default: bool) -> Self { + self.platform_specific.prevent_default = prevent_default; + self + } + + fn with_focusable(mut self, focusable: bool) -> Self { + self.platform_specific.focusable = focusable; + self + } + + fn with_append(mut self, append: bool) -> Self { + self.platform_specific.append = append; + self + } +} + +/// Additional methods on `EventLoop` that are specific to the web. +pub trait EventLoopExtWebSys { + /// A type provided by the user that can be passed through `Event::UserEvent`. + type UserEvent: 'static; + + /// Initializes the winit event loop. + /// + /// Unlike + #[cfg_attr(all(web_platform, target_feature = "exception-handling"), doc = "`run_app()`")] + #[cfg_attr( + not(all(web_platform, target_feature = "exception-handling")), + doc = "[`run_app()`]" + )] + /// [^1], this returns immediately, and doesn't throw an exception in order to + /// satisfy its [`!`] return type. + /// + /// Once the event loop has been destroyed, it's possible to reinitialize another event loop + /// by calling this function again. This can be useful if you want to recreate the event loop + /// while the WebAssembly module is still loaded. For example, this can be used to recreate the + /// event loop when switching between tabs on a single page application. + #[rustfmt::skip] + /// + #[cfg_attr( + not(all(web_platform, target_feature = "exception-handling")), + doc = "[`run_app()`]: EventLoop::run_app()" + )] + /// [^1]: `run_app()` is _not_ available on WASM when the target supports `exception-handling`. + fn spawn_app + 'static>(self, app: A); + + /// See [`spawn_app`]. + /// + /// [`spawn_app`]: Self::spawn_app + #[deprecated = "use EventLoopExtWebSys::spawn_app"] + fn spawn(self, event_handler: F) + where + F: 'static + FnMut(Event, &ActiveEventLoop); + + /// Sets the strategy for [`ControlFlow::Poll`]. + /// + /// See [`PollStrategy`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + fn set_poll_strategy(&self, strategy: PollStrategy); + + /// Gets the strategy for [`ControlFlow::Poll`]. + /// + /// See [`PollStrategy`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + fn poll_strategy(&self) -> PollStrategy; + + /// Sets the strategy for [`ControlFlow::WaitUntil`]. + /// + /// See [`WaitUntilStrategy`]. + /// + /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil + fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy); + + /// Gets the strategy for [`ControlFlow::WaitUntil`]. + /// + /// See [`WaitUntilStrategy`]. + /// + /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil + fn wait_until_strategy(&self) -> WaitUntilStrategy; +} + +impl EventLoopExtWebSys for EventLoop { + type UserEvent = T; + + fn spawn_app + 'static>(self, mut app: A) { + self.event_loop.spawn(move |event, event_loop| { + event_loop::dispatch_event_for_app(&mut app, event_loop, event) + }); + } + + fn spawn(self, event_handler: F) + where + F: 'static + FnMut(Event, &ActiveEventLoop), + { + self.event_loop.spawn(event_handler) + } + + fn set_poll_strategy(&self, strategy: PollStrategy) { + self.event_loop.set_poll_strategy(strategy); + } + + fn poll_strategy(&self) -> PollStrategy { + self.event_loop.poll_strategy() + } + + fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) { + self.event_loop.set_wait_until_strategy(strategy); + } + + fn wait_until_strategy(&self) -> WaitUntilStrategy { + self.event_loop.wait_until_strategy() + } +} + +pub trait ActiveEventLoopExtWebSys { + /// Sets the strategy for [`ControlFlow::Poll`]. + /// + /// See [`PollStrategy`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + fn set_poll_strategy(&self, strategy: PollStrategy); + + /// Gets the strategy for [`ControlFlow::Poll`]. + /// + /// See [`PollStrategy`]. + /// + /// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll + fn poll_strategy(&self) -> PollStrategy; + + /// Sets the strategy for [`ControlFlow::WaitUntil`]. + /// + /// See [`WaitUntilStrategy`]. + /// + /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil + fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy); + + /// Gets the strategy for [`ControlFlow::WaitUntil`]. + /// + /// See [`WaitUntilStrategy`]. + /// + /// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil + fn wait_until_strategy(&self) -> WaitUntilStrategy; + + /// Async version of [`ActiveEventLoop::create_custom_cursor()`] which waits until the + /// cursor has completely finished loading. + fn create_custom_cursor_async(&self, source: CustomCursorSource) -> CustomCursorFuture; +} + +impl ActiveEventLoopExtWebSys for ActiveEventLoop { + #[inline] + fn create_custom_cursor_async(&self, source: CustomCursorSource) -> CustomCursorFuture { + self.p.create_custom_cursor_async(source) + } + + #[inline] + fn set_poll_strategy(&self, strategy: PollStrategy) { + self.p.set_poll_strategy(strategy); + } + + #[inline] + fn poll_strategy(&self) -> PollStrategy { + self.p.poll_strategy() + } + + #[inline] + fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) { + self.p.set_wait_until_strategy(strategy); + } + + #[inline] + fn wait_until_strategy(&self) -> WaitUntilStrategy { + self.p.wait_until_strategy() + } +} + +/// Strategy used for [`ControlFlow::Poll`][crate::event_loop::ControlFlow::Poll]. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum PollStrategy { + /// Uses [`Window.requestIdleCallback()`] to queue the next event loop. If not available + /// this will fallback to [`setTimeout()`]. + /// + /// This strategy will wait for the browser to enter an idle period before running and might + /// be affected by browser throttling. + /// + /// [`Window.requestIdleCallback()`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback + /// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout + IdleCallback, + /// Uses the [Prioritized Task Scheduling API] to queue the next event loop. If not available + /// this will fallback to [`setTimeout()`]. + /// + /// This strategy will run as fast as possible without disturbing users from interacting with + /// the page and is not affected by browser throttling. + /// + /// This is the default strategy. + /// + /// [Prioritized Task Scheduling API]: https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API + /// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout + #[default] + Scheduler, +} + +/// Strategy used for [`ControlFlow::WaitUntil`][crate::event_loop::ControlFlow::WaitUntil]. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum WaitUntilStrategy { + /// Uses the [Prioritized Task Scheduling API] to queue the next event loop. If not available + /// this will fallback to [`setTimeout()`]. + /// + /// This strategy is commonly not affected by browser throttling unless the window is not + /// focused. + /// + /// This is the default strategy. + /// + /// [Prioritized Task Scheduling API]: https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API + /// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout + #[default] + Scheduler, + /// Equal to [`Scheduler`][Self::Scheduler] but wakes up the event loop from a [worker]. + /// + /// This strategy is commonly not affected by browser throttling regardless of window focus. + /// + /// [worker]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API + Worker, +} + +pub trait CustomCursorExtWebSys { + /// Returns if this cursor is an animation. + fn is_animation(&self) -> bool; + + /// Creates a new cursor from a URL pointing to an image. + /// It uses the [url css function](https://developer.mozilla.org/en-US/docs/Web/CSS/url), + /// but browser support for image formats is inconsistent. Using [PNG] is recommended. + /// + /// [PNG]: https://en.wikipedia.org/wiki/PNG + fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorSource; + + /// Crates a new animated cursor from multiple [`CustomCursor`]s. + /// Supplied `cursors` can't be empty or other animations. + fn from_animation( + duration: Duration, + cursors: Vec, + ) -> Result; +} + +impl CustomCursorExtWebSys for CustomCursor { + fn is_animation(&self) -> bool { + self.inner.animation + } + + fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorSource { + CustomCursorSource { inner: PlatformCustomCursorSource::Url { url, hotspot_x, hotspot_y } } + } + + fn from_animation( + duration: Duration, + cursors: Vec, + ) -> Result { + if cursors.is_empty() { + return Err(BadAnimation::Empty); + } + + if cursors.iter().any(CustomCursor::is_animation) { + return Err(BadAnimation::Animation); + } + + Ok(CustomCursorSource { + inner: PlatformCustomCursorSource::Animation { duration, cursors }, + }) + } +} + +/// An error produced when using [`CustomCursor::from_animation`] with invalid arguments. +#[derive(Debug, Clone)] +pub enum BadAnimation { + /// Produced when no cursors were supplied. + Empty, + /// Produced when a supplied cursor is an animation. + Animation, +} + +impl fmt::Display for BadAnimation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Empty => write!(f, "No cursors supplied"), + Self::Animation => write!(f, "A supplied cursor is an animation"), + } + } +} + +impl Error for BadAnimation {} + +#[cfg(not(web_platform))] +struct PlatformCustomCursorFuture; + +#[derive(Debug)] +pub struct CustomCursorFuture(pub(crate) PlatformCustomCursorFuture); + +impl Future for CustomCursorFuture { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.0).poll(cx).map_ok(|cursor| CustomCursor { inner: cursor }) + } +} + +#[derive(Clone, Debug)] +pub enum CustomCursorError { + Blob, + Decode(String), + Animation, +} + +impl Display for CustomCursorError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Blob => write!(f, "failed to create `Blob`"), + Self::Decode(error) => write!(f, "failed to decode image: {error}"), + Self::Animation => { + write!(f, "found `CustomCursor` that is an animation when building an animation") + }, + } + } +} + +impl Error for CustomCursorError {} diff --git a/vendor/winit/src/platform/windows.rs b/vendor/winit/src/platform/windows.rs new file mode 100644 index 00000000..e4593be1 --- /dev/null +++ b/vendor/winit/src/platform/windows.rs @@ -0,0 +1,767 @@ +//! # Windows +//! +//! The supported OS version is Windows 7 or higher, though Windows 10 is +//! tested regularly. +use std::borrow::Borrow; +use std::ffi::c_void; +use std::path::Path; + +use crate::dpi::PhysicalSize; +use crate::event::DeviceId; +use crate::event_loop::EventLoopBuilder; +use crate::monitor::MonitorHandle; +use crate::window::{BadIcon, Icon, Window, WindowAttributes}; + +/// Window Handle type used by Win32 API +pub type HWND = isize; +/// Menu Handle type used by Win32 API +pub type HMENU = isize; +/// Monitor Handle type used by Win32 API +pub type HMONITOR = isize; + +/// Describes a system-drawn backdrop material of a window. +/// +/// For a detailed explanation, see [`DWM_SYSTEMBACKDROP_TYPE docs`]. +/// +/// [`DWM_SYSTEMBACKDROP_TYPE docs`]: https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwm_systembackdrop_type +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum BackdropType { + /// Corresponds to `DWMSBT_AUTO`. + /// + /// Usually draws a default backdrop effect on the title bar. + #[default] + Auto = 0, + + /// Corresponds to `DWMSBT_NONE`. + None = 1, + + /// Corresponds to `DWMSBT_MAINWINDOW`. + /// + /// Draws the Mica backdrop material. + MainWindow = 2, + + /// Corresponds to `DWMSBT_TRANSIENTWINDOW`. + /// + /// Draws the Background Acrylic backdrop material. + TransientWindow = 3, + + /// Corresponds to `DWMSBT_TABBEDWINDOW`. + /// + /// Draws the Alt Mica backdrop material. + TabbedWindow = 4, +} + +/// Describes a color used by Windows +#[repr(transparent)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Color(u32); + +impl Color { + // Special constant only valid for the window border and therefore modeled using Option + // for user facing code + const NONE: Color = Color(0xfffffffe); + /// Use the system's default color + pub const SYSTEM_DEFAULT: Color = Color(0xffffffff); + + /// Create a new color from the given RGB values + pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self { + Self((r as u32) | ((g as u32) << 8) | ((b as u32) << 16)) + } +} + +impl Default for Color { + fn default() -> Self { + Self::SYSTEM_DEFAULT + } +} + +/// Describes how the corners of a window should look like. +/// +/// For a detailed explanation, see [`DWM_WINDOW_CORNER_PREFERENCE docs`]. +/// +/// [`DWM_WINDOW_CORNER_PREFERENCE docs`]: https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwm_window_corner_preference +#[repr(i32)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CornerPreference { + /// Corresponds to `DWMWCP_DEFAULT`. + /// + /// Let the system decide when to round window corners. + #[default] + Default = 0, + + /// Corresponds to `DWMWCP_DONOTROUND`. + /// + /// Never round window corners. + DoNotRound = 1, + + /// Corresponds to `DWMWCP_ROUND`. + /// + /// Round the corners, if appropriate. + Round = 2, + + /// Corresponds to `DWMWCP_ROUNDSMALL`. + /// + /// Round the corners if appropriate, with a small radius. + RoundSmall = 3, +} + +/// A wrapper around a [`Window`] that ignores thread-specific window handle limitations. +/// +/// See [`WindowBorrowExtWindows::any_thread`] for more information. +#[derive(Debug)] +pub struct AnyThread(W); + +impl> AnyThread { + /// Get a reference to the inner window. + #[inline] + pub fn get_ref(&self) -> &Window { + self.0.borrow() + } + + /// Get a reference to the inner object. + #[inline] + pub fn inner(&self) -> &W { + &self.0 + } + + /// Unwrap and get the inner window. + #[inline] + pub fn into_inner(self) -> W { + self.0 + } +} + +impl> AsRef for AnyThread { + fn as_ref(&self) -> &Window { + self.get_ref() + } +} + +impl> Borrow for AnyThread { + fn borrow(&self) -> &Window { + self.get_ref() + } +} + +impl> std::ops::Deref for AnyThread { + type Target = Window; + + fn deref(&self) -> &Self::Target { + self.get_ref() + } +} + +#[cfg(feature = "rwh_06")] +impl> rwh_06::HasWindowHandle for AnyThread { + fn window_handle(&self) -> Result, rwh_06::HandleError> { + // SAFETY: The top level user has asserted this is only used safely. + unsafe { self.get_ref().window_handle_any_thread() } + } +} + +/// Additional methods on `EventLoop` that are specific to Windows. +pub trait EventLoopBuilderExtWindows { + /// Whether to allow the event loop to be created off of the main thread. + /// + /// By default, the window is only allowed to be created on the main + /// thread, to make platform compatibility easier. + /// + /// # `Window` caveats + /// + /// Note that any `Window` created on the new thread will be destroyed when the thread + /// terminates. Attempting to use a `Window` after its parent thread terminates has + /// unspecified, although explicitly not undefined, behavior. + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self; + + /// Whether to enable process-wide DPI awareness. + /// + /// By default, `winit` will attempt to enable process-wide DPI awareness. If + /// that's undesirable, you can disable it with this function. + /// + /// # Example + /// + /// Disable process-wide DPI awareness. + /// + /// ``` + /// use winit::event_loop::EventLoopBuilder; + /// #[cfg(target_os = "windows")] + /// use winit::platform::windows::EventLoopBuilderExtWindows; + /// + /// let mut builder = EventLoopBuilder::new(); + /// #[cfg(target_os = "windows")] + /// builder.with_dpi_aware(false); + /// # if false { // We can't test this part + /// let event_loop = builder.build(); + /// # } + /// ``` + fn with_dpi_aware(&mut self, dpi_aware: bool) -> &mut Self; + + /// A callback to be executed before dispatching a win32 message to the window procedure. + /// Return true to disable winit's internal message dispatching. + /// + /// # Example + /// + /// ``` + /// # use windows_sys::Win32::UI::WindowsAndMessaging::{ACCEL, CreateAcceleratorTableW, TranslateAcceleratorW, DispatchMessageW, TranslateMessage, MSG}; + /// use winit::event_loop::EventLoopBuilder; + /// #[cfg(target_os = "windows")] + /// use winit::platform::windows::EventLoopBuilderExtWindows; + /// + /// let mut builder = EventLoopBuilder::new(); + /// #[cfg(target_os = "windows")] + /// builder.with_msg_hook(|msg|{ + /// let msg = msg as *const MSG; + /// # let accels: Vec = Vec::new(); + /// let translated = unsafe { + /// TranslateAcceleratorW( + /// (*msg).hwnd, + /// CreateAcceleratorTableW(accels.as_ptr() as _, 1), + /// msg, + /// ) == 1 + /// }; + /// translated + /// }); + /// ``` + fn with_msg_hook(&mut self, callback: F) -> &mut Self + where + F: FnMut(*const c_void) -> bool + 'static; +} + +impl EventLoopBuilderExtWindows for EventLoopBuilder { + #[inline] + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self { + self.platform_specific.any_thread = any_thread; + self + } + + #[inline] + fn with_dpi_aware(&mut self, dpi_aware: bool) -> &mut Self { + self.platform_specific.dpi_aware = dpi_aware; + self + } + + #[inline] + fn with_msg_hook(&mut self, callback: F) -> &mut Self + where + F: FnMut(*const c_void) -> bool + 'static, + { + self.platform_specific.msg_hook = Some(Box::new(callback)); + self + } +} + +/// Additional methods on `Window` that are specific to Windows. +pub trait WindowExtWindows { + /// Enables or disables mouse and keyboard input to the specified window. + /// + /// A window must be enabled before it can be activated. + /// If an application has create a modal dialog box by disabling its owner window + /// (as described in [`WindowAttributesExtWindows::with_owner_window`]), the application must + /// enable the owner window before destroying the dialog box. + /// Otherwise, another window will receive the keyboard focus and be activated. + /// + /// If a child window is disabled, it is ignored when the system tries to determine which + /// window should receive mouse messages. + /// + /// For more information, see + /// and + fn set_enable(&self, enabled: bool); + + /// This sets `ICON_BIG`. A good ceiling here is 256x256. + fn set_taskbar_icon(&self, taskbar_icon: Option); + + /// Whether to show or hide the window icon in the taskbar. + fn set_skip_taskbar(&self, skip: bool); + + /// Shows or hides the background drop shadow for undecorated windows. + /// + /// Enabling the shadow causes a thin 1px line to appear on the top of the window. + fn set_undecorated_shadow(&self, shadow: bool); + + /// Sets system-drawn backdrop type. + /// + /// Requires Windows 11 build 22523+. + fn set_system_backdrop(&self, backdrop_type: BackdropType); + + /// Sets the color of the window border. + /// + /// Supported starting with Windows 11 Build 22000. + fn set_border_color(&self, color: Option); + + /// Sets the background color of the title bar. + /// + /// Supported starting with Windows 11 Build 22000. + fn set_title_background_color(&self, color: Option); + + /// Sets the color of the window title. + /// + /// Supported starting with Windows 11 Build 22000. + fn set_title_text_color(&self, color: Color); + + /// Sets the preferred style of the window corners. + /// + /// Supported starting with Windows 11 Build 22000. + fn set_corner_preference(&self, preference: CornerPreference); + + /// Get the raw window handle for this [`Window`] without checking for thread affinity. + /// + /// Window handles in Win32 have a property called "thread affinity" that ties them to their + /// origin thread. Some operations can only happen on the window's origin thread, while others + /// can be called from any thread. For example, [`SetWindowSubclass`] is not thread safe while + /// [`GetDC`] is thread safe. + /// + /// In Rust terms, the window handle is `Send` sometimes but `!Send` other times. + /// + /// Therefore, in order to avoid confusing threading errors, [`Window`] only returns the + /// window handle when the [`window_handle`] function is called from the thread that created + /// the window. In other cases, it returns an [`Unavailable`] error. + /// + /// However in some cases you may already know that you are using the window handle for + /// operations that are guaranteed to be thread-safe. In which case this function aims + /// to provide an escape hatch so these functions are still accessible from other threads. + /// + /// # Safety + /// + /// It is the responsibility of the user to only pass the window handle into thread-safe + /// Win32 APIs. + /// + /// [`SetWindowSubclass`]: https://learn.microsoft.com/en-us/windows/win32/api/commctrl/nf-commctrl-setwindowsubclass + /// [`GetDC`]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdc + /// [`Window`]: crate::window::Window + /// [`window_handle`]: https://docs.rs/raw-window-handle/latest/raw_window_handle/trait.HasWindowHandle.html#tymethod.window_handle + /// [`Unavailable`]: https://docs.rs/raw-window-handle/latest/raw_window_handle/enum.HandleError.html#variant.Unavailable + /// + /// ## Example + /// + /// ```no_run + /// # use winit::window::Window; + /// # fn scope(window: Window) { + /// use std::thread; + /// use winit::platform::windows::WindowExtWindows; + /// use winit::raw_window_handle::HasWindowHandle; + /// + /// // We can get the window handle on the current thread. + /// let handle = window.window_handle().unwrap(); + /// + /// // However, on another thread, we can't! + /// thread::spawn(move || { + /// assert!(window.window_handle().is_err()); + /// + /// // We can use this function as an escape hatch. + /// let handle = unsafe { window.window_handle_any_thread().unwrap() }; + /// }); + /// # } + /// ``` + #[cfg(feature = "rwh_06")] + unsafe fn window_handle_any_thread( + &self, + ) -> Result, rwh_06::HandleError>; +} + +impl WindowExtWindows for Window { + #[inline] + fn set_enable(&self, enabled: bool) { + self.window.set_enable(enabled) + } + + #[inline] + fn set_taskbar_icon(&self, taskbar_icon: Option) { + self.window.set_taskbar_icon(taskbar_icon) + } + + #[inline] + fn set_skip_taskbar(&self, skip: bool) { + self.window.set_skip_taskbar(skip) + } + + #[inline] + fn set_undecorated_shadow(&self, shadow: bool) { + self.window.set_undecorated_shadow(shadow) + } + + #[inline] + fn set_system_backdrop(&self, backdrop_type: BackdropType) { + self.window.set_system_backdrop(backdrop_type) + } + + #[inline] + fn set_border_color(&self, color: Option) { + self.window.set_border_color(color.unwrap_or(Color::NONE)) + } + + #[inline] + fn set_title_background_color(&self, color: Option) { + // The windows docs don't mention NONE as a valid options but it works in practice and is + // useful to circumvent the Windows option "Show accent color on title bars and + // window borders" + self.window.set_title_background_color(color.unwrap_or(Color::NONE)) + } + + #[inline] + fn set_title_text_color(&self, color: Color) { + self.window.set_title_text_color(color) + } + + #[inline] + fn set_corner_preference(&self, preference: CornerPreference) { + self.window.set_corner_preference(preference) + } + + #[cfg(feature = "rwh_06")] + unsafe fn window_handle_any_thread( + &self, + ) -> Result, rwh_06::HandleError> { + unsafe { + let handle = self.window.rwh_06_no_thread_check()?; + + // SAFETY: The handle is valid in this context. + Ok(rwh_06::WindowHandle::borrow_raw(handle)) + } + } +} + +/// Additional methods for anything that dereference to [`Window`]. +/// +/// [`Window`]: crate::window::Window +pub trait WindowBorrowExtWindows: Borrow + Sized { + /// Create an object that allows accessing the inner window handle in a thread-unsafe way. + /// + /// It is possible to call [`window_handle_any_thread`] to get around Windows's thread + /// affinity limitations. However, it may be desired to pass the [`Window`] into something + /// that requires the [`HasWindowHandle`] trait, while ignoring thread affinity limitations. + /// + /// This function wraps anything that implements `Borrow` into a structure that + /// uses the inner window handle as a mean of implementing [`HasWindowHandle`]. It wraps + /// `Window`, `&Window`, `Arc`, and other reference types. + /// + /// # Safety + /// + /// It is the responsibility of the user to only pass the window handle into thread-safe + /// Win32 APIs. + /// + /// [`window_handle_any_thread`]: WindowExtWindows::window_handle_any_thread + /// [`Window`]: crate::window::Window + /// [`HasWindowHandle`]: rwh_06::HasWindowHandle + unsafe fn any_thread(self) -> AnyThread { + AnyThread(self) + } +} + +impl + Sized> WindowBorrowExtWindows for W {} + +/// Additional methods on `WindowAttributes` that are specific to Windows. +#[allow(rustdoc::broken_intra_doc_links)] +pub trait WindowAttributesExtWindows { + /// Set an owner to the window to be created. Can be used to create a dialog box, for example. + /// This only works when [`WindowAttributes::with_parent_window`] isn't called or set to `None`. + /// Can be used in combination with + /// [`WindowExtWindows::set_enable(false)`][WindowExtWindows::set_enable] on the owner + /// window to create a modal dialog box. + /// + /// From MSDN: + /// - An owned window is always above its owner in the z-order. + /// - The system automatically destroys an owned window when its owner is destroyed. + /// - An owned window is hidden when its owner is minimized. + /// + /// For more information, see + fn with_owner_window(self, parent: HWND) -> Self; + + /// Sets a menu on the window to be created. + /// + /// Parent and menu are mutually exclusive; a child window cannot have a menu! + /// + /// The menu must have been manually created beforehand with [`CreateMenu`] or similar. + /// + /// Note: Dark mode cannot be supported for win32 menus, it's simply not possible to change how + /// the menus look. If you use this, it is recommended that you combine it with + /// `with_theme(Some(Theme::Light))` to avoid a jarring effect. + #[cfg_attr( + windows_platform, + doc = "[`CreateMenu`]: windows_sys::Win32::UI::WindowsAndMessaging::CreateMenu" + )] + #[cfg_attr(not(windows_platform), doc = "[`CreateMenu`]: #only-available-on-windows")] + fn with_menu(self, menu: HMENU) -> Self; + + /// This sets `ICON_BIG`. A good ceiling here is 256x256. + fn with_taskbar_icon(self, taskbar_icon: Option) -> Self; + + /// This sets `WS_EX_NOREDIRECTIONBITMAP`. + fn with_no_redirection_bitmap(self, flag: bool) -> Self; + + /// Enables or disables drag and drop support (enabled by default). Will interfere with other + /// crates that use multi-threaded COM API (`CoInitializeEx` with `COINIT_MULTITHREADED` + /// instead of `COINIT_APARTMENTTHREADED`) on the same thread. Note that winit may still + /// attempt to initialize COM API regardless of this option. Currently only fullscreen mode + /// does that, but there may be more in the future. If you need COM API with + /// `COINIT_MULTITHREADED` you must initialize it before calling any winit functions. See for more information. + fn with_drag_and_drop(self, flag: bool) -> Self; + + /// Whether show or hide the window icon in the taskbar. + fn with_skip_taskbar(self, skip: bool) -> Self; + + /// Customize the window class name. + fn with_class_name>(self, class_name: S) -> Self; + + /// Shows or hides the background drop shadow for undecorated windows. + /// + /// The shadow is hidden by default. + /// Enabling the shadow causes a thin 1px line to appear on the top of the window. + fn with_undecorated_shadow(self, shadow: bool) -> Self; + + /// Sets system-drawn backdrop type. + /// + /// Requires Windows 11 build 22523+. + fn with_system_backdrop(self, backdrop_type: BackdropType) -> Self; + + /// This sets or removes `WS_CLIPCHILDREN` style. + fn with_clip_children(self, flag: bool) -> Self; + + /// Sets the color of the window border. + /// + /// Supported starting with Windows 11 Build 22000. + fn with_border_color(self, color: Option) -> Self; + + /// Sets the background color of the title bar. + /// + /// Supported starting with Windows 11 Build 22000. + fn with_title_background_color(self, color: Option) -> Self; + + /// Sets the color of the window title. + /// + /// Supported starting with Windows 11 Build 22000. + fn with_title_text_color(self, color: Color) -> Self; + + /// Sets the preferred style of the window corners. + /// + /// Supported starting with Windows 11 Build 22000. + fn with_corner_preference(self, corners: CornerPreference) -> Self; +} + +impl WindowAttributesExtWindows for WindowAttributes { + #[inline] + fn with_owner_window(mut self, parent: HWND) -> Self { + self.platform_specific.owner = Some(parent); + self + } + + #[inline] + fn with_menu(mut self, menu: HMENU) -> Self { + self.platform_specific.menu = Some(menu); + self + } + + #[inline] + fn with_taskbar_icon(mut self, taskbar_icon: Option) -> Self { + self.platform_specific.taskbar_icon = taskbar_icon; + self + } + + #[inline] + fn with_no_redirection_bitmap(mut self, flag: bool) -> Self { + self.platform_specific.no_redirection_bitmap = flag; + self + } + + #[inline] + fn with_drag_and_drop(mut self, flag: bool) -> Self { + self.platform_specific.drag_and_drop = flag; + self + } + + #[inline] + fn with_skip_taskbar(mut self, skip: bool) -> Self { + self.platform_specific.skip_taskbar = skip; + self + } + + #[inline] + fn with_class_name>(mut self, class_name: S) -> Self { + self.platform_specific.class_name = class_name.into(); + self + } + + #[inline] + fn with_undecorated_shadow(mut self, shadow: bool) -> Self { + self.platform_specific.decoration_shadow = shadow; + self + } + + #[inline] + fn with_system_backdrop(mut self, backdrop_type: BackdropType) -> Self { + self.platform_specific.backdrop_type = backdrop_type; + self + } + + #[inline] + fn with_clip_children(mut self, flag: bool) -> Self { + self.platform_specific.clip_children = flag; + self + } + + #[inline] + fn with_border_color(mut self, color: Option) -> Self { + self.platform_specific.border_color = Some(color.unwrap_or(Color::NONE)); + self + } + + #[inline] + fn with_title_background_color(mut self, color: Option) -> Self { + self.platform_specific.title_background_color = Some(color.unwrap_or(Color::NONE)); + self + } + + #[inline] + fn with_title_text_color(mut self, color: Color) -> Self { + self.platform_specific.title_text_color = Some(color); + self + } + + #[inline] + fn with_corner_preference(mut self, corners: CornerPreference) -> Self { + self.platform_specific.corner_preference = Some(corners); + self + } +} + +/// Additional methods on `MonitorHandle` that are specific to Windows. +pub trait MonitorHandleExtWindows { + /// Returns the name of the monitor adapter specific to the Win32 API. + fn native_id(&self) -> String; + + /// Returns the handle of the monitor - `HMONITOR`. + fn hmonitor(&self) -> HMONITOR; +} + +impl MonitorHandleExtWindows for MonitorHandle { + #[inline] + fn native_id(&self) -> String { + self.inner.native_identifier() + } + + #[inline] + fn hmonitor(&self) -> HMONITOR { + self.inner.hmonitor() + } +} + +/// Additional methods on `DeviceId` that are specific to Windows. +pub trait DeviceIdExtWindows { + /// Returns an identifier that persistently refers to this specific device. + /// + /// Will return `None` if the device is no longer available. + fn persistent_identifier(&self) -> Option; +} + +impl DeviceIdExtWindows for DeviceId { + #[inline] + fn persistent_identifier(&self) -> Option { + self.0.persistent_identifier() + } +} + +/// Additional methods on `Icon` that are specific to Windows. +/// +/// Windows icons can be created from files, or from the [`embedded resources`](https://learn.microsoft.com/en-us/windows/win32/menurc/about-resource-files). +/// +/// The `ICON` resource definition statement use the following syntax: +/// ```rc +/// nameID ICON filename +/// ``` +/// `nameID` is a unique name or a 16-bit unsigned integer value identifying the resource, +/// `filename` is the name of the file that contains the resource. +/// +/// More information about the `ICON` resource can be found at [`Microsoft Learn`](https://learn.microsoft.com/en-us/windows/win32/menurc/icon-resource) portal. +pub trait IconExtWindows: Sized { + /// Create an icon from a file path. + /// + /// Specify `size` to load a specific icon size from the file, or `None` to load the default + /// icon size from the file. + /// + /// In cases where the specified size does not exist in the file, Windows may perform scaling + /// to get an icon of the desired size. + fn from_path>(path: P, size: Option>) + -> Result; + + /// Create an icon from a resource embedded in this executable or library by its ordinal id. + /// + /// The valid `ordinal` values range from 1 to [`u16::MAX`] (inclusive). The value `0` is an + /// invalid ordinal id, but it can be used with [`from_resource_name`] as `"0"`. + /// + /// [`from_resource_name`]: IconExtWindows::from_resource_name + /// + /// Specify `size` to load a specific icon size from the file, or `None` to load the default + /// icon size from the file. + /// + /// In cases where the specified size does not exist in the file, Windows may perform scaling + /// to get an icon of the desired size. + fn from_resource(ordinal: u16, size: Option>) -> Result; + + /// Create an icon from a resource embedded in this executable or library by its name. + /// + /// Specify `size` to load a specific icon size from the file, or `None` to load the default + /// icon size from the file. + /// + /// In cases where the specified size does not exist in the file, Windows may perform scaling + /// to get an icon of the desired size. + /// + /// # Notes + /// + /// Consider the following resource definition statements: + /// ```rc + /// app ICON "app.ico" + /// 1 ICON "a.ico" + /// 0027 ICON "custom.ico" + /// 0 ICON "alt.ico" + /// ``` + /// + /// Due to some internal implementation details of the resource embedding/loading process on + /// Windows platform, strings that can be interpreted as 16-bit unsigned integers (`"1"`, + /// `"002"`, etc.) cannot be used as valid resource names, and instead should be passed into + /// [`from_resource`]: + /// + /// [`from_resource`]: IconExtWindows::from_resource + /// + /// ```rust,no_run + /// use winit::platform::windows::IconExtWindows; + /// use winit::window::Icon; + /// + /// assert!(Icon::from_resource_name("app", None).is_ok()); + /// assert!(Icon::from_resource(1, None).is_ok()); + /// assert!(Icon::from_resource(27, None).is_ok()); + /// assert!(Icon::from_resource_name("27", None).is_err()); + /// assert!(Icon::from_resource_name("0027", None).is_err()); + /// ``` + /// + /// While `0` cannot be used as an ordinal id (see [`from_resource`]), it can be used as a + /// name: + /// + /// [`from_resource`]: IconExtWindows::from_resource + /// + /// ```rust,no_run + /// # use winit::platform::windows::IconExtWindows; + /// # use winit::window::Icon; + /// assert!(Icon::from_resource_name("0", None).is_ok()); + /// assert!(Icon::from_resource(0, None).is_err()); + /// ``` + fn from_resource_name(name: &str, size: Option>) -> Result; +} + +impl IconExtWindows for Icon { + fn from_path>( + path: P, + size: Option>, + ) -> Result { + let win_icon = crate::platform_impl::WinIcon::from_path(path, size)?; + Ok(Icon { inner: win_icon }) + } + + fn from_resource(ordinal: u16, size: Option>) -> Result { + let win_icon = crate::platform_impl::WinIcon::from_resource(ordinal, size)?; + Ok(Icon { inner: win_icon }) + } + + fn from_resource_name(name: &str, size: Option>) -> Result { + let win_icon = crate::platform_impl::WinIcon::from_resource_name(name, size)?; + Ok(Icon { inner: win_icon }) + } +} diff --git a/vendor/winit/src/platform/x11.rs b/vendor/winit/src/platform/x11.rs new file mode 100644 index 00000000..4ab900c9 --- /dev/null +++ b/vendor/winit/src/platform/x11.rs @@ -0,0 +1,254 @@ +//! # X11 +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::event_loop::{ActiveEventLoop, EventLoop, EventLoopBuilder}; +use crate::monitor::MonitorHandle; +use crate::window::{Window, WindowAttributes}; + +use crate::dpi::Size; + +/// X window type. Maps directly to +/// [`_NET_WM_WINDOW_TYPE`](https://specifications.freedesktop.org/wm-spec/wm-spec-1.5.html). +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum WindowType { + /// A desktop feature. This can include a single window containing desktop icons with the same + /// dimensions as the screen, allowing the desktop environment to have full control of the + /// desktop, without the need for proxying root window clicks. + Desktop, + /// A dock or panel feature. Typically a Window Manager would keep such windows on top of all + /// other windows. + Dock, + /// Toolbar windows. "Torn off" from the main application. + Toolbar, + /// Pinnable menu windows. "Torn off" from the main application. + Menu, + /// A small persistent utility window, such as a palette or toolbox. + Utility, + /// The window is a splash screen displayed as an application is starting up. + Splash, + /// This is a dialog window. + Dialog, + /// A dropdown menu that usually appears when the user clicks on an item in a menu bar. + /// This property is typically used on override-redirect windows. + DropdownMenu, + /// A popup menu that usually appears when the user right clicks on an object. + /// This property is typically used on override-redirect windows. + PopupMenu, + /// A tooltip window. Usually used to show additional information when hovering over an object + /// with the cursor. This property is typically used on override-redirect windows. + Tooltip, + /// The window is a notification. + /// This property is typically used on override-redirect windows. + Notification, + /// This should be used on the windows that are popped up by combo boxes. + /// This property is typically used on override-redirect windows. + Combo, + /// This indicates the window is being dragged. + /// This property is typically used on override-redirect windows. + Dnd, + /// This is a normal, top-level window. + #[default] + Normal, +} + +/// The first argument in the provided hook will be the pointer to `XDisplay` +/// and the second one the pointer to [`XErrorEvent`]. The returned `bool` is an +/// indicator whether the error was handled by the callback. +/// +/// [`XErrorEvent`]: https://linux.die.net/man/3/xerrorevent +pub type XlibErrorHook = + Box bool + Send + Sync>; + +/// A unique identifier for an X11 visual. +pub type XVisualID = u32; + +/// A unique identifier for an X11 window. +pub type XWindow = u32; + +/// Hook to winit's xlib error handling callback. +/// +/// This method is provided as a safe way to handle the errors coming from X11 +/// when using xlib in external crates, like glutin for GLX access. Trying to +/// handle errors by speculating with `XSetErrorHandler` is [`unsafe`]. +/// +/// **Be aware that your hook is always invoked and returning `true` from it will +/// prevent `winit` from getting the error itself. It's wise to always return +/// `false` if you're not initiated the `Sync`.** +/// +/// [`unsafe`]: https://www.remlab.net/op/xlib.shtml +#[inline] +pub fn register_xlib_error_hook(hook: XlibErrorHook) { + // Append new hook. + crate::platform_impl::XLIB_ERROR_HOOKS.lock().unwrap().push(hook); +} + +/// Additional methods on [`ActiveEventLoop`] that are specific to X11. +pub trait ActiveEventLoopExtX11 { + /// True if the [`ActiveEventLoop`] uses X11. + fn is_x11(&self) -> bool; +} + +impl ActiveEventLoopExtX11 for ActiveEventLoop { + #[inline] + fn is_x11(&self) -> bool { + !self.p.is_wayland() + } +} + +/// Additional methods on [`EventLoop`] that are specific to X11. +pub trait EventLoopExtX11 { + /// True if the [`EventLoop`] uses X11. + fn is_x11(&self) -> bool; +} + +impl EventLoopExtX11 for EventLoop { + #[inline] + fn is_x11(&self) -> bool { + !self.event_loop.is_wayland() + } +} + +/// Additional methods on [`EventLoopBuilder`] that are specific to X11. +pub trait EventLoopBuilderExtX11 { + /// Force using X11. + fn with_x11(&mut self) -> &mut Self; + + /// Whether to allow the event loop to be created off of the main thread. + /// + /// By default, the window is only allowed to be created on the main + /// thread, to make platform compatibility easier. + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self; +} + +impl EventLoopBuilderExtX11 for EventLoopBuilder { + #[inline] + fn with_x11(&mut self) -> &mut Self { + self.platform_specific.forced_backend = Some(crate::platform_impl::Backend::X); + self + } + + #[inline] + fn with_any_thread(&mut self, any_thread: bool) -> &mut Self { + self.platform_specific.any_thread = any_thread; + self + } +} + +/// Additional methods on [`Window`] that are specific to X11. +pub trait WindowExtX11 {} + +impl WindowExtX11 for Window {} + +/// Additional methods on [`WindowAttributes`] that are specific to X11. +pub trait WindowAttributesExtX11 { + /// Create this window with a specific X11 visual. + fn with_x11_visual(self, visual_id: XVisualID) -> Self; + + fn with_x11_screen(self, screen_id: i32) -> Self; + + /// Build window with the given `general` and `instance` names. + /// + /// The `general` sets general class of `WM_CLASS(STRING)`, while `instance` set the + /// instance part of it. The resulted property looks like `WM_CLASS(STRING) = "instance", + /// "general"`. + /// + /// For details about application ID conventions, see the + /// [Desktop Entry Spec](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#desktop-file-id) + fn with_name(self, general: impl Into, instance: impl Into) -> Self; + + /// Build window with override-redirect flag; defaults to false. + fn with_override_redirect(self, override_redirect: bool) -> Self; + + /// Build window with `_NET_WM_WINDOW_TYPE` hints; defaults to `Normal`. + fn with_x11_window_type(self, x11_window_type: Vec) -> Self; + + /// Build window with base size hint. + /// + /// ``` + /// # use winit::dpi::{LogicalSize, PhysicalSize}; + /// # use winit::window::Window; + /// # use winit::platform::x11::WindowAttributesExtX11; + /// // Specify the size in logical dimensions like this: + /// Window::default_attributes().with_base_size(LogicalSize::new(400.0, 200.0)); + /// + /// // Or specify the size in physical dimensions like this: + /// Window::default_attributes().with_base_size(PhysicalSize::new(400, 200)); + /// ``` + fn with_base_size>(self, base_size: S) -> Self; + + /// Embed this window into another parent window. + /// + /// # Example + /// + /// ```no_run + /// use winit::window::Window; + /// use winit::event_loop::ActiveEventLoop; + /// use winit::platform::x11::{XWindow, WindowAttributesExtX11}; + /// # fn create_window(event_loop: &ActiveEventLoop) -> Result<(), Box> { + /// let parent_window_id = std::env::args().nth(1).unwrap().parse::()?; + /// let window_attributes = Window::default_attributes().with_embed_parent_window(parent_window_id); + /// let window = event_loop.create_window(window_attributes)?; + /// # Ok(()) } + /// ``` + fn with_embed_parent_window(self, parent_window_id: XWindow) -> Self; +} + +impl WindowAttributesExtX11 for WindowAttributes { + #[inline] + fn with_x11_visual(mut self, visual_id: XVisualID) -> Self { + self.platform_specific.x11.visual_id = Some(visual_id); + self + } + + #[inline] + fn with_x11_screen(mut self, screen_id: i32) -> Self { + self.platform_specific.x11.screen_id = Some(screen_id); + self + } + + #[inline] + fn with_name(mut self, general: impl Into, instance: impl Into) -> Self { + self.platform_specific.name = + Some(crate::platform_impl::ApplicationName::new(general.into(), instance.into())); + self + } + + #[inline] + fn with_override_redirect(mut self, override_redirect: bool) -> Self { + self.platform_specific.x11.override_redirect = override_redirect; + self + } + + #[inline] + fn with_x11_window_type(mut self, x11_window_types: Vec) -> Self { + self.platform_specific.x11.x11_window_types = x11_window_types; + self + } + + #[inline] + fn with_base_size>(mut self, base_size: S) -> Self { + self.platform_specific.x11.base_size = Some(base_size.into()); + self + } + + #[inline] + fn with_embed_parent_window(mut self, parent_window_id: XWindow) -> Self { + self.platform_specific.x11.embed_window = Some(parent_window_id); + self + } +} + +/// Additional methods on `MonitorHandle` that are specific to X11. +pub trait MonitorHandleExtX11 { + /// Returns the inner identifier of the monitor. + fn native_id(&self) -> u32; +} + +impl MonitorHandleExtX11 for MonitorHandle { + #[inline] + fn native_id(&self) -> u32 { + self.inner.native_identifier() + } +} diff --git a/vendor/winit/src/platform_impl/android/keycodes.rs b/vendor/winit/src/platform_impl/android/keycodes.rs new file mode 100644 index 00000000..207d549f --- /dev/null +++ b/vendor/winit/src/platform_impl/android/keycodes.rs @@ -0,0 +1,608 @@ +use android_activity::input::{KeyAction, KeyEvent, KeyMapChar, Keycode}; +use android_activity::AndroidApp; + +use crate::keyboard::{Key, KeyCode, KeyLocation, NamedKey, NativeKey, NativeKeyCode, PhysicalKey}; + +pub fn to_physical_key(keycode: Keycode) -> PhysicalKey { + PhysicalKey::Code(match keycode { + Keycode::A => KeyCode::KeyA, + Keycode::B => KeyCode::KeyB, + Keycode::C => KeyCode::KeyC, + Keycode::D => KeyCode::KeyD, + Keycode::E => KeyCode::KeyE, + Keycode::F => KeyCode::KeyF, + Keycode::G => KeyCode::KeyG, + Keycode::H => KeyCode::KeyH, + Keycode::I => KeyCode::KeyI, + Keycode::J => KeyCode::KeyJ, + Keycode::K => KeyCode::KeyK, + Keycode::L => KeyCode::KeyL, + Keycode::M => KeyCode::KeyM, + Keycode::N => KeyCode::KeyN, + Keycode::O => KeyCode::KeyO, + Keycode::P => KeyCode::KeyP, + Keycode::Q => KeyCode::KeyQ, + Keycode::R => KeyCode::KeyR, + Keycode::S => KeyCode::KeyS, + Keycode::T => KeyCode::KeyT, + Keycode::U => KeyCode::KeyU, + Keycode::V => KeyCode::KeyV, + Keycode::W => KeyCode::KeyW, + Keycode::X => KeyCode::KeyX, + Keycode::Y => KeyCode::KeyY, + Keycode::Z => KeyCode::KeyZ, + + Keycode::Keycode0 => KeyCode::Digit0, + Keycode::Keycode1 => KeyCode::Digit1, + Keycode::Keycode2 => KeyCode::Digit2, + Keycode::Keycode3 => KeyCode::Digit3, + Keycode::Keycode4 => KeyCode::Digit4, + Keycode::Keycode5 => KeyCode::Digit5, + Keycode::Keycode6 => KeyCode::Digit6, + Keycode::Keycode7 => KeyCode::Digit7, + Keycode::Keycode8 => KeyCode::Digit8, + Keycode::Keycode9 => KeyCode::Digit9, + + Keycode::Numpad0 => KeyCode::Numpad0, + Keycode::Numpad1 => KeyCode::Numpad1, + Keycode::Numpad2 => KeyCode::Numpad2, + Keycode::Numpad3 => KeyCode::Numpad3, + Keycode::Numpad4 => KeyCode::Numpad4, + Keycode::Numpad5 => KeyCode::Numpad5, + Keycode::Numpad6 => KeyCode::Numpad6, + Keycode::Numpad7 => KeyCode::Numpad7, + Keycode::Numpad8 => KeyCode::Numpad8, + Keycode::Numpad9 => KeyCode::Numpad9, + + Keycode::NumpadAdd => KeyCode::NumpadAdd, + Keycode::NumpadSubtract => KeyCode::NumpadSubtract, + Keycode::NumpadMultiply => KeyCode::NumpadMultiply, + Keycode::NumpadDivide => KeyCode::NumpadDivide, + Keycode::NumpadEnter => KeyCode::NumpadEnter, + Keycode::NumpadEquals => KeyCode::NumpadEqual, + Keycode::NumpadComma => KeyCode::NumpadComma, + Keycode::NumpadDot => KeyCode::NumpadDecimal, + Keycode::NumLock => KeyCode::NumLock, + + Keycode::DpadLeft => KeyCode::ArrowLeft, + Keycode::DpadRight => KeyCode::ArrowRight, + Keycode::DpadUp => KeyCode::ArrowUp, + Keycode::DpadDown => KeyCode::ArrowDown, + + Keycode::F1 => KeyCode::F1, + Keycode::F2 => KeyCode::F2, + Keycode::F3 => KeyCode::F3, + Keycode::F4 => KeyCode::F4, + Keycode::F5 => KeyCode::F5, + Keycode::F6 => KeyCode::F6, + Keycode::F7 => KeyCode::F7, + Keycode::F8 => KeyCode::F8, + Keycode::F9 => KeyCode::F9, + Keycode::F10 => KeyCode::F10, + Keycode::F11 => KeyCode::F11, + Keycode::F12 => KeyCode::F12, + + Keycode::Space => KeyCode::Space, + Keycode::Escape => KeyCode::Escape, + Keycode::Enter => KeyCode::Enter, // not on the Numpad + Keycode::Tab => KeyCode::Tab, + + Keycode::PageUp => KeyCode::PageUp, + Keycode::PageDown => KeyCode::PageDown, + Keycode::MoveHome => KeyCode::Home, + Keycode::MoveEnd => KeyCode::End, + Keycode::Insert => KeyCode::Insert, + + Keycode::Del => KeyCode::Backspace, // Backspace (above Enter) + Keycode::ForwardDel => KeyCode::Delete, // Delete (below Insert) + + Keycode::Copy => KeyCode::Copy, + Keycode::Paste => KeyCode::Paste, + Keycode::Cut => KeyCode::Cut, + + Keycode::VolumeUp => KeyCode::AudioVolumeUp, + Keycode::VolumeDown => KeyCode::AudioVolumeDown, + Keycode::VolumeMute => KeyCode::AudioVolumeMute, + // Keycode::Mute => None, // Microphone mute + Keycode::MediaPlayPause => KeyCode::MediaPlayPause, + Keycode::MediaStop => KeyCode::MediaStop, + Keycode::MediaNext => KeyCode::MediaTrackNext, + Keycode::MediaPrevious => KeyCode::MediaTrackPrevious, + + Keycode::Plus => KeyCode::Equal, + Keycode::Minus => KeyCode::Minus, + // Winit doesn't differentiate both '+' and '=', considering they are usually + // on the same physical key + Keycode::Equals => KeyCode::Equal, + Keycode::Semicolon => KeyCode::Semicolon, + Keycode::Slash => KeyCode::Slash, + Keycode::Backslash => KeyCode::Backslash, + Keycode::Comma => KeyCode::Comma, + Keycode::Period => KeyCode::Period, + Keycode::Apostrophe => KeyCode::Quote, + Keycode::Grave => KeyCode::Backquote, + + // Winit doesn't expose a SysRq code, so map to PrintScreen since it's + // usually the same physical key + Keycode::Sysrq => KeyCode::PrintScreen, + // These are usually the same (Pause/Break) + Keycode::Break => KeyCode::Pause, + // These are exactly the same + Keycode::ScrollLock => KeyCode::ScrollLock, + + Keycode::Yen => KeyCode::IntlYen, + Keycode::Kana => KeyCode::Lang1, + Keycode::KatakanaHiragana => KeyCode::KanaMode, + + Keycode::CtrlLeft => KeyCode::ControlLeft, + Keycode::CtrlRight => KeyCode::ControlRight, + + Keycode::ShiftLeft => KeyCode::ShiftLeft, + Keycode::ShiftRight => KeyCode::ShiftRight, + + Keycode::AltLeft => KeyCode::AltLeft, + Keycode::AltRight => KeyCode::AltRight, + + Keycode::MetaLeft => KeyCode::SuperLeft, + Keycode::MetaRight => KeyCode::SuperRight, + + Keycode::LeftBracket => KeyCode::BracketLeft, + Keycode::RightBracket => KeyCode::BracketRight, + + Keycode::Power => KeyCode::Power, + Keycode::Sleep => KeyCode::Sleep, // what about SoftSleep? + Keycode::Wakeup => KeyCode::WakeUp, + + keycode => return PhysicalKey::Unidentified(NativeKeyCode::Android(keycode.into())), + }) +} + +/// Tries to map the `key_event` to a `KeyMapChar` containing a unicode character or dead key accent +/// +/// This takes a `KeyEvent` and looks up its corresponding `KeyCharacterMap` and +/// uses that to try and map the `key_code` + `meta_state` to a unicode +/// character or a dead key that can be combined with the next key press. +pub fn character_map_and_combine_key( + app: &AndroidApp, + key_event: &KeyEvent<'_>, + combining_accent: &mut Option, +) -> Option { + let device_id = key_event.device_id(); + + let key_map = match app.device_key_character_map(device_id) { + Ok(key_map) => key_map, + Err(err) => { + tracing::warn!("Failed to look up `KeyCharacterMap` for device {device_id}: {err:?}"); + return None; + }, + }; + + match key_map.get(key_event.key_code(), key_event.meta_state()) { + Ok(KeyMapChar::Unicode(unicode)) => { + // Only do dead key combining on key down + if key_event.action() == KeyAction::Down { + let combined_unicode = if let Some(accent) = combining_accent { + match key_map.get_dead_char(*accent, unicode) { + Ok(Some(key)) => Some(key), + Ok(None) => None, + Err(err) => { + tracing::warn!( + "KeyEvent: Failed to combine 'dead key' accent '{accent}' with \ + '{unicode}': {err:?}" + ); + None + }, + } + } else { + Some(unicode) + }; + *combining_accent = None; + combined_unicode.map(KeyMapChar::Unicode) + } else { + Some(KeyMapChar::Unicode(unicode)) + } + }, + Ok(KeyMapChar::CombiningAccent(accent)) => { + if key_event.action() == KeyAction::Down { + *combining_accent = Some(accent); + } + Some(KeyMapChar::CombiningAccent(accent)) + }, + Ok(KeyMapChar::None) => { + // Leave any combining_accent state in tact (seems to match how other + // Android apps work) + None + }, + Err(err) => { + tracing::warn!("KeyEvent: Failed to get key map character: {err:?}"); + *combining_accent = None; + None + }, + } +} + +pub fn to_logical(key_char: Option, keycode: Keycode) -> Key { + use android_activity::input::Keycode::*; + + let native = NativeKey::Android(keycode.into()); + + match key_char { + Some(KeyMapChar::Unicode(c)) => Key::Character(smol_str::SmolStr::from_iter([c])), + Some(KeyMapChar::CombiningAccent(c)) => Key::Dead(Some(c)), + None | Some(KeyMapChar::None) => match keycode { + // Using `BrowserHome` instead of `GoHome` according to + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + Home => Key::Named(NamedKey::BrowserHome), + Back => Key::Named(NamedKey::BrowserBack), + Call => Key::Named(NamedKey::Call), + Endcall => Key::Named(NamedKey::EndCall), + + //------------------------------------------------------------------------------- + // These should be redundant because they should have already been matched + // as `KeyMapChar::Unicode`, but also matched here as a fallback + Keycode0 => Key::Character("0".into()), + Keycode1 => Key::Character("1".into()), + Keycode2 => Key::Character("2".into()), + Keycode3 => Key::Character("3".into()), + Keycode4 => Key::Character("4".into()), + Keycode5 => Key::Character("5".into()), + Keycode6 => Key::Character("6".into()), + Keycode7 => Key::Character("7".into()), + Keycode8 => Key::Character("8".into()), + Keycode9 => Key::Character("9".into()), + Star => Key::Character("*".into()), + Pound => Key::Character("#".into()), + A => Key::Character("a".into()), + B => Key::Character("b".into()), + C => Key::Character("c".into()), + D => Key::Character("d".into()), + E => Key::Character("e".into()), + F => Key::Character("f".into()), + G => Key::Character("g".into()), + H => Key::Character("h".into()), + I => Key::Character("i".into()), + J => Key::Character("j".into()), + K => Key::Character("k".into()), + L => Key::Character("l".into()), + M => Key::Character("m".into()), + N => Key::Character("n".into()), + O => Key::Character("o".into()), + P => Key::Character("p".into()), + Q => Key::Character("q".into()), + R => Key::Character("r".into()), + S => Key::Character("s".into()), + T => Key::Character("t".into()), + U => Key::Character("u".into()), + V => Key::Character("v".into()), + W => Key::Character("w".into()), + X => Key::Character("x".into()), + Y => Key::Character("y".into()), + Z => Key::Character("z".into()), + Comma => Key::Character(",".into()), + Period => Key::Character(".".into()), + Grave => Key::Character("`".into()), + Minus => Key::Character("-".into()), + Equals => Key::Character("=".into()), + LeftBracket => Key::Character("[".into()), + RightBracket => Key::Character("]".into()), + Backslash => Key::Character("\\".into()), + Semicolon => Key::Character(";".into()), + Apostrophe => Key::Character("'".into()), + Slash => Key::Character("/".into()), + At => Key::Character("@".into()), + Plus => Key::Character("+".into()), + //------------------------------------------------------------------------------- + DpadUp => Key::Named(NamedKey::ArrowUp), + DpadDown => Key::Named(NamedKey::ArrowDown), + DpadLeft => Key::Named(NamedKey::ArrowLeft), + DpadRight => Key::Named(NamedKey::ArrowRight), + DpadCenter => Key::Named(NamedKey::Enter), + + VolumeUp => Key::Named(NamedKey::AudioVolumeUp), + VolumeDown => Key::Named(NamedKey::AudioVolumeDown), + Power => Key::Named(NamedKey::Power), + Camera => Key::Named(NamedKey::Camera), + Clear => Key::Named(NamedKey::Clear), + + AltLeft => Key::Named(NamedKey::Alt), + AltRight => Key::Named(NamedKey::Alt), + ShiftLeft => Key::Named(NamedKey::Shift), + ShiftRight => Key::Named(NamedKey::Shift), + Tab => Key::Named(NamedKey::Tab), + Space => Key::Named(NamedKey::Space), + Sym => Key::Named(NamedKey::Symbol), + Explorer => Key::Named(NamedKey::LaunchWebBrowser), + Envelope => Key::Named(NamedKey::LaunchMail), + Enter => Key::Named(NamedKey::Enter), + Del => Key::Named(NamedKey::Backspace), + + // According to https://developer.android.com/reference/android/view/KeyEvent#KEYCODE_NUM + Num => Key::Named(NamedKey::Alt), + + Headsethook => Key::Named(NamedKey::HeadsetHook), + Focus => Key::Named(NamedKey::CameraFocus), + + Notification => Key::Named(NamedKey::Notification), + Search => Key::Named(NamedKey::BrowserSearch), + MediaPlayPause => Key::Named(NamedKey::MediaPlayPause), + MediaStop => Key::Named(NamedKey::MediaStop), + MediaNext => Key::Named(NamedKey::MediaTrackNext), + MediaPrevious => Key::Named(NamedKey::MediaTrackPrevious), + MediaRewind => Key::Named(NamedKey::MediaRewind), + MediaFastForward => Key::Named(NamedKey::MediaFastForward), + Mute => Key::Named(NamedKey::MicrophoneVolumeMute), + PageUp => Key::Named(NamedKey::PageUp), + PageDown => Key::Named(NamedKey::PageDown), + + Escape => Key::Named(NamedKey::Escape), + ForwardDel => Key::Named(NamedKey::Delete), + CtrlLeft => Key::Named(NamedKey::Control), + CtrlRight => Key::Named(NamedKey::Control), + CapsLock => Key::Named(NamedKey::CapsLock), + ScrollLock => Key::Named(NamedKey::ScrollLock), + MetaLeft => Key::Named(NamedKey::Super), + MetaRight => Key::Named(NamedKey::Super), + Function => Key::Named(NamedKey::Fn), + Sysrq => Key::Named(NamedKey::PrintScreen), + Break => Key::Named(NamedKey::Pause), + MoveHome => Key::Named(NamedKey::Home), + MoveEnd => Key::Named(NamedKey::End), + Insert => Key::Named(NamedKey::Insert), + Forward => Key::Named(NamedKey::BrowserForward), + MediaPlay => Key::Named(NamedKey::MediaPlay), + MediaPause => Key::Named(NamedKey::MediaPause), + MediaClose => Key::Named(NamedKey::MediaClose), + MediaEject => Key::Named(NamedKey::Eject), + MediaRecord => Key::Named(NamedKey::MediaRecord), + F1 => Key::Named(NamedKey::F1), + F2 => Key::Named(NamedKey::F2), + F3 => Key::Named(NamedKey::F3), + F4 => Key::Named(NamedKey::F4), + F5 => Key::Named(NamedKey::F5), + F6 => Key::Named(NamedKey::F6), + F7 => Key::Named(NamedKey::F7), + F8 => Key::Named(NamedKey::F8), + F9 => Key::Named(NamedKey::F9), + F10 => Key::Named(NamedKey::F10), + F11 => Key::Named(NamedKey::F11), + F12 => Key::Named(NamedKey::F12), + NumLock => Key::Named(NamedKey::NumLock), + Numpad0 => Key::Character("0".into()), + Numpad1 => Key::Character("1".into()), + Numpad2 => Key::Character("2".into()), + Numpad3 => Key::Character("3".into()), + Numpad4 => Key::Character("4".into()), + Numpad5 => Key::Character("5".into()), + Numpad6 => Key::Character("6".into()), + Numpad7 => Key::Character("7".into()), + Numpad8 => Key::Character("8".into()), + Numpad9 => Key::Character("9".into()), + NumpadDivide => Key::Character("/".into()), + NumpadMultiply => Key::Character("*".into()), + NumpadSubtract => Key::Character("-".into()), + NumpadAdd => Key::Character("+".into()), + NumpadDot => Key::Character(".".into()), + NumpadComma => Key::Character(",".into()), + NumpadEnter => Key::Named(NamedKey::Enter), + NumpadEquals => Key::Character("=".into()), + NumpadLeftParen => Key::Character("(".into()), + NumpadRightParen => Key::Character(")".into()), + + VolumeMute => Key::Named(NamedKey::AudioVolumeMute), + Info => Key::Named(NamedKey::Info), + ChannelUp => Key::Named(NamedKey::ChannelUp), + ChannelDown => Key::Named(NamedKey::ChannelDown), + ZoomIn => Key::Named(NamedKey::ZoomIn), + ZoomOut => Key::Named(NamedKey::ZoomOut), + Tv => Key::Named(NamedKey::TV), + Guide => Key::Named(NamedKey::Guide), + Dvr => Key::Named(NamedKey::DVR), + Bookmark => Key::Named(NamedKey::BrowserFavorites), + Captions => Key::Named(NamedKey::ClosedCaptionToggle), + Settings => Key::Named(NamedKey::Settings), + TvPower => Key::Named(NamedKey::TVPower), + TvInput => Key::Named(NamedKey::TVInput), + StbPower => Key::Named(NamedKey::STBPower), + StbInput => Key::Named(NamedKey::STBInput), + AvrPower => Key::Named(NamedKey::AVRPower), + AvrInput => Key::Named(NamedKey::AVRInput), + ProgRed => Key::Named(NamedKey::ColorF0Red), + ProgGreen => Key::Named(NamedKey::ColorF1Green), + ProgYellow => Key::Named(NamedKey::ColorF2Yellow), + ProgBlue => Key::Named(NamedKey::ColorF3Blue), + AppSwitch => Key::Named(NamedKey::AppSwitch), + LanguageSwitch => Key::Named(NamedKey::GroupNext), + MannerMode => Key::Named(NamedKey::MannerMode), + Keycode3dMode => Key::Named(NamedKey::TV3DMode), + Contacts => Key::Named(NamedKey::LaunchContacts), + Calendar => Key::Named(NamedKey::LaunchCalendar), + Music => Key::Named(NamedKey::LaunchMusicPlayer), + Calculator => Key::Named(NamedKey::LaunchApplication2), + ZenkakuHankaku => Key::Named(NamedKey::ZenkakuHankaku), + Eisu => Key::Named(NamedKey::Eisu), + Muhenkan => Key::Named(NamedKey::NonConvert), + Henkan => Key::Named(NamedKey::Convert), + KatakanaHiragana => Key::Named(NamedKey::HiraganaKatakana), + Kana => Key::Named(NamedKey::KanjiMode), + BrightnessDown => Key::Named(NamedKey::BrightnessDown), + BrightnessUp => Key::Named(NamedKey::BrightnessUp), + MediaAudioTrack => Key::Named(NamedKey::MediaAudioTrack), + Sleep => Key::Named(NamedKey::Standby), + Wakeup => Key::Named(NamedKey::WakeUp), + Pairing => Key::Named(NamedKey::Pairing), + MediaTopMenu => Key::Named(NamedKey::MediaTopMenu), + LastChannel => Key::Named(NamedKey::MediaLast), + TvDataService => Key::Named(NamedKey::TVDataService), + VoiceAssist => Key::Named(NamedKey::VoiceDial), + TvRadioService => Key::Named(NamedKey::TVRadioService), + TvTeletext => Key::Named(NamedKey::Teletext), + TvNumberEntry => Key::Named(NamedKey::TVNumberEntry), + TvTerrestrialAnalog => Key::Named(NamedKey::TVTerrestrialAnalog), + TvTerrestrialDigital => Key::Named(NamedKey::TVTerrestrialDigital), + TvSatellite => Key::Named(NamedKey::TVSatellite), + TvSatelliteBs => Key::Named(NamedKey::TVSatelliteBS), + TvSatelliteCs => Key::Named(NamedKey::TVSatelliteCS), + TvSatelliteService => Key::Named(NamedKey::TVSatelliteToggle), + TvNetwork => Key::Named(NamedKey::TVNetwork), + TvAntennaCable => Key::Named(NamedKey::TVAntennaCable), + TvInputHdmi1 => Key::Named(NamedKey::TVInputHDMI1), + TvInputHdmi2 => Key::Named(NamedKey::TVInputHDMI2), + TvInputHdmi3 => Key::Named(NamedKey::TVInputHDMI3), + TvInputHdmi4 => Key::Named(NamedKey::TVInputHDMI4), + TvInputComposite1 => Key::Named(NamedKey::TVInputComposite1), + TvInputComposite2 => Key::Named(NamedKey::TVInputComposite2), + TvInputComponent1 => Key::Named(NamedKey::TVInputComponent1), + TvInputComponent2 => Key::Named(NamedKey::TVInputComponent2), + TvInputVga1 => Key::Named(NamedKey::TVInputVGA1), + TvAudioDescription => Key::Named(NamedKey::TVAudioDescription), + TvAudioDescriptionMixUp => Key::Named(NamedKey::TVAudioDescriptionMixUp), + TvAudioDescriptionMixDown => Key::Named(NamedKey::TVAudioDescriptionMixDown), + TvZoomMode => Key::Named(NamedKey::ZoomToggle), + TvContentsMenu => Key::Named(NamedKey::TVContentsMenu), + TvMediaContextMenu => Key::Named(NamedKey::TVMediaContext), + TvTimerProgramming => Key::Named(NamedKey::TVTimer), + Help => Key::Named(NamedKey::Help), + NavigatePrevious => Key::Named(NamedKey::NavigatePrevious), + NavigateNext => Key::Named(NamedKey::NavigateNext), + NavigateIn => Key::Named(NamedKey::NavigateIn), + NavigateOut => Key::Named(NamedKey::NavigateOut), + MediaSkipForward => Key::Named(NamedKey::MediaSkipForward), + MediaSkipBackward => Key::Named(NamedKey::MediaSkipBackward), + MediaStepForward => Key::Named(NamedKey::MediaStepForward), + MediaStepBackward => Key::Named(NamedKey::MediaStepBackward), + Cut => Key::Named(NamedKey::Cut), + Copy => Key::Named(NamedKey::Copy), + Paste => Key::Named(NamedKey::Paste), + Refresh => Key::Named(NamedKey::BrowserRefresh), + + // ----------------------------------------------------------------- + // Keycodes that don't have a logical Key mapping + // ----------------------------------------------------------------- + Unknown => Key::Unidentified(native), + + // Can be added on demand + SoftLeft => Key::Unidentified(native), + SoftRight => Key::Unidentified(native), + + Menu => Key::Unidentified(native), + + Pictsymbols => Key::Unidentified(native), + SwitchCharset => Key::Unidentified(native), + + // ----------------------------------------------------------------- + // Gamepad events should be exposed through a separate API, not + // keyboard events + ButtonA => Key::Unidentified(native), + ButtonB => Key::Unidentified(native), + ButtonC => Key::Unidentified(native), + ButtonX => Key::Unidentified(native), + ButtonY => Key::Unidentified(native), + ButtonZ => Key::Unidentified(native), + ButtonL1 => Key::Unidentified(native), + ButtonR1 => Key::Unidentified(native), + ButtonL2 => Key::Unidentified(native), + ButtonR2 => Key::Unidentified(native), + ButtonThumbl => Key::Unidentified(native), + ButtonThumbr => Key::Unidentified(native), + ButtonStart => Key::Unidentified(native), + ButtonSelect => Key::Unidentified(native), + ButtonMode => Key::Unidentified(native), + // ----------------------------------------------------------------- + Window => Key::Unidentified(native), + + Button1 => Key::Unidentified(native), + Button2 => Key::Unidentified(native), + Button3 => Key::Unidentified(native), + Button4 => Key::Unidentified(native), + Button5 => Key::Unidentified(native), + Button6 => Key::Unidentified(native), + Button7 => Key::Unidentified(native), + Button8 => Key::Unidentified(native), + Button9 => Key::Unidentified(native), + Button10 => Key::Unidentified(native), + Button11 => Key::Unidentified(native), + Button12 => Key::Unidentified(native), + Button13 => Key::Unidentified(native), + Button14 => Key::Unidentified(native), + Button15 => Key::Unidentified(native), + Button16 => Key::Unidentified(native), + + Yen => Key::Unidentified(native), + Ro => Key::Unidentified(native), + + Assist => Key::Unidentified(native), + + Keycode11 => Key::Unidentified(native), + Keycode12 => Key::Unidentified(native), + + StemPrimary => Key::Unidentified(native), + Stem1 => Key::Unidentified(native), + Stem2 => Key::Unidentified(native), + Stem3 => Key::Unidentified(native), + + DpadUpLeft => Key::Unidentified(native), + DpadDownLeft => Key::Unidentified(native), + DpadUpRight => Key::Unidentified(native), + DpadDownRight => Key::Unidentified(native), + + SoftSleep => Key::Unidentified(native), + + SystemNavigationUp => Key::Unidentified(native), + SystemNavigationDown => Key::Unidentified(native), + SystemNavigationLeft => Key::Unidentified(native), + SystemNavigationRight => Key::Unidentified(native), + + AllApps => Key::Unidentified(native), + ThumbsUp => Key::Unidentified(native), + ThumbsDown => Key::Unidentified(native), + ProfileSwitch => Key::Unidentified(native), + + // It's always possible that new versions of Android could introduce + // key codes we can't know about at compile time. + _ => Key::Unidentified(native), + }, + } +} + +pub fn to_location(keycode: Keycode) -> KeyLocation { + use android_activity::input::Keycode::*; + + match keycode { + AltLeft => KeyLocation::Left, + AltRight => KeyLocation::Right, + ShiftLeft => KeyLocation::Left, + ShiftRight => KeyLocation::Right, + + // According to https://developer.android.com/reference/android/view/KeyEvent#KEYCODE_NUM + Num => KeyLocation::Left, + + CtrlLeft => KeyLocation::Left, + CtrlRight => KeyLocation::Right, + MetaLeft => KeyLocation::Left, + MetaRight => KeyLocation::Right, + + NumLock => KeyLocation::Numpad, + Numpad0 => KeyLocation::Numpad, + Numpad1 => KeyLocation::Numpad, + Numpad2 => KeyLocation::Numpad, + Numpad3 => KeyLocation::Numpad, + Numpad4 => KeyLocation::Numpad, + Numpad5 => KeyLocation::Numpad, + Numpad6 => KeyLocation::Numpad, + Numpad7 => KeyLocation::Numpad, + Numpad8 => KeyLocation::Numpad, + Numpad9 => KeyLocation::Numpad, + NumpadDivide => KeyLocation::Numpad, + NumpadMultiply => KeyLocation::Numpad, + NumpadSubtract => KeyLocation::Numpad, + NumpadAdd => KeyLocation::Numpad, + NumpadDot => KeyLocation::Numpad, + NumpadComma => KeyLocation::Numpad, + NumpadEnter => KeyLocation::Numpad, + NumpadEquals => KeyLocation::Numpad, + NumpadLeftParen => KeyLocation::Numpad, + NumpadRightParen => KeyLocation::Numpad, + + _ => KeyLocation::Standard, + } +} diff --git a/vendor/winit/src/platform_impl/android/mod.rs b/vendor/winit/src/platform_impl/android/mod.rs new file mode 100644 index 00000000..c6a7416f --- /dev/null +++ b/vendor/winit/src/platform_impl/android/mod.rs @@ -0,0 +1,1141 @@ +use std::cell::Cell; +use std::collections::VecDeque; +use std::hash::Hash; +use std::marker::PhantomData; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{mpsc, Arc, Mutex}; +use std::time::{Duration, Instant}; + +use android_activity::input::{InputEvent, KeyAction, Keycode, MotionAction}; +use android_activity::{ + AndroidApp, AndroidAppWaker, ConfigurationRef, InputStatus, MainEvent, Rect, +}; +use tracing::{debug, trace, warn}; + +use crate::cursor::Cursor; +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error; +use crate::error::EventLoopError; +use crate::event::{self, Force, InnerSizeWriter, StartCause}; +use crate::event_loop::{self, ActiveEventLoop as RootAEL, ControlFlow, DeviceEvents}; +use crate::platform::pump_events::PumpStatus; +use crate::platform_impl::Fullscreen; +use crate::window::{ + self, CursorGrabMode, CustomCursor, CustomCursorSource, ImePurpose, ResizeDirection, Theme, + WindowButtons, WindowLevel, +}; + +mod keycodes; + +pub(crate) use crate::cursor::{ + NoCustomCursor as PlatformCustomCursor, NoCustomCursor as PlatformCustomCursorSource, +}; +pub(crate) use crate::icon::NoIcon as PlatformIcon; + +static HAS_FOCUS: AtomicBool = AtomicBool::new(true); + +/// Returns the minimum `Option`, taking into account that `None` +/// equates to an infinite timeout, not a zero timeout (so can't just use +/// `Option::min`) +fn min_timeout(a: Option, b: Option) -> Option { + a.map_or(b, |a_timeout| b.map_or(Some(a_timeout), |b_timeout| Some(a_timeout.min(b_timeout)))) +} + +struct PeekableReceiver { + recv: mpsc::Receiver, + first: Option, +} + +impl PeekableReceiver { + pub fn from_recv(recv: mpsc::Receiver) -> Self { + Self { recv, first: None } + } + + pub fn has_incoming(&mut self) -> bool { + if self.first.is_some() { + return true; + } + match self.recv.try_recv() { + Ok(v) => { + self.first = Some(v); + true + }, + Err(mpsc::TryRecvError::Empty) => false, + Err(mpsc::TryRecvError::Disconnected) => { + warn!("Channel was disconnected when checking incoming"); + false + }, + } + } + + pub fn try_recv(&mut self) -> Result { + if let Some(first) = self.first.take() { + return Ok(first); + } + self.recv.try_recv() + } +} + +#[derive(Clone)] +struct SharedFlagSetter { + flag: Arc, +} +impl SharedFlagSetter { + pub fn set(&self) -> bool { + self.flag.compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed).is_ok() + } +} + +struct SharedFlag { + flag: Arc, +} + +// Used for queuing redraws from arbitrary threads. We don't care how many +// times a redraw is requested (so don't actually need to queue any data, +// we just need to know at the start of a main loop iteration if a redraw +// was queued and be able to read and clear the state atomically) +impl SharedFlag { + pub fn new() -> Self { + Self { flag: Arc::new(AtomicBool::new(false)) } + } + + pub fn setter(&self) -> SharedFlagSetter { + SharedFlagSetter { flag: self.flag.clone() } + } + + pub fn get_and_reset(&self) -> bool { + self.flag.swap(false, std::sync::atomic::Ordering::AcqRel) + } +} + +#[derive(Clone)] +pub struct RedrawRequester { + flag: SharedFlagSetter, + waker: AndroidAppWaker, +} + +impl RedrawRequester { + fn new(flag: &SharedFlag, waker: AndroidAppWaker) -> Self { + RedrawRequester { flag: flag.setter(), waker } + } + + pub fn request_redraw(&self) { + if self.flag.set() { + // Only explicitly try to wake up the main loop when the flag + // value changes + self.waker.wake(); + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra {} + +pub struct EventLoop { + pub(crate) android_app: AndroidApp, + window_target: event_loop::ActiveEventLoop, + redraw_flag: SharedFlag, + user_events_sender: mpsc::Sender, + user_events_receiver: PeekableReceiver, // must wake looper whenever something gets sent + loop_running: bool, // Dispatched `NewEvents` + running: bool, + pending_redraw: bool, + cause: StartCause, + ignore_volume_keys: bool, + combining_accent: Option, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct PlatformSpecificEventLoopAttributes { + pub(crate) android_app: Option, + pub(crate) ignore_volume_keys: bool, +} + +impl Default for PlatformSpecificEventLoopAttributes { + fn default() -> Self { + Self { android_app: Default::default(), ignore_volume_keys: true } + } +} + +impl EventLoop { + pub(crate) fn new( + attributes: &PlatformSpecificEventLoopAttributes, + ) -> Result { + let (user_events_sender, user_events_receiver) = mpsc::channel(); + + let android_app = attributes.android_app.as_ref().expect( + "An `AndroidApp` as passed to android_main() is required to create an `EventLoop` on \ + Android", + ); + let redraw_flag = SharedFlag::new(); + + Ok(Self { + android_app: android_app.clone(), + window_target: event_loop::ActiveEventLoop { + p: ActiveEventLoop { + app: android_app.clone(), + control_flow: Cell::new(ControlFlow::default()), + exit: Cell::new(false), + redraw_requester: RedrawRequester::new( + &redraw_flag, + android_app.create_waker(), + ), + }, + _marker: PhantomData, + }, + redraw_flag, + user_events_sender, + user_events_receiver: PeekableReceiver::from_recv(user_events_receiver), + loop_running: false, + running: false, + pending_redraw: false, + cause: StartCause::Init, + ignore_volume_keys: attributes.ignore_volume_keys, + combining_accent: None, + }) + } + + fn single_iteration(&mut self, main_event: Option>, callback: &mut F) + where + F: FnMut(event::Event, &RootAEL), + { + trace!("Mainloop iteration"); + + let cause = self.cause; + let mut pending_redraw = self.pending_redraw; + let mut resized = false; + + callback(event::Event::NewEvents(cause), self.window_target()); + + if let Some(event) = main_event { + trace!("Handling main event {:?}", event); + + match event { + MainEvent::InitWindow { .. } => { + callback(event::Event::Resumed, self.window_target()); + }, + MainEvent::TerminateWindow { .. } => { + callback(event::Event::Suspended, self.window_target()); + }, + MainEvent::WindowResized { .. } => resized = true, + MainEvent::RedrawNeeded { .. } => pending_redraw = true, + MainEvent::ContentRectChanged { .. } => { + warn!("TODO: find a way to notify application of content rect change"); + }, + MainEvent::GainedFocus => { + HAS_FOCUS.store(true, Ordering::Relaxed); + callback( + event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::Focused(true), + }, + self.window_target(), + ); + }, + MainEvent::LostFocus => { + HAS_FOCUS.store(false, Ordering::Relaxed); + callback( + event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::Focused(false), + }, + self.window_target(), + ); + }, + MainEvent::ConfigChanged { .. } => { + let monitor = MonitorHandle::new(self.android_app.clone()); + let old_scale_factor = monitor.scale_factor(); + let scale_factor = monitor.scale_factor(); + if (scale_factor - old_scale_factor).abs() < f64::EPSILON { + let new_inner_size = Arc::new(Mutex::new( + MonitorHandle::new(self.android_app.clone()).size(), + )); + let event = event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::ScaleFactorChanged { + inner_size_writer: InnerSizeWriter::new(Arc::downgrade( + &new_inner_size, + )), + scale_factor, + }, + }; + callback(event, self.window_target()); + } + }, + MainEvent::LowMemory => { + callback(event::Event::MemoryWarning, self.window_target()); + }, + MainEvent::Start => { + // XXX: how to forward this state to applications? + warn!("TODO: forward onStart notification to application"); + }, + MainEvent::Resume { .. } => { + debug!("App Resumed - is running"); + self.running = true; + }, + MainEvent::SaveState { .. } => { + // XXX: how to forward this state to applications? + // XXX: also how do we expose state restoration to apps? + warn!("TODO: forward saveState notification to application"); + }, + MainEvent::Pause => { + debug!("App Paused - stopped running"); + self.running = false; + }, + MainEvent::Stop => { + // XXX: how to forward this state to applications? + warn!("TODO: forward onStop notification to application"); + }, + MainEvent::Destroy => { + // XXX: maybe exit mainloop to drop things before being + // killed by the OS? + warn!("TODO: forward onDestroy notification to application"); + }, + MainEvent::InsetsChanged { .. } => { + // XXX: how to forward this state to applications? + warn!("TODO: handle Android InsetsChanged notification"); + }, + unknown => { + trace!("Unknown MainEvent {unknown:?} (ignored)"); + }, + } + } else { + trace!("No main event to handle"); + } + + // temporarily decouple `android_app` from `self` so we aren't holding + // a borrow of `self` while iterating + let android_app = self.android_app.clone(); + + // Process input events + match android_app.input_events_iter() { + Ok(mut input_iter) => loop { + let read_event = + input_iter.next(|event| self.handle_input_event(&android_app, event, callback)); + + if !read_event { + break; + } + }, + Err(err) => { + tracing::warn!("Failed to get input events iterator: {err:?}"); + }, + } + + // Empty the user event buffer + { + while let Ok(event) = self.user_events_receiver.try_recv() { + callback(crate::event::Event::UserEvent(event), self.window_target()); + } + } + + if self.running { + if resized { + let size = if let Some(native_window) = self.android_app.native_window().as_ref() { + let width = native_window.width() as _; + let height = native_window.height() as _; + PhysicalSize::new(width, height) + } else { + PhysicalSize::new(0, 0) + }; + let event = event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::Resized(size), + }; + callback(event, self.window_target()); + } + + pending_redraw |= self.redraw_flag.get_and_reset(); + if pending_redraw { + pending_redraw = false; + let event = event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::RedrawRequested, + }; + callback(event, self.window_target()); + } + } + + // This is always the last event we dispatch before poll again + callback(event::Event::AboutToWait, self.window_target()); + + self.pending_redraw = pending_redraw; + } + + fn handle_input_event( + &mut self, + android_app: &AndroidApp, + event: &InputEvent<'_>, + callback: &mut F, + ) -> InputStatus + where + F: FnMut(event::Event, &RootAEL), + { + let mut input_status = InputStatus::Handled; + match event { + InputEvent::MotionEvent(motion_event) => { + let window_id = window::WindowId(WindowId); + let device_id = event::DeviceId(DeviceId(motion_event.device_id())); + + let phase = match motion_event.action() { + MotionAction::Down | MotionAction::PointerDown => { + Some(event::TouchPhase::Started) + }, + MotionAction::Up | MotionAction::PointerUp => Some(event::TouchPhase::Ended), + MotionAction::Move => Some(event::TouchPhase::Moved), + MotionAction::Cancel => Some(event::TouchPhase::Cancelled), + _ => { + None // TODO mouse events + }, + }; + if let Some(phase) = phase { + let pointers: Box>> = + match phase { + event::TouchPhase::Started | event::TouchPhase::Ended => { + Box::new(std::iter::once( + motion_event.pointer_at_index(motion_event.pointer_index()), + )) + }, + event::TouchPhase::Moved | event::TouchPhase::Cancelled => { + Box::new(motion_event.pointers()) + }, + }; + + for pointer in pointers { + let location = + PhysicalPosition { x: pointer.x() as _, y: pointer.y() as _ }; + trace!( + "Input event {device_id:?}, {phase:?}, loc={location:?}, \ + pointer={pointer:?}" + ); + let event = event::Event::WindowEvent { + window_id, + event: event::WindowEvent::Touch(event::Touch { + device_id, + phase, + location, + id: pointer.pointer_id() as u64, + force: Some(Force::Normalized(pointer.pressure() as f64)), + }), + }; + callback(event, self.window_target()); + } + } + }, + InputEvent::KeyEvent(key) => { + match key.key_code() { + // Flag keys related to volume as unhandled. While winit does not have a way for + // applications to configure what keys to flag as handled, + // this appears to be a good default until winit + // can be configured. + Keycode::VolumeUp | Keycode::VolumeDown | Keycode::VolumeMute + if self.ignore_volume_keys => + { + input_status = InputStatus::Unhandled + }, + keycode => { + let state = match key.action() { + KeyAction::Down => event::ElementState::Pressed, + KeyAction::Up => event::ElementState::Released, + _ => event::ElementState::Released, + }; + + let key_char = keycodes::character_map_and_combine_key( + android_app, + key, + &mut self.combining_accent, + ); + + let logical_key = keycodes::to_logical(key_char, keycode); + let text = if state == event::ElementState::Pressed { + logical_key.to_text().map(smol_str::SmolStr::new) + } else { + None + }; + + let event = event::Event::WindowEvent { + window_id: window::WindowId(WindowId), + event: event::WindowEvent::KeyboardInput { + device_id: event::DeviceId(DeviceId(key.device_id())), + event: event::KeyEvent { + state, + physical_key: keycodes::to_physical_key(keycode), + logical_key, + location: keycodes::to_location(keycode), + repeat: key.repeat_count() > 0, + text, + platform_specific: KeyEventExtra {}, + }, + is_synthetic: false, + }, + }; + callback(event, self.window_target()); + }, + } + }, + _ => { + warn!("Unknown android_activity input event {event:?}") + }, + } + + input_status + } + + pub fn run(mut self, event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(event::Event, &event_loop::ActiveEventLoop), + { + self.run_on_demand(event_handler) + } + + pub fn run_on_demand(&mut self, mut event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(event::Event, &event_loop::ActiveEventLoop), + { + loop { + match self.pump_events(None, &mut event_handler) { + PumpStatus::Exit(0) => { + break Ok(()); + }, + PumpStatus::Exit(code) => { + break Err(EventLoopError::ExitFailure(code)); + }, + _ => { + continue; + }, + } + } + } + + pub fn pump_events(&mut self, timeout: Option, mut callback: F) -> PumpStatus + where + F: FnMut(event::Event, &RootAEL), + { + if !self.loop_running { + self.loop_running = true; + + // Reset the internal state for the loop as we start running to + // ensure consistent behaviour in case the loop runs and exits more + // than once + self.pending_redraw = false; + self.cause = StartCause::Init; + + // run the initial loop iteration + self.single_iteration(None, &mut callback); + } + + // Consider the possibility that the `StartCause::Init` iteration could + // request to Exit + if !self.exiting() { + self.poll_events_with_timeout(timeout, &mut callback); + } + if self.exiting() { + self.loop_running = false; + + callback(event::Event::LoopExiting, self.window_target()); + + PumpStatus::Exit(0) + } else { + PumpStatus::Continue + } + } + + fn poll_events_with_timeout(&mut self, mut timeout: Option, mut callback: F) + where + F: FnMut(event::Event, &RootAEL), + { + let start = Instant::now(); + + self.pending_redraw |= self.redraw_flag.get_and_reset(); + + timeout = + if self.running && (self.pending_redraw || self.user_events_receiver.has_incoming()) { + // If we already have work to do then we don't want to block on the next poll + Some(Duration::ZERO) + } else { + let control_flow_timeout = match self.control_flow() { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Duration::ZERO), + ControlFlow::WaitUntil(wait_deadline) => { + Some(wait_deadline.saturating_duration_since(start)) + }, + }; + + min_timeout(control_flow_timeout, timeout) + }; + + let app = self.android_app.clone(); // Don't borrow self as part of poll expression + app.poll_events(timeout, |poll_event| { + let mut main_event = None; + + match poll_event { + android_activity::PollEvent::Wake => { + // In the X11 backend it's noted that too many false-positive wake ups + // would cause the event loop to run continuously. They handle this by + // re-checking for pending events (assuming they cover all + // valid reasons for a wake up). + // + // For now, user_events and redraw_requests are the only reasons to expect + // a wake up here so we can ignore the wake up if there are no events/requests. + // We also ignore wake ups while suspended. + self.pending_redraw |= self.redraw_flag.get_and_reset(); + if !self.running + || (!self.pending_redraw && !self.user_events_receiver.has_incoming()) + { + return; + } + }, + android_activity::PollEvent::Timeout => {}, + android_activity::PollEvent::Main(event) => { + main_event = Some(event); + }, + unknown_event => { + warn!("Unknown poll event {unknown_event:?} (ignored)"); + }, + } + + self.cause = match self.control_flow() { + ControlFlow::Poll => StartCause::Poll, + ControlFlow::Wait => StartCause::WaitCancelled { start, requested_resume: None }, + ControlFlow::WaitUntil(deadline) => { + if Instant::now() < deadline { + StartCause::WaitCancelled { start, requested_resume: Some(deadline) } + } else { + StartCause::ResumeTimeReached { start, requested_resume: deadline } + } + }, + }; + + self.single_iteration(main_event, &mut callback); + }); + } + + pub fn window_target(&self) -> &event_loop::ActiveEventLoop { + &self.window_target + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { + user_events_sender: self.user_events_sender.clone(), + waker: self.android_app.create_waker(), + } + } + + fn control_flow(&self) -> ControlFlow { + self.window_target.p.control_flow() + } + + fn exiting(&self) -> bool { + self.window_target.p.exiting() + } +} + +pub struct EventLoopProxy { + user_events_sender: mpsc::Sender, + waker: AndroidAppWaker, +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + EventLoopProxy { + user_events_sender: self.user_events_sender.clone(), + waker: self.waker.clone(), + } + } +} + +impl EventLoopProxy { + pub fn send_event(&self, event: T) -> Result<(), event_loop::EventLoopClosed> { + self.user_events_sender.send(event).map_err(|err| event_loop::EventLoopClosed(err.0))?; + self.waker.wake(); + Ok(()) + } +} + +pub struct ActiveEventLoop { + pub(crate) app: AndroidApp, + control_flow: Cell, + exit: Cell, + redraw_requester: RedrawRequester, +} + +impl ActiveEventLoop { + pub fn primary_monitor(&self) -> Option { + Some(MonitorHandle::new(self.app.clone())) + } + + pub fn create_custom_cursor(&self, source: CustomCursorSource) -> CustomCursor { + let _ = source.inner; + CustomCursor { inner: PlatformCustomCursor } + } + + pub fn available_monitors(&self) -> VecDeque { + let mut v = VecDeque::with_capacity(1); + v.push_back(MonitorHandle::new(self.app.clone())); + v + } + + #[inline] + pub fn listen_device_events(&self, _allowed: DeviceEvents) {} + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Android(rwh_05::AndroidDisplayHandle::empty()) + } + + #[inline] + pub fn system_theme(&self) -> Option { + None + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Android(rwh_06::AndroidDisplayHandle::new())) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.control_flow.set(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub(crate) fn exit(&self) { + self.exit.set(true) + } + + pub(crate) fn clear_exit(&self) { + self.exit.set(false) + } + + pub(crate) fn exiting(&self) -> bool { + self.exit.get() + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle + } +} + +#[derive(Clone)] +pub(crate) struct OwnedDisplayHandle; + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::AndroidDisplayHandle::empty().into() + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::AndroidDisplayHandle::new().into()) + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub(crate) struct WindowId; + +impl WindowId { + pub const fn dummy() -> Self { + WindowId + } +} + +impl From for u64 { + fn from(_: WindowId) -> Self { + 0 + } +} + +impl From for WindowId { + fn from(_: u64) -> Self { + Self + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct DeviceId(i32); + +impl DeviceId { + pub const fn dummy() -> Self { + DeviceId(0) + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct PlatformSpecificWindowAttributes; + +pub(crate) struct Window { + app: AndroidApp, + redraw_requester: RedrawRequester, +} + +impl Window { + pub(crate) fn new( + el: &ActiveEventLoop, + _window_attrs: window::WindowAttributes, + ) -> Result { + // FIXME this ignores requested window attributes + + Ok(Self { app: el.app.clone(), redraw_requester: el.redraw_requester.clone() }) + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) { + f(self) + } + + pub(crate) fn maybe_wait_on_main(&self, f: impl FnOnce(&Self) -> R + Send) -> R { + f(self) + } + + pub fn id(&self) -> WindowId { + WindowId + } + + pub fn primary_monitor(&self) -> Option { + Some(MonitorHandle::new(self.app.clone())) + } + + pub fn available_monitors(&self) -> VecDeque { + let mut v = VecDeque::with_capacity(1); + v.push_back(MonitorHandle::new(self.app.clone())); + v + } + + pub fn current_monitor(&self) -> Option { + Some(MonitorHandle::new(self.app.clone())) + } + + pub fn scale_factor(&self) -> f64 { + MonitorHandle::new(self.app.clone()).scale_factor() + } + + pub fn request_redraw(&self) { + self.redraw_requester.request_redraw() + } + + pub fn pre_present_notify(&self) {} + + pub fn inner_position(&self) -> Result, error::NotSupportedError> { + Err(error::NotSupportedError::new()) + } + + pub fn outer_position(&self) -> Result, error::NotSupportedError> { + Err(error::NotSupportedError::new()) + } + + pub fn set_outer_position(&self, _position: Position) { + // no effect + } + + pub fn inner_size(&self) -> PhysicalSize { + self.outer_size() + } + + pub fn request_inner_size(&self, _size: Size) -> Option> { + Some(self.inner_size()) + } + + pub fn outer_size(&self) -> PhysicalSize { + MonitorHandle::new(self.app.clone()).size() + } + + pub fn set_min_inner_size(&self, _: Option) {} + + pub fn set_max_inner_size(&self, _: Option) {} + + pub fn resize_increments(&self) -> Option> { + None + } + + pub fn set_resize_increments(&self, _increments: Option) {} + + pub fn set_title(&self, _title: &str) {} + + pub fn set_transparent(&self, _transparent: bool) {} + + pub fn set_blur(&self, _blur: bool) {} + + pub fn set_visible(&self, _visibility: bool) {} + + pub fn is_visible(&self) -> Option { + None + } + + pub fn set_resizable(&self, _resizeable: bool) {} + + pub fn is_resizable(&self) -> bool { + false + } + + pub fn set_enabled_buttons(&self, _buttons: WindowButtons) {} + + pub fn enabled_buttons(&self) -> WindowButtons { + WindowButtons::all() + } + + pub fn set_minimized(&self, _minimized: bool) {} + + pub fn is_minimized(&self) -> Option { + None + } + + pub fn set_maximized(&self, _maximized: bool) {} + + pub fn is_maximized(&self) -> bool { + false + } + + pub fn set_fullscreen(&self, _monitor: Option) { + warn!("Cannot set fullscreen on Android"); + } + + pub fn fullscreen(&self) -> Option { + None + } + + pub fn set_decorations(&self, _decorations: bool) {} + + pub fn is_decorated(&self) -> bool { + true + } + + pub fn set_window_level(&self, _level: WindowLevel) {} + + pub fn set_window_icon(&self, _window_icon: Option) {} + + pub fn set_ime_cursor_area(&self, _position: Position, _size: Size) {} + + pub fn set_ime_allowed(&self, allowed: bool) { + if allowed { + self.app.show_soft_input(true); + } else { + self.app.hide_soft_input(true); + } + } + + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + + pub fn focus_window(&self) {} + + pub fn request_user_attention(&self, _request_type: Option) {} + + pub fn set_cursor(&self, _: Cursor) {} + + pub fn set_cursor_position(&self, _: Position) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported(error::NotSupportedError::new())) + } + + pub fn set_cursor_grab(&self, _: CursorGrabMode) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported(error::NotSupportedError::new())) + } + + pub fn set_cursor_visible(&self, _: bool) {} + + pub fn drag_window(&self) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported(error::NotSupportedError::new())) + } + + pub fn drag_resize_window( + &self, + _direction: ResizeDirection, + ) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported(error::NotSupportedError::new())) + } + + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + + pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported(error::NotSupportedError::new())) + } + + #[cfg(feature = "rwh_04")] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + use rwh_04::HasRawWindowHandle; + + if let Some(native_window) = self.app.native_window().as_ref() { + native_window.raw_window_handle() + } else { + panic!( + "Cannot get the native window, it's null and will always be null before \ + Event::Resumed and after Event::Suspended. Make sure you only call this function \ + between those events." + ); + } + } + + #[cfg(feature = "rwh_05")] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + use rwh_05::HasRawWindowHandle; + + if let Some(native_window) = self.app.native_window().as_ref() { + native_window.raw_window_handle() + } else { + panic!( + "Cannot get the native window, it's null and will always be null before \ + Event::Resumed and after Event::Suspended. Make sure you only call this function \ + between those events." + ); + } + } + + #[cfg(feature = "rwh_05")] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Android(rwh_05::AndroidDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + // Allow the usage of HasRawWindowHandle inside this function + #[allow(deprecated)] + pub fn raw_window_handle_rwh_06(&self) -> Result { + use rwh_06::HasRawWindowHandle; + + if let Some(native_window) = self.app.native_window().as_ref() { + native_window.raw_window_handle() + } else { + tracing::error!( + "Cannot get the native window, it's null and will always be null before \ + Event::Resumed and after Event::Suspended. Make sure you only call this function \ + between those events." + ); + Err(rwh_06::HandleError::Unavailable) + } + } + + #[cfg(feature = "rwh_06")] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Android(rwh_06::AndroidDisplayHandle::new())) + } + + pub fn config(&self) -> ConfigurationRef { + self.app.config() + } + + pub fn content_rect(&self) -> Rect { + self.app.content_rect() + } + + pub fn set_theme(&self, _theme: Option) {} + + pub fn theme(&self) -> Option { + None + } + + pub fn set_content_protected(&self, _protected: bool) {} + + pub fn has_focus(&self) -> bool { + HAS_FOCUS.load(Ordering::Relaxed) + } + + pub fn title(&self) -> String { + String::new() + } + + pub fn reset_dead_keys(&self) {} +} + +#[derive(Default, Clone, Debug)] +pub struct OsError; + +use std::fmt::{self, Display, Formatter}; +impl Display for OsError { + fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> { + write!(fmt, "Android OS Error") + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct MonitorHandle { + app: AndroidApp, +} +impl PartialOrd for MonitorHandle { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for MonitorHandle { + fn cmp(&self, _other: &Self) -> std::cmp::Ordering { + std::cmp::Ordering::Equal + } +} + +impl MonitorHandle { + pub(crate) fn new(app: AndroidApp) -> Self { + Self { app } + } + + pub fn name(&self) -> Option { + Some("Android Device".to_owned()) + } + + pub fn size(&self) -> PhysicalSize { + if let Some(native_window) = self.app.native_window() { + PhysicalSize::new(native_window.width() as _, native_window.height() as _) + } else { + PhysicalSize::new(0, 0) + } + } + + pub fn position(&self) -> PhysicalPosition { + (0, 0).into() + } + + pub fn scale_factor(&self) -> f64 { + self.app.config().density().map(|dpi| dpi as f64 / 160.0).unwrap_or(1.0) + } + + pub fn refresh_rate_millihertz(&self) -> Option { + // FIXME no way to get real refresh rate for now. + None + } + + pub fn video_modes(&self) -> impl Iterator { + let size = self.size().into(); + // FIXME this is not the real refresh rate + // (it is guaranteed to support 32 bit color though) + std::iter::once(VideoModeHandle { + size, + bit_depth: 32, + refresh_rate_millihertz: 60000, + monitor: self.clone(), + }) + } +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct VideoModeHandle { + size: (u32, u32), + bit_depth: u16, + refresh_rate_millihertz: u32, + monitor: MonitorHandle, +} + +impl VideoModeHandle { + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone() + } +} diff --git a/vendor/winit/src/platform_impl/ios/app_state.rs b/vendor/winit/src/platform_impl/ios/app_state.rs new file mode 100644 index 00000000..e34bf43c --- /dev/null +++ b/vendor/winit/src/platform_impl/ios/app_state.rs @@ -0,0 +1,925 @@ +#![deny(unused_results)] + +use std::cell::{RefCell, RefMut}; +use std::collections::HashSet; +use std::os::raw::c_void; +use std::sync::{Arc, Mutex, OnceLock}; +use std::time::Instant; +use std::{fmt, mem, ptr}; + +use core_foundation::base::CFRelease; +use core_foundation::date::CFAbsoluteTimeGetCurrent; +use core_foundation::runloop::{ + kCFRunLoopCommonModes, CFRunLoopAddTimer, CFRunLoopGetMain, CFRunLoopRef, CFRunLoopTimerCreate, + CFRunLoopTimerInvalidate, CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate, +}; +use objc2::rc::Retained; +use objc2::runtime::AnyObject; +use objc2::{msg_send, sel}; +use objc2_foundation::{ + CGRect, CGSize, MainThreadMarker, NSInteger, NSObjectProtocol, NSOperatingSystemVersion, + NSProcessInfo, +}; +use objc2_ui_kit::{UIApplication, UICoordinateSpace, UIView, UIWindow}; + +use super::window::WinitUIWindow; +use crate::dpi::PhysicalSize; +use crate::event::{Event, InnerSizeWriter, StartCause, WindowEvent}; +use crate::event_loop::{ActiveEventLoop as RootActiveEventLoop, ControlFlow}; +use crate::window::WindowId as RootWindowId; + +macro_rules! bug { + ($($msg:tt)*) => { + panic!("winit iOS bug, file an issue: {}", format!($($msg)*)) + }; +} + +macro_rules! bug_assert { + ($test:expr, $($msg:tt)*) => { + assert!($test, "winit iOS bug, file an issue: {}", format!($($msg)*)) + }; +} + +#[derive(Debug)] +pub(crate) struct HandlePendingUserEvents; + +pub(crate) struct EventLoopHandler { + #[allow(clippy::type_complexity)] + pub(crate) handler: Box, &RootActiveEventLoop)>, + pub(crate) event_loop: RootActiveEventLoop, +} + +impl fmt::Debug for EventLoopHandler { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("EventLoopHandler") + .field("handler", &"...") + .field("event_loop", &self.event_loop) + .finish() + } +} + +impl EventLoopHandler { + fn handle_event(&mut self, event: Event) { + (self.handler)(event, &self.event_loop) + } +} + +#[derive(Debug)] +pub(crate) enum EventWrapper { + StaticEvent(Event), + ScaleFactorChanged(ScaleFactorChanged), +} + +#[derive(Debug)] +pub struct ScaleFactorChanged { + pub(super) window: Retained, + pub(super) suggested_size: PhysicalSize, + pub(super) scale_factor: f64, +} + +enum UserCallbackTransitionResult<'a> { + Success { + handler: EventLoopHandler, + active_control_flow: ControlFlow, + processing_redraws: bool, + }, + ReentrancyPrevented { + queued_events: &'a mut Vec, + }, +} + +impl Event { + fn is_redraw(&self) -> bool { + matches!(self, Event::WindowEvent { event: WindowEvent::RedrawRequested, .. }) + } +} + +// this is the state machine for the app lifecycle +#[derive(Debug)] +#[must_use = "dropping `AppStateImpl` without inspecting it is probably a bug"] +enum AppStateImpl { + NotLaunched { + queued_windows: Vec>, + queued_events: Vec, + queued_gpu_redraws: HashSet>, + }, + Launching { + queued_windows: Vec>, + queued_events: Vec, + queued_handler: EventLoopHandler, + queued_gpu_redraws: HashSet>, + }, + ProcessingEvents { + handler: EventLoopHandler, + queued_gpu_redraws: HashSet>, + active_control_flow: ControlFlow, + }, + // special state to deal with reentrancy and prevent mutable aliasing. + InUserCallback { + queued_events: Vec, + queued_gpu_redraws: HashSet>, + }, + ProcessingRedraws { + handler: EventLoopHandler, + active_control_flow: ControlFlow, + }, + Waiting { + waiting_handler: EventLoopHandler, + start: Instant, + }, + PollFinished { + waiting_handler: EventLoopHandler, + }, + Terminated, +} + +pub(crate) struct AppState { + // This should never be `None`, except for briefly during a state transition. + app_state: Option, + control_flow: ControlFlow, + waker: EventLoopWaker, +} + +impl AppState { + pub(crate) fn get_mut(_mtm: MainThreadMarker) -> RefMut<'static, AppState> { + // basically everything in UIKit requires the main thread, so it's pointless to use the + // std::sync APIs. + // must be mut because plain `static` requires `Sync` + static mut APP_STATE: RefCell> = RefCell::new(None); + + #[allow(unknown_lints)] // New lint below + #[allow(static_mut_refs)] // TODO: Use `MainThreadBound` instead. + let mut guard = unsafe { APP_STATE.borrow_mut() }; + if guard.is_none() { + #[inline(never)] + #[cold] + fn init_guard(guard: &mut RefMut<'static, Option>) { + let waker = EventLoopWaker::new(unsafe { CFRunLoopGetMain() }); + **guard = Some(AppState { + app_state: Some(AppStateImpl::NotLaunched { + queued_windows: Vec::new(), + queued_events: Vec::new(), + queued_gpu_redraws: HashSet::new(), + }), + control_flow: ControlFlow::default(), + waker, + }); + } + init_guard(&mut guard); + } + RefMut::map(guard, |state| state.as_mut().unwrap()) + } + + fn state(&self) -> &AppStateImpl { + match &self.app_state { + Some(ref state) => state, + None => bug!("`AppState` previously failed a state transition"), + } + } + + fn state_mut(&mut self) -> &mut AppStateImpl { + match &mut self.app_state { + Some(ref mut state) => state, + None => bug!("`AppState` previously failed a state transition"), + } + } + + fn take_state(&mut self) -> AppStateImpl { + match self.app_state.take() { + Some(state) => state, + None => bug!("`AppState` previously failed a state transition"), + } + } + + fn set_state(&mut self, new_state: AppStateImpl) { + bug_assert!( + self.app_state.is_none(), + "attempted to set an `AppState` without calling `take_state` first {:?}", + self.app_state + ); + self.app_state = Some(new_state) + } + + fn replace_state(&mut self, new_state: AppStateImpl) -> AppStateImpl { + match &mut self.app_state { + Some(ref mut state) => mem::replace(state, new_state), + None => bug!("`AppState` previously failed a state transition"), + } + } + + fn has_launched(&self) -> bool { + !matches!(self.state(), AppStateImpl::NotLaunched { .. } | AppStateImpl::Launching { .. }) + } + + fn has_terminated(&self) -> bool { + matches!(self.state(), AppStateImpl::Terminated) + } + + fn will_launch_transition(&mut self, queued_handler: EventLoopHandler) { + let (queued_windows, queued_events, queued_gpu_redraws) = match self.take_state() { + AppStateImpl::NotLaunched { queued_windows, queued_events, queued_gpu_redraws } => { + (queued_windows, queued_events, queued_gpu_redraws) + }, + s => bug!("unexpected state {:?}", s), + }; + self.set_state(AppStateImpl::Launching { + queued_windows, + queued_events, + queued_handler, + queued_gpu_redraws, + }); + } + + fn did_finish_launching_transition( + &mut self, + ) -> (Vec>, Vec) { + let (windows, events, handler, queued_gpu_redraws) = match self.take_state() { + AppStateImpl::Launching { + queued_windows, + queued_events, + queued_handler, + queued_gpu_redraws, + } => (queued_windows, queued_events, queued_handler, queued_gpu_redraws), + s => bug!("unexpected state {:?}", s), + }; + self.set_state(AppStateImpl::ProcessingEvents { + handler, + active_control_flow: self.control_flow, + queued_gpu_redraws, + }); + (windows, events) + } + + fn wakeup_transition(&mut self) -> Option { + // before `AppState::did_finish_launching` is called, pretend there is no running + // event loop. + if !self.has_launched() || self.has_terminated() { + return None; + } + + let (handler, event) = match (self.control_flow, self.take_state()) { + (ControlFlow::Poll, AppStateImpl::PollFinished { waiting_handler }) => { + (waiting_handler, EventWrapper::StaticEvent(Event::NewEvents(StartCause::Poll))) + }, + (ControlFlow::Wait, AppStateImpl::Waiting { waiting_handler, start }) => ( + waiting_handler, + EventWrapper::StaticEvent(Event::NewEvents(StartCause::WaitCancelled { + start, + requested_resume: None, + })), + ), + ( + ControlFlow::WaitUntil(requested_resume), + AppStateImpl::Waiting { waiting_handler, start }, + ) => { + let event = if Instant::now() >= requested_resume { + EventWrapper::StaticEvent(Event::NewEvents(StartCause::ResumeTimeReached { + start, + requested_resume, + })) + } else { + EventWrapper::StaticEvent(Event::NewEvents(StartCause::WaitCancelled { + start, + requested_resume: Some(requested_resume), + })) + }; + (waiting_handler, event) + }, + s => bug!("`EventHandler` unexpectedly woke up {:?}", s), + }; + + self.set_state(AppStateImpl::ProcessingEvents { + handler, + queued_gpu_redraws: Default::default(), + active_control_flow: self.control_flow, + }); + Some(event) + } + + fn try_user_callback_transition(&mut self) -> UserCallbackTransitionResult<'_> { + // If we're not able to process an event due to recursion or `Init` not having been sent out + // yet, then queue the events up. + match self.state_mut() { + &mut AppStateImpl::Launching { ref mut queued_events, .. } + | &mut AppStateImpl::NotLaunched { ref mut queued_events, .. } + | &mut AppStateImpl::InUserCallback { ref mut queued_events, .. } => { + // A lifetime cast: early returns are not currently handled well with NLL, but + // polonius handles them well. This transmute is a safe workaround. + return unsafe { + mem::transmute::< + UserCallbackTransitionResult<'_>, + UserCallbackTransitionResult<'_>, + >(UserCallbackTransitionResult::ReentrancyPrevented { + queued_events, + }) + }; + }, + + &mut AppStateImpl::ProcessingEvents { .. } + | &mut AppStateImpl::ProcessingRedraws { .. } => {}, + + s @ &mut AppStateImpl::PollFinished { .. } + | s @ &mut AppStateImpl::Waiting { .. } + | s @ &mut AppStateImpl::Terminated => { + bug!("unexpected attempted to process an event {:?}", s) + }, + } + + let (handler, queued_gpu_redraws, active_control_flow, processing_redraws) = match self + .take_state() + { + AppStateImpl::Launching { .. } + | AppStateImpl::NotLaunched { .. } + | AppStateImpl::InUserCallback { .. } => unreachable!(), + AppStateImpl::ProcessingEvents { handler, queued_gpu_redraws, active_control_flow } => { + (handler, queued_gpu_redraws, active_control_flow, false) + }, + AppStateImpl::ProcessingRedraws { handler, active_control_flow } => { + (handler, Default::default(), active_control_flow, true) + }, + AppStateImpl::PollFinished { .. } + | AppStateImpl::Waiting { .. } + | AppStateImpl::Terminated => unreachable!(), + }; + self.set_state(AppStateImpl::InUserCallback { + queued_events: Vec::new(), + queued_gpu_redraws, + }); + UserCallbackTransitionResult::Success { handler, active_control_flow, processing_redraws } + } + + fn main_events_cleared_transition(&mut self) -> HashSet> { + let (handler, queued_gpu_redraws, active_control_flow) = match self.take_state() { + AppStateImpl::ProcessingEvents { handler, queued_gpu_redraws, active_control_flow } => { + (handler, queued_gpu_redraws, active_control_flow) + }, + s => bug!("unexpected state {:?}", s), + }; + self.set_state(AppStateImpl::ProcessingRedraws { handler, active_control_flow }); + queued_gpu_redraws + } + + fn events_cleared_transition(&mut self) { + if !self.has_launched() || self.has_terminated() { + return; + } + let (waiting_handler, old) = match self.take_state() { + AppStateImpl::ProcessingRedraws { handler, active_control_flow } => { + (handler, active_control_flow) + }, + s => bug!("unexpected state {:?}", s), + }; + + let new = self.control_flow; + match (old, new) { + (ControlFlow::Wait, ControlFlow::Wait) => { + let start = Instant::now(); + self.set_state(AppStateImpl::Waiting { waiting_handler, start }); + self.waker.stop() + }, + (ControlFlow::WaitUntil(old_instant), ControlFlow::WaitUntil(new_instant)) + if old_instant == new_instant => + { + let start = Instant::now(); + self.set_state(AppStateImpl::Waiting { waiting_handler, start }); + }, + (_, ControlFlow::Wait) => { + let start = Instant::now(); + self.set_state(AppStateImpl::Waiting { waiting_handler, start }); + self.waker.stop() + }, + (_, ControlFlow::WaitUntil(new_instant)) => { + let start = Instant::now(); + self.set_state(AppStateImpl::Waiting { waiting_handler, start }); + self.waker.start_at(new_instant) + }, + // Unlike on macOS, handle Poll to Poll transition here to call the waker + (_, ControlFlow::Poll) => { + self.set_state(AppStateImpl::PollFinished { waiting_handler }); + self.waker.start() + }, + } + } + + fn terminated_transition(&mut self) -> EventLoopHandler { + match self.replace_state(AppStateImpl::Terminated) { + AppStateImpl::ProcessingEvents { handler, .. } => handler, + s => bug!("`LoopExiting` happened while not processing events {:?}", s), + } + } + + pub(crate) fn set_control_flow(&mut self, control_flow: ControlFlow) { + self.control_flow = control_flow; + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.control_flow + } +} + +pub(crate) fn set_key_window(mtm: MainThreadMarker, window: &Retained) { + let mut this = AppState::get_mut(mtm); + match this.state_mut() { + &mut AppStateImpl::NotLaunched { ref mut queued_windows, .. } => { + return queued_windows.push(window.clone()) + }, + &mut AppStateImpl::ProcessingEvents { .. } + | &mut AppStateImpl::InUserCallback { .. } + | &mut AppStateImpl::ProcessingRedraws { .. } => {}, + s @ &mut AppStateImpl::Launching { .. } + | s @ &mut AppStateImpl::Waiting { .. } + | s @ &mut AppStateImpl::PollFinished { .. } => bug!("unexpected state {:?}", s), + &mut AppStateImpl::Terminated => { + panic!("Attempt to create a `Window` after the app has terminated") + }, + } + drop(this); + window.makeKeyAndVisible(); +} + +pub(crate) fn queue_gl_or_metal_redraw(mtm: MainThreadMarker, window: Retained) { + let mut this = AppState::get_mut(mtm); + match this.state_mut() { + &mut AppStateImpl::NotLaunched { ref mut queued_gpu_redraws, .. } + | &mut AppStateImpl::Launching { ref mut queued_gpu_redraws, .. } + | &mut AppStateImpl::ProcessingEvents { ref mut queued_gpu_redraws, .. } + | &mut AppStateImpl::InUserCallback { ref mut queued_gpu_redraws, .. } => { + let _ = queued_gpu_redraws.insert(window); + }, + s @ &mut AppStateImpl::ProcessingRedraws { .. } + | s @ &mut AppStateImpl::Waiting { .. } + | s @ &mut AppStateImpl::PollFinished { .. } => bug!("unexpected state {:?}", s), + &mut AppStateImpl::Terminated => { + panic!("Attempt to create a `Window` after the app has terminated") + }, + } +} + +pub(crate) fn will_launch(mtm: MainThreadMarker, queued_handler: EventLoopHandler) { + AppState::get_mut(mtm).will_launch_transition(queued_handler) +} + +pub fn did_finish_launching(mtm: MainThreadMarker) { + let mut this = AppState::get_mut(mtm); + let windows = match this.state_mut() { + AppStateImpl::Launching { queued_windows, .. } => mem::take(queued_windows), + s => bug!("unexpected state {:?}", s), + }; + + this.waker.start(); + + // have to drop RefMut because the window setup code below can trigger new events + drop(this); + + for window in windows { + // Do a little screen dance here to account for windows being created before + // `UIApplicationMain` is called. This fixes visual issues such as being + // offcenter and sized incorrectly. Additionally, to fix orientation issues, we + // gotta reset the `rootViewController`. + // + // relevant iOS log: + // ``` + // [ApplicationLifecycle] Windows were created before application initialization + // completed. This may result in incorrect visual appearance. + // ``` + let screen = window.screen(); + let _: () = unsafe { msg_send![&window, setScreen: ptr::null::()] }; + window.setScreen(&screen); + + let controller = window.rootViewController(); + window.setRootViewController(None); + window.setRootViewController(controller.as_deref()); + + window.makeKeyAndVisible(); + } + + let (windows, events) = AppState::get_mut(mtm).did_finish_launching_transition(); + + let events = std::iter::once(EventWrapper::StaticEvent(Event::NewEvents(StartCause::Init))) + .chain(events); + handle_nonuser_events(mtm, events); + + // the above window dance hack, could possibly trigger new windows to be created. + // we can just set those windows up normally, as they were created after didFinishLaunching + for window in windows { + window.makeKeyAndVisible(); + } +} + +// AppState::did_finish_launching handles the special transition `Init` +pub fn handle_wakeup_transition(mtm: MainThreadMarker) { + let mut this = AppState::get_mut(mtm); + let wakeup_event = match this.wakeup_transition() { + None => return, + Some(wakeup_event) => wakeup_event, + }; + drop(this); + + handle_nonuser_event(mtm, wakeup_event) +} + +pub(crate) fn handle_nonuser_event(mtm: MainThreadMarker, event: EventWrapper) { + handle_nonuser_events(mtm, std::iter::once(event)) +} + +pub(crate) fn handle_nonuser_events>( + mtm: MainThreadMarker, + events: I, +) { + let mut this = AppState::get_mut(mtm); + if this.has_terminated() { + return; + } + + let (mut handler, active_control_flow, processing_redraws) = + match this.try_user_callback_transition() { + UserCallbackTransitionResult::ReentrancyPrevented { queued_events } => { + queued_events.extend(events); + return; + }, + UserCallbackTransitionResult::Success { + handler, + active_control_flow, + processing_redraws, + } => (handler, active_control_flow, processing_redraws), + }; + drop(this); + + for wrapper in events { + match wrapper { + EventWrapper::StaticEvent(event) => { + if !processing_redraws && event.is_redraw() { + tracing::info!("processing `RedrawRequested` during the main event loop"); + } else if processing_redraws && !event.is_redraw() { + tracing::warn!( + "processing non `RedrawRequested` event after the main event loop: {:#?}", + event + ); + } + handler.handle_event(event) + }, + EventWrapper::ScaleFactorChanged(event) => handle_hidpi_proxy(&mut handler, event), + } + } + + loop { + let mut this = AppState::get_mut(mtm); + let queued_events = match this.state_mut() { + &mut AppStateImpl::InUserCallback { ref mut queued_events, queued_gpu_redraws: _ } => { + mem::take(queued_events) + }, + s => bug!("unexpected state {:?}", s), + }; + if queued_events.is_empty() { + let queued_gpu_redraws = match this.take_state() { + AppStateImpl::InUserCallback { queued_events: _, queued_gpu_redraws } => { + queued_gpu_redraws + }, + _ => unreachable!(), + }; + this.app_state = Some(if processing_redraws { + bug_assert!( + queued_gpu_redraws.is_empty(), + "redraw queued while processing redraws" + ); + AppStateImpl::ProcessingRedraws { handler, active_control_flow } + } else { + AppStateImpl::ProcessingEvents { handler, queued_gpu_redraws, active_control_flow } + }); + break; + } + drop(this); + + for wrapper in queued_events { + match wrapper { + EventWrapper::StaticEvent(event) => { + if !processing_redraws && event.is_redraw() { + tracing::info!("processing `RedrawRequested` during the main event loop"); + } else if processing_redraws && !event.is_redraw() { + tracing::warn!( + "processing non-`RedrawRequested` event after the main event loop: \ + {:#?}", + event + ); + } + handler.handle_event(event) + }, + EventWrapper::ScaleFactorChanged(event) => handle_hidpi_proxy(&mut handler, event), + } + } + } +} + +fn handle_user_events(mtm: MainThreadMarker) { + let mut this = AppState::get_mut(mtm); + let (mut handler, active_control_flow, processing_redraws) = + match this.try_user_callback_transition() { + UserCallbackTransitionResult::ReentrancyPrevented { .. } => { + bug!("unexpected attempted to process an event") + }, + UserCallbackTransitionResult::Success { + handler, + active_control_flow, + processing_redraws, + } => (handler, active_control_flow, processing_redraws), + }; + if processing_redraws { + bug!("user events attempted to be sent out while `ProcessingRedraws`"); + } + drop(this); + + handler.handle_event(Event::UserEvent(HandlePendingUserEvents)); + + loop { + let mut this = AppState::get_mut(mtm); + let queued_events = match this.state_mut() { + &mut AppStateImpl::InUserCallback { ref mut queued_events, queued_gpu_redraws: _ } => { + mem::take(queued_events) + }, + s => bug!("unexpected state {:?}", s), + }; + if queued_events.is_empty() { + let queued_gpu_redraws = match this.take_state() { + AppStateImpl::InUserCallback { queued_events: _, queued_gpu_redraws } => { + queued_gpu_redraws + }, + _ => unreachable!(), + }; + this.app_state = Some(AppStateImpl::ProcessingEvents { + handler, + queued_gpu_redraws, + active_control_flow, + }); + break; + } + drop(this); + + for wrapper in queued_events { + match wrapper { + EventWrapper::StaticEvent(event) => handler.handle_event(event), + EventWrapper::ScaleFactorChanged(event) => handle_hidpi_proxy(&mut handler, event), + } + } + + handler.handle_event(Event::UserEvent(HandlePendingUserEvents)); + } +} + +pub(crate) fn send_occluded_event_for_all_windows(application: &UIApplication, occluded: bool) { + let mtm = MainThreadMarker::from(application); + + let mut events = Vec::new(); + #[allow(deprecated)] + for window in application.windows().iter() { + if window.is_kind_of::() { + // SAFETY: We just checked that the window is a `winit` window + let window = unsafe { + let ptr: *const UIWindow = window; + let ptr: *const WinitUIWindow = ptr.cast(); + &*ptr + }; + events.push(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::Occluded(occluded), + })); + } + } + handle_nonuser_events(mtm, events); +} + +pub fn handle_main_events_cleared(mtm: MainThreadMarker) { + let mut this = AppState::get_mut(mtm); + if !this.has_launched() || this.has_terminated() { + return; + } + match this.state_mut() { + AppStateImpl::ProcessingEvents { .. } => {}, + _ => bug!("`ProcessingRedraws` happened unexpectedly"), + }; + drop(this); + + handle_user_events(mtm); + + let mut this = AppState::get_mut(mtm); + let redraw_events: Vec = this + .main_events_cleared_transition() + .into_iter() + .map(|window| { + EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::RedrawRequested, + }) + }) + .collect(); + drop(this); + + handle_nonuser_events(mtm, redraw_events); + handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::AboutToWait)); +} + +pub fn handle_events_cleared(mtm: MainThreadMarker) { + AppState::get_mut(mtm).events_cleared_transition(); +} + +pub(crate) fn terminated(application: &UIApplication) { + let mtm = MainThreadMarker::from(application); + + let mut events = Vec::new(); + #[allow(deprecated)] + for window in application.windows().iter() { + if window.is_kind_of::() { + // SAFETY: We just checked that the window is a `winit` window + let window = unsafe { + let ptr: *const UIWindow = window; + let ptr: *const WinitUIWindow = ptr.cast(); + &*ptr + }; + events.push(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::Destroyed, + })); + } + } + handle_nonuser_events(mtm, events); + + let mut this = AppState::get_mut(mtm); + let mut handler = this.terminated_transition(); + drop(this); + + handler.handle_event(Event::LoopExiting) +} + +fn handle_hidpi_proxy(handler: &mut EventLoopHandler, event: ScaleFactorChanged) { + let ScaleFactorChanged { suggested_size, scale_factor, window } = event; + let new_inner_size = Arc::new(Mutex::new(suggested_size)); + let event = Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::ScaleFactorChanged { + scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&new_inner_size)), + }, + }; + handler.handle_event(event); + let (view, screen_frame) = get_view_and_screen_frame(&window); + let physical_size = *new_inner_size.lock().unwrap(); + drop(new_inner_size); + let logical_size = physical_size.to_logical(scale_factor); + let size = CGSize::new(logical_size.width, logical_size.height); + let new_frame: CGRect = CGRect::new(screen_frame.origin, size); + view.setFrame(new_frame); +} + +fn get_view_and_screen_frame(window: &WinitUIWindow) -> (Retained, CGRect) { + let view_controller = window.rootViewController().unwrap(); + let view = view_controller.view().unwrap(); + let bounds = window.bounds(); + let screen = window.screen(); + let screen_space = screen.coordinateSpace(); + let screen_frame = window.convertRect_toCoordinateSpace(bounds, &screen_space); + (view, screen_frame) +} + +struct EventLoopWaker { + timer: CFRunLoopTimerRef, +} + +impl Drop for EventLoopWaker { + fn drop(&mut self) { + unsafe { + CFRunLoopTimerInvalidate(self.timer); + CFRelease(self.timer as _); + } + } +} + +impl EventLoopWaker { + fn new(rl: CFRunLoopRef) -> EventLoopWaker { + extern "C" fn wakeup_main_loop(_timer: CFRunLoopTimerRef, _info: *mut c_void) {} + unsafe { + // Create a timer with a 0.1µs interval (1ns does not work) to mimic polling. + // It is initially setup with a first fire time really far into the + // future, but that gets changed to fire immediately in did_finish_launching + let timer = CFRunLoopTimerCreate( + ptr::null_mut(), + f64::MAX, + 0.000_000_1, + 0, + 0, + wakeup_main_loop, + ptr::null_mut(), + ); + CFRunLoopAddTimer(rl, timer, kCFRunLoopCommonModes); + + EventLoopWaker { timer } + } + } + + fn stop(&mut self) { + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MAX) } + } + + fn start(&mut self) { + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MIN) } + } + + fn start_at(&mut self, instant: Instant) { + let now = Instant::now(); + if now >= instant { + self.start(); + } else { + unsafe { + let current = CFAbsoluteTimeGetCurrent(); + let duration = instant - now; + let fsecs = + duration.subsec_nanos() as f64 / 1_000_000_000.0 + duration.as_secs() as f64; + CFRunLoopTimerSetNextFireDate(self.timer, current + fsecs) + } + } + } +} + +macro_rules! os_capabilities { + ( + $( + $(#[$attr:meta])* + $error_name:ident: $objc_call:literal, + $name:ident: $major:literal-$minor:literal + ),* + $(,)* + ) => { + #[derive(Clone, Debug)] + pub struct OSCapabilities { + $( + pub $name: bool, + )* + + os_version: NSOperatingSystemVersion, + } + + impl OSCapabilities { + fn from_os_version(os_version: NSOperatingSystemVersion) -> Self { + $(let $name = meets_requirements(os_version, $major, $minor);)* + Self { $($name,)* os_version, } + } + } + + impl OSCapabilities {$( + $(#[$attr])* + pub fn $error_name(&self, extra_msg: &str) { + tracing::warn!( + concat!("`", $objc_call, "` requires iOS {}.{}+. This device is running iOS {}.{}.{}. {}"), + $major, $minor, self.os_version.majorVersion, self.os_version.minorVersion, self.os_version.patchVersion, + extra_msg + ) + } + )*} + }; +} + +os_capabilities! { + /// + #[allow(unused)] // error message unused + safe_area_err_msg: "-[UIView safeAreaInsets]", + safe_area: 11-0, + /// + home_indicator_hidden_err_msg: "-[UIViewController setNeedsUpdateOfHomeIndicatorAutoHidden]", + home_indicator_hidden: 11-0, + /// + defer_system_gestures_err_msg: "-[UIViewController setNeedsUpdateOfScreenEdgesDeferringSystem]", + defer_system_gestures: 11-0, + /// + maximum_frames_per_second_err_msg: "-[UIScreen maximumFramesPerSecond]", + maximum_frames_per_second: 10-3, + /// + #[allow(unused)] // error message unused + force_touch_err_msg: "-[UITouch force]", + force_touch: 9-0, +} + +fn meets_requirements( + version: NSOperatingSystemVersion, + required_major: NSInteger, + required_minor: NSInteger, +) -> bool { + (version.majorVersion, version.minorVersion) >= (required_major, required_minor) +} + +fn get_version() -> NSOperatingSystemVersion { + let process_info = NSProcessInfo::processInfo(); + let atleast_ios_8 = process_info.respondsToSelector(sel!(operatingSystemVersion)); + // Winit requires atleast iOS 8 because no one has put the time into supporting earlier os + // versions. Older iOS versions are increasingly difficult to test. For example, Xcode 11 does + // not support debugging on devices with an iOS version of less than 8. Another example, in + // order to use an iOS simulator older than iOS 8, you must download an older version of Xcode + // (<9), and at least Xcode 7 has been tested to not even run on macOS 10.15 - Xcode 8 might? + // + // The minimum required iOS version is likely to grow in the future. + assert!(atleast_ios_8, "`winit` requires iOS version 8 or greater"); + process_info.operatingSystemVersion() +} + +pub fn os_capabilities() -> OSCapabilities { + // Cache the version lookup for efficiency + static OS_CAPABILITIES: OnceLock = OnceLock::new(); + OS_CAPABILITIES.get_or_init(|| OSCapabilities::from_os_version(get_version())).clone() +} diff --git a/vendor/winit/src/platform_impl/ios/event_loop.rs b/vendor/winit/src/platform_impl/ios/event_loop.rs new file mode 100644 index 00000000..a093b7b5 --- /dev/null +++ b/vendor/winit/src/platform_impl/ios/event_loop.rs @@ -0,0 +1,494 @@ +use std::collections::VecDeque; +use std::ffi::{c_char, c_int, c_void}; +use std::marker::PhantomData; +use std::ptr::{self, NonNull}; +use std::sync::mpsc::{self, Receiver, Sender}; + +use core_foundation::base::{CFIndex, CFRelease}; +use core_foundation::runloop::{ + kCFRunLoopAfterWaiting, kCFRunLoopBeforeWaiting, kCFRunLoopCommonModes, kCFRunLoopDefaultMode, + kCFRunLoopExit, CFRunLoopActivity, CFRunLoopAddObserver, CFRunLoopAddSource, CFRunLoopGetMain, + CFRunLoopObserverCreate, CFRunLoopObserverRef, CFRunLoopSourceContext, CFRunLoopSourceCreate, + CFRunLoopSourceInvalidate, CFRunLoopSourceRef, CFRunLoopSourceSignal, CFRunLoopWakeUp, +}; +use objc2::rc::Retained; +use objc2::{msg_send_id, ClassType}; +use objc2_foundation::{MainThreadMarker, NSNotificationCenter, NSObject}; +use objc2_ui_kit::{ + UIApplication, UIApplicationDidBecomeActiveNotification, + UIApplicationDidEnterBackgroundNotification, UIApplicationDidFinishLaunchingNotification, + UIApplicationDidReceiveMemoryWarningNotification, UIApplicationMain, + UIApplicationWillEnterForegroundNotification, UIApplicationWillResignActiveNotification, + UIApplicationWillTerminateNotification, UIDevice, UIScreen, UIUserInterfaceIdiom, +}; + +use crate::error::EventLoopError; +use crate::event::Event; +use crate::event_loop::{ + ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, EventLoopClosed, +}; +use crate::platform::ios::Idiom; +use crate::platform_impl::ios::app_state::{EventLoopHandler, HandlePendingUserEvents}; +use crate::window::{CustomCursor, CustomCursorSource, Theme}; + +use super::app_state::{send_occluded_event_for_all_windows, AppState, EventWrapper}; +use super::notification_center::create_observer; +use super::{app_state, monitor, MonitorHandle}; + +#[derive(Debug)] +pub struct ActiveEventLoop { + pub(super) mtm: MainThreadMarker, +} + +impl ActiveEventLoop { + pub fn create_custom_cursor(&self, source: CustomCursorSource) -> CustomCursor { + let _ = source.inner; + CustomCursor { inner: super::PlatformCustomCursor } + } + + pub fn available_monitors(&self) -> VecDeque { + monitor::uiscreens(self.mtm) + } + + pub fn primary_monitor(&self) -> Option { + #[allow(deprecated)] + Some(MonitorHandle::new(UIScreen::mainScreen(self.mtm))) + } + + #[inline] + pub fn listen_device_events(&self, _allowed: DeviceEvents) {} + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::UiKit(rwh_05::UiKitDisplayHandle::empty()) + } + + #[inline] + pub fn system_theme(&self) -> Option { + None + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::UiKit(rwh_06::UiKitDisplayHandle::new())) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + AppState::get_mut(self.mtm).set_control_flow(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + AppState::get_mut(self.mtm).control_flow() + } + + pub(crate) fn exit(&self) { + // https://developer.apple.com/library/archive/qa/qa1561/_index.html + // it is not possible to quit an iOS app gracefully and programmatically + tracing::warn!("`ControlFlow::Exit` ignored on iOS"); + } + + pub(crate) fn exiting(&self) -> bool { + false + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle + } +} + +#[derive(Clone)] +pub(crate) struct OwnedDisplayHandle; + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::UiKitDisplayHandle::empty().into() + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::UiKitDisplayHandle::new().into()) + } +} + +fn map_user_event( + mut handler: impl FnMut(Event, &RootActiveEventLoop), + receiver: mpsc::Receiver, +) -> impl FnMut(Event, &RootActiveEventLoop) { + move |event, window_target| match event.map_nonuser_event() { + Ok(event) => (handler)(event, window_target), + Err(_) => { + for event in receiver.try_iter() { + (handler)(Event::UserEvent(event), window_target); + } + }, + } +} + +pub struct EventLoop { + mtm: MainThreadMarker, + sender: Sender, + receiver: Receiver, + window_target: RootActiveEventLoop, + + // Since iOS 9.0, we no longer need to remove the observers before they are deallocated; the + // system instead cleans it up next time it would have posted a notification to it. + // + // Though we do still need to keep the observers around to prevent them from being deallocated. + _did_finish_launching_observer: Retained, + _did_become_active_observer: Retained, + _will_resign_active_observer: Retained, + _will_enter_foreground_observer: Retained, + _did_enter_background_observer: Retained, + _will_terminate_observer: Retained, + _did_receive_memory_warning_observer: Retained, +} + +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PlatformSpecificEventLoopAttributes {} + +impl EventLoop { + pub(crate) fn new( + _: &PlatformSpecificEventLoopAttributes, + ) -> Result, EventLoopError> { + let mtm = MainThreadMarker::new() + .expect("On iOS, `EventLoop` must be created on the main thread"); + + static mut SINGLETON_INIT: bool = false; + unsafe { + assert!( + !SINGLETON_INIT, + "Only one `EventLoop` is supported on iOS. `EventLoopProxy` might be helpful" + ); + SINGLETON_INIT = true; + } + + let (sender, receiver) = mpsc::channel(); + + // this line sets up the main run loop before `UIApplicationMain` + setup_control_flow_observers(); + + let center = unsafe { NSNotificationCenter::defaultCenter() }; + + let _did_finish_launching_observer = create_observer( + ¢er, + // `application:didFinishLaunchingWithOptions:` + unsafe { UIApplicationDidFinishLaunchingNotification }, + move |_| { + app_state::did_finish_launching(mtm); + }, + ); + let _did_become_active_observer = create_observer( + ¢er, + // `applicationDidBecomeActive:` + unsafe { UIApplicationDidBecomeActiveNotification }, + move |_| { + app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::Resumed)); + }, + ); + let _will_resign_active_observer = create_observer( + ¢er, + // `applicationWillResignActive:` + unsafe { UIApplicationWillResignActiveNotification }, + move |_| { + app_state::handle_nonuser_event(mtm, EventWrapper::StaticEvent(Event::Suspended)); + }, + ); + let _will_enter_foreground_observer = create_observer( + ¢er, + // `applicationWillEnterForeground:` + unsafe { UIApplicationWillEnterForegroundNotification }, + move |notification| { + let app = unsafe { notification.object() }.expect( + "UIApplicationWillEnterForegroundNotification to have application object", + ); + // SAFETY: The `object` in `UIApplicationWillEnterForegroundNotification` is + // documented to be `UIApplication`. + let app: Retained = unsafe { Retained::cast(app) }; + send_occluded_event_for_all_windows(&app, false); + }, + ); + let _did_enter_background_observer = create_observer( + ¢er, + // `applicationDidEnterBackground:` + unsafe { UIApplicationDidEnterBackgroundNotification }, + move |notification| { + let app = unsafe { notification.object() }.expect( + "UIApplicationDidEnterBackgroundNotification to have application object", + ); + // SAFETY: The `object` in `UIApplicationDidEnterBackgroundNotification` is + // documented to be `UIApplication`. + let app: Retained = unsafe { Retained::cast(app) }; + send_occluded_event_for_all_windows(&app, true); + }, + ); + let _will_terminate_observer = create_observer( + ¢er, + // `applicationWillTerminate:` + unsafe { UIApplicationWillTerminateNotification }, + move |notification| { + let app = unsafe { notification.object() } + .expect("UIApplicationWillTerminateNotification to have application object"); + // SAFETY: The `object` in `UIApplicationWillTerminateNotification` is + // (somewhat) documented to be `UIApplication`. + let app: Retained = unsafe { Retained::cast(app) }; + app_state::terminated(&app); + }, + ); + let _did_receive_memory_warning_observer = create_observer( + ¢er, + // `applicationDidReceiveMemoryWarning:` + unsafe { UIApplicationDidReceiveMemoryWarningNotification }, + move |_| { + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::MemoryWarning), + ); + }, + ); + + Ok(EventLoop { + mtm, + sender, + receiver, + window_target: RootActiveEventLoop { p: ActiveEventLoop { mtm }, _marker: PhantomData }, + _did_finish_launching_observer, + _did_become_active_observer, + _will_resign_active_observer, + _will_enter_foreground_observer, + _did_enter_background_observer, + _will_terminate_observer, + _did_receive_memory_warning_observer, + }) + } + + pub fn run(self, handler: F) -> ! + where + F: FnMut(Event, &RootActiveEventLoop), + { + let application: Option> = + unsafe { msg_send_id![UIApplication::class(), sharedApplication] }; + assert!( + application.is_none(), + "\ + `EventLoop` cannot be `run` after a call to `UIApplicationMain` on iOS\nNote: \ + `EventLoop::run_app` calls `UIApplicationMain` on iOS", + ); + + let handler = map_user_event(handler, self.receiver); + + let handler = unsafe { + std::mem::transmute::< + Box, &RootActiveEventLoop)>, + Box, &RootActiveEventLoop)>, + >(Box::new(handler)) + }; + + let handler = EventLoopHandler { handler, event_loop: self.window_target }; + + app_state::will_launch(self.mtm, handler); + + extern "C" { + // These functions are in crt_externs.h. + fn _NSGetArgc() -> *mut c_int; + fn _NSGetArgv() -> *mut *mut *mut c_char; + } + + unsafe { + UIApplicationMain( + *_NSGetArgc(), + NonNull::new(*_NSGetArgv()).unwrap(), + // We intentionally override neither the application nor the delegate, to allow the + // user to do so themselves! + None, + None, + ) + }; + unreachable!() + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy::new(self.sender.clone()) + } + + pub fn window_target(&self) -> &RootActiveEventLoop { + &self.window_target + } +} + +// EventLoopExtIOS +impl EventLoop { + pub fn idiom(&self) -> Idiom { + match UIDevice::currentDevice(self.mtm).userInterfaceIdiom() { + UIUserInterfaceIdiom::Unspecified => Idiom::Unspecified, + UIUserInterfaceIdiom::Phone => Idiom::Phone, + UIUserInterfaceIdiom::Pad => Idiom::Pad, + UIUserInterfaceIdiom::TV => Idiom::TV, + UIUserInterfaceIdiom::CarPlay => Idiom::CarPlay, + _ => Idiom::Unspecified, + } + } +} + +pub struct EventLoopProxy { + sender: Sender, + source: CFRunLoopSourceRef, +} + +unsafe impl Send for EventLoopProxy {} +unsafe impl Sync for EventLoopProxy {} + +impl Clone for EventLoopProxy { + fn clone(&self) -> EventLoopProxy { + EventLoopProxy::new(self.sender.clone()) + } +} + +impl Drop for EventLoopProxy { + fn drop(&mut self) { + unsafe { + CFRunLoopSourceInvalidate(self.source); + CFRelease(self.source as _); + } + } +} + +impl EventLoopProxy { + fn new(sender: Sender) -> EventLoopProxy { + unsafe { + // just wake up the eventloop + extern "C" fn event_loop_proxy_handler(_: *const c_void) {} + + // adding a Source to the main CFRunLoop lets us wake it up and + // process user events through the normal OS EventLoop mechanisms. + let rl = CFRunLoopGetMain(); + let mut context = CFRunLoopSourceContext { + version: 0, + info: ptr::null_mut(), + retain: None, + release: None, + copyDescription: None, + equal: None, + hash: None, + schedule: None, + cancel: None, + perform: event_loop_proxy_handler, + }; + let source = CFRunLoopSourceCreate(ptr::null_mut(), CFIndex::MAX - 1, &mut context); + CFRunLoopAddSource(rl, source, kCFRunLoopCommonModes); + CFRunLoopWakeUp(rl); + + EventLoopProxy { sender, source } + } + } + + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.sender.send(event).map_err(|::std::sync::mpsc::SendError(x)| EventLoopClosed(x))?; + unsafe { + // let the main thread know there's a new event + CFRunLoopSourceSignal(self.source); + let rl = CFRunLoopGetMain(); + CFRunLoopWakeUp(rl); + } + Ok(()) + } +} + +fn setup_control_flow_observers() { + unsafe { + // begin is queued with the highest priority to ensure it is processed before other + // observers + extern "C" fn control_flow_begin_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + _: *mut c_void, + ) { + let mtm = MainThreadMarker::new().unwrap(); + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopAfterWaiting => app_state::handle_wakeup_transition(mtm), + _ => unreachable!(), + } + } + + // Core Animation registers its `CFRunLoopObserver` that performs drawing operations in + // `CA::Transaction::ensure_implicit` with a priority of `0x1e8480`. We set the main_end + // priority to be 0, in order to send AboutToWait before RedrawRequested. This value was + // chosen conservatively to guard against apple using different priorities for their redraw + // observers in different OS's or on different devices. If it so happens that it's too + // conservative, the main symptom would be non-redraw events coming in after `AboutToWait`. + // + // The value of `0x1e8480` was determined by inspecting stack traces and the associated + // registers for every `CFRunLoopAddObserver` call on an iPad Air 2 running iOS 11.4. + // + // Also tested to be `0x1e8480` on iPhone 8, iOS 13 beta 4. + extern "C" fn control_flow_main_end_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + _: *mut c_void, + ) { + let mtm = MainThreadMarker::new().unwrap(); + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopBeforeWaiting => app_state::handle_main_events_cleared(mtm), + kCFRunLoopExit => {}, // may happen when running on macOS + _ => unreachable!(), + } + } + + // end is queued with the lowest priority to ensure it is processed after other observers + extern "C" fn control_flow_end_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + _: *mut c_void, + ) { + let mtm = MainThreadMarker::new().unwrap(); + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopBeforeWaiting => app_state::handle_events_cleared(mtm), + kCFRunLoopExit => {}, // may happen when running on macOS + _ => unreachable!(), + } + } + + let main_loop = CFRunLoopGetMain(); + + let begin_observer = CFRunLoopObserverCreate( + ptr::null_mut(), + kCFRunLoopAfterWaiting, + 1, // repeat = true + CFIndex::MIN, + control_flow_begin_handler, + ptr::null_mut(), + ); + CFRunLoopAddObserver(main_loop, begin_observer, kCFRunLoopDefaultMode); + + let main_end_observer = CFRunLoopObserverCreate( + ptr::null_mut(), + kCFRunLoopExit | kCFRunLoopBeforeWaiting, + 1, // repeat = true + 0, // see comment on `control_flow_main_end_handler` + control_flow_main_end_handler, + ptr::null_mut(), + ); + CFRunLoopAddObserver(main_loop, main_end_observer, kCFRunLoopDefaultMode); + + let end_observer = CFRunLoopObserverCreate( + ptr::null_mut(), + kCFRunLoopExit | kCFRunLoopBeforeWaiting, + 1, // repeat = true + CFIndex::MAX, + control_flow_end_handler, + ptr::null_mut(), + ); + CFRunLoopAddObserver(main_loop, end_observer, kCFRunLoopDefaultMode); + } +} diff --git a/vendor/winit/src/platform_impl/ios/mod.rs b/vendor/winit/src/platform_impl/ios/mod.rs new file mode 100644 index 00000000..69e79c92 --- /dev/null +++ b/vendor/winit/src/platform_impl/ios/mod.rs @@ -0,0 +1,52 @@ +#![allow(clippy::let_unit_value)] + +mod app_state; +mod event_loop; +mod monitor; +mod notification_center; +mod view; +mod view_controller; +mod window; + +use std::fmt; + +use crate::event::DeviceId as RootDeviceId; + +pub(crate) use self::event_loop::{ + ActiveEventLoop, EventLoop, EventLoopProxy, OwnedDisplayHandle, + PlatformSpecificEventLoopAttributes, +}; +pub(crate) use self::monitor::{MonitorHandle, VideoModeHandle}; +pub(crate) use self::window::{PlatformSpecificWindowAttributes, Window, WindowId}; +pub(crate) use crate::cursor::{ + NoCustomCursor as PlatformCustomCursor, NoCustomCursor as PlatformCustomCursorSource, +}; +pub(crate) use crate::icon::NoIcon as PlatformIcon; +pub(crate) use crate::platform_impl::Fullscreen; + +/// There is no way to detect which device that performed a certain event in +/// UIKit (i.e. you can't differentiate between different external keyboards, +/// or whether it was the main touchscreen, assistive technologies, or some +/// other pointer device that caused a touch event). +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId; + +impl DeviceId { + pub const fn dummy() -> Self { + DeviceId + } +} + +pub(crate) const DEVICE_ID: RootDeviceId = RootDeviceId(DeviceId); + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct KeyEventExtra {} + +#[derive(Debug)] +pub enum OsError {} + +impl fmt::Display for OsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "os error") + } +} diff --git a/vendor/winit/src/platform_impl/ios/monitor.rs b/vendor/winit/src/platform_impl/ios/monitor.rs new file mode 100644 index 00000000..9f017a24 --- /dev/null +++ b/vendor/winit/src/platform_impl/ios/monitor.rs @@ -0,0 +1,277 @@ +#![allow(clippy::unnecessary_cast)] + +use std::collections::{BTreeSet, VecDeque}; +use std::{fmt, hash, ptr}; + +use objc2::mutability::IsRetainable; +use objc2::rc::Retained; +use objc2::Message; +use objc2_foundation::{run_on_main, MainThreadBound, MainThreadMarker, NSInteger}; +use objc2_ui_kit::{UIScreen, UIScreenMode}; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::monitor::VideoModeHandle as RootVideoModeHandle; +use crate::platform_impl::platform::app_state; + +// Workaround for `MainThreadBound` implementing almost no traits +#[derive(Debug)] +struct MainThreadBoundDelegateImpls(MainThreadBound>); + +impl Clone for MainThreadBoundDelegateImpls { + fn clone(&self) -> Self { + Self(run_on_main(|mtm| MainThreadBound::new(Retained::clone(self.0.get(mtm)), mtm))) + } +} + +impl hash::Hash for MainThreadBoundDelegateImpls { + fn hash(&self, state: &mut H) { + // SAFETY: Marker only used to get the pointer + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + Retained::as_ptr(self.0.get(mtm)).hash(state); + } +} + +impl PartialEq for MainThreadBoundDelegateImpls { + fn eq(&self, other: &Self) -> bool { + // SAFETY: Marker only used to get the pointer + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + Retained::as_ptr(self.0.get(mtm)) == Retained::as_ptr(other.0.get(mtm)) + } +} + +impl Eq for MainThreadBoundDelegateImpls {} + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct VideoModeHandle { + pub(crate) size: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate_millihertz: u32, + screen_mode: MainThreadBoundDelegateImpls, + pub(crate) monitor: MonitorHandle, +} + +impl VideoModeHandle { + fn new( + uiscreen: Retained, + screen_mode: Retained, + mtm: MainThreadMarker, + ) -> VideoModeHandle { + let refresh_rate_millihertz = refresh_rate_millihertz(&uiscreen); + let size = screen_mode.size(); + VideoModeHandle { + size: (size.width as u32, size.height as u32), + bit_depth: 32, + refresh_rate_millihertz, + screen_mode: MainThreadBoundDelegateImpls(MainThreadBound::new(screen_mode, mtm)), + monitor: MonitorHandle::new(uiscreen), + } + } + + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone() + } + + pub(super) fn screen_mode(&self, mtm: MainThreadMarker) -> &Retained { + self.screen_mode.0.get(mtm) + } +} + +pub struct MonitorHandle { + ui_screen: MainThreadBound>, +} + +impl Clone for MonitorHandle { + fn clone(&self) -> Self { + run_on_main(|mtm| Self { + ui_screen: MainThreadBound::new(self.ui_screen.get(mtm).clone(), mtm), + }) + } +} + +impl hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + // SAFETY: Only getting the pointer. + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + Retained::as_ptr(self.ui_screen.get(mtm)).hash(state); + } +} + +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + // SAFETY: Only getting the pointer. + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + ptr::eq( + Retained::as_ptr(self.ui_screen.get(mtm)), + Retained::as_ptr(other.ui_screen.get(mtm)), + ) + } +} + +impl Eq for MonitorHandle {} + +impl PartialOrd for MonitorHandle { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for MonitorHandle { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // SAFETY: Only getting the pointer. + // TODO: Make a better ordering + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + Retained::as_ptr(self.ui_screen.get(mtm)).cmp(&Retained::as_ptr(other.ui_screen.get(mtm))) + } +} + +impl fmt::Debug for MonitorHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MonitorHandle") + .field("name", &self.name()) + .field("size", &self.size()) + .field("position", &self.position()) + .field("scale_factor", &self.scale_factor()) + .field("refresh_rate_millihertz", &self.refresh_rate_millihertz()) + .finish_non_exhaustive() + } +} + +impl MonitorHandle { + pub(crate) fn new(ui_screen: Retained) -> Self { + // Holding `Retained` implies we're on the main thread. + let mtm = MainThreadMarker::new().unwrap(); + Self { ui_screen: MainThreadBound::new(ui_screen, mtm) } + } + + pub fn name(&self) -> Option { + run_on_main(|mtm| { + #[allow(deprecated)] + let main = UIScreen::mainScreen(mtm); + if *self.ui_screen(mtm) == main { + Some("Primary".to_string()) + } else if Some(self.ui_screen(mtm)) == main.mirroredScreen().as_ref() { + Some("Mirrored".to_string()) + } else { + #[allow(deprecated)] + UIScreen::screens(mtm) + .iter() + .position(|rhs| rhs == &**self.ui_screen(mtm)) + .map(|idx| idx.to_string()) + } + }) + } + + pub fn size(&self) -> PhysicalSize { + let bounds = self.ui_screen.get_on_main(|ui_screen| ui_screen.nativeBounds()); + PhysicalSize::new(bounds.size.width as u32, bounds.size.height as u32) + } + + pub fn position(&self) -> PhysicalPosition { + let bounds = self.ui_screen.get_on_main(|ui_screen| ui_screen.nativeBounds()); + (bounds.origin.x as f64, bounds.origin.y as f64).into() + } + + pub fn scale_factor(&self) -> f64 { + self.ui_screen.get_on_main(|ui_screen| ui_screen.nativeScale()) as f64 + } + + pub fn refresh_rate_millihertz(&self) -> Option { + Some(self.ui_screen.get_on_main(|ui_screen| refresh_rate_millihertz(ui_screen))) + } + + pub fn video_modes(&self) -> impl Iterator { + run_on_main(|mtm| { + let ui_screen = self.ui_screen(mtm); + // Use Ord impl of RootVideoModeHandle + + let modes: BTreeSet<_> = ui_screen + .availableModes() + .into_iter() + .map(|mode| RootVideoModeHandle { + video_mode: VideoModeHandle::new(ui_screen.clone(), mode, mtm), + }) + .collect(); + + modes.into_iter().map(|mode| mode.video_mode) + }) + } + + pub(crate) fn ui_screen(&self, mtm: MainThreadMarker) -> &Retained { + self.ui_screen.get(mtm) + } + + pub fn preferred_video_mode(&self) -> VideoModeHandle { + run_on_main(|mtm| { + VideoModeHandle::new( + self.ui_screen(mtm).clone(), + self.ui_screen(mtm).preferredMode().unwrap(), + mtm, + ) + }) + } +} + +fn refresh_rate_millihertz(uiscreen: &UIScreen) -> u32 { + let refresh_rate_millihertz: NSInteger = { + let os_capabilities = app_state::os_capabilities(); + if os_capabilities.maximum_frames_per_second { + uiscreen.maximumFramesPerSecond() + } else { + // https://developer.apple.com/library/archive/technotes/tn2460/_index.html + // https://en.wikipedia.org/wiki/IPad_Pro#Model_comparison + // + // All iOS devices support 60 fps, and on devices where `maximumFramesPerSecond` is not + // supported, they are all guaranteed to have 60hz refresh rates. This does not + // correctly handle external displays. ProMotion displays support 120fps, but they were + // introduced at the same time as the `maximumFramesPerSecond` API. + // + // FIXME: earlier OSs could calculate the refresh rate using + // `-[CADisplayLink duration]`. + os_capabilities.maximum_frames_per_second_err_msg("defaulting to 60 fps"); + 60 + } + }; + + refresh_rate_millihertz as u32 * 1000 +} + +pub fn uiscreens(mtm: MainThreadMarker) -> VecDeque { + #[allow(deprecated)] + UIScreen::screens(mtm).into_iter().map(MonitorHandle::new).collect() +} + +#[cfg(test)] +mod tests { + use objc2_foundation::NSSet; + + use super::*; + + // Test that UIScreen pointer comparisons are correct. + #[test] + #[allow(deprecated)] + fn screen_comparisons() { + // Test code, doesn't matter that it's not thread safe + let mtm = unsafe { MainThreadMarker::new_unchecked() }; + + assert!(ptr::eq(&*UIScreen::mainScreen(mtm), &*UIScreen::mainScreen(mtm))); + + let main = UIScreen::mainScreen(mtm); + assert!(UIScreen::screens(mtm).iter().any(|screen| ptr::eq(screen, &*main))); + + assert!(unsafe { + NSSet::setWithArray(&UIScreen::screens(mtm)).containsObject(&UIScreen::mainScreen(mtm)) + }); + } +} diff --git a/vendor/winit/src/platform_impl/ios/notification_center.rs b/vendor/winit/src/platform_impl/ios/notification_center.rs new file mode 100644 index 00000000..652bf1d0 --- /dev/null +++ b/vendor/winit/src/platform_impl/ios/notification_center.rs @@ -0,0 +1,27 @@ +use std::ptr::NonNull; + +use block2::RcBlock; +use objc2::rc::Retained; +use objc2_foundation::{NSNotification, NSNotificationCenter, NSNotificationName, NSObject}; + +/// Observe the given notification. +/// +/// This is used in Winit as an alternative to declaring an application delegate, as we want to +/// give the user full control over those. +pub fn create_observer( + center: &NSNotificationCenter, + name: &NSNotificationName, + handler: impl Fn(&NSNotification) + 'static, +) -> Retained { + let block = RcBlock::new(move |notification: NonNull| { + handler(unsafe { notification.as_ref() }); + }); + unsafe { + center.addObserverForName_object_queue_usingBlock( + Some(name), + None, // No sender filter + None, // No queue, run on posting thread (i.e. main thread) + &block, + ) + } +} diff --git a/vendor/winit/src/platform_impl/ios/view.rs b/vendor/winit/src/platform_impl/ios/view.rs new file mode 100644 index 00000000..418968c7 --- /dev/null +++ b/vendor/winit/src/platform_impl/ios/view.rs @@ -0,0 +1,607 @@ +#![allow(clippy::unnecessary_cast)] +use std::cell::{Cell, RefCell}; + +use objc2::rc::Retained; +use objc2::runtime::{NSObjectProtocol, ProtocolObject}; +use objc2::{declare_class, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass}; +use objc2_foundation::{CGFloat, CGPoint, CGRect, MainThreadMarker, NSObject, NSSet, NSString}; +use objc2_ui_kit::{ + UICoordinateSpace, UIEvent, UIForceTouchCapability, UIGestureRecognizer, + UIGestureRecognizerDelegate, UIGestureRecognizerState, UIKeyInput, UIPanGestureRecognizer, + UIPinchGestureRecognizer, UIResponder, UIRotationGestureRecognizer, UITapGestureRecognizer, + UITextInputTraits, UITouch, UITouchPhase, UITouchType, UITraitEnvironment, UIView, +}; + +use super::app_state::{self, EventWrapper}; +use super::window::WinitUIWindow; +use crate::dpi::PhysicalPosition; +use crate::event::{ElementState, Event, Force, KeyEvent, Touch, TouchPhase, WindowEvent}; +use crate::keyboard::{Key, KeyCode, KeyLocation, NamedKey, NativeKeyCode, PhysicalKey}; +use crate::platform_impl::platform::DEVICE_ID; +use crate::platform_impl::KeyEventExtra; +use crate::window::{WindowAttributes, WindowId as RootWindowId}; + +pub struct WinitViewState { + pinch_gesture_recognizer: RefCell>>, + doubletap_gesture_recognizer: RefCell>>, + rotation_gesture_recognizer: RefCell>>, + pan_gesture_recognizer: RefCell>>, + + // for iOS delta references the start of the Gesture + rotation_last_delta: Cell, + pinch_last_delta: Cell, + pan_last_delta: Cell, +} + +declare_class!( + pub(crate) struct WinitView; + + unsafe impl ClassType for WinitView { + #[inherits(UIResponder, NSObject)] + type Super = UIView; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitUIView"; + } + + impl DeclaredClass for WinitView { + type Ivars = WinitViewState; + } + + unsafe impl WinitView { + #[method(drawRect:)] + fn draw_rect(&self, rect: CGRect) { + let mtm = MainThreadMarker::new().unwrap(); + let window = self.window().unwrap(); + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::RedrawRequested, + }), + ); + let _: () = unsafe { msg_send![super(self), drawRect: rect] }; + } + + #[method(layoutSubviews)] + fn layout_subviews(&self) { + let mtm = MainThreadMarker::new().unwrap(); + let _: () = unsafe { msg_send![super(self), layoutSubviews] }; + + let window = self.window().unwrap(); + let window_bounds = window.bounds(); + let screen = window.screen(); + let screen_space = screen.coordinateSpace(); + let screen_frame = self.convertRect_toCoordinateSpace(window_bounds, &screen_space); + let scale_factor = screen.scale(); + let size = crate::dpi::LogicalSize { + width: screen_frame.size.width as f64, + height: screen_frame.size.height as f64, + } + .to_physical(scale_factor as f64); + + // If the app is started in landscape, the view frame and window bounds can be mismatched. + // The view frame will be in portrait and the window bounds in landscape. So apply the + // window bounds to the view frame to make it consistent. + let view_frame = self.frame(); + if view_frame != window_bounds { + self.setFrame(window_bounds); + } + + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::Resized(size), + }), + ); + } + + #[method(setContentScaleFactor:)] + fn set_content_scale_factor(&self, untrusted_scale_factor: CGFloat) { + let mtm = MainThreadMarker::new().unwrap(); + let _: () = + unsafe { msg_send![super(self), setContentScaleFactor: untrusted_scale_factor] }; + + // `window` is null when `setContentScaleFactor` is invoked prior to `[UIWindow + // makeKeyAndVisible]` at window creation time (either manually or internally by + // UIKit when the `UIView` is first created), in which case we send no events here + let window = match self.window() { + Some(window) => window, + None => return, + }; + // `setContentScaleFactor` may be called with a value of 0, which means "reset the + // content scale factor to a device-specific default value", so we can't use the + // parameter here. We can query the actual factor using the getter + let scale_factor = self.contentScaleFactor(); + assert!( + !scale_factor.is_nan() + && scale_factor.is_finite() + && scale_factor.is_sign_positive() + && scale_factor > 0.0, + "invalid scale_factor set on UIView", + ); + let scale_factor = scale_factor as f64; + let bounds = self.bounds(); + let screen = window.screen(); + let screen_space = screen.coordinateSpace(); + let screen_frame = self.convertRect_toCoordinateSpace(bounds, &screen_space); + let size = crate::dpi::LogicalSize { + width: screen_frame.size.width as f64, + height: screen_frame.size.height as f64, + }; + let window_id = RootWindowId(window.id()); + app_state::handle_nonuser_events( + mtm, + std::iter::once(EventWrapper::ScaleFactorChanged( + app_state::ScaleFactorChanged { + window, + scale_factor, + suggested_size: size.to_physical(scale_factor), + }, + )) + .chain(std::iter::once(EventWrapper::StaticEvent( + Event::WindowEvent { + window_id, + event: WindowEvent::Resized(size.to_physical(scale_factor)), + }, + ))), + ); + } + + #[method(touchesBegan:withEvent:)] + fn touches_began(&self, touches: &NSSet, _event: Option<&UIEvent>) { + self.handle_touches(touches) + } + + #[method(touchesMoved:withEvent:)] + fn touches_moved(&self, touches: &NSSet, _event: Option<&UIEvent>) { + self.handle_touches(touches) + } + + #[method(touchesEnded:withEvent:)] + fn touches_ended(&self, touches: &NSSet, _event: Option<&UIEvent>) { + self.handle_touches(touches) + } + + #[method(touchesCancelled:withEvent:)] + fn touches_cancelled(&self, touches: &NSSet, _event: Option<&UIEvent>) { + self.handle_touches(touches) + } + + #[method(pinchGesture:)] + fn pinch_gesture(&self, recognizer: &UIPinchGestureRecognizer) { + let window = self.window().unwrap(); + + let (phase, delta) = match recognizer.state() { + UIGestureRecognizerState::Began => { + self.ivars().pinch_last_delta.set(recognizer.scale()); + (TouchPhase::Started, 0.0) + } + UIGestureRecognizerState::Changed => { + let last_scale: f64 = self.ivars().pinch_last_delta.replace(recognizer.scale()); + (TouchPhase::Moved, recognizer.scale() - last_scale) + } + UIGestureRecognizerState::Ended => { + let last_scale: f64 = self.ivars().pinch_last_delta.replace(0.0); + (TouchPhase::Moved, recognizer.scale() - last_scale) + } + UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => { + self.ivars().rotation_last_delta.set(0.0); + // Pass -delta so that action is reversed + (TouchPhase::Cancelled, -recognizer.scale()) + } + state => panic!("unexpected recognizer state: {state:?}"), + }; + + let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::PinchGesture { + device_id: DEVICE_ID, + delta: delta as f64, + phase, + }, + }); + + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event(mtm, gesture_event); + } + + #[method(doubleTapGesture:)] + fn double_tap_gesture(&self, recognizer: &UITapGestureRecognizer) { + let window = self.window().unwrap(); + + if recognizer.state() == UIGestureRecognizerState::Ended { + let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::DoubleTapGesture { + device_id: DEVICE_ID, + }, + }); + + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event(mtm, gesture_event); + } + } + + #[method(rotationGesture:)] + fn rotation_gesture(&self, recognizer: &UIRotationGestureRecognizer) { + let window = self.window().unwrap(); + + let (phase, delta) = match recognizer.state() { + UIGestureRecognizerState::Began => { + self.ivars().rotation_last_delta.set(0.0); + + (TouchPhase::Started, 0.0) + } + UIGestureRecognizerState::Changed => { + let last_rotation = self.ivars().rotation_last_delta.replace(recognizer.rotation()); + + (TouchPhase::Moved, recognizer.rotation() - last_rotation) + } + UIGestureRecognizerState::Ended => { + let last_rotation = self.ivars().rotation_last_delta.replace(0.0); + + (TouchPhase::Ended, recognizer.rotation() - last_rotation) + } + UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => { + self.ivars().rotation_last_delta.set(0.0); + + // Pass -delta so that action is reversed + (TouchPhase::Cancelled, -recognizer.rotation()) + } + state => panic!("unexpected recognizer state: {state:?}"), + }; + + // Make delta negative to match macos, convert to degrees + let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::RotationGesture { + device_id: DEVICE_ID, + delta: -delta.to_degrees() as _, + phase, + }, + }); + + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event(mtm, gesture_event); + } + + #[method(panGesture:)] + fn pan_gesture(&self, recognizer: &UIPanGestureRecognizer) { + let window = self.window().unwrap(); + + let translation = recognizer.translationInView(Some(self)); + + let (phase, dx, dy) = match recognizer.state() { + UIGestureRecognizerState::Began => { + self.ivars().pan_last_delta.set(translation); + + (TouchPhase::Started, 0.0, 0.0) + } + UIGestureRecognizerState::Changed => { + let last_pan: CGPoint = self.ivars().pan_last_delta.replace(translation); + + let dx = translation.x - last_pan.x; + let dy = translation.y - last_pan.y; + + (TouchPhase::Moved, dx, dy) + } + UIGestureRecognizerState::Ended => { + let last_pan: CGPoint = self.ivars().pan_last_delta.replace(CGPoint{x:0.0, y:0.0}); + + let dx = translation.x - last_pan.x; + let dy = translation.y - last_pan.y; + + (TouchPhase::Ended, dx, dy) + } + UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => { + let last_pan: CGPoint = self.ivars().pan_last_delta.replace(CGPoint{x:0.0, y:0.0}); + + // Pass -delta so that action is reversed + (TouchPhase::Cancelled, -last_pan.x, -last_pan.y) + } + state => panic!("unexpected recognizer state: {state:?}"), + }; + + + let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::PanGesture { + device_id: DEVICE_ID, + delta: PhysicalPosition::new(dx as _, dy as _), + phase, + }, + }); + + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event(mtm, gesture_event); + } + + #[method(canBecomeFirstResponder)] + fn can_become_first_responder(&self) -> bool { + true + } + } + + unsafe impl NSObjectProtocol for WinitView {} + + unsafe impl UIGestureRecognizerDelegate for WinitView { + #[method(gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:)] + fn should_recognize_simultaneously(&self, _gesture_recognizer: &UIGestureRecognizer, _other_gesture_recognizer: &UIGestureRecognizer) -> bool { + true + } + } + + unsafe impl UITextInputTraits for WinitView { + } + + unsafe impl UIKeyInput for WinitView { + #[method(hasText)] + fn has_text(&self) -> bool { + true + } + + #[method(insertText:)] + fn insert_text(&self, text: &NSString) { + self.handle_insert_text(text) + } + + #[method(deleteBackward)] + fn delete_backward(&self) { + self.handle_delete_backward() + } + } +); + +impl WinitView { + pub(crate) fn new( + mtm: MainThreadMarker, + window_attributes: &WindowAttributes, + frame: CGRect, + ) -> Retained { + let this = mtm.alloc().set_ivars(WinitViewState { + pinch_gesture_recognizer: RefCell::new(None), + doubletap_gesture_recognizer: RefCell::new(None), + rotation_gesture_recognizer: RefCell::new(None), + pan_gesture_recognizer: RefCell::new(None), + + rotation_last_delta: Cell::new(0.0), + pinch_last_delta: Cell::new(0.0), + pan_last_delta: Cell::new(CGPoint { x: 0.0, y: 0.0 }), + }); + let this: Retained = unsafe { msg_send_id![super(this), initWithFrame: frame] }; + + this.setMultipleTouchEnabled(true); + + if let Some(scale_factor) = window_attributes.platform_specific.scale_factor { + this.setContentScaleFactor(scale_factor as _); + } + + this + } + + fn window(&self) -> Option> { + // SAFETY: `WinitView`s are always installed in a `WinitUIWindow` + (**self).window().map(|window| unsafe { Retained::cast(window) }) + } + + pub(crate) fn recognize_pinch_gesture(&self, should_recognize: bool) { + let mtm = MainThreadMarker::from(self); + if should_recognize { + if self.ivars().pinch_gesture_recognizer.borrow().is_none() { + let pinch = unsafe { + UIPinchGestureRecognizer::initWithTarget_action( + mtm.alloc(), + Some(self), + Some(sel!(pinchGesture:)), + ) + }; + pinch.setDelegate(Some(ProtocolObject::from_ref(self))); + self.addGestureRecognizer(&pinch); + self.ivars().pinch_gesture_recognizer.replace(Some(pinch)); + } + } else if let Some(recognizer) = self.ivars().pinch_gesture_recognizer.take() { + self.removeGestureRecognizer(&recognizer); + } + } + + pub(crate) fn recognize_pan_gesture( + &self, + should_recognize: bool, + minimum_number_of_touches: u8, + maximum_number_of_touches: u8, + ) { + let mtm = MainThreadMarker::from(self); + if should_recognize { + if self.ivars().pan_gesture_recognizer.borrow().is_none() { + let pan = unsafe { + UIPanGestureRecognizer::initWithTarget_action( + mtm.alloc(), + Some(self), + Some(sel!(panGesture:)), + ) + }; + pan.setDelegate(Some(ProtocolObject::from_ref(self))); + pan.setMinimumNumberOfTouches(minimum_number_of_touches as _); + pan.setMaximumNumberOfTouches(maximum_number_of_touches as _); + self.addGestureRecognizer(&pan); + self.ivars().pan_gesture_recognizer.replace(Some(pan)); + } + } else if let Some(recognizer) = self.ivars().pan_gesture_recognizer.take() { + self.removeGestureRecognizer(&recognizer); + } + } + + pub(crate) fn recognize_doubletap_gesture(&self, should_recognize: bool) { + let mtm = MainThreadMarker::from(self); + if should_recognize { + if self.ivars().doubletap_gesture_recognizer.borrow().is_none() { + let tap = unsafe { + UITapGestureRecognizer::initWithTarget_action( + mtm.alloc(), + Some(self), + Some(sel!(doubleTapGesture:)), + ) + }; + tap.setDelegate(Some(ProtocolObject::from_ref(self))); + tap.setNumberOfTapsRequired(2); + tap.setNumberOfTouchesRequired(1); + self.addGestureRecognizer(&tap); + self.ivars().doubletap_gesture_recognizer.replace(Some(tap)); + } + } else if let Some(recognizer) = self.ivars().doubletap_gesture_recognizer.take() { + self.removeGestureRecognizer(&recognizer); + } + } + + pub(crate) fn recognize_rotation_gesture(&self, should_recognize: bool) { + let mtm = MainThreadMarker::from(self); + if should_recognize { + if self.ivars().rotation_gesture_recognizer.borrow().is_none() { + let rotation = unsafe { + UIRotationGestureRecognizer::initWithTarget_action( + mtm.alloc(), + Some(self), + Some(sel!(rotationGesture:)), + ) + }; + rotation.setDelegate(Some(ProtocolObject::from_ref(self))); + self.addGestureRecognizer(&rotation); + self.ivars().rotation_gesture_recognizer.replace(Some(rotation)); + } + } else if let Some(recognizer) = self.ivars().rotation_gesture_recognizer.take() { + self.removeGestureRecognizer(&recognizer); + } + } + + fn handle_touches(&self, touches: &NSSet) { + let window = self.window().unwrap(); + let mut touch_events = Vec::new(); + let os_supports_force = app_state::os_capabilities().force_touch; + for touch in touches { + let logical_location = touch.locationInView(None); + let touch_type = touch.r#type(); + let force = if os_supports_force { + let trait_collection = self.traitCollection(); + let touch_capability = trait_collection.forceTouchCapability(); + // Both the OS _and_ the device need to be checked for force touch support. + if touch_capability == UIForceTouchCapability::Available + || touch_type == UITouchType::Pencil + { + let force = touch.force(); + let max_possible_force = touch.maximumPossibleForce(); + let altitude_angle: Option = if touch_type == UITouchType::Pencil { + let angle = touch.altitudeAngle(); + Some(angle as _) + } else { + None + }; + Some(Force::Calibrated { + force: force as _, + max_possible_force: max_possible_force as _, + altitude_angle, + }) + } else { + None + } + } else { + None + }; + let touch_id = touch as *const UITouch as u64; + let phase = touch.phase(); + let phase = match phase { + UITouchPhase::Began => TouchPhase::Started, + UITouchPhase::Moved => TouchPhase::Moved, + // 2 is UITouchPhase::Stationary and is not expected here + UITouchPhase::Ended => TouchPhase::Ended, + UITouchPhase::Cancelled => TouchPhase::Cancelled, + _ => panic!("unexpected touch phase: {phase:?}"), + }; + + let physical_location = { + let scale_factor = self.contentScaleFactor(); + PhysicalPosition::from_logical::<(f64, f64), f64>( + (logical_location.x as _, logical_location.y as _), + scale_factor as f64, + ) + }; + touch_events.push(EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::Touch(Touch { + device_id: DEVICE_ID, + id: touch_id, + location: physical_location, + force, + phase, + }), + })); + } + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_events(mtm, touch_events); + } + + fn handle_insert_text(&self, text: &NSString) { + let window = self.window().unwrap(); + let window_id = RootWindowId(window.id()); + let mtm = MainThreadMarker::new().unwrap(); + // send individual events for each character + app_state::handle_nonuser_events( + mtm, + text.to_string().chars().flat_map(|c| { + let text = smol_str::SmolStr::from_iter([c]); + // Emit both press and release events + [ElementState::Pressed, ElementState::Released].map(|state| { + EventWrapper::StaticEvent(Event::WindowEvent { + window_id, + event: WindowEvent::KeyboardInput { + event: KeyEvent { + text: if state == ElementState::Pressed { + Some(text.clone()) + } else { + None + }, + state, + location: KeyLocation::Standard, + repeat: false, + logical_key: Key::Character(text.clone()), + physical_key: PhysicalKey::Unidentified( + NativeKeyCode::Unidentified, + ), + platform_specific: KeyEventExtra {}, + }, + is_synthetic: false, + device_id: DEVICE_ID, + }, + }) + }) + }), + ); + } + + fn handle_delete_backward(&self) { + let window = self.window().unwrap(); + let window_id = RootWindowId(window.id()); + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_events( + mtm, + [ElementState::Pressed, ElementState::Released].map(|state| { + EventWrapper::StaticEvent(Event::WindowEvent { + window_id, + event: WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event: KeyEvent { + state, + logical_key: Key::Named(NamedKey::Backspace), + physical_key: PhysicalKey::Code(KeyCode::Backspace), + platform_specific: KeyEventExtra {}, + repeat: false, + location: KeyLocation::Standard, + text: None, + }, + is_synthetic: false, + }, + }) + }), + ); + } +} diff --git a/vendor/winit/src/platform_impl/ios/view_controller.rs b/vendor/winit/src/platform_impl/ios/view_controller.rs new file mode 100644 index 00000000..dd658682 --- /dev/null +++ b/vendor/winit/src/platform_impl/ios/view_controller.rs @@ -0,0 +1,176 @@ +use std::cell::Cell; + +use objc2::rc::Retained; +use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass}; +use objc2_foundation::{MainThreadMarker, NSObject}; +use objc2_ui_kit::{ + UIDevice, UIInterfaceOrientationMask, UIRectEdge, UIResponder, UIStatusBarStyle, + UIUserInterfaceIdiom, UIView, UIViewController, +}; + +use super::app_state::{self}; +use crate::platform::ios::{ScreenEdge, StatusBarStyle, ValidOrientations}; +use crate::window::WindowAttributes; + +pub struct ViewControllerState { + prefers_status_bar_hidden: Cell, + preferred_status_bar_style: Cell, + prefers_home_indicator_auto_hidden: Cell, + supported_orientations: Cell, + preferred_screen_edges_deferring_system_gestures: Cell, +} + +declare_class!( + pub(crate) struct WinitViewController; + + unsafe impl ClassType for WinitViewController { + #[inherits(UIResponder, NSObject)] + type Super = UIViewController; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitUIViewController"; + } + + impl DeclaredClass for WinitViewController { + type Ivars = ViewControllerState; + } + + unsafe impl WinitViewController { + #[method(shouldAutorotate)] + fn should_autorotate(&self) -> bool { + true + } + + #[method(prefersStatusBarHidden)] + fn prefers_status_bar_hidden(&self) -> bool { + self.ivars().prefers_status_bar_hidden.get() + } + + #[method(preferredStatusBarStyle)] + fn preferred_status_bar_style(&self) -> UIStatusBarStyle { + self.ivars().preferred_status_bar_style.get() + } + + #[method(prefersHomeIndicatorAutoHidden)] + fn prefers_home_indicator_auto_hidden(&self) -> bool { + self.ivars().prefers_home_indicator_auto_hidden.get() + } + + #[method(supportedInterfaceOrientations)] + fn supported_orientations(&self) -> UIInterfaceOrientationMask { + self.ivars().supported_orientations.get() + } + + #[method(preferredScreenEdgesDeferringSystemGestures)] + fn preferred_screen_edges_deferring_system_gestures(&self) -> UIRectEdge { + self.ivars() + .preferred_screen_edges_deferring_system_gestures + .get() + } + } +); + +impl WinitViewController { + pub(crate) fn set_prefers_status_bar_hidden(&self, val: bool) { + self.ivars().prefers_status_bar_hidden.set(val); + self.setNeedsStatusBarAppearanceUpdate(); + } + + pub(crate) fn set_preferred_status_bar_style(&self, val: StatusBarStyle) { + let val = match val { + StatusBarStyle::Default => UIStatusBarStyle::Default, + StatusBarStyle::LightContent => UIStatusBarStyle::LightContent, + StatusBarStyle::DarkContent => UIStatusBarStyle::DarkContent, + }; + self.ivars().preferred_status_bar_style.set(val); + self.setNeedsStatusBarAppearanceUpdate(); + } + + pub(crate) fn set_prefers_home_indicator_auto_hidden(&self, val: bool) { + self.ivars().prefers_home_indicator_auto_hidden.set(val); + let os_capabilities = app_state::os_capabilities(); + if os_capabilities.home_indicator_hidden { + self.setNeedsUpdateOfHomeIndicatorAutoHidden(); + } else { + os_capabilities.home_indicator_hidden_err_msg("ignoring") + } + } + + pub(crate) fn set_preferred_screen_edges_deferring_system_gestures(&self, val: ScreenEdge) { + let val = { + assert_eq!(val.bits() & !ScreenEdge::ALL.bits(), 0, "invalid `ScreenEdge`"); + UIRectEdge(val.bits().into()) + }; + self.ivars().preferred_screen_edges_deferring_system_gestures.set(val); + let os_capabilities = app_state::os_capabilities(); + if os_capabilities.defer_system_gestures { + self.setNeedsUpdateOfScreenEdgesDeferringSystemGestures(); + } else { + os_capabilities.defer_system_gestures_err_msg("ignoring") + } + } + + pub(crate) fn set_supported_interface_orientations( + &self, + mtm: MainThreadMarker, + valid_orientations: ValidOrientations, + ) { + let mask = match (valid_orientations, UIDevice::currentDevice(mtm).userInterfaceIdiom()) { + (ValidOrientations::LandscapeAndPortrait, UIUserInterfaceIdiom::Phone) => { + UIInterfaceOrientationMask::AllButUpsideDown + }, + (ValidOrientations::LandscapeAndPortrait, _) => UIInterfaceOrientationMask::All, + (ValidOrientations::Landscape, _) => UIInterfaceOrientationMask::Landscape, + (ValidOrientations::Portrait, UIUserInterfaceIdiom::Phone) => { + UIInterfaceOrientationMask::Portrait + }, + (ValidOrientations::Portrait, _) => { + UIInterfaceOrientationMask::Portrait + | UIInterfaceOrientationMask::PortraitUpsideDown + }, + }; + self.ivars().supported_orientations.set(mask); + #[allow(deprecated)] + UIViewController::attemptRotationToDeviceOrientation(mtm); + } + + pub(crate) fn new( + mtm: MainThreadMarker, + window_attributes: &WindowAttributes, + view: &UIView, + ) -> Retained { + // These are set properly below, we just to set them to something in the meantime. + let this = mtm.alloc().set_ivars(ViewControllerState { + prefers_status_bar_hidden: Cell::new(false), + preferred_status_bar_style: Cell::new(UIStatusBarStyle::Default), + prefers_home_indicator_auto_hidden: Cell::new(false), + supported_orientations: Cell::new(UIInterfaceOrientationMask::All), + preferred_screen_edges_deferring_system_gestures: Cell::new(UIRectEdge::empty()), + }); + let this: Retained = unsafe { msg_send_id![super(this), init] }; + + this.set_prefers_status_bar_hidden( + window_attributes.platform_specific.prefers_status_bar_hidden, + ); + + this.set_preferred_status_bar_style( + window_attributes.platform_specific.preferred_status_bar_style, + ); + + this.set_supported_interface_orientations( + mtm, + window_attributes.platform_specific.valid_orientations, + ); + + this.set_prefers_home_indicator_auto_hidden( + window_attributes.platform_specific.prefers_home_indicator_hidden, + ); + + this.set_preferred_screen_edges_deferring_system_gestures( + window_attributes.platform_specific.preferred_screen_edges_deferring_system_gestures, + ); + + this.setView(Some(view)); + + this + } +} diff --git a/vendor/winit/src/platform_impl/ios/window.rs b/vendor/winit/src/platform_impl/ios/window.rs new file mode 100644 index 00000000..be027595 --- /dev/null +++ b/vendor/winit/src/platform_impl/ios/window.rs @@ -0,0 +1,750 @@ +#![allow(clippy::unnecessary_cast)] + +use std::collections::VecDeque; + +use objc2::rc::Retained; +use objc2::runtime::{AnyObject, NSObject}; +use objc2::{class, declare_class, msg_send, msg_send_id, mutability, ClassType, DeclaredClass}; +use objc2_foundation::{ + CGFloat, CGPoint, CGRect, CGSize, MainThreadBound, MainThreadMarker, NSObjectProtocol, +}; +use objc2_ui_kit::{ + UIApplication, UICoordinateSpace, UIResponder, UIScreen, UIScreenOverscanCompensation, + UIViewController, UIWindow, +}; +use tracing::{debug, warn}; + +use super::app_state::EventWrapper; +use super::view::WinitView; +use super::view_controller::WinitViewController; +use crate::cursor::Cursor; +use crate::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::event::{Event, WindowEvent}; +use crate::icon::Icon; +use crate::platform::ios::{ScreenEdge, StatusBarStyle, ValidOrientations}; +use crate::platform_impl::platform::{ + app_state, monitor, ActiveEventLoop, Fullscreen, MonitorHandle, +}; +use crate::window::{ + CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes, + WindowButtons, WindowId as RootWindowId, WindowLevel, +}; + +declare_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct WinitUIWindow; + + unsafe impl ClassType for WinitUIWindow { + #[inherits(UIResponder, NSObject)] + type Super = UIWindow; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitUIWindow"; + } + + impl DeclaredClass for WinitUIWindow {} + + unsafe impl WinitUIWindow { + #[method(becomeKeyWindow)] + fn become_key_window(&self) { + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(self.id()), + event: WindowEvent::Focused(true), + }), + ); + let _: () = unsafe { msg_send![super(self), becomeKeyWindow] }; + } + + #[method(resignKeyWindow)] + fn resign_key_window(&self) { + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(self.id()), + event: WindowEvent::Focused(false), + }), + ); + let _: () = unsafe { msg_send![super(self), resignKeyWindow] }; + } + } +); + +impl WinitUIWindow { + pub(crate) fn new( + mtm: MainThreadMarker, + window_attributes: &WindowAttributes, + frame: CGRect, + view_controller: &UIViewController, + ) -> Retained { + let this: Retained = unsafe { msg_send_id![mtm.alloc(), initWithFrame: frame] }; + + this.setRootViewController(Some(view_controller)); + + match window_attributes.fullscreen.clone().map(Into::into) { + Some(Fullscreen::Exclusive(ref video_mode)) => { + let monitor = video_mode.monitor(); + let screen = monitor.ui_screen(mtm); + screen.setCurrentMode(Some(video_mode.screen_mode(mtm))); + this.setScreen(screen); + }, + Some(Fullscreen::Borderless(Some(ref monitor))) => { + let screen = monitor.ui_screen(mtm); + this.setScreen(screen); + }, + _ => (), + } + + this + } + + pub(crate) fn id(&self) -> WindowId { + (self as *const Self as usize as u64).into() + } +} + +pub struct Inner { + window: Retained, + view_controller: Retained, + view: Retained, + gl_or_metal_backed: bool, +} + +impl Inner { + pub fn set_title(&self, _title: &str) { + debug!("`Window::set_title` is ignored on iOS") + } + + pub fn set_transparent(&self, _transparent: bool) { + debug!("`Window::set_transparent` is ignored on iOS") + } + + pub fn set_blur(&self, _blur: bool) { + debug!("`Window::set_blur` is ignored on iOS") + } + + pub fn set_visible(&self, visible: bool) { + self.window.setHidden(!visible) + } + + pub fn is_visible(&self) -> Option { + warn!("`Window::is_visible` is ignored on iOS"); + None + } + + pub fn request_redraw(&self) { + if self.gl_or_metal_backed { + let mtm = MainThreadMarker::new().unwrap(); + // `setNeedsDisplay` does nothing on UIViews which are directly backed by CAEAGLLayer or + // CAMetalLayer. Ordinarily the OS sets up a bunch of UIKit state before + // calling drawRect: on a UIView, but when using raw or gl/metal for drawing + // this work is completely avoided. + // + // The docs for `setNeedsDisplay` don't mention `CAMetalLayer`; however, this has been + // confirmed via testing. + // + // https://developer.apple.com/documentation/uikit/uiview/1622437-setneedsdisplay?language=objc + app_state::queue_gl_or_metal_redraw(mtm, self.window.clone()); + } else { + self.view.setNeedsDisplay(); + } + } + + pub fn pre_present_notify(&self) {} + + pub fn inner_position(&self) -> Result, NotSupportedError> { + let safe_area = self.safe_area_screen_space(); + let position = + LogicalPosition { x: safe_area.origin.x as f64, y: safe_area.origin.y as f64 }; + let scale_factor = self.scale_factor(); + Ok(position.to_physical(scale_factor)) + } + + pub fn outer_position(&self) -> Result, NotSupportedError> { + let screen_frame = self.screen_frame(); + let position = + LogicalPosition { x: screen_frame.origin.x as f64, y: screen_frame.origin.y as f64 }; + let scale_factor = self.scale_factor(); + Ok(position.to_physical(scale_factor)) + } + + pub fn set_outer_position(&self, physical_position: Position) { + let scale_factor = self.scale_factor(); + let position = physical_position.to_logical::(scale_factor); + let screen_frame = self.screen_frame(); + let new_screen_frame = CGRect { + origin: CGPoint { x: position.x as _, y: position.y as _ }, + size: screen_frame.size, + }; + let bounds = self.rect_from_screen_space(new_screen_frame); + self.window.setBounds(bounds); + } + + pub fn inner_size(&self) -> PhysicalSize { + let scale_factor = self.scale_factor(); + let safe_area = self.safe_area_screen_space(); + let size = LogicalSize { + width: safe_area.size.width as f64, + height: safe_area.size.height as f64, + }; + size.to_physical(scale_factor) + } + + pub fn outer_size(&self) -> PhysicalSize { + let scale_factor = self.scale_factor(); + let screen_frame = self.screen_frame(); + let size = LogicalSize { + width: screen_frame.size.width as f64, + height: screen_frame.size.height as f64, + }; + size.to_physical(scale_factor) + } + + pub fn request_inner_size(&self, _size: Size) -> Option> { + Some(self.inner_size()) + } + + pub fn set_min_inner_size(&self, _dimensions: Option) { + warn!("`Window::set_min_inner_size` is ignored on iOS") + } + + pub fn set_max_inner_size(&self, _dimensions: Option) { + warn!("`Window::set_max_inner_size` is ignored on iOS") + } + + pub fn resize_increments(&self) -> Option> { + None + } + + #[inline] + pub fn set_resize_increments(&self, _increments: Option) { + warn!("`Window::set_resize_increments` is ignored on iOS") + } + + pub fn set_resizable(&self, _resizable: bool) { + warn!("`Window::set_resizable` is ignored on iOS") + } + + pub fn is_resizable(&self) -> bool { + warn!("`Window::is_resizable` is ignored on iOS"); + false + } + + #[inline] + pub fn set_enabled_buttons(&self, _buttons: WindowButtons) { + warn!("`Window::set_enabled_buttons` is ignored on iOS"); + } + + #[inline] + pub fn enabled_buttons(&self) -> WindowButtons { + warn!("`Window::enabled_buttons` is ignored on iOS"); + WindowButtons::all() + } + + pub fn scale_factor(&self) -> f64 { + self.view.contentScaleFactor() as _ + } + + pub fn set_cursor(&self, _cursor: Cursor) { + debug!("`Window::set_cursor` ignored on iOS") + } + + pub fn set_cursor_position(&self, _position: Position) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + pub fn set_cursor_grab(&self, _: CursorGrabMode) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + pub fn set_cursor_visible(&self, _visible: bool) { + debug!("`Window::set_cursor_visible` is ignored on iOS") + } + + pub fn drag_window(&self) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + pub fn drag_resize_window(&self, _direction: ResizeDirection) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + + pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + pub fn set_minimized(&self, _minimized: bool) { + warn!("`Window::set_minimized` is ignored on iOS") + } + + pub fn is_minimized(&self) -> Option { + warn!("`Window::is_minimized` is ignored on iOS"); + None + } + + pub fn set_maximized(&self, _maximized: bool) { + warn!("`Window::set_maximized` is ignored on iOS") + } + + pub fn is_maximized(&self) -> bool { + warn!("`Window::is_maximized` is ignored on iOS"); + false + } + + pub(crate) fn set_fullscreen(&self, monitor: Option) { + let mtm = MainThreadMarker::new().unwrap(); + let uiscreen = match &monitor { + Some(Fullscreen::Exclusive(video_mode)) => { + let uiscreen = video_mode.monitor.ui_screen(mtm); + uiscreen.setCurrentMode(Some(video_mode.screen_mode(mtm))); + uiscreen.clone() + }, + Some(Fullscreen::Borderless(Some(monitor))) => monitor.ui_screen(mtm).clone(), + Some(Fullscreen::Borderless(None)) => { + self.current_monitor_inner().ui_screen(mtm).clone() + }, + None => { + warn!("`Window::set_fullscreen(None)` ignored on iOS"); + return; + }, + }; + + // this is pretty slow on iOS, so avoid doing it if we can + let current = self.window.screen(); + if uiscreen != current { + self.window.setScreen(&uiscreen); + } + + let bounds = uiscreen.bounds(); + self.window.setFrame(bounds); + + // For external displays, we must disable overscan compensation or + // the displayed image will have giant black bars surrounding it on + // each side + uiscreen.setOverscanCompensation(UIScreenOverscanCompensation::None); + } + + pub(crate) fn fullscreen(&self) -> Option { + let mtm = MainThreadMarker::new().unwrap(); + let monitor = self.current_monitor_inner(); + let uiscreen = monitor.ui_screen(mtm); + let screen_space_bounds = self.screen_frame(); + let screen_bounds = uiscreen.bounds(); + + // TODO: track fullscreen instead of relying on brittle float comparisons + if screen_space_bounds.origin.x == screen_bounds.origin.x + && screen_space_bounds.origin.y == screen_bounds.origin.y + && screen_space_bounds.size.width == screen_bounds.size.width + && screen_space_bounds.size.height == screen_bounds.size.height + { + Some(Fullscreen::Borderless(Some(monitor))) + } else { + None + } + } + + pub fn set_decorations(&self, _decorations: bool) {} + + pub fn is_decorated(&self) -> bool { + true + } + + pub fn set_window_level(&self, _level: WindowLevel) { + warn!("`Window::set_window_level` is ignored on iOS") + } + + pub fn set_window_icon(&self, _icon: Option) { + warn!("`Window::set_window_icon` is ignored on iOS") + } + + pub fn set_ime_cursor_area(&self, _position: Position, _size: Size) { + warn!("`Window::set_ime_cursor_area` is ignored on iOS") + } + + /// Show / hide the keyboard. To show the keyboard, we call `becomeFirstResponder`, + /// requesting focus for the [WinitView]. Since [WinitView] implements + /// [objc2_ui_kit::UIKeyInput], the keyboard will be shown. + /// + pub fn set_ime_allowed(&self, allowed: bool) { + if allowed { + unsafe { + self.view.becomeFirstResponder(); + } + } else { + unsafe { + self.view.resignFirstResponder(); + } + } + } + + pub fn set_ime_purpose(&self, _purpose: ImePurpose) { + warn!("`Window::set_ime_purpose` is ignored on iOS") + } + + pub fn focus_window(&self) { + warn!("`Window::set_focus` is ignored on iOS") + } + + pub fn request_user_attention(&self, _request_type: Option) { + warn!("`Window::request_user_attention` is ignored on iOS") + } + + // Allow directly accessing the current monitor internally without unwrapping. + fn current_monitor_inner(&self) -> MonitorHandle { + MonitorHandle::new(self.window.screen()) + } + + pub fn current_monitor(&self) -> Option { + Some(self.current_monitor_inner()) + } + + pub fn available_monitors(&self) -> VecDeque { + monitor::uiscreens(MainThreadMarker::new().unwrap()) + } + + pub fn primary_monitor(&self) -> Option { + #[allow(deprecated)] + Some(MonitorHandle::new(UIScreen::mainScreen(MainThreadMarker::new().unwrap()))) + } + + pub fn id(&self) -> WindowId { + self.window.id() + } + + #[cfg(feature = "rwh_04")] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut window_handle = rwh_04::UiKitHandle::empty(); + window_handle.ui_window = Retained::as_ptr(&self.window) as _; + window_handle.ui_view = Retained::as_ptr(&self.view) as _; + window_handle.ui_view_controller = Retained::as_ptr(&self.view_controller) as _; + rwh_04::RawWindowHandle::UiKit(window_handle) + } + + #[cfg(feature = "rwh_05")] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut window_handle = rwh_05::UiKitWindowHandle::empty(); + window_handle.ui_window = Retained::as_ptr(&self.window) as _; + window_handle.ui_view = Retained::as_ptr(&self.view) as _; + window_handle.ui_view_controller = Retained::as_ptr(&self.view_controller) as _; + rwh_05::RawWindowHandle::UiKit(window_handle) + } + + #[cfg(feature = "rwh_05")] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::UiKit(rwh_05::UiKitDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + pub fn raw_window_handle_rwh_06(&self) -> rwh_06::RawWindowHandle { + let mut window_handle = rwh_06::UiKitWindowHandle::new({ + let ui_view = Retained::as_ptr(&self.view) as _; + std::ptr::NonNull::new(ui_view).expect("Retained should never be null") + }); + window_handle.ui_view_controller = + std::ptr::NonNull::new(Retained::as_ptr(&self.view_controller) as _); + rwh_06::RawWindowHandle::UiKit(window_handle) + } + + pub fn theme(&self) -> Option { + warn!("`Window::theme` is ignored on iOS"); + None + } + + pub fn set_content_protected(&self, _protected: bool) {} + + pub fn has_focus(&self) -> bool { + self.window.isKeyWindow() + } + + #[inline] + pub fn set_theme(&self, _theme: Option) { + warn!("`Window::set_theme` is ignored on iOS"); + } + + pub fn title(&self) -> String { + warn!("`Window::title` is ignored on iOS"); + String::new() + } + + pub fn reset_dead_keys(&self) { + // Noop + } +} + +pub struct Window { + inner: MainThreadBound, +} + +impl Window { + pub(crate) fn new( + event_loop: &ActiveEventLoop, + window_attributes: WindowAttributes, + ) -> Result { + let mtm = event_loop.mtm; + + if window_attributes.min_inner_size.is_some() { + warn!("`WindowAttributes::min_inner_size` is ignored on iOS"); + } + if window_attributes.max_inner_size.is_some() { + warn!("`WindowAttributes::max_inner_size` is ignored on iOS"); + } + + // TODO: transparency, visible + + #[allow(deprecated)] + let main_screen = UIScreen::mainScreen(mtm); + let fullscreen = window_attributes.fullscreen.clone().map(Into::into); + let screen = match fullscreen { + Some(Fullscreen::Exclusive(ref video_mode)) => video_mode.monitor.ui_screen(mtm), + Some(Fullscreen::Borderless(Some(ref monitor))) => monitor.ui_screen(mtm), + Some(Fullscreen::Borderless(None)) | None => &main_screen, + }; + + let screen_bounds = screen.bounds(); + + let frame = match window_attributes.inner_size { + Some(dim) => { + let scale_factor = screen.scale(); + let size = dim.to_logical::(scale_factor as f64); + CGRect { + origin: screen_bounds.origin, + size: CGSize { width: size.width as _, height: size.height as _ }, + } + }, + None => screen_bounds, + }; + + let view = WinitView::new(mtm, &window_attributes, frame); + + let gl_or_metal_backed = + view.isKindOfClass(class!(CAMetalLayer)) || view.isKindOfClass(class!(CAEAGLLayer)); + + let view_controller = WinitViewController::new(mtm, &window_attributes, &view); + let window = WinitUIWindow::new(mtm, &window_attributes, frame, &view_controller); + + app_state::set_key_window(mtm, &window); + + // Like the Windows and macOS backends, we send a `ScaleFactorChanged` and `Resized` + // event on window creation if the DPI factor != 1.0 + let scale_factor = view.contentScaleFactor(); + let scale_factor = scale_factor as f64; + if scale_factor != 1.0 { + let bounds = view.bounds(); + let screen = window.screen(); + let screen_space = screen.coordinateSpace(); + let screen_frame = view.convertRect_toCoordinateSpace(bounds, &screen_space); + let size = LogicalSize { + width: screen_frame.size.width as f64, + height: screen_frame.size.height as f64, + }; + let window_id = RootWindowId(window.id()); + app_state::handle_nonuser_events( + mtm, + std::iter::once(EventWrapper::ScaleFactorChanged(app_state::ScaleFactorChanged { + window: window.clone(), + scale_factor, + suggested_size: size.to_physical(scale_factor), + })) + .chain(std::iter::once(EventWrapper::StaticEvent( + Event::WindowEvent { + window_id, + event: WindowEvent::Resized(size.to_physical(scale_factor)), + }, + ))), + ); + } + + let inner = Inner { window, view_controller, view, gl_or_metal_backed }; + Ok(Window { inner: MainThreadBound::new(inner, mtm) }) + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Inner) + Send + 'static) { + // For now, don't actually do queuing, since it may be less predictable + self.maybe_wait_on_main(f) + } + + pub(crate) fn maybe_wait_on_main(&self, f: impl FnOnce(&Inner) -> R + Send) -> R { + self.inner.get_on_main(|inner| f(inner)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub(crate) fn raw_window_handle_rwh_06( + &self, + ) -> Result { + if let Some(mtm) = MainThreadMarker::new() { + Ok(self.inner.get(mtm).raw_window_handle_rwh_06()) + } else { + Err(rwh_06::HandleError::Unavailable) + } + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub(crate) fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::UiKit(rwh_06::UiKitDisplayHandle::new())) + } +} + +// WindowExtIOS +impl Inner { + pub fn set_scale_factor(&self, scale_factor: f64) { + assert!( + dpi::validate_scale_factor(scale_factor), + "`WindowExtIOS::set_scale_factor` received an invalid hidpi factor" + ); + let scale_factor = scale_factor as CGFloat; + self.view.setContentScaleFactor(scale_factor); + } + + pub fn set_valid_orientations(&self, valid_orientations: ValidOrientations) { + self.view_controller.set_supported_interface_orientations( + MainThreadMarker::new().unwrap(), + valid_orientations, + ); + } + + pub fn set_prefers_home_indicator_hidden(&self, hidden: bool) { + self.view_controller.set_prefers_home_indicator_auto_hidden(hidden); + } + + pub fn set_preferred_screen_edges_deferring_system_gestures(&self, edges: ScreenEdge) { + self.view_controller.set_preferred_screen_edges_deferring_system_gestures(edges); + } + + pub fn set_prefers_status_bar_hidden(&self, hidden: bool) { + self.view_controller.set_prefers_status_bar_hidden(hidden); + } + + pub fn set_preferred_status_bar_style(&self, status_bar_style: StatusBarStyle) { + self.view_controller.set_preferred_status_bar_style(status_bar_style); + } + + pub fn recognize_pinch_gesture(&self, should_recognize: bool) { + self.view.recognize_pinch_gesture(should_recognize); + } + + pub fn recognize_pan_gesture( + &self, + should_recognize: bool, + minimum_number_of_touches: u8, + maximum_number_of_touches: u8, + ) { + self.view.recognize_pan_gesture( + should_recognize, + minimum_number_of_touches, + maximum_number_of_touches, + ); + } + + pub fn recognize_doubletap_gesture(&self, should_recognize: bool) { + self.view.recognize_doubletap_gesture(should_recognize); + } + + pub fn recognize_rotation_gesture(&self, should_recognize: bool) { + self.view.recognize_rotation_gesture(should_recognize); + } +} + +impl Inner { + fn screen_frame(&self) -> CGRect { + self.rect_to_screen_space(self.window.bounds()) + } + + fn rect_to_screen_space(&self, rect: CGRect) -> CGRect { + let screen_space = self.window.screen().coordinateSpace(); + self.window.convertRect_toCoordinateSpace(rect, &screen_space) + } + + fn rect_from_screen_space(&self, rect: CGRect) -> CGRect { + let screen_space = self.window.screen().coordinateSpace(); + self.window.convertRect_fromCoordinateSpace(rect, &screen_space) + } + + fn safe_area_screen_space(&self) -> CGRect { + let bounds = self.window.bounds(); + if app_state::os_capabilities().safe_area { + let safe_area = self.window.safeAreaInsets(); + let safe_bounds = CGRect { + origin: CGPoint { + x: bounds.origin.x + safe_area.left, + y: bounds.origin.y + safe_area.top, + }, + size: CGSize { + width: bounds.size.width - safe_area.left - safe_area.right, + height: bounds.size.height - safe_area.top - safe_area.bottom, + }, + }; + self.rect_to_screen_space(safe_bounds) + } else { + let screen_frame = self.rect_to_screen_space(bounds); + let status_bar_frame = { + let app = UIApplication::sharedApplication(MainThreadMarker::new().unwrap()); + #[allow(deprecated)] + app.statusBarFrame() + }; + let (y, height) = if screen_frame.origin.y > status_bar_frame.size.height { + (screen_frame.origin.y, screen_frame.size.height) + } else { + let y = status_bar_frame.size.height; + let height = screen_frame.size.height + - (status_bar_frame.size.height - screen_frame.origin.y); + (y, height) + }; + CGRect { + origin: CGPoint { x: screen_frame.origin.x, y }, + size: CGSize { width: screen_frame.size.width, height }, + } + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId { + window: *mut WinitUIWindow, +} + +impl WindowId { + pub const fn dummy() -> Self { + WindowId { window: std::ptr::null_mut() } + } +} + +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.window as u64 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self { window: raw_id as _ } + } +} + +unsafe impl Send for WindowId {} +unsafe impl Sync for WindowId {} + +impl From<&AnyObject> for WindowId { + fn from(window: &AnyObject) -> WindowId { + WindowId { window: window as *const _ as _ } + } +} + +#[derive(Clone, Debug, Default)] +pub struct PlatformSpecificWindowAttributes { + pub scale_factor: Option, + pub valid_orientations: ValidOrientations, + pub prefers_home_indicator_hidden: bool, + pub prefers_status_bar_hidden: bool, + pub preferred_status_bar_style: StatusBarStyle, + pub preferred_screen_edges_deferring_system_gestures: ScreenEdge, +} diff --git a/vendor/winit/src/platform_impl/linux/common/mod.rs b/vendor/winit/src/platform_impl/linux/common/mod.rs new file mode 100644 index 00000000..ed7f82cd --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/common/mod.rs @@ -0,0 +1 @@ +pub mod xkb; diff --git a/vendor/winit/src/platform_impl/linux/common/xkb/compose.rs b/vendor/winit/src/platform_impl/linux/common/xkb/compose.rs new file mode 100644 index 00000000..9d4f590a --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/common/xkb/compose.rs @@ -0,0 +1,123 @@ +//! XKB compose handling. + +use std::env; +use std::ffi::CString; +use std::ops::Deref; +use std::os::unix::ffi::OsStringExt; +use std::ptr::NonNull; + +use super::{XkbContext, XKBCH}; +use smol_str::SmolStr; +use xkbcommon_dl::{ + xkb_compose_compile_flags, xkb_compose_feed_result, xkb_compose_state, xkb_compose_state_flags, + xkb_compose_status, xkb_compose_table, xkb_keysym_t, +}; + +#[derive(Debug)] +pub struct XkbComposeTable { + table: NonNull, +} + +impl XkbComposeTable { + pub fn new(context: &XkbContext) -> Option { + let locale = env::var_os("LC_ALL") + .and_then(|v| if v.is_empty() { None } else { Some(v) }) + .or_else(|| env::var_os("LC_CTYPE")) + .and_then(|v| if v.is_empty() { None } else { Some(v) }) + .or_else(|| env::var_os("LANG")) + .and_then(|v| if v.is_empty() { None } else { Some(v) }) + .unwrap_or_else(|| "C".into()); + let locale = CString::new(locale.into_vec()).unwrap(); + + let table = unsafe { + (XKBCH.xkb_compose_table_new_from_locale)( + context.as_ptr(), + locale.as_ptr(), + xkb_compose_compile_flags::XKB_COMPOSE_COMPILE_NO_FLAGS, + ) + }; + + let table = NonNull::new(table)?; + Some(Self { table }) + } + + /// Create new state with the given compose table. + pub fn new_state(&self) -> Option { + let state = unsafe { + (XKBCH.xkb_compose_state_new)( + self.table.as_ptr(), + xkb_compose_state_flags::XKB_COMPOSE_STATE_NO_FLAGS, + ) + }; + + let state = NonNull::new(state)?; + Some(XkbComposeState { state }) + } +} + +impl Deref for XkbComposeTable { + type Target = NonNull; + + fn deref(&self) -> &Self::Target { + &self.table + } +} + +impl Drop for XkbComposeTable { + fn drop(&mut self) { + unsafe { + (XKBCH.xkb_compose_table_unref)(self.table.as_ptr()); + } + } +} + +#[derive(Debug)] +pub struct XkbComposeState { + state: NonNull, +} + +impl XkbComposeState { + pub fn get_string(&mut self, scratch_buffer: &mut Vec) -> Option { + super::make_string_with(scratch_buffer, |ptr, len| unsafe { + (XKBCH.xkb_compose_state_get_utf8)(self.state.as_ptr(), ptr, len) + }) + } + + #[inline] + pub fn feed(&mut self, keysym: xkb_keysym_t) -> ComposeStatus { + let feed_result = unsafe { (XKBCH.xkb_compose_state_feed)(self.state.as_ptr(), keysym) }; + match feed_result { + xkb_compose_feed_result::XKB_COMPOSE_FEED_IGNORED => ComposeStatus::Ignored, + xkb_compose_feed_result::XKB_COMPOSE_FEED_ACCEPTED => { + ComposeStatus::Accepted(self.status()) + }, + } + } + + #[inline] + pub fn reset(&mut self) { + unsafe { + (XKBCH.xkb_compose_state_reset)(self.state.as_ptr()); + } + } + + #[inline] + pub fn status(&mut self) -> xkb_compose_status { + unsafe { (XKBCH.xkb_compose_state_get_status)(self.state.as_ptr()) } + } +} + +impl Drop for XkbComposeState { + fn drop(&mut self) { + unsafe { + (XKBCH.xkb_compose_state_unref)(self.state.as_ptr()); + }; + } +} + +#[derive(Copy, Clone, Debug)] +pub enum ComposeStatus { + Accepted(xkb_compose_status), + Ignored, + None, +} diff --git a/vendor/winit/src/platform_impl/linux/common/xkb/keymap.rs b/vendor/winit/src/platform_impl/linux/common/xkb/keymap.rs new file mode 100644 index 00000000..67aac5c1 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/common/xkb/keymap.rs @@ -0,0 +1,1044 @@ +//! XKB keymap. + +use std::ffi::c_char; +use std::ops::Deref; +use std::ptr::{self, NonNull}; + +#[cfg(x11_platform)] +use x11_dl::xlib_xcb::xcb_connection_t; +#[cfg(wayland_platform)] +use {memmap2::MmapOptions, std::os::unix::io::OwnedFd}; + +use xkb::XKB_MOD_INVALID; +use xkbcommon_dl::{ + self as xkb, xkb_keycode_t, xkb_keymap, xkb_keymap_compile_flags, xkb_keysym_t, + xkb_layout_index_t, xkb_mod_index_t, +}; + +use crate::keyboard::{Key, KeyCode, KeyLocation, NamedKey, NativeKey, NativeKeyCode, PhysicalKey}; +#[cfg(x11_platform)] +use crate::platform_impl::common::xkb::XKBXH; +use crate::platform_impl::common::xkb::{XkbContext, XKBH}; + +/// Map the raw X11-style keycode to the `KeyCode` enum. +/// +/// X11-style keycodes are offset by 8 from the keycodes the Linux kernel uses. +pub fn raw_keycode_to_physicalkey(keycode: u32) -> PhysicalKey { + scancode_to_physicalkey(keycode.saturating_sub(8)) +} + +/// Map the linux scancode to Keycode. +/// +/// Both X11 and Wayland use keys with `+ 8` offset to linux scancode. +pub fn scancode_to_physicalkey(scancode: u32) -> PhysicalKey { + // The keycode values are taken from linux/include/uapi/linux/input-event-codes.h, as + // libxkbcommon's documentation seems to suggest that the keycode values we're interested in + // are defined by the Linux kernel. If Winit programs end up being run on other Unix-likes, + // I can only hope they agree on what the keycodes mean. + // + // Some of the keycodes are likely superfluous for our purposes, and some are ones which are + // difficult to test the correctness of, or discover the purpose of. Because of this, they've + // either been commented out here, or not included at all. + PhysicalKey::Code(match scancode { + 0 => return PhysicalKey::Unidentified(NativeKeyCode::Xkb(0)), + 1 => KeyCode::Escape, + 2 => KeyCode::Digit1, + 3 => KeyCode::Digit2, + 4 => KeyCode::Digit3, + 5 => KeyCode::Digit4, + 6 => KeyCode::Digit5, + 7 => KeyCode::Digit6, + 8 => KeyCode::Digit7, + 9 => KeyCode::Digit8, + 10 => KeyCode::Digit9, + 11 => KeyCode::Digit0, + 12 => KeyCode::Minus, + 13 => KeyCode::Equal, + 14 => KeyCode::Backspace, + 15 => KeyCode::Tab, + 16 => KeyCode::KeyQ, + 17 => KeyCode::KeyW, + 18 => KeyCode::KeyE, + 19 => KeyCode::KeyR, + 20 => KeyCode::KeyT, + 21 => KeyCode::KeyY, + 22 => KeyCode::KeyU, + 23 => KeyCode::KeyI, + 24 => KeyCode::KeyO, + 25 => KeyCode::KeyP, + 26 => KeyCode::BracketLeft, + 27 => KeyCode::BracketRight, + 28 => KeyCode::Enter, + 29 => KeyCode::ControlLeft, + 30 => KeyCode::KeyA, + 31 => KeyCode::KeyS, + 32 => KeyCode::KeyD, + 33 => KeyCode::KeyF, + 34 => KeyCode::KeyG, + 35 => KeyCode::KeyH, + 36 => KeyCode::KeyJ, + 37 => KeyCode::KeyK, + 38 => KeyCode::KeyL, + 39 => KeyCode::Semicolon, + 40 => KeyCode::Quote, + 41 => KeyCode::Backquote, + 42 => KeyCode::ShiftLeft, + 43 => KeyCode::Backslash, + 44 => KeyCode::KeyZ, + 45 => KeyCode::KeyX, + 46 => KeyCode::KeyC, + 47 => KeyCode::KeyV, + 48 => KeyCode::KeyB, + 49 => KeyCode::KeyN, + 50 => KeyCode::KeyM, + 51 => KeyCode::Comma, + 52 => KeyCode::Period, + 53 => KeyCode::Slash, + 54 => KeyCode::ShiftRight, + 55 => KeyCode::NumpadMultiply, + 56 => KeyCode::AltLeft, + 57 => KeyCode::Space, + 58 => KeyCode::CapsLock, + 59 => KeyCode::F1, + 60 => KeyCode::F2, + 61 => KeyCode::F3, + 62 => KeyCode::F4, + 63 => KeyCode::F5, + 64 => KeyCode::F6, + 65 => KeyCode::F7, + 66 => KeyCode::F8, + 67 => KeyCode::F9, + 68 => KeyCode::F10, + 69 => KeyCode::NumLock, + 70 => KeyCode::ScrollLock, + 71 => KeyCode::Numpad7, + 72 => KeyCode::Numpad8, + 73 => KeyCode::Numpad9, + 74 => KeyCode::NumpadSubtract, + 75 => KeyCode::Numpad4, + 76 => KeyCode::Numpad5, + 77 => KeyCode::Numpad6, + 78 => KeyCode::NumpadAdd, + 79 => KeyCode::Numpad1, + 80 => KeyCode::Numpad2, + 81 => KeyCode::Numpad3, + 82 => KeyCode::Numpad0, + 83 => KeyCode::NumpadDecimal, + 85 => KeyCode::Lang5, + 86 => KeyCode::IntlBackslash, + 87 => KeyCode::F11, + 88 => KeyCode::F12, + 89 => KeyCode::IntlRo, + 90 => KeyCode::Lang3, + 91 => KeyCode::Lang4, + 92 => KeyCode::Convert, + 93 => KeyCode::KanaMode, + 94 => KeyCode::NonConvert, + // 95 => KeyCode::KPJPCOMMA, + 96 => KeyCode::NumpadEnter, + 97 => KeyCode::ControlRight, + 98 => KeyCode::NumpadDivide, + 99 => KeyCode::PrintScreen, + 100 => KeyCode::AltRight, + // 101 => KeyCode::LINEFEED, + 102 => KeyCode::Home, + 103 => KeyCode::ArrowUp, + 104 => KeyCode::PageUp, + 105 => KeyCode::ArrowLeft, + 106 => KeyCode::ArrowRight, + 107 => KeyCode::End, + 108 => KeyCode::ArrowDown, + 109 => KeyCode::PageDown, + 110 => KeyCode::Insert, + 111 => KeyCode::Delete, + // 112 => KeyCode::MACRO, + 113 => KeyCode::AudioVolumeMute, + 114 => KeyCode::AudioVolumeDown, + 115 => KeyCode::AudioVolumeUp, + // 116 => KeyCode::POWER, + 117 => KeyCode::NumpadEqual, + // 118 => KeyCode::KPPLUSMINUS, + 119 => KeyCode::Pause, + // 120 => KeyCode::SCALE, + 121 => KeyCode::NumpadComma, + 122 => KeyCode::Lang1, + 123 => KeyCode::Lang2, + 124 => KeyCode::IntlYen, + 125 => KeyCode::SuperLeft, + 126 => KeyCode::SuperRight, + 127 => KeyCode::ContextMenu, + // 128 => KeyCode::STOP, + // 129 => KeyCode::AGAIN, + // 130 => KeyCode::PROPS, + // 131 => KeyCode::UNDO, + // 132 => KeyCode::FRONT, + // 133 => KeyCode::COPY, + // 134 => KeyCode::OPEN, + // 135 => KeyCode::PASTE, + // 136 => KeyCode::FIND, + // 137 => KeyCode::CUT, + // 138 => KeyCode::HELP, + // 139 => KeyCode::MENU, + // 140 => KeyCode::CALC, + // 141 => KeyCode::SETUP, + // 142 => KeyCode::SLEEP, + // 143 => KeyCode::WAKEUP, + // 144 => KeyCode::FILE, + // 145 => KeyCode::SENDFILE, + // 146 => KeyCode::DELETEFILE, + // 147 => KeyCode::XFER, + // 148 => KeyCode::PROG1, + // 149 => KeyCode::PROG2, + // 150 => KeyCode::WWW, + // 151 => KeyCode::MSDOS, + // 152 => KeyCode::COFFEE, + // 153 => KeyCode::ROTATE_DISPLAY, + // 154 => KeyCode::CYCLEWINDOWS, + // 155 => KeyCode::MAIL, + // 156 => KeyCode::BOOKMARKS, + // 157 => KeyCode::COMPUTER, + // 158 => KeyCode::BACK, + // 159 => KeyCode::FORWARD, + // 160 => KeyCode::CLOSECD, + // 161 => KeyCode::EJECTCD, + // 162 => KeyCode::EJECTCLOSECD, + 163 => KeyCode::MediaTrackNext, + 164 => KeyCode::MediaPlayPause, + 165 => KeyCode::MediaTrackPrevious, + 166 => KeyCode::MediaStop, + // 167 => KeyCode::RECORD, + // 168 => KeyCode::REWIND, + // 169 => KeyCode::PHONE, + // 170 => KeyCode::ISO, + // 171 => KeyCode::CONFIG, + // 172 => KeyCode::HOMEPAGE, + // 173 => KeyCode::REFRESH, + // 174 => KeyCode::EXIT, + // 175 => KeyCode::MOVE, + // 176 => KeyCode::EDIT, + // 177 => KeyCode::SCROLLUP, + // 178 => KeyCode::SCROLLDOWN, + // 179 => KeyCode::KPLEFTPAREN, + // 180 => KeyCode::KPRIGHTPAREN, + // 181 => KeyCode::NEW, + // 182 => KeyCode::REDO, + 183 => KeyCode::F13, + 184 => KeyCode::F14, + 185 => KeyCode::F15, + 186 => KeyCode::F16, + 187 => KeyCode::F17, + 188 => KeyCode::F18, + 189 => KeyCode::F19, + 190 => KeyCode::F20, + 191 => KeyCode::F21, + 192 => KeyCode::F22, + 193 => KeyCode::F23, + 194 => KeyCode::F24, + // 200 => KeyCode::PLAYCD, + // 201 => KeyCode::PAUSECD, + // 202 => KeyCode::PROG3, + // 203 => KeyCode::PROG4, + // 204 => KeyCode::DASHBOARD, + // 205 => KeyCode::SUSPEND, + // 206 => KeyCode::CLOSE, + // 207 => KeyCode::PLAY, + // 208 => KeyCode::FASTFORWARD, + // 209 => KeyCode::BASSBOOST, + // 210 => KeyCode::PRINT, + // 211 => KeyCode::HP, + // 212 => KeyCode::CAMERA, + // 213 => KeyCode::SOUND, + // 214 => KeyCode::QUESTION, + // 215 => KeyCode::EMAIL, + // 216 => KeyCode::CHAT, + // 217 => KeyCode::SEARCH, + // 218 => KeyCode::CONNECT, + // 219 => KeyCode::FINANCE, + // 220 => KeyCode::SPORT, + // 221 => KeyCode::SHOP, + // 222 => KeyCode::ALTERASE, + // 223 => KeyCode::CANCEL, + // 224 => KeyCode::BRIGHTNESSDOW, + // 225 => KeyCode::BRIGHTNESSU, + // 226 => KeyCode::MEDIA, + // 227 => KeyCode::SWITCHVIDEOMODE, + // 228 => KeyCode::KBDILLUMTOGGLE, + // 229 => KeyCode::KBDILLUMDOWN, + // 230 => KeyCode::KBDILLUMUP, + // 231 => KeyCode::SEND, + // 232 => KeyCode::REPLY, + // 233 => KeyCode::FORWARDMAIL, + // 234 => KeyCode::SAVE, + // 235 => KeyCode::DOCUMENTS, + // 236 => KeyCode::BATTERY, + // 237 => KeyCode::BLUETOOTH, + // 238 => KeyCode::WLAN, + // 239 => KeyCode::UWB, + 240 => return PhysicalKey::Unidentified(NativeKeyCode::Unidentified), + // 241 => KeyCode::VIDEO_NEXT, + // 242 => KeyCode::VIDEO_PREV, + // 243 => KeyCode::BRIGHTNESS_CYCLE, + // 244 => KeyCode::BRIGHTNESS_AUTO, + // 245 => KeyCode::DISPLAY_OFF, + // 246 => KeyCode::WWAN, + // 247 => KeyCode::RFKILL, + // 248 => KeyCode::KEY_MICMUTE, + _ => return PhysicalKey::Unidentified(NativeKeyCode::Xkb(scancode)), + }) +} + +pub fn physicalkey_to_scancode(key: PhysicalKey) -> Option { + let code = match key { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(code) => { + return match code { + NativeKeyCode::Unidentified => Some(240), + NativeKeyCode::Xkb(raw) => Some(raw), + _ => None, + }; + }, + }; + + match code { + KeyCode::Escape => Some(1), + KeyCode::Digit1 => Some(2), + KeyCode::Digit2 => Some(3), + KeyCode::Digit3 => Some(4), + KeyCode::Digit4 => Some(5), + KeyCode::Digit5 => Some(6), + KeyCode::Digit6 => Some(7), + KeyCode::Digit7 => Some(8), + KeyCode::Digit8 => Some(9), + KeyCode::Digit9 => Some(10), + KeyCode::Digit0 => Some(11), + KeyCode::Minus => Some(12), + KeyCode::Equal => Some(13), + KeyCode::Backspace => Some(14), + KeyCode::Tab => Some(15), + KeyCode::KeyQ => Some(16), + KeyCode::KeyW => Some(17), + KeyCode::KeyE => Some(18), + KeyCode::KeyR => Some(19), + KeyCode::KeyT => Some(20), + KeyCode::KeyY => Some(21), + KeyCode::KeyU => Some(22), + KeyCode::KeyI => Some(23), + KeyCode::KeyO => Some(24), + KeyCode::KeyP => Some(25), + KeyCode::BracketLeft => Some(26), + KeyCode::BracketRight => Some(27), + KeyCode::Enter => Some(28), + KeyCode::ControlLeft => Some(29), + KeyCode::KeyA => Some(30), + KeyCode::KeyS => Some(31), + KeyCode::KeyD => Some(32), + KeyCode::KeyF => Some(33), + KeyCode::KeyG => Some(34), + KeyCode::KeyH => Some(35), + KeyCode::KeyJ => Some(36), + KeyCode::KeyK => Some(37), + KeyCode::KeyL => Some(38), + KeyCode::Semicolon => Some(39), + KeyCode::Quote => Some(40), + KeyCode::Backquote => Some(41), + KeyCode::ShiftLeft => Some(42), + KeyCode::Backslash => Some(43), + KeyCode::KeyZ => Some(44), + KeyCode::KeyX => Some(45), + KeyCode::KeyC => Some(46), + KeyCode::KeyV => Some(47), + KeyCode::KeyB => Some(48), + KeyCode::KeyN => Some(49), + KeyCode::KeyM => Some(50), + KeyCode::Comma => Some(51), + KeyCode::Period => Some(52), + KeyCode::Slash => Some(53), + KeyCode::ShiftRight => Some(54), + KeyCode::NumpadMultiply => Some(55), + KeyCode::AltLeft => Some(56), + KeyCode::Space => Some(57), + KeyCode::CapsLock => Some(58), + KeyCode::F1 => Some(59), + KeyCode::F2 => Some(60), + KeyCode::F3 => Some(61), + KeyCode::F4 => Some(62), + KeyCode::F5 => Some(63), + KeyCode::F6 => Some(64), + KeyCode::F7 => Some(65), + KeyCode::F8 => Some(66), + KeyCode::F9 => Some(67), + KeyCode::F10 => Some(68), + KeyCode::NumLock => Some(69), + KeyCode::ScrollLock => Some(70), + KeyCode::Numpad7 => Some(71), + KeyCode::Numpad8 => Some(72), + KeyCode::Numpad9 => Some(73), + KeyCode::NumpadSubtract => Some(74), + KeyCode::Numpad4 => Some(75), + KeyCode::Numpad5 => Some(76), + KeyCode::Numpad6 => Some(77), + KeyCode::NumpadAdd => Some(78), + KeyCode::Numpad1 => Some(79), + KeyCode::Numpad2 => Some(80), + KeyCode::Numpad3 => Some(81), + KeyCode::Numpad0 => Some(82), + KeyCode::NumpadDecimal => Some(83), + KeyCode::Lang5 => Some(85), + KeyCode::IntlBackslash => Some(86), + KeyCode::F11 => Some(87), + KeyCode::F12 => Some(88), + KeyCode::IntlRo => Some(89), + KeyCode::Lang3 => Some(90), + KeyCode::Lang4 => Some(91), + KeyCode::Convert => Some(92), + KeyCode::KanaMode => Some(93), + KeyCode::NonConvert => Some(94), + KeyCode::NumpadEnter => Some(96), + KeyCode::ControlRight => Some(97), + KeyCode::NumpadDivide => Some(98), + KeyCode::PrintScreen => Some(99), + KeyCode::AltRight => Some(100), + KeyCode::Home => Some(102), + KeyCode::ArrowUp => Some(103), + KeyCode::PageUp => Some(104), + KeyCode::ArrowLeft => Some(105), + KeyCode::ArrowRight => Some(106), + KeyCode::End => Some(107), + KeyCode::ArrowDown => Some(108), + KeyCode::PageDown => Some(109), + KeyCode::Insert => Some(110), + KeyCode::Delete => Some(111), + KeyCode::AudioVolumeMute => Some(113), + KeyCode::AudioVolumeDown => Some(114), + KeyCode::AudioVolumeUp => Some(115), + KeyCode::NumpadEqual => Some(117), + KeyCode::Pause => Some(119), + KeyCode::NumpadComma => Some(121), + KeyCode::Lang1 => Some(122), + KeyCode::Lang2 => Some(123), + KeyCode::IntlYen => Some(124), + KeyCode::SuperLeft => Some(125), + KeyCode::SuperRight => Some(126), + KeyCode::ContextMenu => Some(127), + KeyCode::MediaTrackNext => Some(163), + KeyCode::MediaPlayPause => Some(164), + KeyCode::MediaTrackPrevious => Some(165), + KeyCode::MediaStop => Some(166), + KeyCode::F13 => Some(183), + KeyCode::F14 => Some(184), + KeyCode::F15 => Some(185), + KeyCode::F16 => Some(186), + KeyCode::F17 => Some(187), + KeyCode::F18 => Some(188), + KeyCode::F19 => Some(189), + KeyCode::F20 => Some(190), + KeyCode::F21 => Some(191), + KeyCode::F22 => Some(192), + KeyCode::F23 => Some(193), + KeyCode::F24 => Some(194), + _ => None, + } +} + +pub fn keysym_to_key(keysym: u32) -> Key { + use xkbcommon_dl::keysyms; + Key::Named(match keysym { + // TTY function keys + keysyms::BackSpace => NamedKey::Backspace, + keysyms::Tab => NamedKey::Tab, + // keysyms::Linefeed => NamedKey::Linefeed, + keysyms::Clear => NamedKey::Clear, + keysyms::Return => NamedKey::Enter, + keysyms::Pause => NamedKey::Pause, + keysyms::Scroll_Lock => NamedKey::ScrollLock, + keysyms::Sys_Req => NamedKey::PrintScreen, + keysyms::Escape => NamedKey::Escape, + keysyms::Delete => NamedKey::Delete, + + // IME keys + keysyms::Multi_key => NamedKey::Compose, + keysyms::Codeinput => NamedKey::CodeInput, + keysyms::SingleCandidate => NamedKey::SingleCandidate, + keysyms::MultipleCandidate => NamedKey::AllCandidates, + keysyms::PreviousCandidate => NamedKey::PreviousCandidate, + + // Japanese keys + keysyms::Kanji => NamedKey::KanjiMode, + keysyms::Muhenkan => NamedKey::NonConvert, + keysyms::Henkan_Mode => NamedKey::Convert, + keysyms::Romaji => NamedKey::Romaji, + keysyms::Hiragana => NamedKey::Hiragana, + keysyms::Hiragana_Katakana => NamedKey::HiraganaKatakana, + keysyms::Zenkaku => NamedKey::Zenkaku, + keysyms::Hankaku => NamedKey::Hankaku, + keysyms::Zenkaku_Hankaku => NamedKey::ZenkakuHankaku, + // keysyms::Touroku => NamedKey::Touroku, + // keysyms::Massyo => NamedKey::Massyo, + keysyms::Kana_Lock => NamedKey::KanaMode, + keysyms::Kana_Shift => NamedKey::KanaMode, + keysyms::Eisu_Shift => NamedKey::Alphanumeric, + keysyms::Eisu_toggle => NamedKey::Alphanumeric, + // NOTE: The next three items are aliases for values we've already mapped. + // keysyms::Kanji_Bangou => NamedKey::CodeInput, + // keysyms::Zen_Koho => NamedKey::AllCandidates, + // keysyms::Mae_Koho => NamedKey::PreviousCandidate, + + // Cursor control & motion + keysyms::Home => NamedKey::Home, + keysyms::Left => NamedKey::ArrowLeft, + keysyms::Up => NamedKey::ArrowUp, + keysyms::Right => NamedKey::ArrowRight, + keysyms::Down => NamedKey::ArrowDown, + // keysyms::Prior => NamedKey::PageUp, + keysyms::Page_Up => NamedKey::PageUp, + // keysyms::Next => NamedKey::PageDown, + keysyms::Page_Down => NamedKey::PageDown, + keysyms::End => NamedKey::End, + // keysyms::Begin => NamedKey::Begin, + + // Misc. functions + keysyms::Select => NamedKey::Select, + keysyms::Print => NamedKey::PrintScreen, + keysyms::Execute => NamedKey::Execute, + keysyms::Insert => NamedKey::Insert, + keysyms::Undo => NamedKey::Undo, + keysyms::Redo => NamedKey::Redo, + keysyms::Menu => NamedKey::ContextMenu, + keysyms::Find => NamedKey::Find, + keysyms::Cancel => NamedKey::Cancel, + keysyms::Help => NamedKey::Help, + keysyms::Break => NamedKey::Pause, + keysyms::Mode_switch => NamedKey::ModeChange, + // keysyms::script_switch => NamedKey::ModeChange, + keysyms::Num_Lock => NamedKey::NumLock, + + // Keypad keys + // keysyms::KP_Space => return Key::Character(" "), + keysyms::KP_Tab => NamedKey::Tab, + keysyms::KP_Enter => NamedKey::Enter, + keysyms::KP_F1 => NamedKey::F1, + keysyms::KP_F2 => NamedKey::F2, + keysyms::KP_F3 => NamedKey::F3, + keysyms::KP_F4 => NamedKey::F4, + keysyms::KP_Home => NamedKey::Home, + keysyms::KP_Left => NamedKey::ArrowLeft, + keysyms::KP_Up => NamedKey::ArrowUp, + keysyms::KP_Right => NamedKey::ArrowRight, + keysyms::KP_Down => NamedKey::ArrowDown, + // keysyms::KP_Prior => NamedKey::PageUp, + keysyms::KP_Page_Up => NamedKey::PageUp, + // keysyms::KP_Next => NamedKey::PageDown, + keysyms::KP_Page_Down => NamedKey::PageDown, + keysyms::KP_End => NamedKey::End, + // This is the key labeled "5" on the numpad when NumLock is off. + // keysyms::KP_Begin => NamedKey::Begin, + keysyms::KP_Insert => NamedKey::Insert, + keysyms::KP_Delete => NamedKey::Delete, + // keysyms::KP_Equal => NamedKey::Equal, + // keysyms::KP_Multiply => NamedKey::Multiply, + // keysyms::KP_Add => NamedKey::Add, + // keysyms::KP_Separator => NamedKey::Separator, + // keysyms::KP_Subtract => NamedKey::Subtract, + // keysyms::KP_Decimal => NamedKey::Decimal, + // keysyms::KP_Divide => NamedKey::Divide, + + // keysyms::KP_0 => return Key::Character("0"), + // keysyms::KP_1 => return Key::Character("1"), + // keysyms::KP_2 => return Key::Character("2"), + // keysyms::KP_3 => return Key::Character("3"), + // keysyms::KP_4 => return Key::Character("4"), + // keysyms::KP_5 => return Key::Character("5"), + // keysyms::KP_6 => return Key::Character("6"), + // keysyms::KP_7 => return Key::Character("7"), + // keysyms::KP_8 => return Key::Character("8"), + // keysyms::KP_9 => return Key::Character("9"), + + // Function keys + keysyms::F1 => NamedKey::F1, + keysyms::F2 => NamedKey::F2, + keysyms::F3 => NamedKey::F3, + keysyms::F4 => NamedKey::F4, + keysyms::F5 => NamedKey::F5, + keysyms::F6 => NamedKey::F6, + keysyms::F7 => NamedKey::F7, + keysyms::F8 => NamedKey::F8, + keysyms::F9 => NamedKey::F9, + keysyms::F10 => NamedKey::F10, + keysyms::F11 => NamedKey::F11, + keysyms::F12 => NamedKey::F12, + keysyms::F13 => NamedKey::F13, + keysyms::F14 => NamedKey::F14, + keysyms::F15 => NamedKey::F15, + keysyms::F16 => NamedKey::F16, + keysyms::F17 => NamedKey::F17, + keysyms::F18 => NamedKey::F18, + keysyms::F19 => NamedKey::F19, + keysyms::F20 => NamedKey::F20, + keysyms::F21 => NamedKey::F21, + keysyms::F22 => NamedKey::F22, + keysyms::F23 => NamedKey::F23, + keysyms::F24 => NamedKey::F24, + keysyms::F25 => NamedKey::F25, + keysyms::F26 => NamedKey::F26, + keysyms::F27 => NamedKey::F27, + keysyms::F28 => NamedKey::F28, + keysyms::F29 => NamedKey::F29, + keysyms::F30 => NamedKey::F30, + keysyms::F31 => NamedKey::F31, + keysyms::F32 => NamedKey::F32, + keysyms::F33 => NamedKey::F33, + keysyms::F34 => NamedKey::F34, + keysyms::F35 => NamedKey::F35, + + // Modifiers + keysyms::Shift_L => NamedKey::Shift, + keysyms::Shift_R => NamedKey::Shift, + keysyms::Control_L => NamedKey::Control, + keysyms::Control_R => NamedKey::Control, + keysyms::Caps_Lock => NamedKey::CapsLock, + // keysyms::Shift_Lock => NamedKey::ShiftLock, + + // keysyms::Meta_L => NamedKey::Meta, + // keysyms::Meta_R => NamedKey::Meta, + keysyms::Alt_L => NamedKey::Alt, + keysyms::Alt_R => NamedKey::Alt, + keysyms::Super_L => NamedKey::Super, + keysyms::Super_R => NamedKey::Super, + keysyms::Hyper_L => NamedKey::Hyper, + keysyms::Hyper_R => NamedKey::Hyper, + + // XKB function and modifier keys + // keysyms::ISO_Lock => NamedKey::IsoLock, + // keysyms::ISO_Level2_Latch => NamedKey::IsoLevel2Latch, + keysyms::ISO_Level3_Shift => NamedKey::AltGraph, + keysyms::ISO_Level3_Latch => NamedKey::AltGraph, + keysyms::ISO_Level3_Lock => NamedKey::AltGraph, + // keysyms::ISO_Level5_Shift => NamedKey::IsoLevel5Shift, + // keysyms::ISO_Level5_Latch => NamedKey::IsoLevel5Latch, + // keysyms::ISO_Level5_Lock => NamedKey::IsoLevel5Lock, + // keysyms::ISO_Group_Shift => NamedKey::IsoGroupShift, + // keysyms::ISO_Group_Latch => NamedKey::IsoGroupLatch, + // keysyms::ISO_Group_Lock => NamedKey::IsoGroupLock, + keysyms::ISO_Next_Group => NamedKey::GroupNext, + // keysyms::ISO_Next_Group_Lock => NamedKey::GroupNextLock, + keysyms::ISO_Prev_Group => NamedKey::GroupPrevious, + // keysyms::ISO_Prev_Group_Lock => NamedKey::GroupPreviousLock, + keysyms::ISO_First_Group => NamedKey::GroupFirst, + // keysyms::ISO_First_Group_Lock => NamedKey::GroupFirstLock, + keysyms::ISO_Last_Group => NamedKey::GroupLast, + // keysyms::ISO_Last_Group_Lock => NamedKey::GroupLastLock, + keysyms::ISO_Left_Tab => NamedKey::Tab, + // keysyms::ISO_Move_Line_Up => NamedKey::IsoMoveLineUp, + // keysyms::ISO_Move_Line_Down => NamedKey::IsoMoveLineDown, + // keysyms::ISO_Partial_Line_Up => NamedKey::IsoPartialLineUp, + // keysyms::ISO_Partial_Line_Down => NamedKey::IsoPartialLineDown, + // keysyms::ISO_Partial_Space_Left => NamedKey::IsoPartialSpaceLeft, + // keysyms::ISO_Partial_Space_Right => NamedKey::IsoPartialSpaceRight, + // keysyms::ISO_Set_Margin_Left => NamedKey::IsoSetMarginLeft, + // keysyms::ISO_Set_Margin_Right => NamedKey::IsoSetMarginRight, + // keysyms::ISO_Release_Margin_Left => NamedKey::IsoReleaseMarginLeft, + // keysyms::ISO_Release_Margin_Right => NamedKey::IsoReleaseMarginRight, + // keysyms::ISO_Release_Both_Margins => NamedKey::IsoReleaseBothMargins, + // keysyms::ISO_Fast_Cursor_Left => NamedKey::IsoFastCursorLeft, + // keysyms::ISO_Fast_Cursor_Right => NamedKey::IsoFastCursorRight, + // keysyms::ISO_Fast_Cursor_Up => NamedKey::IsoFastCursorUp, + // keysyms::ISO_Fast_Cursor_Down => NamedKey::IsoFastCursorDown, + // keysyms::ISO_Continuous_Underline => NamedKey::IsoContinuousUnderline, + // keysyms::ISO_Discontinuous_Underline => NamedKey::IsoDiscontinuousUnderline, + // keysyms::ISO_Emphasize => NamedKey::IsoEmphasize, + // keysyms::ISO_Center_Object => NamedKey::IsoCenterObject, + keysyms::ISO_Enter => NamedKey::Enter, + + // dead_grave..dead_currency + + // dead_lowline..dead_longsolidusoverlay + + // dead_a..dead_capital_schwa + + // dead_greek + + // First_Virtual_Screen..Terminate_Server + + // AccessX_Enable..AudibleBell_Enable + + // Pointer_Left..Pointer_Drag5 + + // Pointer_EnableKeys..Pointer_DfltBtnPrev + + // ch..C_H + + // 3270 terminal keys + // keysyms::3270_Duplicate => NamedKey::Duplicate, + // keysyms::3270_FieldMark => NamedKey::FieldMark, + // keysyms::3270_Right2 => NamedKey::Right2, + // keysyms::3270_Left2 => NamedKey::Left2, + // keysyms::3270_BackTab => NamedKey::BackTab, + keysyms::_3270_EraseEOF => NamedKey::EraseEof, + // keysyms::3270_EraseInput => NamedKey::EraseInput, + // keysyms::3270_Reset => NamedKey::Reset, + // keysyms::3270_Quit => NamedKey::Quit, + // keysyms::3270_PA1 => NamedKey::Pa1, + // keysyms::3270_PA2 => NamedKey::Pa2, + // keysyms::3270_PA3 => NamedKey::Pa3, + // keysyms::3270_Test => NamedKey::Test, + keysyms::_3270_Attn => NamedKey::Attn, + // keysyms::3270_CursorBlink => NamedKey::CursorBlink, + // keysyms::3270_AltCursor => NamedKey::AltCursor, + // keysyms::3270_KeyClick => NamedKey::KeyClick, + // keysyms::3270_Jump => NamedKey::Jump, + // keysyms::3270_Ident => NamedKey::Ident, + // keysyms::3270_Rule => NamedKey::Rule, + // keysyms::3270_Copy => NamedKey::Copy, + keysyms::_3270_Play => NamedKey::Play, + // keysyms::3270_Setup => NamedKey::Setup, + // keysyms::3270_Record => NamedKey::Record, + // keysyms::3270_ChangeScreen => NamedKey::ChangeScreen, + // keysyms::3270_DeleteWord => NamedKey::DeleteWord, + keysyms::_3270_ExSelect => NamedKey::ExSel, + keysyms::_3270_CursorSelect => NamedKey::CrSel, + keysyms::_3270_PrintScreen => NamedKey::PrintScreen, + keysyms::_3270_Enter => NamedKey::Enter, + + keysyms::space => NamedKey::Space, + // exclam..Sinh_kunddaliya + + // XFree86 + // keysyms::XF86_ModeLock => NamedKey::ModeLock, + + // XFree86 - Backlight controls + keysyms::XF86_MonBrightnessUp => NamedKey::BrightnessUp, + keysyms::XF86_MonBrightnessDown => NamedKey::BrightnessDown, + // keysyms::XF86_KbdLightOnOff => NamedKey::LightOnOff, + // keysyms::XF86_KbdBrightnessUp => NamedKey::KeyboardBrightnessUp, + // keysyms::XF86_KbdBrightnessDown => NamedKey::KeyboardBrightnessDown, + + // XFree86 - "Internet" + keysyms::XF86_Standby => NamedKey::Standby, + keysyms::XF86_AudioLowerVolume => NamedKey::AudioVolumeDown, + keysyms::XF86_AudioRaiseVolume => NamedKey::AudioVolumeUp, + keysyms::XF86_AudioPlay => NamedKey::MediaPlay, + keysyms::XF86_AudioStop => NamedKey::MediaStop, + keysyms::XF86_AudioPrev => NamedKey::MediaTrackPrevious, + keysyms::XF86_AudioNext => NamedKey::MediaTrackNext, + keysyms::XF86_HomePage => NamedKey::BrowserHome, + keysyms::XF86_Mail => NamedKey::LaunchMail, + // keysyms::XF86_Start => NamedKey::Start, + keysyms::XF86_Search => NamedKey::BrowserSearch, + keysyms::XF86_AudioRecord => NamedKey::MediaRecord, + + // XFree86 - PDA + keysyms::XF86_Calculator => NamedKey::LaunchApplication2, + // keysyms::XF86_Memo => NamedKey::Memo, + // keysyms::XF86_ToDoList => NamedKey::ToDoList, + keysyms::XF86_Calendar => NamedKey::LaunchCalendar, + keysyms::XF86_PowerDown => NamedKey::Power, + // keysyms::XF86_ContrastAdjust => NamedKey::AdjustContrast, + // keysyms::XF86_RockerUp => NamedKey::RockerUp, + // keysyms::XF86_RockerDown => NamedKey::RockerDown, + // keysyms::XF86_RockerEnter => NamedKey::RockerEnter, + + // XFree86 - More "Internet" + keysyms::XF86_Back => NamedKey::BrowserBack, + keysyms::XF86_Forward => NamedKey::BrowserForward, + // keysyms::XF86_Stop => NamedKey::Stop, + keysyms::XF86_Refresh => NamedKey::BrowserRefresh, + keysyms::XF86_PowerOff => NamedKey::Power, + keysyms::XF86_WakeUp => NamedKey::WakeUp, + keysyms::XF86_Eject => NamedKey::Eject, + keysyms::XF86_ScreenSaver => NamedKey::LaunchScreenSaver, + keysyms::XF86_WWW => NamedKey::LaunchWebBrowser, + keysyms::XF86_Sleep => NamedKey::Standby, + keysyms::XF86_Favorites => NamedKey::BrowserFavorites, + keysyms::XF86_AudioPause => NamedKey::MediaPause, + // keysyms::XF86_AudioMedia => NamedKey::AudioMedia, + keysyms::XF86_MyComputer => NamedKey::LaunchApplication1, + // keysyms::XF86_VendorHome => NamedKey::VendorHome, + // keysyms::XF86_LightBulb => NamedKey::LightBulb, + // keysyms::XF86_Shop => NamedKey::BrowserShop, + // keysyms::XF86_History => NamedKey::BrowserHistory, + // keysyms::XF86_OpenURL => NamedKey::OpenUrl, + // keysyms::XF86_AddFavorite => NamedKey::AddFavorite, + // keysyms::XF86_HotLinks => NamedKey::HotLinks, + // keysyms::XF86_BrightnessAdjust => NamedKey::BrightnessAdjust, + // keysyms::XF86_Finance => NamedKey::BrowserFinance, + // keysyms::XF86_Community => NamedKey::BrowserCommunity, + keysyms::XF86_AudioRewind => NamedKey::MediaRewind, + // keysyms::XF86_BackForward => Key::???, + // XF86_Launch0..XF86_LaunchF + + // XF86_ApplicationLeft..XF86_CD + keysyms::XF86_Calculater => NamedKey::LaunchApplication2, // Nice typo, libxkbcommon :) + // XF86_Clear + keysyms::XF86_Close => NamedKey::Close, + keysyms::XF86_Copy => NamedKey::Copy, + keysyms::XF86_Cut => NamedKey::Cut, + // XF86_Display..XF86_Documents + keysyms::XF86_Excel => NamedKey::LaunchSpreadsheet, + // XF86_Explorer..XF86iTouch + keysyms::XF86_LogOff => NamedKey::LogOff, + // XF86_Market..XF86_MenuPB + keysyms::XF86_MySites => NamedKey::BrowserFavorites, + keysyms::XF86_New => NamedKey::New, + // XF86_News..XF86_OfficeHome + keysyms::XF86_Open => NamedKey::Open, + // XF86_Option + keysyms::XF86_Paste => NamedKey::Paste, + keysyms::XF86_Phone => NamedKey::LaunchPhone, + // XF86_Q + keysyms::XF86_Reply => NamedKey::MailReply, + keysyms::XF86_Reload => NamedKey::BrowserRefresh, + // XF86_RotateWindows..XF86_RotationKB + keysyms::XF86_Save => NamedKey::Save, + // XF86_ScrollUp..XF86_ScrollClick + keysyms::XF86_Send => NamedKey::MailSend, + keysyms::XF86_Spell => NamedKey::SpellCheck, + keysyms::XF86_SplitScreen => NamedKey::SplitScreenToggle, + // XF86_Support..XF86_User2KB + keysyms::XF86_Video => NamedKey::LaunchMediaPlayer, + // XF86_WheelButton + keysyms::XF86_Word => NamedKey::LaunchWordProcessor, + // XF86_Xfer + keysyms::XF86_ZoomIn => NamedKey::ZoomIn, + keysyms::XF86_ZoomOut => NamedKey::ZoomOut, + + // XF86_Away..XF86_Messenger + keysyms::XF86_WebCam => NamedKey::LaunchWebCam, + keysyms::XF86_MailForward => NamedKey::MailForward, + // XF86_Pictures + keysyms::XF86_Music => NamedKey::LaunchMusicPlayer, + + // XF86_Battery..XF86_UWB + keysyms::XF86_AudioForward => NamedKey::MediaFastForward, + // XF86_AudioRepeat + keysyms::XF86_AudioRandomPlay => NamedKey::RandomToggle, + keysyms::XF86_Subtitle => NamedKey::Subtitle, + keysyms::XF86_AudioCycleTrack => NamedKey::MediaAudioTrack, + // XF86_CycleAngle..XF86_Blue + keysyms::XF86_Suspend => NamedKey::Standby, + keysyms::XF86_Hibernate => NamedKey::Hibernate, + // XF86_TouchpadToggle..XF86_TouchpadOff + keysyms::XF86_AudioMute => NamedKey::AudioVolumeMute, + + // XF86_Switch_VT_1..XF86_Switch_VT_12 + + // XF86_Ungrab..XF86_ClearGrab + keysyms::XF86_Next_VMode => NamedKey::VideoModeNext, + // keysyms::XF86_Prev_VMode => NamedKey::VideoModePrevious, + // XF86_LogWindowTree..XF86_LogGrabInfo + + // SunFA_Grave..SunFA_Cedilla + + // keysyms::SunF36 => NamedKey::F36 | NamedKey::F11, + // keysyms::SunF37 => NamedKey::F37 | NamedKey::F12, + + // keysyms::SunSys_Req => NamedKey::PrintScreen, + // The next couple of xkb (until SunStop) are already handled. + // SunPrint_Screen..SunPageDown + + // SunUndo..SunFront + keysyms::SUN_Copy => NamedKey::Copy, + keysyms::SUN_Open => NamedKey::Open, + keysyms::SUN_Paste => NamedKey::Paste, + keysyms::SUN_Cut => NamedKey::Cut, + + // SunPowerSwitch + keysyms::SUN_AudioLowerVolume => NamedKey::AudioVolumeDown, + keysyms::SUN_AudioMute => NamedKey::AudioVolumeMute, + keysyms::SUN_AudioRaiseVolume => NamedKey::AudioVolumeUp, + // SUN_VideoDegauss + keysyms::SUN_VideoLowerBrightness => NamedKey::BrightnessDown, + keysyms::SUN_VideoRaiseBrightness => NamedKey::BrightnessUp, + // SunPowerSwitchShift + 0 => return Key::Unidentified(NativeKey::Unidentified), + _ => return Key::Unidentified(NativeKey::Xkb(keysym)), + }) +} + +pub fn keysym_location(keysym: u32) -> KeyLocation { + use xkbcommon_dl::keysyms; + match keysym { + keysyms::Shift_L + | keysyms::Control_L + | keysyms::Meta_L + | keysyms::Alt_L + | keysyms::Super_L + | keysyms::Hyper_L => KeyLocation::Left, + keysyms::Shift_R + | keysyms::Control_R + | keysyms::Meta_R + | keysyms::Alt_R + | keysyms::Super_R + | keysyms::Hyper_R => KeyLocation::Right, + keysyms::KP_0 + | keysyms::KP_1 + | keysyms::KP_2 + | keysyms::KP_3 + | keysyms::KP_4 + | keysyms::KP_5 + | keysyms::KP_6 + | keysyms::KP_7 + | keysyms::KP_8 + | keysyms::KP_9 + | keysyms::KP_Space + | keysyms::KP_Tab + | keysyms::KP_Enter + | keysyms::KP_F1 + | keysyms::KP_F2 + | keysyms::KP_F3 + | keysyms::KP_F4 + | keysyms::KP_Home + | keysyms::KP_Left + | keysyms::KP_Up + | keysyms::KP_Right + | keysyms::KP_Down + | keysyms::KP_Page_Up + | keysyms::KP_Page_Down + | keysyms::KP_End + | keysyms::KP_Begin + | keysyms::KP_Insert + | keysyms::KP_Delete + | keysyms::KP_Equal + | keysyms::KP_Multiply + | keysyms::KP_Add + | keysyms::KP_Separator + | keysyms::KP_Subtract + | keysyms::KP_Decimal + | keysyms::KP_Divide => KeyLocation::Numpad, + _ => KeyLocation::Standard, + } +} + +#[derive(Debug)] +pub struct XkbKeymap { + keymap: NonNull, + _mods_indices: ModsIndices, + pub _core_keyboard_id: i32, +} + +impl XkbKeymap { + #[cfg(wayland_platform)] + pub fn from_fd(context: &XkbContext, fd: OwnedFd, size: usize) -> Option { + let map = unsafe { MmapOptions::new().len(size).map_copy_read_only(&fd).ok()? }; + + let keymap = unsafe { + let keymap = (XKBH.xkb_keymap_new_from_string)( + (*context).as_ptr(), + map.as_ptr() as *const _, + xkb::xkb_keymap_format::XKB_KEYMAP_FORMAT_TEXT_V1, + xkb_keymap_compile_flags::XKB_KEYMAP_COMPILE_NO_FLAGS, + ); + NonNull::new(keymap)? + }; + + Some(Self::new_inner(keymap, 0)) + } + + #[cfg(x11_platform)] + pub fn from_x11_keymap( + context: &XkbContext, + xcb: *mut xcb_connection_t, + core_keyboard_id: i32, + ) -> Option { + let keymap = unsafe { + (XKBXH.xkb_x11_keymap_new_from_device)( + context.as_ptr(), + xcb, + core_keyboard_id, + xkb_keymap_compile_flags::XKB_KEYMAP_COMPILE_NO_FLAGS, + ) + }; + let keymap = NonNull::new(keymap)?; + Some(Self::new_inner(keymap, core_keyboard_id)) + } + + fn new_inner(keymap: NonNull, _core_keyboard_id: i32) -> Self { + let mods_indices = ModsIndices { + shift: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_SHIFT), + caps: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_CAPS), + ctrl: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_CTRL), + alt: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_ALT), + num: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_NUM), + mod3: mod_index_for_name(keymap, b"Mod3\0"), + logo: mod_index_for_name(keymap, xkb::XKB_MOD_NAME_LOGO), + mod5: mod_index_for_name(keymap, b"Mod5\0"), + }; + + Self { keymap, _mods_indices: mods_indices, _core_keyboard_id } + } + + #[cfg(x11_platform)] + pub fn mods_indices(&self) -> ModsIndices { + self._mods_indices + } + + pub fn first_keysym_by_level( + &mut self, + layout: xkb_layout_index_t, + keycode: xkb_keycode_t, + ) -> xkb_keysym_t { + unsafe { + let mut keysyms = ptr::null(); + let count = (XKBH.xkb_keymap_key_get_syms_by_level)( + self.keymap.as_ptr(), + keycode, + layout, + // NOTE: The level should be zero to ignore modifiers. + 0, + &mut keysyms, + ); + + if count == 1 { + *keysyms + } else { + 0 + } + } + } + + /// Check whether the given key repeats. + pub fn key_repeats(&mut self, keycode: xkb_keycode_t) -> bool { + unsafe { (XKBH.xkb_keymap_key_repeats)(self.keymap.as_ptr(), keycode) == 1 } + } +} + +impl Drop for XkbKeymap { + fn drop(&mut self) { + unsafe { + (XKBH.xkb_keymap_unref)(self.keymap.as_ptr()); + }; + } +} + +impl Deref for XkbKeymap { + type Target = NonNull; + + fn deref(&self) -> &Self::Target { + &self.keymap + } +} + +/// Modifier index in the keymap. +#[cfg_attr(not(x11_platform), allow(dead_code))] +#[derive(Default, Debug, Clone, Copy)] +pub struct ModsIndices { + pub shift: Option, + pub caps: Option, + pub ctrl: Option, + pub alt: Option, + pub num: Option, + pub mod3: Option, + pub logo: Option, + pub mod5: Option, +} + +fn mod_index_for_name(keymap: NonNull, name: &[u8]) -> Option { + unsafe { + let mod_index = + (XKBH.xkb_keymap_mod_get_index)(keymap.as_ptr(), name.as_ptr() as *const c_char); + if mod_index == XKB_MOD_INVALID { + None + } else { + Some(mod_index) + } + } +} diff --git a/vendor/winit/src/platform_impl/linux/common/xkb/mod.rs b/vendor/winit/src/platform_impl/linux/common/xkb/mod.rs new file mode 100644 index 00000000..706397f6 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/common/xkb/mod.rs @@ -0,0 +1,416 @@ +use std::ops::Deref; +use std::os::raw::c_char; +use std::ptr::{self, NonNull}; +use std::sync::atomic::{AtomicBool, Ordering}; + +use crate::utils::Lazy; +use smol_str::SmolStr; +#[cfg(wayland_platform)] +use std::os::unix::io::OwnedFd; +use tracing::warn; +use xkbcommon_dl::{ + self as xkb, xkb_compose_status, xkb_context, xkb_context_flags, xkbcommon_compose_handle, + xkbcommon_handle, XkbCommon, XkbCommonCompose, +}; +#[cfg(x11_platform)] +use {x11_dl::xlib_xcb::xcb_connection_t, xkbcommon_dl::x11::xkbcommon_x11_handle}; + +use crate::event::{ElementState, KeyEvent}; +use crate::keyboard::{Key, KeyLocation}; +use crate::platform_impl::KeyEventExtra; + +mod compose; +mod keymap; +mod state; + +use compose::{ComposeStatus, XkbComposeState, XkbComposeTable}; +use keymap::XkbKeymap; + +#[cfg(x11_platform)] +pub use keymap::raw_keycode_to_physicalkey; +pub use keymap::{physicalkey_to_scancode, scancode_to_physicalkey}; +pub use state::XkbState; + +// TODO: Wire this up without using a static `AtomicBool`. +static RESET_DEAD_KEYS: AtomicBool = AtomicBool::new(false); + +static XKBH: Lazy<&'static XkbCommon> = Lazy::new(xkbcommon_handle); +static XKBCH: Lazy<&'static XkbCommonCompose> = Lazy::new(xkbcommon_compose_handle); +#[cfg(feature = "x11")] +static XKBXH: Lazy<&'static xkb::x11::XkbCommonX11> = Lazy::new(xkbcommon_x11_handle); + +#[inline(always)] +pub fn reset_dead_keys() { + RESET_DEAD_KEYS.store(true, Ordering::SeqCst); +} + +#[derive(Debug)] +pub enum Error { + /// libxkbcommon is not available + XKBNotFound, +} + +#[derive(Debug)] +pub struct Context { + // NOTE: field order matters. + #[cfg(x11_platform)] + pub core_keyboard_id: i32, + state: Option, + keymap: Option, + compose_state1: Option, + compose_state2: Option, + _compose_table: Option, + context: XkbContext, + scratch_buffer: Vec, +} + +impl Context { + pub fn new() -> Result { + if xkb::xkbcommon_option().is_none() { + return Err(Error::XKBNotFound); + } + + let context = XkbContext::new()?; + let mut compose_table = XkbComposeTable::new(&context); + let mut compose_state1 = compose_table.as_ref().and_then(|table| table.new_state()); + let mut compose_state2 = compose_table.as_ref().and_then(|table| table.new_state()); + + // Disable compose if anything compose related failed to initialize. + if compose_table.is_none() || compose_state1.is_none() || compose_state2.is_none() { + compose_state2 = None; + compose_state1 = None; + compose_table = None; + } + + Ok(Self { + state: None, + keymap: None, + compose_state1, + compose_state2, + #[cfg(x11_platform)] + core_keyboard_id: 0, + _compose_table: compose_table, + context, + scratch_buffer: Vec::with_capacity(8), + }) + } + + #[cfg(feature = "x11")] + pub fn from_x11_xkb(xcb: *mut xcb_connection_t) -> Result { + let result = unsafe { + (XKBXH.xkb_x11_setup_xkb_extension)( + xcb, + 1, + 2, + xkbcommon_dl::x11::xkb_x11_setup_xkb_extension_flags::XKB_X11_SETUP_XKB_EXTENSION_NO_FLAGS, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ) + }; + + if result != 1 { + return Err(Error::XKBNotFound); + } + + let mut this = Self::new()?; + this.core_keyboard_id = unsafe { (XKBXH.xkb_x11_get_core_keyboard_device_id)(xcb) }; + this.set_keymap_from_x11(xcb); + Ok(this) + } + + pub fn state_mut(&mut self) -> Option<&mut XkbState> { + self.state.as_mut() + } + + pub fn keymap_mut(&mut self) -> Option<&mut XkbKeymap> { + self.keymap.as_mut() + } + + #[cfg(wayland_platform)] + pub fn set_keymap_from_fd(&mut self, fd: OwnedFd, size: usize) { + let keymap = XkbKeymap::from_fd(&self.context, fd, size); + let state = keymap.as_ref().and_then(XkbState::new_wayland); + if keymap.is_none() || state.is_none() { + warn!("failed to update xkb keymap"); + } + self.state = state; + self.keymap = keymap; + } + + #[cfg(x11_platform)] + pub fn set_keymap_from_x11(&mut self, xcb: *mut xcb_connection_t) { + let keymap = XkbKeymap::from_x11_keymap(&self.context, xcb, self.core_keyboard_id); + let state = keymap.as_ref().and_then(|keymap| XkbState::new_x11(xcb, keymap)); + if keymap.is_none() || state.is_none() { + warn!("failed to update xkb keymap"); + } + self.state = state; + self.keymap = keymap; + } + + /// Key builder context with the user provided xkb state. + pub fn key_context(&mut self) -> Option> { + let state = self.state.as_mut()?; + let keymap = self.keymap.as_mut()?; + let compose_state1 = self.compose_state1.as_mut(); + let compose_state2 = self.compose_state2.as_mut(); + let scratch_buffer = &mut self.scratch_buffer; + Some(KeyContext { state, keymap, compose_state1, compose_state2, scratch_buffer }) + } + + /// Key builder context with the user provided xkb state. + /// + /// Should be used when the original context must not be altered. + #[cfg(x11_platform)] + pub fn key_context_with_state<'a>( + &'a mut self, + state: &'a mut XkbState, + ) -> Option> { + let keymap = self.keymap.as_mut()?; + let compose_state1 = self.compose_state1.as_mut(); + let compose_state2 = self.compose_state2.as_mut(); + let scratch_buffer = &mut self.scratch_buffer; + Some(KeyContext { state, keymap, compose_state1, compose_state2, scratch_buffer }) + } +} + +pub struct KeyContext<'a> { + pub state: &'a mut XkbState, + pub keymap: &'a mut XkbKeymap, + compose_state1: Option<&'a mut XkbComposeState>, + compose_state2: Option<&'a mut XkbComposeState>, + scratch_buffer: &'a mut Vec, +} + +impl KeyContext<'_> { + pub fn process_key_event( + &mut self, + keycode: u32, + state: ElementState, + repeat: bool, + ) -> KeyEvent { + let mut event = + KeyEventResults::new(self, keycode, !repeat && state == ElementState::Pressed); + let physical_key = keymap::raw_keycode_to_physicalkey(keycode); + let (logical_key, location) = event.key(); + let text = event.text(); + let (key_without_modifiers, _) = event.key_without_modifiers(); + let text_with_all_modifiers = event.text_with_all_modifiers(); + + let platform_specific = KeyEventExtra { text_with_all_modifiers, key_without_modifiers }; + + KeyEvent { physical_key, logical_key, text, location, state, repeat, platform_specific } + } + + fn keysym_to_utf8_raw(&mut self, keysym: u32) -> Option { + self.scratch_buffer.clear(); + self.scratch_buffer.reserve(8); + loop { + let bytes_written = unsafe { + (XKBH.xkb_keysym_to_utf8)( + keysym, + self.scratch_buffer.as_mut_ptr().cast(), + self.scratch_buffer.capacity(), + ) + }; + if bytes_written == 0 { + return None; + } else if bytes_written == -1 { + self.scratch_buffer.reserve(8); + } else { + unsafe { self.scratch_buffer.set_len(bytes_written.try_into().unwrap()) }; + break; + } + } + + // Remove the null-terminator + self.scratch_buffer.pop(); + byte_slice_to_smol_str(self.scratch_buffer) + } +} + +struct KeyEventResults<'a, 'b> { + context: &'a mut KeyContext<'b>, + keycode: u32, + keysym: u32, + compose: ComposeStatus, +} + +impl<'a, 'b> KeyEventResults<'a, 'b> { + fn new(context: &'a mut KeyContext<'b>, keycode: u32, compose: bool) -> Self { + let keysym = context.state.get_one_sym_raw(keycode); + + let compose = if let Some(state) = context.compose_state1.as_mut().filter(|_| compose) { + if RESET_DEAD_KEYS.swap(false, Ordering::SeqCst) { + state.reset(); + context.compose_state2.as_mut().unwrap().reset(); + } + state.feed(keysym) + } else { + ComposeStatus::None + }; + + KeyEventResults { context, keycode, keysym, compose } + } + + pub fn key(&mut self) -> (Key, KeyLocation) { + let (key, location) = match self.keysym_to_key(self.keysym) { + Ok(known) => return known, + Err(undefined) => undefined, + }; + + if let ComposeStatus::Accepted(xkb_compose_status::XKB_COMPOSE_COMPOSING) = self.compose { + let compose_state = self.context.compose_state2.as_mut().unwrap(); + // When pressing a dead key twice, the non-combining variant of that character will + // be produced. Since this function only concerns itself with a single keypress, we + // simulate this double press here by feeding the keysym to the compose state + // twice. + + compose_state.feed(self.keysym); + if matches!(compose_state.feed(self.keysym), ComposeStatus::Accepted(_)) { + // Extracting only a single `char` here *should* be fine, assuming that no + // dead key's non-combining variant ever occupies more than one `char`. + let text = compose_state.get_string(self.context.scratch_buffer); + let key = Key::Dead(text.and_then(|s| s.chars().next())); + (key, location) + } else { + (key, location) + } + } else { + let key = self + .composed_text() + .unwrap_or_else(|_| self.context.keysym_to_utf8_raw(self.keysym)) + .map(Key::Character) + .unwrap_or(key); + (key, location) + } + } + + pub fn key_without_modifiers(&mut self) -> (Key, KeyLocation) { + // This will become a pointer to an array which libxkbcommon owns, so we don't need to + // deallocate it. + let layout = self.context.state.layout(self.keycode); + let keysym = self.context.keymap.first_keysym_by_level(layout, self.keycode); + + match self.keysym_to_key(keysym) { + Ok((key, location)) => (key, location), + Err((key, location)) => { + let key = + self.context.keysym_to_utf8_raw(keysym).map(Key::Character).unwrap_or(key); + (key, location) + }, + } + } + + fn keysym_to_key(&self, keysym: u32) -> Result<(Key, KeyLocation), (Key, KeyLocation)> { + let location = keymap::keysym_location(keysym); + let key = keymap::keysym_to_key(keysym); + if matches!(key, Key::Unidentified(_)) { + Err((key, location)) + } else { + Ok((key, location)) + } + } + + pub fn text(&mut self) -> Option { + self.composed_text().unwrap_or_else(|_| self.context.keysym_to_utf8_raw(self.keysym)) + } + + // The current behaviour makes it so composing a character overrides attempts to input a + // control character with the `Ctrl` key. We can potentially add a configuration option + // if someone specifically wants the opposite behaviour. + pub fn text_with_all_modifiers(&mut self) -> Option { + match self.composed_text() { + Ok(text) => text, + Err(_) => self.context.state.get_utf8_raw(self.keycode, self.context.scratch_buffer), + } + } + + fn composed_text(&mut self) -> Result, ()> { + match self.compose { + ComposeStatus::Accepted(status) => match status { + xkb_compose_status::XKB_COMPOSE_COMPOSED => { + let state = self.context.compose_state1.as_mut().unwrap(); + Ok(state.get_string(self.context.scratch_buffer)) + }, + xkb_compose_status::XKB_COMPOSE_COMPOSING + | xkb_compose_status::XKB_COMPOSE_CANCELLED => Ok(None), + xkb_compose_status::XKB_COMPOSE_NOTHING => Err(()), + }, + _ => Err(()), + } + } +} + +#[derive(Debug)] +pub struct XkbContext { + context: NonNull, +} + +impl XkbContext { + pub fn new() -> Result { + let context = unsafe { (XKBH.xkb_context_new)(xkb_context_flags::XKB_CONTEXT_NO_FLAGS) }; + + let context = match NonNull::new(context) { + Some(context) => context, + None => return Err(Error::XKBNotFound), + }; + + Ok(Self { context }) + } +} + +impl Drop for XkbContext { + fn drop(&mut self) { + unsafe { + (XKBH.xkb_context_unref)(self.context.as_ptr()); + } + } +} + +impl Deref for XkbContext { + type Target = NonNull; + + fn deref(&self) -> &Self::Target { + &self.context + } +} + +/// Shared logic for constructing a string with `xkb_compose_state_get_utf8` and +/// `xkb_state_key_get_utf8`. +fn make_string_with(scratch_buffer: &mut Vec, mut f: F) -> Option +where + F: FnMut(*mut c_char, usize) -> i32, +{ + let size = f(ptr::null_mut(), 0); + if size == 0 { + return None; + } + let size = usize::try_from(size).unwrap(); + scratch_buffer.clear(); + // The allocated buffer must include space for the null-terminator. + scratch_buffer.reserve(size + 1); + unsafe { + let written = f(scratch_buffer.as_mut_ptr().cast(), scratch_buffer.capacity()); + if usize::try_from(written).unwrap() != size { + // This will likely never happen. + return None; + } + scratch_buffer.set_len(size); + }; + + byte_slice_to_smol_str(scratch_buffer) +} + +// NOTE: This is track_caller so we can have more informative line numbers when logging +#[track_caller] +fn byte_slice_to_smol_str(bytes: &[u8]) -> Option { + std::str::from_utf8(bytes) + .map(SmolStr::new) + .map_err(|e| { + tracing::warn!("UTF-8 received from libxkbcommon ({:?}) was invalid: {e}", bytes) + }) + .ok() +} diff --git a/vendor/winit/src/platform_impl/linux/common/xkb/state.rs b/vendor/winit/src/platform_impl/linux/common/xkb/state.rs new file mode 100644 index 00000000..27c055aa --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/common/xkb/state.rs @@ -0,0 +1,189 @@ +//! XKB state. + +use std::os::raw::c_char; +use std::ptr::NonNull; + +use smol_str::SmolStr; +#[cfg(x11_platform)] +use x11_dl::xlib_xcb::xcb_connection_t; +use xkbcommon_dl::{ + self as xkb, xkb_keycode_t, xkb_keysym_t, xkb_layout_index_t, xkb_state, xkb_state_component, +}; + +use crate::platform_impl::common::xkb::keymap::XkbKeymap; +#[cfg(x11_platform)] +use crate::platform_impl::common::xkb::XKBXH; +use crate::platform_impl::common::xkb::{make_string_with, XKBH}; + +#[derive(Debug)] +pub struct XkbState { + state: NonNull, + modifiers: ModifiersState, +} + +impl XkbState { + #[cfg(wayland_platform)] + pub fn new_wayland(keymap: &XkbKeymap) -> Option { + let state = NonNull::new(unsafe { (XKBH.xkb_state_new)(keymap.as_ptr()) })?; + Some(Self::new_inner(state)) + } + + #[cfg(x11_platform)] + pub fn new_x11(xcb: *mut xcb_connection_t, keymap: &XkbKeymap) -> Option { + let state = unsafe { + (XKBXH.xkb_x11_state_new_from_device)(keymap.as_ptr(), xcb, keymap._core_keyboard_id) + }; + let state = NonNull::new(state)?; + Some(Self::new_inner(state)) + } + + fn new_inner(state: NonNull) -> Self { + let modifiers = ModifiersState::default(); + let mut this = Self { state, modifiers }; + this.reload_modifiers(); + this + } + + pub fn get_one_sym_raw(&mut self, keycode: xkb_keycode_t) -> xkb_keysym_t { + unsafe { (XKBH.xkb_state_key_get_one_sym)(self.state.as_ptr(), keycode) } + } + + pub fn layout(&mut self, key: xkb_keycode_t) -> xkb_layout_index_t { + unsafe { (XKBH.xkb_state_key_get_layout)(self.state.as_ptr(), key) } + } + + #[cfg(x11_platform)] + pub fn depressed_modifiers(&mut self) -> xkb::xkb_mod_mask_t { + unsafe { + (XKBH.xkb_state_serialize_mods)( + self.state.as_ptr(), + xkb_state_component::XKB_STATE_MODS_DEPRESSED, + ) + } + } + + #[cfg(x11_platform)] + pub fn latched_modifiers(&mut self) -> xkb::xkb_mod_mask_t { + unsafe { + (XKBH.xkb_state_serialize_mods)( + self.state.as_ptr(), + xkb_state_component::XKB_STATE_MODS_LATCHED, + ) + } + } + + #[cfg(x11_platform)] + pub fn locked_modifiers(&mut self) -> xkb::xkb_mod_mask_t { + unsafe { + (XKBH.xkb_state_serialize_mods)( + self.state.as_ptr(), + xkb_state_component::XKB_STATE_MODS_LOCKED, + ) + } + } + + pub fn get_utf8_raw( + &mut self, + keycode: xkb_keycode_t, + scratch_buffer: &mut Vec, + ) -> Option { + make_string_with(scratch_buffer, |ptr, len| unsafe { + (XKBH.xkb_state_key_get_utf8)(self.state.as_ptr(), keycode, ptr, len) + }) + } + + pub fn modifiers(&self) -> ModifiersState { + self.modifiers + } + + pub fn update_modifiers( + &mut self, + mods_depressed: u32, + mods_latched: u32, + mods_locked: u32, + depressed_group: u32, + latched_group: u32, + locked_group: u32, + ) { + let mask = unsafe { + (XKBH.xkb_state_update_mask)( + self.state.as_ptr(), + mods_depressed, + mods_latched, + mods_locked, + depressed_group, + latched_group, + locked_group, + ) + }; + + if mask.contains(xkb_state_component::XKB_STATE_MODS_EFFECTIVE) { + // Effective value of mods have changed, we need to update our state. + self.reload_modifiers(); + } + } + + /// Reload the modifiers. + fn reload_modifiers(&mut self) { + self.modifiers.ctrl = self.mod_name_is_active(xkb::XKB_MOD_NAME_CTRL); + self.modifiers.alt = self.mod_name_is_active(xkb::XKB_MOD_NAME_ALT); + self.modifiers.shift = self.mod_name_is_active(xkb::XKB_MOD_NAME_SHIFT); + self.modifiers.caps_lock = self.mod_name_is_active(xkb::XKB_MOD_NAME_CAPS); + self.modifiers.logo = self.mod_name_is_active(xkb::XKB_MOD_NAME_LOGO); + self.modifiers.num_lock = self.mod_name_is_active(xkb::XKB_MOD_NAME_NUM); + } + + /// Check if the modifier is active within xkb. + fn mod_name_is_active(&mut self, name: &[u8]) -> bool { + unsafe { + (XKBH.xkb_state_mod_name_is_active)( + self.state.as_ptr(), + name.as_ptr() as *const c_char, + xkb_state_component::XKB_STATE_MODS_EFFECTIVE, + ) > 0 + } + } +} + +impl Drop for XkbState { + fn drop(&mut self) { + unsafe { + (XKBH.xkb_state_unref)(self.state.as_ptr()); + } + } +} + +/// Represents the current state of the keyboard modifiers +/// +/// Each field of this struct represents a modifier and is `true` if this modifier is active. +/// +/// For some modifiers, this means that the key is currently pressed, others are toggled +/// (like caps lock). +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct ModifiersState { + /// The "control" key + pub ctrl: bool, + /// The "alt" key + pub alt: bool, + /// The "shift" key + pub shift: bool, + /// The "Caps lock" key + pub caps_lock: bool, + /// The "logo" key + /// + /// Also known as the "windows" key on most keyboards + pub logo: bool, + /// The "Num lock" key + pub num_lock: bool, +} + +impl From for crate::keyboard::ModifiersState { + fn from(mods: ModifiersState) -> crate::keyboard::ModifiersState { + let mut to_mods = crate::keyboard::ModifiersState::empty(); + to_mods.set(crate::keyboard::ModifiersState::SHIFT, mods.shift); + to_mods.set(crate::keyboard::ModifiersState::CONTROL, mods.ctrl); + to_mods.set(crate::keyboard::ModifiersState::ALT, mods.alt); + to_mods.set(crate::keyboard::ModifiersState::SUPER, mods.logo); + to_mods + } +} diff --git a/vendor/winit/src/platform_impl/linux/mod.rs b/vendor/winit/src/platform_impl/linux/mod.rs new file mode 100644 index 00000000..bc0e71c8 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/mod.rs @@ -0,0 +1,1044 @@ +#![cfg(free_unix)] + +#[cfg(all(not(x11_platform), not(wayland_platform)))] +compile_error!("Please select a feature to build for unix: `x11`, `wayland`"); + +use std::collections::VecDeque; +use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; +use std::sync::Arc; +use std::time::Duration; +use std::{env, fmt}; +#[cfg(x11_platform)] +use std::{ffi::CStr, mem::MaybeUninit, os::raw::*, sync::Mutex}; + +#[cfg(x11_platform)] +use crate::utils::Lazy; +use smol_str::SmolStr; + +#[cfg(x11_platform)] +use self::x11::{X11Error, XConnection, XError, XNotSupported}; +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{EventLoopError, ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::event_loop::{ + ActiveEventLoop as RootELW, AsyncRequestSerial, ControlFlow, DeviceEvents, EventLoopClosed, +}; +use crate::icon::Icon; +use crate::keyboard::Key; +use crate::platform::pump_events::PumpStatus; +#[cfg(x11_platform)] +use crate::platform::x11::{WindowType as XWindowType, XlibErrorHook}; +use crate::window::{ + ActivationToken, Cursor, CursorGrabMode, CustomCursor, CustomCursorSource, ImePurpose, + ResizeDirection, Theme, UserAttentionType, WindowAttributes, WindowButtons, WindowLevel, +}; + +pub(crate) use self::common::xkb::{physicalkey_to_scancode, scancode_to_physicalkey}; +pub(crate) use crate::cursor::OnlyCursorImageSource as PlatformCustomCursorSource; +pub(crate) use crate::icon::RgbaIcon as PlatformIcon; +pub(crate) use crate::platform_impl::Fullscreen; + +pub(crate) mod common; +#[cfg(wayland_platform)] +pub(crate) mod wayland; +#[cfg(x11_platform)] +pub(crate) mod x11; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) enum Backend { + #[cfg(x11_platform)] + X, + #[cfg(wayland_platform)] + Wayland, +} + +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PlatformSpecificEventLoopAttributes { + pub(crate) forced_backend: Option, + pub(crate) any_thread: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ApplicationName { + pub general: String, + pub instance: String, +} + +impl ApplicationName { + pub fn new(general: String, instance: String) -> Self { + Self { general, instance } + } +} + +#[derive(Clone, Debug)] +pub struct PlatformSpecificWindowAttributes { + pub name: Option, + pub activation_token: Option, + #[cfg(x11_platform)] + pub x11: X11WindowAttributes, +} + +#[derive(Clone, Debug)] +#[cfg(x11_platform)] +pub struct X11WindowAttributes { + pub visual_id: Option, + pub screen_id: Option, + pub base_size: Option, + pub override_redirect: bool, + pub x11_window_types: Vec, + + /// The parent window to embed this window into. + pub embed_window: Option, +} + +#[cfg_attr(not(x11_platform), allow(clippy::derivable_impls))] +impl Default for PlatformSpecificWindowAttributes { + fn default() -> Self { + Self { + name: None, + activation_token: None, + #[cfg(x11_platform)] + x11: X11WindowAttributes { + visual_id: None, + screen_id: None, + base_size: None, + override_redirect: false, + x11_window_types: vec![XWindowType::Normal], + embed_window: None, + }, + } + } +} + +#[cfg(x11_platform)] +pub(crate) static X11_BACKEND: Lazy, XNotSupported>>> = + Lazy::new(|| Mutex::new(XConnection::new(Some(x_error_callback)).map(Arc::new))); + +#[derive(Debug, Clone)] +pub enum OsError { + Misc(&'static str), + #[cfg(x11_platform)] + XNotSupported(XNotSupported), + #[cfg(x11_platform)] + XError(Arc), + #[cfg(wayland_platform)] + WaylandError(Arc), +} + +impl fmt::Display for OsError { + fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match *self { + OsError::Misc(e) => _f.pad(e), + #[cfg(x11_platform)] + OsError::XNotSupported(ref e) => fmt::Display::fmt(e, _f), + #[cfg(x11_platform)] + OsError::XError(ref e) => fmt::Display::fmt(e, _f), + #[cfg(wayland_platform)] + OsError::WaylandError(ref e) => fmt::Display::fmt(e, _f), + } + } +} + +pub(crate) enum Window { + #[cfg(x11_platform)] + X(x11::Window), + #[cfg(wayland_platform)] + Wayland(wayland::Window), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId(u64); + +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id) + } +} + +impl WindowId { + pub const fn dummy() -> Self { + Self(0) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum DeviceId { + #[cfg(x11_platform)] + X(x11::DeviceId), + #[cfg(wayland_platform)] + Wayland(wayland::DeviceId), +} + +impl DeviceId { + pub const fn dummy() -> Self { + #[cfg(wayland_platform)] + return DeviceId::Wayland(wayland::DeviceId::dummy()); + #[cfg(all(not(wayland_platform), x11_platform))] + return DeviceId::X(x11::DeviceId::dummy()); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum MonitorHandle { + #[cfg(x11_platform)] + X(x11::MonitorHandle), + #[cfg(wayland_platform)] + Wayland(wayland::MonitorHandle), +} + +/// `x11_or_wayland!(match expr; Enum(foo) => foo.something())` +/// expands to the equivalent of +/// ```ignore +/// match self { +/// Enum::X(foo) => foo.something(), +/// Enum::Wayland(foo) => foo.something(), +/// } +/// ``` +/// The result can be converted to another enum by adding `; as AnotherEnum` +macro_rules! x11_or_wayland { + (match $what:expr; $enum:ident ( $($c1:tt)* ) => $x:expr; as $enum2:ident ) => { + match $what { + #[cfg(x11_platform)] + $enum::X($($c1)*) => $enum2::X($x), + #[cfg(wayland_platform)] + $enum::Wayland($($c1)*) => $enum2::Wayland($x), + } + }; + (match $what:expr; $enum:ident ( $($c1:tt)* ) => $x:expr) => { + match $what { + #[cfg(x11_platform)] + $enum::X($($c1)*) => $x, + #[cfg(wayland_platform)] + $enum::Wayland($($c1)*) => $x, + } + }; +} + +impl MonitorHandle { + #[inline] + pub fn name(&self) -> Option { + x11_or_wayland!(match self; MonitorHandle(m) => m.name()) + } + + #[inline] + pub fn native_identifier(&self) -> u32 { + x11_or_wayland!(match self; MonitorHandle(m) => m.native_identifier()) + } + + #[inline] + pub fn size(&self) -> PhysicalSize { + x11_or_wayland!(match self; MonitorHandle(m) => m.size()) + } + + #[inline] + pub fn position(&self) -> PhysicalPosition { + x11_or_wayland!(match self; MonitorHandle(m) => m.position()) + } + + #[inline] + pub fn refresh_rate_millihertz(&self) -> Option { + x11_or_wayland!(match self; MonitorHandle(m) => m.refresh_rate_millihertz()) + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + x11_or_wayland!(match self; MonitorHandle(m) => m.scale_factor() as _) + } + + #[inline] + pub fn video_modes(&self) -> Box> { + x11_or_wayland!(match self; MonitorHandle(m) => Box::new(m.video_modes())) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum VideoModeHandle { + #[cfg(x11_platform)] + X(x11::VideoModeHandle), + #[cfg(wayland_platform)] + Wayland(wayland::VideoModeHandle), +} + +impl VideoModeHandle { + #[inline] + pub fn size(&self) -> PhysicalSize { + x11_or_wayland!(match self; VideoModeHandle(m) => m.size()) + } + + #[inline] + pub fn bit_depth(&self) -> u16 { + x11_or_wayland!(match self; VideoModeHandle(m) => m.bit_depth()) + } + + #[inline] + pub fn refresh_rate_millihertz(&self) -> u32 { + x11_or_wayland!(match self; VideoModeHandle(m) => m.refresh_rate_millihertz()) + } + + #[inline] + pub fn monitor(&self) -> MonitorHandle { + x11_or_wayland!(match self; VideoModeHandle(m) => m.monitor(); as MonitorHandle) + } +} + +impl Window { + #[inline] + pub(crate) fn new( + window_target: &ActiveEventLoop, + attribs: WindowAttributes, + ) -> Result { + match *window_target { + #[cfg(wayland_platform)] + ActiveEventLoop::Wayland(ref window_target) => { + wayland::Window::new(window_target, attribs).map(Window::Wayland) + }, + #[cfg(x11_platform)] + ActiveEventLoop::X(ref window_target) => { + x11::Window::new(window_target, attribs).map(Window::X) + }, + } + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) { + f(self) + } + + pub(crate) fn maybe_wait_on_main(&self, f: impl FnOnce(&Self) -> R + Send) -> R { + f(self) + } + + #[inline] + pub fn id(&self) -> WindowId { + x11_or_wayland!(match self; Window(w) => w.id()) + } + + #[inline] + pub fn set_title(&self, title: &str) { + x11_or_wayland!(match self; Window(w) => w.set_title(title)); + } + + #[inline] + pub fn set_transparent(&self, transparent: bool) { + x11_or_wayland!(match self; Window(w) => w.set_transparent(transparent)); + } + + #[inline] + pub fn set_blur(&self, blur: bool) { + x11_or_wayland!(match self; Window(w) => w.set_blur(blur)); + } + + #[inline] + pub fn set_visible(&self, visible: bool) { + x11_or_wayland!(match self; Window(w) => w.set_visible(visible)) + } + + #[inline] + pub fn is_visible(&self) -> Option { + x11_or_wayland!(match self; Window(w) => w.is_visible()) + } + + #[inline] + pub fn outer_position(&self) -> Result, NotSupportedError> { + x11_or_wayland!(match self; Window(w) => w.outer_position()) + } + + #[inline] + pub fn inner_position(&self) -> Result, NotSupportedError> { + x11_or_wayland!(match self; Window(w) => w.inner_position()) + } + + #[inline] + pub fn set_outer_position(&self, position: Position) { + x11_or_wayland!(match self; Window(w) => w.set_outer_position(position)) + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + x11_or_wayland!(match self; Window(w) => w.inner_size()) + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + x11_or_wayland!(match self; Window(w) => w.outer_size()) + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + x11_or_wayland!(match self; Window(w) => w.request_inner_size(size)) + } + + #[inline] + pub(crate) fn request_activation_token(&self) -> Result { + x11_or_wayland!(match self; Window(w) => w.request_activation_token()) + } + + #[inline] + pub fn set_min_inner_size(&self, dimensions: Option) { + x11_or_wayland!(match self; Window(w) => w.set_min_inner_size(dimensions)) + } + + #[inline] + pub fn set_max_inner_size(&self, dimensions: Option) { + x11_or_wayland!(match self; Window(w) => w.set_max_inner_size(dimensions)) + } + + #[inline] + pub fn resize_increments(&self) -> Option> { + x11_or_wayland!(match self; Window(w) => w.resize_increments()) + } + + #[inline] + pub fn set_resize_increments(&self, increments: Option) { + x11_or_wayland!(match self; Window(w) => w.set_resize_increments(increments)) + } + + #[inline] + pub fn set_resizable(&self, resizable: bool) { + x11_or_wayland!(match self; Window(w) => w.set_resizable(resizable)) + } + + #[inline] + pub fn is_resizable(&self) -> bool { + x11_or_wayland!(match self; Window(w) => w.is_resizable()) + } + + #[inline] + pub fn set_enabled_buttons(&self, buttons: WindowButtons) { + x11_or_wayland!(match self; Window(w) => w.set_enabled_buttons(buttons)) + } + + #[inline] + pub fn enabled_buttons(&self) -> WindowButtons { + x11_or_wayland!(match self; Window(w) => w.enabled_buttons()) + } + + #[inline] + pub fn set_cursor(&self, cursor: Cursor) { + x11_or_wayland!(match self; Window(w) => w.set_cursor(cursor)) + } + + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(window) => window.set_cursor_grab(mode)) + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + x11_or_wayland!(match self; Window(window) => window.set_cursor_visible(visible)) + } + + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(window) => window.drag_window()) + } + + #[inline] + pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(window) => window.drag_resize_window(direction)) + } + + #[inline] + pub fn show_window_menu(&self, position: Position) { + x11_or_wayland!(match self; Window(w) => w.show_window_menu(position)) + } + + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(w) => w.set_cursor_hittest(hittest)) + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + x11_or_wayland!(match self; Window(w) => w.scale_factor()) + } + + #[inline] + pub fn set_cursor_position(&self, position: Position) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(w) => w.set_cursor_position(position)) + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + x11_or_wayland!(match self; Window(w) => w.set_maximized(maximized)) + } + + #[inline] + pub fn is_maximized(&self) -> bool { + x11_or_wayland!(match self; Window(w) => w.is_maximized()) + } + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + x11_or_wayland!(match self; Window(w) => w.set_minimized(minimized)) + } + + #[inline] + pub fn is_minimized(&self) -> Option { + x11_or_wayland!(match self; Window(w) => w.is_minimized()) + } + + #[inline] + pub(crate) fn fullscreen(&self) -> Option { + x11_or_wayland!(match self; Window(w) => w.fullscreen()) + } + + #[inline] + pub(crate) fn set_fullscreen(&self, monitor: Option) { + x11_or_wayland!(match self; Window(w) => w.set_fullscreen(monitor)) + } + + #[inline] + pub fn set_decorations(&self, decorations: bool) { + x11_or_wayland!(match self; Window(w) => w.set_decorations(decorations)) + } + + #[inline] + pub fn is_decorated(&self) -> bool { + x11_or_wayland!(match self; Window(w) => w.is_decorated()) + } + + #[inline] + pub fn set_window_level(&self, level: WindowLevel) { + x11_or_wayland!(match self; Window(w) => w.set_window_level(level)) + } + + #[inline] + pub fn set_window_icon(&self, window_icon: Option) { + x11_or_wayland!(match self; Window(w) => w.set_window_icon(window_icon.map(|icon| icon.inner))) + } + + #[inline] + pub fn set_ime_cursor_area(&self, position: Position, size: Size) { + x11_or_wayland!(match self; Window(w) => w.set_ime_cursor_area(position, size)) + } + + #[inline] + pub fn reset_dead_keys(&self) { + common::xkb::reset_dead_keys() + } + + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + x11_or_wayland!(match self; Window(w) => w.set_ime_allowed(allowed)) + } + + #[inline] + pub fn set_ime_purpose(&self, purpose: ImePurpose) { + x11_or_wayland!(match self; Window(w) => w.set_ime_purpose(purpose)) + } + + #[inline] + pub fn focus_window(&self) { + x11_or_wayland!(match self; Window(w) => w.focus_window()) + } + + pub fn request_user_attention(&self, request_type: Option) { + x11_or_wayland!(match self; Window(w) => w.request_user_attention(request_type)) + } + + #[inline] + pub fn request_redraw(&self) { + x11_or_wayland!(match self; Window(w) => w.request_redraw()) + } + + #[inline] + pub fn pre_present_notify(&self) { + x11_or_wayland!(match self; Window(w) => w.pre_present_notify()) + } + + #[inline] + pub fn current_monitor(&self) -> Option { + Some(x11_or_wayland!(match self; Window(w) => w.current_monitor()?; as MonitorHandle)) + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + match self { + #[cfg(x11_platform)] + Window::X(ref window) => { + window.available_monitors().into_iter().map(MonitorHandle::X).collect() + }, + #[cfg(wayland_platform)] + Window::Wayland(ref window) => { + window.available_monitors().into_iter().map(MonitorHandle::Wayland).collect() + }, + } + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + Some(x11_or_wayland!(match self; Window(w) => w.primary_monitor()?; as MonitorHandle)) + } + + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + x11_or_wayland!(match self; Window(window) => window.raw_window_handle_rwh_04()) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + x11_or_wayland!(match self; Window(window) => window.raw_window_handle_rwh_05()) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + x11_or_wayland!(match self; Window(window) => window.raw_display_handle_rwh_05()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> Result { + x11_or_wayland!(match self; Window(window) => window.raw_window_handle_rwh_06()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + x11_or_wayland!(match self; Window(window) => window.raw_display_handle_rwh_06()) + } + + #[inline] + pub fn set_theme(&self, theme: Option) { + x11_or_wayland!(match self; Window(window) => window.set_theme(theme)) + } + + #[inline] + pub fn theme(&self) -> Option { + x11_or_wayland!(match self; Window(window) => window.theme()) + } + + pub fn set_content_protected(&self, protected: bool) { + x11_or_wayland!(match self; Window(window) => window.set_content_protected(protected)) + } + + #[inline] + pub fn has_focus(&self) -> bool { + x11_or_wayland!(match self; Window(window) => window.has_focus()) + } + + pub fn title(&self) -> String { + x11_or_wayland!(match self; Window(window) => window.title()) + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra { + pub text_with_all_modifiers: Option, + pub key_without_modifiers: Key, +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub(crate) enum PlatformCustomCursor { + #[cfg(wayland_platform)] + Wayland(wayland::CustomCursor), + #[cfg(x11_platform)] + X(x11::CustomCursor), +} + +/// Hooks for X11 errors. +#[cfg(x11_platform)] +pub(crate) static XLIB_ERROR_HOOKS: Mutex> = Mutex::new(Vec::new()); + +#[cfg(x11_platform)] +unsafe extern "C" fn x_error_callback( + display: *mut x11::ffi::Display, + event: *mut x11::ffi::XErrorEvent, +) -> c_int { + let xconn_lock = X11_BACKEND.lock().unwrap_or_else(|e| e.into_inner()); + if let Ok(ref xconn) = *xconn_lock { + // Call all the hooks. + let mut error_handled = false; + for hook in XLIB_ERROR_HOOKS.lock().unwrap().iter() { + error_handled |= hook(display as *mut _, event as *mut _); + } + + // `assume_init` is safe here because the array consists of `MaybeUninit` values, + // which do not require initialization. + let mut buf: [MaybeUninit; 1024] = unsafe { MaybeUninit::uninit().assume_init() }; + unsafe { + (xconn.xlib.XGetErrorText)( + display, + (*event).error_code as c_int, + buf.as_mut_ptr() as *mut c_char, + buf.len() as c_int, + ) + }; + let description = + unsafe { CStr::from_ptr(buf.as_ptr() as *const c_char) }.to_string_lossy(); + + let error = unsafe { + XError { + description: description.into_owned(), + error_code: (*event).error_code, + request_code: (*event).request_code, + minor_code: (*event).minor_code, + } + }; + + // Don't log error. + if !error_handled { + tracing::error!("X11 error: {:#?}", error); + // XXX only update the error, if it wasn't handled by any of the hooks. + *xconn.latest_error.lock().unwrap() = Some(error); + } + } + // Fun fact: this return value is completely ignored. + 0 +} + +#[allow(clippy::large_enum_variant)] +pub enum EventLoop { + #[cfg(wayland_platform)] + Wayland(Box>), + #[cfg(x11_platform)] + X(x11::EventLoop), +} + +pub enum EventLoopProxy { + #[cfg(x11_platform)] + X(x11::EventLoopProxy), + #[cfg(wayland_platform)] + Wayland(wayland::EventLoopProxy), +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + x11_or_wayland!(match self; EventLoopProxy(proxy) => proxy.clone(); as EventLoopProxy) + } +} + +impl EventLoop { + pub(crate) fn new( + attributes: &PlatformSpecificEventLoopAttributes, + ) -> Result { + if !attributes.any_thread && !is_main_thread() { + panic!( + "Initializing the event loop outside of the main thread is a significant \ + cross-platform compatibility hazard. If you absolutely need to create an \ + EventLoop on a different thread, you can use the \ + `EventLoopBuilderExtX11::any_thread` or `EventLoopBuilderExtWayland::any_thread` \ + functions." + ); + } + + // NOTE: Wayland first because of X11 could be present under Wayland as well. Empty + // variables are also treated as not set. + let backend = match ( + attributes.forced_backend, + env::var("WAYLAND_DISPLAY") + .ok() + .filter(|var| !var.is_empty()) + .or_else(|| env::var("WAYLAND_SOCKET").ok()) + .filter(|var| !var.is_empty()) + .is_some(), + env::var("DISPLAY").map(|var| !var.is_empty()).unwrap_or(false), + ) { + // User is forcing a backend. + (Some(backend), ..) => backend, + // Wayland is present. + #[cfg(wayland_platform)] + (None, true, _) => Backend::Wayland, + // X11 is present. + #[cfg(x11_platform)] + (None, _, true) => Backend::X, + // No backend is present. + (_, wayland_display, x11_display) => { + let msg = if wayland_display && !cfg!(wayland_platform) { + "DISPLAY is not set; note: enable the `winit/wayland` feature to support \ + Wayland" + } else if x11_display && !cfg!(x11_platform) { + "neither WAYLAND_DISPLAY nor WAYLAND_SOCKET is set; note: enable the \ + `winit/x11` feature to support X11" + } else { + "neither WAYLAND_DISPLAY nor WAYLAND_SOCKET nor DISPLAY is set." + }; + return Err(EventLoopError::Os(os_error!(OsError::Misc(msg)))); + }, + }; + + // Create the display based on the backend. + match backend { + #[cfg(wayland_platform)] + Backend::Wayland => EventLoop::new_wayland_any_thread(), + #[cfg(x11_platform)] + Backend::X => EventLoop::new_x11_any_thread(), + } + } + + #[cfg(wayland_platform)] + fn new_wayland_any_thread() -> Result, EventLoopError> { + wayland::EventLoop::new().map(|evlp| EventLoop::Wayland(Box::new(evlp))) + } + + #[cfg(x11_platform)] + fn new_x11_any_thread() -> Result, EventLoopError> { + let xconn = match X11_BACKEND.lock().unwrap_or_else(|e| e.into_inner()).as_ref() { + Ok(xconn) => xconn.clone(), + Err(err) => { + return Err(EventLoopError::Os(os_error!(OsError::XNotSupported(err.clone())))) + }, + }; + + Ok(EventLoop::X(x11::EventLoop::new(xconn))) + } + + #[inline] + pub fn is_wayland(&self) -> bool { + match *self { + #[cfg(wayland_platform)] + EventLoop::Wayland(_) => true, + #[cfg(x11_platform)] + _ => false, + } + } + + pub fn create_proxy(&self) -> EventLoopProxy { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.create_proxy(); as EventLoopProxy) + } + + pub fn run(mut self, callback: F) -> Result<(), EventLoopError> + where + F: FnMut(crate::event::Event, &RootELW), + { + self.run_on_demand(callback) + } + + pub fn run_on_demand(&mut self, callback: F) -> Result<(), EventLoopError> + where + F: FnMut(crate::event::Event, &RootELW), + { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.run_on_demand(callback)) + } + + pub fn pump_events(&mut self, timeout: Option, callback: F) -> PumpStatus + where + F: FnMut(crate::event::Event, &RootELW), + { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.pump_events(timeout, callback)) + } + + pub fn window_target(&self) -> &crate::event_loop::ActiveEventLoop { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.window_target()) + } +} + +impl AsFd for EventLoop { + fn as_fd(&self) -> BorrowedFd<'_> { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.as_fd()) + } +} + +impl AsRawFd for EventLoop { + fn as_raw_fd(&self) -> RawFd { + x11_or_wayland!(match self; EventLoop(evlp) => evlp.as_raw_fd()) + } +} + +impl EventLoopProxy { + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + x11_or_wayland!(match self; EventLoopProxy(proxy) => proxy.send_event(event)) + } +} + +#[allow(clippy::large_enum_variant)] +pub enum ActiveEventLoop { + #[cfg(wayland_platform)] + Wayland(wayland::ActiveEventLoop), + #[cfg(x11_platform)] + X(x11::ActiveEventLoop), +} + +impl ActiveEventLoop { + #[inline] + pub fn is_wayland(&self) -> bool { + match *self { + #[cfg(wayland_platform)] + ActiveEventLoop::Wayland(_) => true, + #[cfg(x11_platform)] + _ => false, + } + } + + pub fn create_custom_cursor(&self, cursor: CustomCursorSource) -> CustomCursor { + x11_or_wayland!(match self; ActiveEventLoop(evlp) => evlp.create_custom_cursor(cursor)) + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + match *self { + #[cfg(wayland_platform)] + ActiveEventLoop::Wayland(ref evlp) => { + evlp.available_monitors().map(MonitorHandle::Wayland).collect() + }, + #[cfg(x11_platform)] + ActiveEventLoop::X(ref evlp) => { + evlp.available_monitors().map(MonitorHandle::X).collect() + }, + } + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + Some( + x11_or_wayland!(match self; ActiveEventLoop(evlp) => evlp.primary_monitor()?; as MonitorHandle), + ) + } + + #[inline] + pub fn listen_device_events(&self, allowed: DeviceEvents) { + x11_or_wayland!(match self; Self(evlp) => evlp.listen_device_events(allowed)) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + x11_or_wayland!(match self; Self(evlp) => evlp.raw_display_handle_rwh_05()) + } + + #[inline] + pub fn system_theme(&self) -> Option { + None + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + x11_or_wayland!(match self; Self(evlp) => evlp.raw_display_handle_rwh_06()) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + x11_or_wayland!(match self; Self(evlp) => evlp.set_control_flow(control_flow)) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + x11_or_wayland!(match self; Self(evlp) => evlp.control_flow()) + } + + pub(crate) fn clear_exit(&self) { + x11_or_wayland!(match self; Self(evlp) => evlp.clear_exit()) + } + + pub(crate) fn exit(&self) { + x11_or_wayland!(match self; Self(evlp) => evlp.exit()) + } + + pub(crate) fn exiting(&self) -> bool { + x11_or_wayland!(match self; Self(evlp) => evlp.exiting()) + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + match self { + #[cfg(x11_platform)] + Self::X(conn) => OwnedDisplayHandle::X(conn.x_connection().clone()), + #[cfg(wayland_platform)] + Self::Wayland(conn) => OwnedDisplayHandle::Wayland(conn.connection.clone()), + } + } + + #[allow(dead_code)] + fn set_exit_code(&self, code: i32) { + x11_or_wayland!(match self; Self(evlp) => evlp.set_exit_code(code)) + } + + #[allow(dead_code)] + fn exit_code(&self) -> Option { + x11_or_wayland!(match self; Self(evlp) => evlp.exit_code()) + } +} + +#[derive(Clone)] +#[allow(dead_code)] +pub(crate) enum OwnedDisplayHandle { + #[cfg(x11_platform)] + X(Arc), + #[cfg(wayland_platform)] + Wayland(wayland_client::Connection), +} + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + match self { + #[cfg(x11_platform)] + Self::X(xconn) => { + let mut xlib_handle = rwh_05::XlibDisplayHandle::empty(); + xlib_handle.display = xconn.display.cast(); + xlib_handle.screen = xconn.default_screen_index() as _; + xlib_handle.into() + }, + + #[cfg(wayland_platform)] + Self::Wayland(conn) => { + use sctk::reexports::client::Proxy; + + let mut wayland_handle = rwh_05::WaylandDisplayHandle::empty(); + wayland_handle.display = conn.display().id().as_ptr() as *mut _; + wayland_handle.into() + }, + } + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + use std::ptr::NonNull; + + match self { + #[cfg(x11_platform)] + Self::X(xconn) => Ok(rwh_06::XlibDisplayHandle::new( + NonNull::new(xconn.display.cast()), + xconn.default_screen_index() as _, + ) + .into()), + + #[cfg(wayland_platform)] + Self::Wayland(conn) => { + use sctk::reexports::client::Proxy; + + Ok(rwh_06::WaylandDisplayHandle::new( + NonNull::new(conn.display().id().as_ptr().cast()).unwrap(), + ) + .into()) + }, + } + } +} + +/// Returns the minimum `Option`, taking into account that `None` +/// equates to an infinite timeout, not a zero timeout (so can't just use +/// `Option::min`) +fn min_timeout(a: Option, b: Option) -> Option { + a.map_or(b, |a_timeout| b.map_or(Some(a_timeout), |b_timeout| Some(a_timeout.min(b_timeout)))) +} + +#[cfg(target_os = "linux")] +fn is_main_thread() -> bool { + rustix::thread::gettid() == rustix::process::getpid() +} + +#[cfg(any(target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd"))] +fn is_main_thread() -> bool { + use libc::pthread_main_np; + + unsafe { pthread_main_np() == 1 } +} + +#[cfg(target_os = "netbsd")] +fn is_main_thread() -> bool { + std::thread::current().name() == Some("main") +} diff --git a/vendor/winit/src/platform_impl/linux/wayland/event_loop/mod.rs b/vendor/winit/src/platform_impl/linux/wayland/event_loop/mod.rs new file mode 100644 index 00000000..fef13f45 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/event_loop/mod.rs @@ -0,0 +1,810 @@ +//! The event-loop routines. + +use std::cell::{Cell, RefCell}; +use std::io::Result as IOResult; +use std::marker::PhantomData; +use std::mem; +use std::os::fd::OwnedFd; +use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; +use std::rc::Rc; +use std::sync::atomic::Ordering; +use std::sync::{Arc, Condvar, Mutex}; +use std::thread::JoinHandle; +use std::time::{Duration, Instant}; + +use calloop::ping::Ping; +use rustix::event::{PollFd, PollFlags}; +use rustix::pipe::{self, PipeFlags}; +use sctk::reexports::calloop::Error as CalloopError; +use sctk::reexports::calloop_wayland_source::WaylandSource; +use sctk::reexports::client::{globals, Connection, QueueHandle}; +use tracing::warn; + +use crate::cursor::OnlyCursorImage; +use crate::dpi::LogicalSize; +use crate::error::{EventLoopError, OsError as RootOsError}; +use crate::event::{Event, InnerSizeWriter, StartCause, WindowEvent}; +use crate::event_loop::{ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents}; +use crate::platform::pump_events::PumpStatus; +use crate::platform_impl::platform::min_timeout; +use crate::platform_impl::{ + ActiveEventLoop as PlatformActiveEventLoop, OsError, PlatformCustomCursor, +}; +use crate::window::{CustomCursor as RootCustomCursor, CustomCursorSource}; + +mod proxy; +pub mod sink; + +pub use proxy::EventLoopProxy; +use sink::EventSink; + +use super::state::{WindowCompositorUpdate, WinitState}; +use super::window::state::FrameCallbackState; +use super::{logical_to_physical_rounded, DeviceId, WaylandError, WindowId}; + +type WaylandDispatcher = calloop::Dispatcher<'static, WaylandSource, WinitState>; + +/// The Wayland event loop. +pub struct EventLoop { + /// Has `run` or `run_on_demand` been called or a call to `pump_events` that starts the loop + loop_running: bool, + + buffer_sink: EventSink, + compositor_updates: Vec, + window_ids: Vec, + + /// Sender of user events. + user_events_sender: calloop::channel::Sender, + + // XXX can't remove RefCell out of here, unless we can plumb generics into the `Window`, which + // we don't really want, since it'll break public API by a lot. + /// Pending events from the user. + pending_user_events: Rc>>, + + /// The Wayland dispatcher to has raw access to the queue when needed, such as + /// when creating a new window. + wayland_dispatcher: WaylandDispatcher, + + /// Connection to the wayland server. + connection: Connection, + + /// Event loop window target. + window_target: RootActiveEventLoop, + + // XXX drop after everything else, just to be safe. + /// Calloop's event loop. + event_loop: calloop::EventLoop<'static, WinitState>, + + pump_event_notifier: Option, +} + +impl EventLoop { + pub fn new() -> Result, EventLoopError> { + macro_rules! map_err { + ($e:expr, $err:expr) => { + $e.map_err(|error| os_error!($err(error).into())) + }; + } + + let connection = map_err!(Connection::connect_to_env(), WaylandError::Connection)?; + + let (globals, mut event_queue) = + map_err!(globals::registry_queue_init(&connection), WaylandError::Global)?; + let queue_handle = event_queue.handle(); + + let event_loop = + map_err!(calloop::EventLoop::::try_new(), WaylandError::Calloop)?; + + let mut winit_state = WinitState::new(&globals, &queue_handle, event_loop.handle()) + .map_err(|error| os_error!(error))?; + + // NOTE: do a roundtrip after binding the globals to prevent potential + // races with the server. + map_err!(event_queue.roundtrip(&mut winit_state), WaylandError::Dispatch)?; + + // Register Wayland source. + let wayland_source = WaylandSource::new(connection.clone(), event_queue); + let wayland_dispatcher = + calloop::Dispatcher::new(wayland_source, |_, queue, winit_state: &mut WinitState| { + let result = queue.dispatch_pending(winit_state); + if result.is_ok() + && (!winit_state.events_sink.is_empty() + || !winit_state.window_compositor_updates.is_empty()) + { + winit_state.dispatched_events = true; + } + result + }); + + map_err!( + event_loop.handle().register_dispatcher(wayland_dispatcher.clone()), + WaylandError::Calloop + )?; + + // Setup the user proxy. + let pending_user_events = Rc::new(RefCell::new(Vec::new())); + let pending_user_events_clone = pending_user_events.clone(); + let (user_events_sender, user_events_channel) = calloop::channel::channel(); + let result = event_loop + .handle() + .insert_source(user_events_channel, move |event, _, winit_state: &mut WinitState| { + if let calloop::channel::Event::Msg(msg) = event { + winit_state.dispatched_events = true; + pending_user_events_clone.borrow_mut().push(msg); + } + }) + .map_err(|error| error.error); + map_err!(result, WaylandError::Calloop)?; + + // An event's loop awakener to wake up for window events from winit's windows. + let (event_loop_awakener, event_loop_awakener_source) = map_err!( + calloop::ping::make_ping() + .map_err(|error| CalloopError::OtherError(Box::new(error).into())), + WaylandError::Calloop + )?; + + let result = event_loop + .handle() + .insert_source(event_loop_awakener_source, move |_, _, winit_state: &mut WinitState| { + // Mark that we have something to dispatch. + winit_state.dispatched_events = true; + }) + .map_err(|error| error.error); + map_err!(result, WaylandError::Calloop)?; + + let window_target = ActiveEventLoop { + connection: connection.clone(), + wayland_dispatcher: wayland_dispatcher.clone(), + event_loop_awakener, + queue_handle, + control_flow: Cell::new(ControlFlow::default()), + exit: Cell::new(None), + state: RefCell::new(winit_state), + }; + + let event_loop = Self { + loop_running: false, + compositor_updates: Vec::new(), + buffer_sink: EventSink::default(), + window_ids: Vec::new(), + connection, + wayland_dispatcher, + user_events_sender, + pending_user_events, + event_loop, + window_target: RootActiveEventLoop { + p: PlatformActiveEventLoop::Wayland(window_target), + _marker: PhantomData, + }, + pump_event_notifier: None, + }; + + Ok(event_loop) + } + + pub fn run_on_demand(&mut self, mut event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &RootActiveEventLoop), + { + let exit = loop { + match self.pump_events(None, &mut event_handler) { + PumpStatus::Exit(0) => { + break Ok(()); + }, + PumpStatus::Exit(code) => { + break Err(EventLoopError::ExitFailure(code)); + }, + _ => { + continue; + }, + } + }; + + // Applications aren't allowed to carry windows between separate + // `run_on_demand` calls but if they have only just dropped their + // windows we need to make sure those last requests are sent to the + // compositor. + let _ = self.roundtrip().map_err(EventLoopError::Os); + + exit + } + + pub fn pump_events(&mut self, timeout: Option, mut callback: F) -> PumpStatus + where + F: FnMut(Event, &RootActiveEventLoop), + { + if !self.loop_running { + self.loop_running = true; + + // Run the initial loop iteration. + self.single_iteration(&mut callback, StartCause::Init); + } + + // Consider the possibility that the `StartCause::Init` iteration could + // request to Exit. + if !self.exiting() { + self.poll_events_with_timeout(timeout, &mut callback); + } + if let Some(code) = self.exit_code() { + self.loop_running = false; + + callback(Event::LoopExiting, self.window_target()); + + PumpStatus::Exit(code) + } else { + // NOTE: spawn a wake-up thread, thus if we have code reading the wayland connection + // in parallel to winit, we ensure that the loop itself is marked as having events. + if timeout.is_some() && self.pump_event_notifier.is_none() { + let awakener = match &self.window_target.p { + PlatformActiveEventLoop::Wayland(window_target) => { + window_target.event_loop_awakener.clone() + }, + #[cfg(x11_platform)] + PlatformActiveEventLoop::X(_) => unreachable!(), + }; + + self.pump_event_notifier = + Some(PumpEventNotifier::spawn(self.connection.clone(), awakener)); + } + + if let Some(pump_event_notifier) = self.pump_event_notifier.as_ref() { + // Notify that we don't have to wait, since we're out of winit. + *pump_event_notifier.control.0.lock().unwrap() = PumpEventNotifierAction::Monitor; + pump_event_notifier.control.1.notify_one(); + } + + PumpStatus::Continue + } + } + + pub fn poll_events_with_timeout(&mut self, mut timeout: Option, mut callback: F) + where + F: FnMut(Event, &RootActiveEventLoop), + { + let cause = loop { + let start = Instant::now(); + + timeout = { + let control_flow_timeout = match self.control_flow() { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Duration::ZERO), + ControlFlow::WaitUntil(wait_deadline) => { + Some(wait_deadline.saturating_duration_since(start)) + }, + }; + min_timeout(control_flow_timeout, timeout) + }; + + // NOTE Ideally we should flush as the last thing we do before polling + // to wait for events, and this should be done by the calloop + // WaylandSource but we currently need to flush writes manually. + // + // Checking for flush error is essential to perform an exit with error, since + // once we have a protocol error, we could get stuck retrying... + if self.connection.flush().is_err() { + self.set_exit_code(1); + return; + } + + if let Err(error) = self.loop_dispatch(timeout) { + // NOTE We exit on errors from dispatches, since if we've got protocol error + // libwayland-client/wayland-rs will inform us anyway, but crashing downstream is + // not really an option. Instead we inform that the event loop got + // destroyed. We may communicate an error that something was + // terminated, but winit doesn't provide us with an API to do that + // via some event. Still, we set the exit code to the error's OS + // error code, or to 1 if not possible. + let exit_code = error.raw_os_error().unwrap_or(1); + self.set_exit_code(exit_code); + return; + } + + // NB: `StartCause::Init` is handled as a special case and doesn't need + // to be considered here + let cause = match self.control_flow() { + ControlFlow::Poll => StartCause::Poll, + ControlFlow::Wait => StartCause::WaitCancelled { start, requested_resume: None }, + ControlFlow::WaitUntil(deadline) => { + if Instant::now() < deadline { + StartCause::WaitCancelled { start, requested_resume: Some(deadline) } + } else { + StartCause::ResumeTimeReached { start, requested_resume: deadline } + } + }, + }; + + // Reduce spurious wake-ups. + let dispatched_events = self.with_state(|state| state.dispatched_events); + if matches!(cause, StartCause::WaitCancelled { .. }) + && !dispatched_events + && timeout.is_none() + { + continue; + } + + break cause; + }; + + self.single_iteration(&mut callback, cause); + } + + fn single_iteration(&mut self, callback: &mut F, cause: StartCause) + where + F: FnMut(Event, &RootActiveEventLoop), + { + // NOTE currently just indented to simplify the diff + + // We retain these grow-only scratch buffers as part of the EventLoop + // for the sake of avoiding lots of reallocs. We take them here to avoid + // trying to mutably borrow `self` more than once and we swap them back + // when finished. + let mut compositor_updates = std::mem::take(&mut self.compositor_updates); + let mut buffer_sink = std::mem::take(&mut self.buffer_sink); + let mut window_ids = std::mem::take(&mut self.window_ids); + + callback(Event::NewEvents(cause), &self.window_target); + + // NB: For consistency all platforms must emit a 'resumed' event even though Wayland + // applications don't themselves have a formal suspend/resume lifecycle. + if cause == StartCause::Init { + callback(Event::Resumed, &self.window_target); + } + + // Handle pending user events. We don't need back buffer, since we can't dispatch + // user events indirectly via callback to the user. + for user_event in self.pending_user_events.borrow_mut().drain(..) { + callback(Event::UserEvent(user_event), &self.window_target); + } + + // Drain the pending compositor updates. + self.with_state(|state| compositor_updates.append(&mut state.window_compositor_updates)); + + for mut compositor_update in compositor_updates.drain(..) { + let window_id = compositor_update.window_id; + if compositor_update.scale_changed { + let (physical_size, scale_factor) = self.with_state(|state| { + let windows = state.windows.get_mut(); + let window = windows.get(&window_id).unwrap().lock().unwrap(); + let scale_factor = window.scale_factor(); + let size = logical_to_physical_rounded(window.inner_size(), scale_factor); + (size, scale_factor) + }); + + // Stash the old window size. + let old_physical_size = physical_size; + + let new_inner_size = Arc::new(Mutex::new(physical_size)); + callback( + Event::WindowEvent { + window_id: crate::window::WindowId(window_id), + event: WindowEvent::ScaleFactorChanged { + scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade( + &new_inner_size, + )), + }, + }, + &self.window_target, + ); + + let physical_size = *new_inner_size.lock().unwrap(); + drop(new_inner_size); + + // Resize the window when user altered the size. + if old_physical_size != physical_size { + self.with_state(|state| { + let windows = state.windows.get_mut(); + let mut window = windows.get(&window_id).unwrap().lock().unwrap(); + + let new_logical_size: LogicalSize = + physical_size.to_logical(scale_factor); + window.request_inner_size(new_logical_size.into()); + }); + + // Make it queue resize. + compositor_update.resized = true; + } + } + + // NOTE: Rescale changed the physical size which winit operates in, thus we should + // resize. + if compositor_update.resized || compositor_update.scale_changed { + let physical_size = self.with_state(|state| { + let windows = state.windows.get_mut(); + let window = windows.get(&window_id).unwrap().lock().unwrap(); + + let scale_factor = window.scale_factor(); + let size = logical_to_physical_rounded(window.inner_size(), scale_factor); + + // Mark the window as needed a redraw. + state + .window_requests + .get_mut() + .get_mut(&window_id) + .unwrap() + .redraw_requested + .store(true, Ordering::Relaxed); + + size + }); + + callback( + Event::WindowEvent { + window_id: crate::window::WindowId(window_id), + event: WindowEvent::Resized(physical_size), + }, + &self.window_target, + ); + } + + if compositor_update.close_window { + callback( + Event::WindowEvent { + window_id: crate::window::WindowId(window_id), + event: WindowEvent::CloseRequested, + }, + &self.window_target, + ); + } + } + + // Push the events directly from the window. + self.with_state(|state| { + buffer_sink.append(&mut state.window_events_sink.lock().unwrap()); + }); + for event in buffer_sink.drain() { + let event = event.map_nonuser_event().unwrap(); + callback(event, &self.window_target); + } + + // Handle non-synthetic events. + self.with_state(|state| { + buffer_sink.append(&mut state.events_sink); + }); + for event in buffer_sink.drain() { + let event = event.map_nonuser_event().unwrap(); + callback(event, &self.window_target); + } + + // Collect the window ids + self.with_state(|state| { + window_ids.extend(state.window_requests.get_mut().keys()); + }); + + for window_id in window_ids.iter() { + let event = self.with_state(|state| { + let window_requests = state.window_requests.get_mut(); + if window_requests.get(window_id).unwrap().take_closed() { + mem::drop(window_requests.remove(window_id)); + mem::drop(state.windows.get_mut().remove(window_id)); + return Some(WindowEvent::Destroyed); + } + + let mut window = + state.windows.get_mut().get_mut(window_id).unwrap().lock().unwrap(); + + if window.frame_callback_state() == FrameCallbackState::Requested { + return None; + } + + // Reset the frame callbacks state. + window.frame_callback_reset(); + let mut redraw_requested = + window_requests.get(window_id).unwrap().take_redraw_requested(); + + // Redraw the frame while at it. + redraw_requested |= window.refresh_frame(); + + redraw_requested.then_some(WindowEvent::RedrawRequested) + }); + + if let Some(event) = event { + callback( + Event::WindowEvent { window_id: crate::window::WindowId(*window_id), event }, + &self.window_target, + ); + } + } + + // Reset the hint that we've dispatched events. + self.with_state(|state| { + state.dispatched_events = false; + }); + + // This is always the last event we dispatch before poll again + callback(Event::AboutToWait, &self.window_target); + + // Update the window frames and schedule redraws. + let mut wake_up = false; + for window_id in window_ids.drain(..) { + wake_up |= self.with_state(|state| match state.windows.get_mut().get_mut(&window_id) { + Some(window) => { + let refresh = window.lock().unwrap().refresh_frame(); + if refresh { + state + .window_requests + .get_mut() + .get_mut(&window_id) + .unwrap() + .redraw_requested + .store(true, Ordering::Relaxed); + } + + refresh + }, + None => false, + }); + } + + // Wakeup event loop if needed. + // + // If the user draws from the `AboutToWait` this is likely not required, however + // we can't do much about it. + if wake_up { + match &self.window_target.p { + PlatformActiveEventLoop::Wayland(window_target) => { + window_target.event_loop_awakener.ping(); + }, + #[cfg(x11_platform)] + PlatformActiveEventLoop::X(_) => unreachable!(), + } + } + + std::mem::swap(&mut self.compositor_updates, &mut compositor_updates); + std::mem::swap(&mut self.buffer_sink, &mut buffer_sink); + std::mem::swap(&mut self.window_ids, &mut window_ids); + } + + #[inline] + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy::new(self.user_events_sender.clone()) + } + + #[inline] + pub fn window_target(&self) -> &RootActiveEventLoop { + &self.window_target + } + + fn with_state<'a, U: 'a, F: FnOnce(&'a mut WinitState) -> U>(&'a mut self, callback: F) -> U { + let state = match &mut self.window_target.p { + PlatformActiveEventLoop::Wayland(window_target) => window_target.state.get_mut(), + #[cfg(x11_platform)] + _ => unreachable!(), + }; + + callback(state) + } + + fn loop_dispatch>>(&mut self, timeout: D) -> IOResult<()> { + let state = match &mut self.window_target.p { + PlatformActiveEventLoop::Wayland(window_target) => window_target.state.get_mut(), + #[cfg(feature = "x11")] + _ => unreachable!(), + }; + + self.event_loop.dispatch(timeout, state).map_err(|error| { + tracing::error!("Error dispatching event loop: {}", error); + error.into() + }) + } + + fn roundtrip(&mut self) -> Result { + let state = match &mut self.window_target.p { + PlatformActiveEventLoop::Wayland(window_target) => window_target.state.get_mut(), + #[cfg(feature = "x11")] + _ => unreachable!(), + }; + + let mut wayland_source = self.wayland_dispatcher.as_source_mut(); + let event_queue = wayland_source.queue(); + event_queue.roundtrip(state).map_err(|error| { + os_error!(OsError::WaylandError(Arc::new(WaylandError::Dispatch(error)))) + }) + } + + fn control_flow(&self) -> ControlFlow { + self.window_target.p.control_flow() + } + + fn exiting(&self) -> bool { + self.window_target.p.exiting() + } + + fn set_exit_code(&self, code: i32) { + self.window_target.p.set_exit_code(code) + } + + fn exit_code(&self) -> Option { + self.window_target.p.exit_code() + } +} + +impl AsFd for EventLoop { + fn as_fd(&self) -> BorrowedFd<'_> { + self.event_loop.as_fd() + } +} + +impl AsRawFd for EventLoop { + fn as_raw_fd(&self) -> RawFd { + self.event_loop.as_raw_fd() + } +} + +pub struct ActiveEventLoop { + /// The event loop wakeup source. + pub event_loop_awakener: Ping, + + /// The main queue used by the event loop. + pub queue_handle: QueueHandle, + + /// The application's latest control_flow state + pub(crate) control_flow: Cell, + + /// The application's exit state. + pub(crate) exit: Cell>, + + // TODO remove that RefCell once we can pass `&mut` in `Window::new`. + /// Winit state. + pub state: RefCell, + + /// Dispatcher of Wayland events. + pub wayland_dispatcher: WaylandDispatcher, + + /// Connection to the wayland server. + pub connection: Connection, +} + +impl ActiveEventLoop { + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.control_flow.set(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub(crate) fn exit(&self) { + self.exit.set(Some(0)) + } + + pub(crate) fn clear_exit(&self) { + self.exit.set(None) + } + + pub(crate) fn exiting(&self) -> bool { + self.exit.get().is_some() + } + + pub(crate) fn set_exit_code(&self, code: i32) { + self.exit.set(Some(code)) + } + + pub(crate) fn exit_code(&self) -> Option { + self.exit.get() + } + + #[inline] + pub fn listen_device_events(&self, _allowed: DeviceEvents) {} + + pub(crate) fn create_custom_cursor(&self, cursor: CustomCursorSource) -> RootCustomCursor { + RootCustomCursor { + inner: PlatformCustomCursor::Wayland(OnlyCursorImage(Arc::from(cursor.inner.0))), + } + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + use sctk::reexports::client::Proxy; + + let mut display_handle = rwh_05::WaylandDisplayHandle::empty(); + display_handle.display = self.connection.display().id().as_ptr() as *mut _; + rwh_05::RawDisplayHandle::Wayland(display_handle) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + use sctk::reexports::client::Proxy; + + Ok(rwh_06::WaylandDisplayHandle::new({ + let ptr = self.connection.display().id().as_ptr(); + std::ptr::NonNull::new(ptr as *mut _).expect("wl_display should never be null") + }) + .into()) + } +} + +#[derive(Debug)] +struct PumpEventNotifier { + /// Whether we're in winit or not. + control: Arc<(Mutex, Condvar)>, + /// Waker handle for the working thread. + worker_waker: Option, + /// Thread handle. + handle: Option>, +} + +impl Drop for PumpEventNotifier { + fn drop(&mut self) { + // Wake-up the thread. + if let Some(worker_waker) = self.worker_waker.as_ref() { + let _ = rustix::io::write(worker_waker.as_fd(), &[0u8]); + } + *self.control.0.lock().unwrap() = PumpEventNotifierAction::Shutdown; + self.control.1.notify_one(); + + if let Some(handle) = self.handle.take() { + let _ = handle.join(); + } + } +} + +impl PumpEventNotifier { + fn spawn(connection: Connection, awakener: Ping) -> Self { + // Start from the waiting state. + let control = Arc::new((Mutex::new(PumpEventNotifierAction::Pause), Condvar::new())); + let control_thread = Arc::clone(&control); + + let (read, write) = match pipe::pipe_with(PipeFlags::CLOEXEC | PipeFlags::NONBLOCK) { + Ok((read, write)) => (read, write), + Err(_) => return Self { control, handle: None, worker_waker: None }, + }; + + let handle = + std::thread::Builder::new().name(String::from("pump_events mon")).spawn(move || { + let (lock, cvar) = &*control_thread; + 'outer: loop { + let mut wait = lock.lock().unwrap(); + while *wait == PumpEventNotifierAction::Pause { + wait = cvar.wait(wait).unwrap(); + } + + // Exit the loop when we're asked to. Given that we poll + // only once we can take the `prepare_read`, but in some cases + // it could be not possible, we may block on `join`. + if *wait == PumpEventNotifierAction::Shutdown { + break 'outer; + } + + // Wake-up the main loop and put this one back to sleep. + *wait = PumpEventNotifierAction::Pause; + drop(wait); + + while let Some(read_guard) = connection.prepare_read() { + let _ = connection.flush(); + let poll_fd = PollFd::from_borrowed_fd(connection.as_fd(), PollFlags::IN); + let pipe_poll_fd = PollFd::from_borrowed_fd(read.as_fd(), PollFlags::IN); + // Read from the `fd` before going back to poll. + if Ok(1) == rustix::io::read(read.as_fd(), &mut [0u8; 1]) { + break 'outer; + } + let _ = rustix::event::poll(&mut [poll_fd, pipe_poll_fd], -1); + // Non-blocking read the connection. + let _ = read_guard.read_without_dispatch(); + } + + awakener.ping(); + } + }); + + if let Some(err) = handle.as_ref().err() { + warn!("failed to spawn pump_events wake-up thread: {err}"); + } + + PumpEventNotifier { control, handle: handle.ok(), worker_waker: Some(write) } + } +} + +#[derive(Debug, PartialEq, Eq)] +enum PumpEventNotifierAction { + /// Monitor the wayland queue. + Monitor, + /// Pause monitoring. + Pause, + /// Shutdown the thread. + Shutdown, +} diff --git a/vendor/winit/src/platform_impl/linux/wayland/event_loop/proxy.rs b/vendor/winit/src/platform_impl/linux/wayland/event_loop/proxy.rs new file mode 100644 index 00000000..9dc7d992 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/event_loop/proxy.rs @@ -0,0 +1,28 @@ +//! An event loop proxy. + +use std::sync::mpsc::SendError; + +use sctk::reexports::calloop::channel::Sender; + +use crate::event_loop::EventLoopClosed; + +/// A handle that can be sent across the threads and used to wake up the `EventLoop`. +pub struct EventLoopProxy { + user_events_sender: Sender, +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + EventLoopProxy { user_events_sender: self.user_events_sender.clone() } + } +} + +impl EventLoopProxy { + pub fn new(user_events_sender: Sender) -> Self { + Self { user_events_sender } + } + + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.user_events_sender.send(event).map_err(|SendError(error)| EventLoopClosed(error)) + } +} diff --git a/vendor/winit/src/platform_impl/linux/wayland/event_loop/sink.rs b/vendor/winit/src/platform_impl/linux/wayland/event_loop/sink.rs new file mode 100644 index 00000000..e506b4a8 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/event_loop/sink.rs @@ -0,0 +1,53 @@ +//! An event loop's sink to deliver events from the Wayland event callbacks. + +use std::vec::Drain; + +use crate::event::{DeviceEvent, DeviceId as RootDeviceId, Event, WindowEvent}; +use crate::platform_impl::platform::DeviceId as PlatformDeviceId; +use crate::window::WindowId as RootWindowId; + +use super::{DeviceId, WindowId}; + +/// An event loop's sink to deliver events from the Wayland event callbacks +/// to the winit's user. +#[derive(Default)] +pub struct EventSink { + pub window_events: Vec>, +} + +impl EventSink { + pub fn new() -> Self { + Default::default() + } + + /// Return `true` if there're pending events. + #[inline] + pub fn is_empty(&self) -> bool { + self.window_events.is_empty() + } + + /// Add new device event to a queue. + #[inline] + pub fn push_device_event(&mut self, event: DeviceEvent, device_id: DeviceId) { + self.window_events.push(Event::DeviceEvent { + event, + device_id: RootDeviceId(PlatformDeviceId::Wayland(device_id)), + }); + } + + /// Add new window event to a queue. + #[inline] + pub fn push_window_event(&mut self, event: WindowEvent, window_id: WindowId) { + self.window_events.push(Event::WindowEvent { event, window_id: RootWindowId(window_id) }); + } + + #[inline] + pub fn append(&mut self, other: &mut Self) { + self.window_events.append(&mut other.window_events); + } + + #[inline] + pub fn drain(&mut self) -> Drain<'_, Event<()>> { + self.window_events.drain(..) + } +} diff --git a/vendor/winit/src/platform_impl/linux/wayland/mod.rs b/vendor/winit/src/platform_impl/linux/wayland/mod.rs new file mode 100644 index 00000000..63052b78 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/mod.rs @@ -0,0 +1,85 @@ +//! Winit's Wayland backend. + +use std::fmt::Display; +use std::sync::Arc; + +use sctk::reexports::client::globals::{BindError, GlobalError}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{self, ConnectError, DispatchError, Proxy}; + +pub(super) use crate::cursor::OnlyCursorImage as CustomCursor; +use crate::dpi::{LogicalSize, PhysicalSize}; +pub use crate::platform_impl::platform::{OsError, WindowId}; +pub use event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy}; +pub use output::{MonitorHandle, VideoModeHandle}; +pub use window::Window; + +mod event_loop; +mod output; +mod seat; +mod state; +mod types; +mod window; + +#[derive(Debug)] +pub enum WaylandError { + /// Error connecting to the socket. + Connection(ConnectError), + + /// Error binding the global. + Global(GlobalError), + + // Bind error. + Bind(BindError), + + /// Error during the dispatching the event queue. + Dispatch(DispatchError), + + /// Calloop error. + Calloop(calloop::Error), + + /// Wayland + Wire(client::backend::WaylandError), +} + +impl Display for WaylandError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + WaylandError::Connection(error) => error.fmt(f), + WaylandError::Global(error) => error.fmt(f), + WaylandError::Bind(error) => error.fmt(f), + WaylandError::Dispatch(error) => error.fmt(f), + WaylandError::Calloop(error) => error.fmt(f), + WaylandError::Wire(error) => error.fmt(f), + } + } +} + +impl From for OsError { + fn from(value: WaylandError) -> Self { + Self::WaylandError(Arc::new(value)) + } +} + +/// Dummy device id, since Wayland doesn't have device events. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId; + +impl DeviceId { + pub const fn dummy() -> Self { + DeviceId + } +} + +/// Get the WindowId out of the surface. +#[inline] +fn make_wid(surface: &WlSurface) -> WindowId { + WindowId(surface.id().as_ptr() as u64) +} + +/// The default routine does floor, but we need round on Wayland. +fn logical_to_physical_rounded(size: LogicalSize, scale_factor: f64) -> PhysicalSize { + let width = size.width as f64 * scale_factor; + let height = size.height as f64 * scale_factor; + (width.round(), height.round()).into() +} diff --git a/vendor/winit/src/platform_impl/linux/wayland/output.rs b/vendor/winit/src/platform_impl/linux/wayland/output.rs new file mode 100644 index 00000000..ae6c1b06 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/output.rs @@ -0,0 +1,163 @@ +use sctk::reexports::client::protocol::wl_output::WlOutput; +use sctk::reexports::client::Proxy; + +use sctk::output::OutputData; + +use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; +use crate::platform_impl::platform::VideoModeHandle as PlatformVideoModeHandle; + +use super::event_loop::ActiveEventLoop; + +impl ActiveEventLoop { + #[inline] + pub fn available_monitors(&self) -> impl Iterator { + self.state.borrow().output_state.outputs().map(MonitorHandle::new) + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + // There's no primary monitor on Wayland. + None + } +} + +#[derive(Clone, Debug)] +pub struct MonitorHandle { + pub(crate) proxy: WlOutput, +} + +impl MonitorHandle { + #[inline] + pub(crate) fn new(proxy: WlOutput) -> Self { + Self { proxy } + } + + #[inline] + pub fn name(&self) -> Option { + let output_data = self.proxy.data::().unwrap(); + output_data.with_output_info(|info| info.name.clone()) + } + + #[inline] + pub fn native_identifier(&self) -> u32 { + let output_data = self.proxy.data::().unwrap(); + output_data.with_output_info(|info| info.id) + } + + #[inline] + pub fn size(&self) -> PhysicalSize { + let output_data = self.proxy.data::().unwrap(); + let dimensions = output_data.with_output_info(|info| { + info.modes.iter().find_map(|mode| mode.current.then_some(mode.dimensions)) + }); + + match dimensions { + Some((width, height)) => (width as u32, height as u32), + _ => (0, 0), + } + .into() + } + + #[inline] + pub fn position(&self) -> PhysicalPosition { + let output_data = self.proxy.data::().unwrap(); + output_data.with_output_info(|info| { + info.logical_position.map_or_else( + || { + LogicalPosition::::from(info.location) + .to_physical(info.scale_factor as f64) + }, + |logical_position| { + LogicalPosition::::from(logical_position) + .to_physical(info.scale_factor as f64) + }, + ) + }) + } + + #[inline] + pub fn refresh_rate_millihertz(&self) -> Option { + let output_data = self.proxy.data::().unwrap(); + output_data.with_output_info(|info| { + info.modes.iter().find_map(|mode| mode.current.then_some(mode.refresh_rate as u32)) + }) + } + + #[inline] + pub fn scale_factor(&self) -> i32 { + let output_data = self.proxy.data::().unwrap(); + output_data.scale_factor() + } + + #[inline] + pub fn video_modes(&self) -> impl Iterator { + let output_data = self.proxy.data::().unwrap(); + let modes = output_data.with_output_info(|info| info.modes.clone()); + + let monitor = self.clone(); + + modes.into_iter().map(move |mode| { + PlatformVideoModeHandle::Wayland(VideoModeHandle { + size: (mode.dimensions.0 as u32, mode.dimensions.1 as u32).into(), + refresh_rate_millihertz: mode.refresh_rate as u32, + bit_depth: 32, + monitor: monitor.clone(), + }) + }) + } +} + +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + self.native_identifier() == other.native_identifier() + } +} + +impl Eq for MonitorHandle {} + +impl PartialOrd for MonitorHandle { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for MonitorHandle { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.native_identifier().cmp(&other.native_identifier()) + } +} + +impl std::hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + self.native_identifier().hash(state); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct VideoModeHandle { + pub(crate) size: PhysicalSize, + pub(crate) bit_depth: u16, + pub(crate) refresh_rate_millihertz: u32, + pub(crate) monitor: MonitorHandle, +} + +impl VideoModeHandle { + #[inline] + pub fn size(&self) -> PhysicalSize { + self.size + } + + #[inline] + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + #[inline] + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone() + } +} diff --git a/vendor/winit/src/platform_impl/linux/wayland/seat/keyboard/mod.rs b/vendor/winit/src/platform_impl/linux/wayland/seat/keyboard/mod.rs new file mode 100644 index 00000000..f84c3867 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/seat/keyboard/mod.rs @@ -0,0 +1,380 @@ +//! The keyboard input handling. + +use std::sync::Mutex; +use std::time::Duration; + +use calloop::timer::{TimeoutAction, Timer}; +use calloop::{LoopHandle, RegistrationToken}; +use tracing::warn; + +use sctk::reexports::client::protocol::wl_keyboard::{ + Event as WlKeyboardEvent, KeyState as WlKeyState, KeymapFormat as WlKeymapFormat, WlKeyboard, +}; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::{Connection, Dispatch, Proxy, QueueHandle, WEnum}; + +use crate::event::{ElementState, WindowEvent}; +use crate::keyboard::ModifiersState; + +use crate::platform_impl::common::xkb::Context; +use crate::platform_impl::wayland::event_loop::sink::EventSink; +use crate::platform_impl::wayland::state::WinitState; +use crate::platform_impl::wayland::{self, DeviceId, WindowId}; + +impl Dispatch for WinitState { + fn event( + state: &mut WinitState, + wl_keyboard: &WlKeyboard, + event: ::Event, + data: &KeyboardData, + _: &Connection, + _: &QueueHandle, + ) { + let seat_state = match state.seats.get_mut(&data.seat.id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received keyboard event {event:?} without seat"); + return; + }, + }; + let keyboard_state = match seat_state.keyboard_state.as_mut() { + Some(keyboard_state) => keyboard_state, + None => { + warn!("Received keyboard event {event:?} without keyboard"); + return; + }, + }; + + match event { + WlKeyboardEvent::Keymap { format, fd, size } => match format { + WEnum::Value(format) => match format { + WlKeymapFormat::NoKeymap => { + warn!("non-xkb compatible keymap") + }, + WlKeymapFormat::XkbV1 => { + let context = &mut keyboard_state.xkb_context; + context.set_keymap_from_fd(fd, size as usize); + }, + _ => unreachable!(), + }, + WEnum::Unknown(value) => { + warn!("unknown keymap format 0x{:x}", value) + }, + }, + WlKeyboardEvent::Enter { surface, .. } => { + let window_id = wayland::make_wid(&surface); + + // Mark the window as focused. + let was_unfocused = match state.windows.get_mut().get(&window_id) { + Some(window) => { + let mut window = window.lock().unwrap(); + let was_unfocused = !window.has_focus(); + window.add_seat_focus(data.seat.id()); + was_unfocused + }, + None => return, + }; + + // Drop the repeat, if there were any. + keyboard_state.current_repeat = None; + if let Some(token) = keyboard_state.repeat_token.take() { + keyboard_state.loop_handle.remove(token); + } + + *data.window_id.lock().unwrap() = Some(window_id); + + // The keyboard focus is considered as general focus. + if was_unfocused { + state.events_sink.push_window_event(WindowEvent::Focused(true), window_id); + } + + // HACK: this is just for GNOME not fixing their ordering issue of modifiers. + if std::mem::take(&mut seat_state.modifiers_pending) { + state.events_sink.push_window_event( + WindowEvent::ModifiersChanged(seat_state.modifiers.into()), + window_id, + ); + } + }, + WlKeyboardEvent::Leave { surface, .. } => { + let window_id = wayland::make_wid(&surface); + + // NOTE: we should drop the repeat regardless whether it was for the present + // window of for the window which just went gone. + keyboard_state.current_repeat = None; + if let Some(token) = keyboard_state.repeat_token.take() { + keyboard_state.loop_handle.remove(token); + } + + // NOTE: The check whether the window exists is essential as we might get a + // nil surface, regardless of what protocol says. + let focused = match state.windows.get_mut().get(&window_id) { + Some(window) => { + let mut window = window.lock().unwrap(); + window.remove_seat_focus(&data.seat.id()); + window.has_focus() + }, + None => return, + }; + + // We don't need to update it above, because the next `Enter` will overwrite + // anyway. + *data.window_id.lock().unwrap() = None; + + if !focused { + // Notify that no modifiers are being pressed. + state.events_sink.push_window_event( + WindowEvent::ModifiersChanged(ModifiersState::empty().into()), + window_id, + ); + + state.events_sink.push_window_event(WindowEvent::Focused(false), window_id); + } + }, + WlKeyboardEvent::Key { key, state: WEnum::Value(WlKeyState::Pressed), .. } => { + let key = key + 8; + + key_input( + keyboard_state, + &mut state.events_sink, + data, + key, + ElementState::Pressed, + false, + ); + + let delay = match keyboard_state.repeat_info { + RepeatInfo::Repeat { delay, .. } => delay, + RepeatInfo::Disable => return, + }; + + if !keyboard_state.xkb_context.keymap_mut().unwrap().key_repeats(key) { + return; + } + + keyboard_state.current_repeat = Some(key); + + // NOTE terminate ongoing timer and start a new timer. + + if let Some(token) = keyboard_state.repeat_token.take() { + keyboard_state.loop_handle.remove(token); + } + + let timer = Timer::from_duration(delay); + let wl_keyboard = wl_keyboard.clone(); + keyboard_state.repeat_token = keyboard_state + .loop_handle + .insert_source(timer, move |_, _, state| { + // Required to handle the wakeups from the repeat sources. + state.dispatched_events = true; + + let data = wl_keyboard.data::().unwrap(); + let seat_state = match state.seats.get_mut(&data.seat.id()) { + Some(seat_state) => seat_state, + None => return TimeoutAction::Drop, + }; + + let keyboard_state = match seat_state.keyboard_state.as_mut() { + Some(keyboard_state) => keyboard_state, + None => return TimeoutAction::Drop, + }; + + // NOTE: The removed on event source is batched, but key change to `None` + // is instant. + let repeat_keycode = match keyboard_state.current_repeat { + Some(repeat_keycode) => repeat_keycode, + None => return TimeoutAction::Drop, + }; + + key_input( + keyboard_state, + &mut state.events_sink, + data, + repeat_keycode, + ElementState::Pressed, + true, + ); + + // NOTE: the gap could change dynamically while repeat is going. + match keyboard_state.repeat_info { + RepeatInfo::Repeat { gap, .. } => TimeoutAction::ToDuration(gap), + RepeatInfo::Disable => TimeoutAction::Drop, + } + }) + .ok(); + }, + WlKeyboardEvent::Key { key, state: WEnum::Value(WlKeyState::Released), .. } => { + let key = key + 8; + + key_input( + keyboard_state, + &mut state.events_sink, + data, + key, + ElementState::Released, + false, + ); + + if keyboard_state.repeat_info != RepeatInfo::Disable + && keyboard_state.xkb_context.keymap_mut().unwrap().key_repeats(key) + && Some(key) == keyboard_state.current_repeat + { + keyboard_state.current_repeat = None; + if let Some(token) = keyboard_state.repeat_token.take() { + keyboard_state.loop_handle.remove(token); + } + } + }, + WlKeyboardEvent::Modifiers { + mods_depressed, mods_latched, mods_locked, group, .. + } => { + let xkb_context = &mut keyboard_state.xkb_context; + let xkb_state = match xkb_context.state_mut() { + Some(state) => state, + None => return, + }; + + xkb_state.update_modifiers(mods_depressed, mods_latched, mods_locked, 0, 0, group); + seat_state.modifiers = xkb_state.modifiers().into(); + + // HACK: part of the workaround from `WlKeyboardEvent::Enter`. + let window_id = match *data.window_id.lock().unwrap() { + Some(window_id) => window_id, + None => { + seat_state.modifiers_pending = true; + return; + }, + }; + + state.events_sink.push_window_event( + WindowEvent::ModifiersChanged(seat_state.modifiers.into()), + window_id, + ); + }, + WlKeyboardEvent::RepeatInfo { rate, delay } => { + keyboard_state.repeat_info = if rate == 0 { + // Stop the repeat once we get a disable event. + keyboard_state.current_repeat = None; + if let Some(repeat_token) = keyboard_state.repeat_token.take() { + keyboard_state.loop_handle.remove(repeat_token); + } + RepeatInfo::Disable + } else { + let gap = Duration::from_micros(1_000_000 / rate as u64); + let delay = Duration::from_millis(delay as u64); + RepeatInfo::Repeat { gap, delay } + }; + }, + _ => unreachable!(), + } + } +} + +/// The state of the keyboard on the current seat. +#[derive(Debug)] +pub struct KeyboardState { + /// The underlying WlKeyboard. + pub keyboard: WlKeyboard, + + /// Loop handle to handle key repeat. + pub loop_handle: LoopHandle<'static, WinitState>, + + /// The state of the keyboard. + pub xkb_context: Context, + + /// The information about the repeat rate obtained from the compositor. + pub repeat_info: RepeatInfo, + + /// The token of the current handle inside the calloop's event loop. + pub repeat_token: Option, + + /// The current repeat raw key. + pub current_repeat: Option, +} + +impl KeyboardState { + pub fn new(keyboard: WlKeyboard, loop_handle: LoopHandle<'static, WinitState>) -> Self { + Self { + keyboard, + loop_handle, + xkb_context: Context::new().unwrap(), + repeat_info: RepeatInfo::default(), + repeat_token: None, + current_repeat: None, + } + } +} + +impl Drop for KeyboardState { + fn drop(&mut self) { + if self.keyboard.version() >= 3 { + self.keyboard.release(); + } + + if let Some(token) = self.repeat_token.take() { + self.loop_handle.remove(token); + } + } +} + +/// The rate at which a pressed key is repeated. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RepeatInfo { + /// Keys will be repeated at the specified rate and delay. + Repeat { + /// The time between the key repeats. + gap: Duration, + + /// Delay (in milliseconds) between a key press and the start of repetition. + delay: Duration, + }, + + /// Keys should not be repeated. + Disable, +} + +impl Default for RepeatInfo { + /// The default repeat rate is 25 keys per second with the delay of 200ms. + /// + /// The values are picked based on the default in various compositors and Xorg. + fn default() -> Self { + Self::Repeat { gap: Duration::from_millis(40), delay: Duration::from_millis(200) } + } +} + +/// Keyboard user data. +#[derive(Debug)] +pub struct KeyboardData { + /// The currently focused window surface. Could be `None` on bugged compositors, like mutter. + window_id: Mutex>, + + /// The seat used to create this keyboard. + seat: WlSeat, +} + +impl KeyboardData { + pub fn new(seat: WlSeat) -> Self { + Self { window_id: Default::default(), seat } + } +} + +fn key_input( + keyboard_state: &mut KeyboardState, + event_sink: &mut EventSink, + data: &KeyboardData, + keycode: u32, + state: ElementState, + repeat: bool, +) { + let window_id = match *data.window_id.lock().unwrap() { + Some(window_id) => window_id, + None => return, + }; + + let device_id = crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland(DeviceId)); + if let Some(mut key_context) = keyboard_state.xkb_context.key_context() { + let event = key_context.process_key_event(keycode, state, repeat); + let event = WindowEvent::KeyboardInput { device_id, event, is_synthetic: false }; + event_sink.push_window_event(event, window_id); + } +} diff --git a/vendor/winit/src/platform_impl/linux/wayland/seat/mod.rs b/vendor/winit/src/platform_impl/linux/wayland/seat/mod.rs new file mode 100644 index 00000000..eaecd93b --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/seat/mod.rs @@ -0,0 +1,235 @@ +//! Seat handling. + +use std::sync::Arc; + +use ahash::AHashMap; +use tracing::warn; + +use sctk::reexports::client::backend::ObjectId; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::protocol::wl_touch::WlTouch; +use sctk::reexports::client::{Connection, Proxy, QueueHandle}; +use sctk::reexports::protocols::wp::relative_pointer::zv1::client::zwp_relative_pointer_v1::ZwpRelativePointerV1; +use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::ZwpTextInputV3; + +use sctk::seat::pointer::{ThemeSpec, ThemedPointer}; +use sctk::seat::{Capability as SeatCapability, SeatHandler, SeatState}; + +use crate::event::WindowEvent; +use crate::keyboard::ModifiersState; +use crate::platform_impl::wayland::state::WinitState; + +mod keyboard; +mod pointer; +mod text_input; +mod touch; + +pub use pointer::relative_pointer::RelativePointerState; +pub use pointer::{PointerConstraintsState, WinitPointerData, WinitPointerDataExt}; +pub use text_input::{TextInputState, ZwpTextInputV3Ext}; + +use keyboard::{KeyboardData, KeyboardState}; +use text_input::TextInputData; +use touch::TouchPoint; + +#[derive(Debug, Default)] +pub struct WinitSeatState { + /// The pointer bound on the seat. + pointer: Option>>, + + /// The touch bound on the seat. + touch: Option, + + /// The mapping from touched points to the surfaces they're present. + touch_map: AHashMap, + + /// The text input bound on the seat. + text_input: Option>, + + /// The relative pointer bound on the seat. + relative_pointer: Option, + + /// The keyboard bound on the seat. + keyboard_state: Option, + + /// The current modifiers state on the seat. + modifiers: ModifiersState, + + /// Whether we have pending modifiers. + modifiers_pending: bool, +} + +impl WinitSeatState { + pub fn new() -> Self { + Default::default() + } +} + +impl SeatHandler for WinitState { + fn seat_state(&mut self) -> &mut SeatState { + &mut self.seat_state + } + + fn new_capability( + &mut self, + _: &Connection, + queue_handle: &QueueHandle, + seat: WlSeat, + capability: SeatCapability, + ) { + let seat_state = match self.seats.get_mut(&seat.id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_seat::new_capability for unknown seat"); + return; + }, + }; + + match capability { + SeatCapability::Touch if seat_state.touch.is_none() => { + seat_state.touch = self.seat_state.get_touch(queue_handle, &seat).ok(); + }, + SeatCapability::Keyboard if seat_state.keyboard_state.is_none() => { + let keyboard = seat.get_keyboard(queue_handle, KeyboardData::new(seat.clone())); + seat_state.keyboard_state = + Some(KeyboardState::new(keyboard, self.loop_handle.clone())); + }, + SeatCapability::Pointer if seat_state.pointer.is_none() => { + let surface = self.compositor_state.create_surface(queue_handle); + let viewport = self + .viewporter_state + .as_ref() + .map(|state| state.get_viewport(&surface, queue_handle)); + let surface_id = surface.id(); + let pointer_data = WinitPointerData::new(seat.clone(), viewport); + let themed_pointer = self + .seat_state + .get_pointer_with_theme_and_data( + queue_handle, + &seat, + self.shm.wl_shm(), + surface, + ThemeSpec::System, + pointer_data, + ) + .expect("failed to create pointer with present capability."); + + seat_state.relative_pointer = self.relative_pointer.as_ref().map(|manager| { + manager.get_relative_pointer( + themed_pointer.pointer(), + queue_handle, + sctk::globals::GlobalData, + ) + }); + + let themed_pointer = Arc::new(themed_pointer); + + // Register cursor surface. + self.pointer_surfaces.insert(surface_id, themed_pointer.clone()); + + seat_state.pointer = Some(themed_pointer); + }, + _ => (), + } + + if let Some(text_input_state) = + seat_state.text_input.is_none().then_some(self.text_input_state.as_ref()).flatten() + { + seat_state.text_input = Some(Arc::new(text_input_state.get_text_input( + &seat, + queue_handle, + TextInputData::default(), + ))); + } + } + + fn remove_capability( + &mut self, + _: &Connection, + _queue_handle: &QueueHandle, + seat: WlSeat, + capability: SeatCapability, + ) { + let seat_state = match self.seats.get_mut(&seat.id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_seat::remove_capability for unknown seat"); + return; + }, + }; + + if let Some(text_input) = seat_state.text_input.take() { + text_input.destroy(); + } + + match capability { + SeatCapability::Touch => { + if let Some(touch) = seat_state.touch.take() { + if touch.version() >= 3 { + touch.release(); + } + } + }, + SeatCapability::Pointer => { + if let Some(relative_pointer) = seat_state.relative_pointer.take() { + relative_pointer.destroy(); + } + + if let Some(pointer) = seat_state.pointer.take() { + let pointer_data = pointer.pointer().winit_data(); + + // Remove the cursor from the mapping. + let surface_id = pointer.surface().id(); + let _ = self.pointer_surfaces.remove(&surface_id); + + // Remove the inner locks/confines before dropping the pointer. + pointer_data.unlock_pointer(); + pointer_data.unconfine_pointer(); + + if pointer.pointer().version() >= 3 { + pointer.pointer().release(); + } + } + }, + SeatCapability::Keyboard => { + seat_state.keyboard_state = None; + self.on_keyboard_destroy(&seat.id()); + }, + _ => (), + } + } + + fn new_seat( + &mut self, + _connection: &Connection, + _queue_handle: &QueueHandle, + seat: WlSeat, + ) { + self.seats.insert(seat.id(), WinitSeatState::new()); + } + + fn remove_seat( + &mut self, + _connection: &Connection, + _queue_handle: &QueueHandle, + seat: WlSeat, + ) { + let _ = self.seats.remove(&seat.id()); + self.on_keyboard_destroy(&seat.id()); + } +} + +impl WinitState { + fn on_keyboard_destroy(&mut self, seat: &ObjectId) { + for (window_id, window) in self.windows.get_mut() { + let mut window = window.lock().unwrap(); + let had_focus = window.has_focus(); + window.remove_seat_focus(seat); + if had_focus != window.has_focus() { + self.events_sink.push_window_event(WindowEvent::Focused(false), *window_id); + } + } + } +} + +sctk::delegate_seat!(WinitState); diff --git a/vendor/winit/src/platform_impl/linux/wayland/seat/pointer/mod.rs b/vendor/winit/src/platform_impl/linux/wayland/seat/pointer/mod.rs new file mode 100644 index 00000000..3dcb00b2 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/seat/pointer/mod.rs @@ -0,0 +1,502 @@ +//! The pointer events. + +use std::ops::Deref; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use tracing::warn; + +use sctk::reexports::client::delegate_dispatch; +use sctk::reexports::client::protocol::wl_pointer::WlPointer; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{Connection, Proxy, QueueHandle, Dispatch}; +use sctk::reexports::protocols::wp::pointer_constraints::zv1::client::zwp_confined_pointer_v1::ZwpConfinedPointerV1; +use sctk::reexports::protocols::wp::pointer_constraints::zv1::client::zwp_locked_pointer_v1::ZwpLockedPointerV1; +use sctk::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::WpCursorShapeDeviceV1; +use sctk::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_manager_v1::WpCursorShapeManagerV1; +use sctk::reexports::protocols::wp::pointer_constraints::zv1::client::zwp_pointer_constraints_v1::{Lifetime, ZwpPointerConstraintsV1}; +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::csd_frame::FrameClick; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport; + +use sctk::compositor::SurfaceData; +use sctk::globals::GlobalData; +use sctk::seat::pointer::{ + PointerData, PointerDataExt, PointerEvent, PointerEventKind, PointerHandler, +}; +use sctk::seat::SeatState; + +use crate::dpi::{LogicalPosition, PhysicalPosition}; +use crate::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent}; + +use crate::platform_impl::wayland::state::WinitState; +use crate::platform_impl::wayland::{self, DeviceId, WindowId}; + +pub mod relative_pointer; + +impl PointerHandler for WinitState { + fn pointer_frame( + &mut self, + connection: &Connection, + _: &QueueHandle, + pointer: &WlPointer, + events: &[PointerEvent], + ) { + let seat = pointer.winit_data().seat(); + let seat_state = match self.seats.get(&seat.id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received pointer event without seat"); + return; + }, + }; + + let themed_pointer = match seat_state.pointer.as_ref() { + Some(pointer) => pointer, + None => { + warn!("Received pointer event without pointer"); + return; + }, + }; + + let device_id = crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland(DeviceId)); + + for event in events { + let surface = &event.surface; + + // The parent surface. + let parent_surface = match event.surface.data::() { + Some(data) => data.parent_surface().unwrap_or(surface), + None => continue, + }; + + let window_id = wayland::make_wid(parent_surface); + + // Ensure that window exists. + let mut window = match self.windows.get_mut().get_mut(&window_id) { + Some(window) => window.lock().unwrap(), + None => continue, + }; + + let scale_factor = window.scale_factor(); + let position: PhysicalPosition = + LogicalPosition::new(event.position.0, event.position.1).to_physical(scale_factor); + + match event.kind { + // Pointer movements on decorations. + PointerEventKind::Enter { .. } | PointerEventKind::Motion { .. } + if parent_surface != surface => + { + if let Some(icon) = window.frame_point_moved( + seat, + surface, + Duration::ZERO, + event.position.0, + event.position.1, + ) { + let _ = themed_pointer.set_cursor(connection, icon); + } + }, + PointerEventKind::Leave { .. } if parent_surface != surface => { + window.frame_point_left(); + }, + ref kind @ PointerEventKind::Press { button, serial, time } + | ref kind @ PointerEventKind::Release { button, serial, time } + if parent_surface != surface => + { + let click = match wayland_button_to_winit(button) { + MouseButton::Left => FrameClick::Normal, + MouseButton::Right => FrameClick::Alternate, + _ => continue, + }; + let pressed = matches!(kind, PointerEventKind::Press { .. }); + + // Emulate click on the frame. + window.frame_click( + click, + pressed, + seat, + serial, + Duration::from_millis(time as u64), + window_id, + &mut self.window_compositor_updates, + ); + }, + // Regular events on the main surface. + PointerEventKind::Enter { .. } => { + self.events_sink + .push_window_event(WindowEvent::CursorEntered { device_id }, window_id); + + window.pointer_entered(Arc::downgrade(themed_pointer)); + + // Set the currently focused surface. + pointer.winit_data().inner.lock().unwrap().surface = Some(window_id); + + self.events_sink.push_window_event( + WindowEvent::CursorMoved { device_id, position }, + window_id, + ); + }, + PointerEventKind::Leave { .. } => { + window.pointer_left(Arc::downgrade(themed_pointer)); + + // Remove the active surface. + pointer.winit_data().inner.lock().unwrap().surface = None; + + self.events_sink + .push_window_event(WindowEvent::CursorLeft { device_id }, window_id); + }, + PointerEventKind::Motion { .. } => { + self.events_sink.push_window_event( + WindowEvent::CursorMoved { device_id, position }, + window_id, + ); + }, + ref kind @ PointerEventKind::Press { button, serial, .. } + | ref kind @ PointerEventKind::Release { button, serial, .. } => { + // Update the last button serial. + pointer.winit_data().inner.lock().unwrap().latest_button_serial = serial; + + let button = wayland_button_to_winit(button); + let state = if matches!(kind, PointerEventKind::Press { .. }) { + ElementState::Pressed + } else { + ElementState::Released + }; + self.events_sink.push_window_event( + WindowEvent::MouseInput { device_id, state, button }, + window_id, + ); + }, + PointerEventKind::Axis { horizontal, vertical, .. } => { + // Get the current phase. + let mut pointer_data = pointer.winit_data().inner.lock().unwrap(); + + let has_discrete_scroll = horizontal.discrete != 0 || vertical.discrete != 0; + + // Figure out what to do about start/ended phases here. + // + // Figure out how to deal with `Started`. Also the `Ended` is not guaranteed + // to be sent for mouse wheels. + let phase = if horizontal.stop || vertical.stop { + TouchPhase::Ended + } else { + match pointer_data.phase { + // Discrete scroll only results in moved events. + _ if has_discrete_scroll => TouchPhase::Moved, + TouchPhase::Started | TouchPhase::Moved => TouchPhase::Moved, + _ => TouchPhase::Started, + } + }; + + // Update the phase. + pointer_data.phase = phase; + + // Mice events have both pixel and discrete delta's at the same time. So prefer + // the discrete values if they are present. + let delta = if has_discrete_scroll { + // NOTE: Wayland sign convention is the inverse of winit. + MouseScrollDelta::LineDelta( + (-horizontal.discrete) as f32, + (-vertical.discrete) as f32, + ) + } else { + // NOTE: Wayland sign convention is the inverse of winit. + MouseScrollDelta::PixelDelta( + LogicalPosition::new(-horizontal.absolute, -vertical.absolute) + .to_physical(scale_factor), + ) + }; + + self.events_sink.push_window_event( + WindowEvent::MouseWheel { device_id, delta, phase }, + window_id, + ) + }, + } + } + } +} + +#[derive(Debug)] +pub struct WinitPointerData { + /// The inner winit data associated with the pointer. + inner: Mutex, + + /// The data required by the sctk. + sctk_data: PointerData, + + /// Viewport for fractional cursor. + viewport: Option, +} + +impl WinitPointerData { + pub fn new(seat: WlSeat, viewport: Option) -> Self { + Self { + inner: Mutex::new(WinitPointerDataInner::default()), + sctk_data: PointerData::new(seat), + viewport, + } + } + + pub fn lock_pointer( + &self, + pointer_constraints: &PointerConstraintsState, + surface: &WlSurface, + pointer: &WlPointer, + queue_handle: &QueueHandle, + ) { + let mut inner = self.inner.lock().unwrap(); + if inner.locked_pointer.is_none() { + inner.locked_pointer = Some(pointer_constraints.lock_pointer( + surface, + pointer, + None, + Lifetime::Persistent, + queue_handle, + GlobalData, + )); + } + } + + pub fn unlock_pointer(&self) { + let mut inner = self.inner.lock().unwrap(); + if let Some(locked_pointer) = inner.locked_pointer.take() { + locked_pointer.destroy(); + } + } + + pub fn confine_pointer( + &self, + pointer_constraints: &PointerConstraintsState, + surface: &WlSurface, + pointer: &WlPointer, + queue_handle: &QueueHandle, + ) { + self.inner.lock().unwrap().confined_pointer = Some(pointer_constraints.confine_pointer( + surface, + pointer, + None, + Lifetime::Persistent, + queue_handle, + GlobalData, + )); + } + + pub fn unconfine_pointer(&self) { + let inner = self.inner.lock().unwrap(); + if let Some(confined_pointer) = inner.confined_pointer.as_ref() { + confined_pointer.destroy(); + } + } + + /// Seat associated with this pointer. + pub fn seat(&self) -> &WlSeat { + self.sctk_data.seat() + } + + /// Active window. + pub fn focused_window(&self) -> Option { + self.inner.lock().unwrap().surface + } + + /// Last button serial. + pub fn latest_button_serial(&self) -> u32 { + self.sctk_data.latest_button_serial().unwrap_or_default() + } + + /// Last enter serial. + pub fn latest_enter_serial(&self) -> u32 { + self.sctk_data.latest_enter_serial().unwrap_or_default() + } + + pub fn set_locked_cursor_position(&self, surface_x: f64, surface_y: f64) { + let inner = self.inner.lock().unwrap(); + if let Some(locked_pointer) = inner.locked_pointer.as_ref() { + locked_pointer.set_cursor_position_hint(surface_x, surface_y); + } + } + + pub fn viewport(&self) -> Option<&WpViewport> { + self.viewport.as_ref() + } +} + +impl Drop for WinitPointerData { + fn drop(&mut self) { + if let Some(viewport) = self.viewport.take() { + viewport.destroy(); + } + } +} + +impl PointerDataExt for WinitPointerData { + fn pointer_data(&self) -> &PointerData { + &self.sctk_data + } +} + +#[derive(Debug)] +pub struct WinitPointerDataInner { + /// The associated locked pointer. + locked_pointer: Option, + + /// The associated confined pointer. + confined_pointer: Option, + + /// Serial of the last button event. + latest_button_serial: u32, + + /// Currently focused window. + surface: Option, + + /// Current axis phase. + phase: TouchPhase, +} + +impl Drop for WinitPointerDataInner { + fn drop(&mut self) { + if let Some(locked_pointer) = self.locked_pointer.take() { + locked_pointer.destroy(); + } + + if let Some(confined_pointer) = self.confined_pointer.take() { + confined_pointer.destroy(); + } + } +} + +impl Default for WinitPointerDataInner { + fn default() -> Self { + Self { + surface: None, + locked_pointer: None, + confined_pointer: None, + latest_button_serial: 0, + phase: TouchPhase::Ended, + } + } +} + +/// Convert the Wayland button into winit. +fn wayland_button_to_winit(button: u32) -> MouseButton { + // These values are coming from . + const BTN_LEFT: u32 = 0x110; + const BTN_RIGHT: u32 = 0x111; + const BTN_MIDDLE: u32 = 0x112; + const BTN_SIDE: u32 = 0x113; + const BTN_EXTRA: u32 = 0x114; + const BTN_FORWARD: u32 = 0x115; + const BTN_BACK: u32 = 0x116; + + match button { + BTN_LEFT => MouseButton::Left, + BTN_RIGHT => MouseButton::Right, + BTN_MIDDLE => MouseButton::Middle, + BTN_BACK | BTN_SIDE => MouseButton::Back, + BTN_FORWARD | BTN_EXTRA => MouseButton::Forward, + button => MouseButton::Other(button as u16), + } +} + +pub trait WinitPointerDataExt { + fn winit_data(&self) -> &WinitPointerData; +} + +impl WinitPointerDataExt for WlPointer { + fn winit_data(&self) -> &WinitPointerData { + self.data::().expect("failed to get pointer data.") + } +} + +pub struct PointerConstraintsState { + pointer_constraints: ZwpPointerConstraintsV1, +} + +impl PointerConstraintsState { + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let pointer_constraints = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { pointer_constraints }) + } +} + +impl Deref for PointerConstraintsState { + type Target = ZwpPointerConstraintsV1; + + fn deref(&self) -> &Self::Target { + &self.pointer_constraints + } +} + +impl Dispatch for PointerConstraintsState { + fn event( + _state: &mut WinitState, + _proxy: &ZwpPointerConstraintsV1, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl Dispatch for PointerConstraintsState { + fn event( + _state: &mut WinitState, + _proxy: &ZwpLockedPointerV1, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl Dispatch for PointerConstraintsState { + fn event( + _state: &mut WinitState, + _proxy: &ZwpConfinedPointerV1, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl Dispatch for SeatState { + fn event( + _: &mut WinitState, + _: &WpCursorShapeDeviceV1, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + unreachable!("wp_cursor_shape_manager has no events") + } +} + +impl Dispatch for SeatState { + fn event( + _: &mut WinitState, + _: &WpCursorShapeManagerV1, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + unreachable!("wp_cursor_device_manager has no events") + } +} + +delegate_dispatch!(WinitState: [ WlPointer: WinitPointerData] => SeatState); +delegate_dispatch!(WinitState: [ WpCursorShapeManagerV1: GlobalData] => SeatState); +delegate_dispatch!(WinitState: [ WpCursorShapeDeviceV1: GlobalData] => SeatState); +delegate_dispatch!(WinitState: [ZwpPointerConstraintsV1: GlobalData] => PointerConstraintsState); +delegate_dispatch!(WinitState: [ZwpLockedPointerV1: GlobalData] => PointerConstraintsState); +delegate_dispatch!(WinitState: [ZwpConfinedPointerV1: GlobalData] => PointerConstraintsState); diff --git a/vendor/winit/src/platform_impl/linux/wayland/seat/pointer/relative_pointer.rs b/vendor/winit/src/platform_impl/linux/wayland/seat/pointer/relative_pointer.rs new file mode 100644 index 00000000..a9ce2760 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/seat/pointer/relative_pointer.rs @@ -0,0 +1,83 @@ +//! Relative pointer. + +use std::ops::Deref; + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::{delegate_dispatch, Dispatch}; +use sctk::reexports::client::{Connection, QueueHandle}; +use sctk::reexports::protocols::wp::relative_pointer::zv1::{ + client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, + client::zwp_relative_pointer_v1::{self, ZwpRelativePointerV1}, +}; + +use sctk::globals::GlobalData; + +use crate::event::DeviceEvent; +use crate::platform_impl::wayland::state::WinitState; + +/// Wrapper around the relative pointer. +pub struct RelativePointerState { + manager: ZwpRelativePointerManagerV1, +} + +impl RelativePointerState { + /// Create new relative pointer manager. + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { manager }) + } +} + +impl Deref for RelativePointerState { + type Target = ZwpRelativePointerManagerV1; + + fn deref(&self) -> &Self::Target { + &self.manager + } +} + +impl Dispatch for RelativePointerState { + fn event( + _state: &mut WinitState, + _proxy: &ZwpRelativePointerManagerV1, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl Dispatch for RelativePointerState { + fn event( + state: &mut WinitState, + _proxy: &ZwpRelativePointerV1, + event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + let (dx_unaccel, dy_unaccel) = match event { + zwp_relative_pointer_v1::Event::RelativeMotion { dx_unaccel, dy_unaccel, .. } => { + (dx_unaccel, dy_unaccel) + }, + _ => return, + }; + state + .events_sink + .push_device_event(DeviceEvent::Motion { axis: 0, value: dx_unaccel }, super::DeviceId); + state + .events_sink + .push_device_event(DeviceEvent::Motion { axis: 1, value: dy_unaccel }, super::DeviceId); + state.events_sink.push_device_event( + DeviceEvent::MouseMotion { delta: (dx_unaccel, dy_unaccel) }, + super::DeviceId, + ); + } +} + +delegate_dispatch!(WinitState: [ZwpRelativePointerV1: GlobalData] => RelativePointerState); +delegate_dispatch!(WinitState: [ZwpRelativePointerManagerV1: GlobalData] => RelativePointerState); diff --git a/vendor/winit/src/platform_impl/linux/wayland/seat/text_input/mod.rs b/vendor/winit/src/platform_impl/linux/wayland/seat/text_input/mod.rs new file mode 100644 index 00000000..db724893 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/seat/text_input/mod.rs @@ -0,0 +1,201 @@ +use std::ops::Deref; + +use sctk::globals::GlobalData; +use sctk::reexports::client::{Connection, Proxy, QueueHandle}; + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{delegate_dispatch, Dispatch}; +use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3; +use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::{ + ContentHint, ContentPurpose, Event as TextInputEvent, ZwpTextInputV3, +}; + +use crate::event::{Ime, WindowEvent}; +use crate::platform_impl::wayland; +use crate::platform_impl::wayland::state::WinitState; +use crate::window::ImePurpose; + +pub struct TextInputState { + text_input_manager: ZwpTextInputManagerV3, +} + +impl TextInputState { + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let text_input_manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { text_input_manager }) + } +} + +impl Deref for TextInputState { + type Target = ZwpTextInputManagerV3; + + fn deref(&self) -> &Self::Target { + &self.text_input_manager + } +} + +impl Dispatch for TextInputState { + fn event( + _state: &mut WinitState, + _proxy: &ZwpTextInputManagerV3, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl Dispatch for TextInputState { + fn event( + state: &mut WinitState, + text_input: &ZwpTextInputV3, + event: ::Event, + data: &TextInputData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + let windows = state.windows.get_mut(); + let mut text_input_data = data.inner.lock().unwrap(); + match event { + TextInputEvent::Enter { surface } => { + let window_id = wayland::make_wid(&surface); + text_input_data.surface = Some(surface); + + let mut window = match windows.get(&window_id) { + Some(window) => window.lock().unwrap(), + None => return, + }; + + if window.ime_allowed() { + text_input.enable(); + text_input.set_content_type_by_purpose(window.ime_purpose()); + text_input.commit(); + state.events_sink.push_window_event(WindowEvent::Ime(Ime::Enabled), window_id); + } + + window.text_input_entered(text_input); + }, + TextInputEvent::Leave { surface } => { + text_input_data.surface = None; + + // Always issue a disable. + text_input.disable(); + text_input.commit(); + + let window_id = wayland::make_wid(&surface); + + // XXX this check is essential, because `leave` could have a + // reference to nil surface... + let mut window = match windows.get(&window_id) { + Some(window) => window.lock().unwrap(), + None => return, + }; + + window.text_input_left(text_input); + + state.events_sink.push_window_event(WindowEvent::Ime(Ime::Disabled), window_id); + }, + TextInputEvent::PreeditString { text, cursor_begin, cursor_end } => { + let text = text.unwrap_or_default(); + let cursor_begin = usize::try_from(cursor_begin) + .ok() + .and_then(|idx| text.is_char_boundary(idx).then_some(idx)); + let cursor_end = usize::try_from(cursor_end) + .ok() + .and_then(|idx| text.is_char_boundary(idx).then_some(idx)); + + text_input_data.pending_preedit = Some(Preedit { text, cursor_begin, cursor_end }) + }, + TextInputEvent::CommitString { text } => { + text_input_data.pending_preedit = None; + text_input_data.pending_commit = text; + }, + TextInputEvent::Done { .. } => { + let window_id = match text_input_data.surface.as_ref() { + Some(surface) => wayland::make_wid(surface), + None => return, + }; + + // Clear preedit, unless all we'll be doing next is sending a new preedit. + if text_input_data.pending_commit.is_some() + || text_input_data.pending_preedit.is_none() + { + state.events_sink.push_window_event( + WindowEvent::Ime(Ime::Preedit(String::new(), None)), + window_id, + ); + } + + // Send `Commit`. + if let Some(text) = text_input_data.pending_commit.take() { + state + .events_sink + .push_window_event(WindowEvent::Ime(Ime::Commit(text)), window_id); + } + + // Send preedit. + if let Some(preedit) = text_input_data.pending_preedit.take() { + let cursor_range = + preedit.cursor_begin.map(|b| (b, preedit.cursor_end.unwrap_or(b))); + + state.events_sink.push_window_event( + WindowEvent::Ime(Ime::Preedit(preedit.text, cursor_range)), + window_id, + ); + } + }, + TextInputEvent::DeleteSurroundingText { .. } => { + // Not handled. + }, + _ => {}, + } + } +} + +pub trait ZwpTextInputV3Ext { + fn set_content_type_by_purpose(&self, purpose: ImePurpose); +} + +impl ZwpTextInputV3Ext for ZwpTextInputV3 { + fn set_content_type_by_purpose(&self, purpose: ImePurpose) { + let (hint, purpose) = match purpose { + ImePurpose::Normal => (ContentHint::None, ContentPurpose::Normal), + ImePurpose::Password => (ContentHint::SensitiveData, ContentPurpose::Password), + ImePurpose::Terminal => (ContentHint::None, ContentPurpose::Terminal), + }; + self.set_content_type(hint, purpose); + } +} + +/// The Data associated with the text input. +#[derive(Default)] +pub struct TextInputData { + inner: std::sync::Mutex, +} + +#[derive(Default)] +pub struct TextInputDataInner { + /// The `WlSurface` we're performing input to. + surface: Option, + + /// The commit to submit on `done`. + pending_commit: Option, + + /// The preedit to submit on `done`. + pending_preedit: Option, +} + +/// The state of the preedit. +struct Preedit { + text: String, + cursor_begin: Option, + cursor_end: Option, +} + +delegate_dispatch!(WinitState: [ZwpTextInputManagerV3: GlobalData] => TextInputState); +delegate_dispatch!(WinitState: [ZwpTextInputV3: TextInputData] => TextInputState); diff --git a/vendor/winit/src/platform_impl/linux/wayland/seat/touch/mod.rs b/vendor/winit/src/platform_impl/linux/wayland/seat/touch/mod.rs new file mode 100644 index 00000000..124504fe --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/seat/touch/mod.rs @@ -0,0 +1,220 @@ +//! Touch handling. + +use tracing::warn; + +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::protocol::wl_touch::WlTouch; +use sctk::reexports::client::{Connection, Proxy, QueueHandle}; + +use sctk::seat::touch::{TouchData, TouchHandler}; + +use crate::dpi::LogicalPosition; +use crate::event::{Touch, TouchPhase, WindowEvent}; + +use crate::platform_impl::wayland::state::WinitState; +use crate::platform_impl::wayland::{self, DeviceId}; + +impl TouchHandler for WinitState { + fn down( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + _: u32, + _: u32, + surface: WlSurface, + id: i32, + position: (f64, f64), + ) { + let window_id = wayland::make_wid(&surface); + let scale_factor = match self.windows.get_mut().get(&window_id) { + Some(window) => window.lock().unwrap().scale_factor(), + None => return, + }; + + let seat_state = match self.seats.get_mut(&touch.seat().id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_touch::down without seat"); + return; + }, + }; + + // Update the state of the point. + let location = LogicalPosition::::from(position); + seat_state.touch_map.insert(id, TouchPoint { surface, location }); + + self.events_sink.push_window_event( + WindowEvent::Touch(Touch { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + phase: TouchPhase::Started, + location: location.to_physical(scale_factor), + force: None, + id: id as u64, + }), + window_id, + ); + } + + fn up( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + _: u32, + _: u32, + id: i32, + ) { + let seat_state = match self.seats.get_mut(&touch.seat().id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_touch::up without seat"); + return; + }, + }; + + // Remove the touch point. + let touch_point = match seat_state.touch_map.remove(&id) { + Some(touch_point) => touch_point, + None => return, + }; + + let window_id = wayland::make_wid(&touch_point.surface); + let scale_factor = match self.windows.get_mut().get(&window_id) { + Some(window) => window.lock().unwrap().scale_factor(), + None => return, + }; + + self.events_sink.push_window_event( + WindowEvent::Touch(Touch { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + phase: TouchPhase::Ended, + location: touch_point.location.to_physical(scale_factor), + force: None, + id: id as u64, + }), + window_id, + ); + } + + fn motion( + &mut self, + _: &Connection, + _: &QueueHandle, + touch: &WlTouch, + _: u32, + id: i32, + position: (f64, f64), + ) { + let seat_state = match self.seats.get_mut(&touch.seat().id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_touch::motion without seat"); + return; + }, + }; + + // Remove the touch point. + let touch_point = match seat_state.touch_map.get_mut(&id) { + Some(touch_point) => touch_point, + None => return, + }; + + let window_id = wayland::make_wid(&touch_point.surface); + let scale_factor = match self.windows.get_mut().get(&window_id) { + Some(window) => window.lock().unwrap().scale_factor(), + None => return, + }; + + touch_point.location = LogicalPosition::::from(position); + + self.events_sink.push_window_event( + WindowEvent::Touch(Touch { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + phase: TouchPhase::Moved, + location: touch_point.location.to_physical(scale_factor), + force: None, + id: id as u64, + }), + window_id, + ); + } + + fn cancel(&mut self, _: &Connection, _: &QueueHandle, touch: &WlTouch) { + let seat_state = match self.seats.get_mut(&touch.seat().id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received wl_touch::cancel without seat"); + return; + }, + }; + + for (id, touch_point) in seat_state.touch_map.drain() { + let window_id = wayland::make_wid(&touch_point.surface); + let scale_factor = match self.windows.get_mut().get(&window_id) { + Some(window) => window.lock().unwrap().scale_factor(), + None => return, + }; + + let location = touch_point.location.to_physical(scale_factor); + + self.events_sink.push_window_event( + WindowEvent::Touch(Touch { + device_id: crate::event::DeviceId(crate::platform_impl::DeviceId::Wayland( + DeviceId, + )), + phase: TouchPhase::Cancelled, + location, + force: None, + id: id as u64, + }), + window_id, + ); + } + } + + fn shape( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlTouch, + _: i32, + _: f64, + _: f64, + ) { + // Blank. + } + + fn orientation(&mut self, _: &Connection, _: &QueueHandle, _: &WlTouch, _: i32, _: f64) { + // Blank. + } +} + +/// The state of the touch point. +#[derive(Debug)] +pub struct TouchPoint { + /// The surface on which the point is present. + pub surface: WlSurface, + + /// The location of the point on the surface. + pub location: LogicalPosition, +} + +pub trait TouchDataExt { + fn seat(&self) -> &WlSeat; +} + +impl TouchDataExt for WlTouch { + fn seat(&self) -> &WlSeat { + self.data::().expect("failed to get touch data.").seat() + } +} + +sctk::delegate_touch!(WinitState); diff --git a/vendor/winit/src/platform_impl/linux/wayland/state.rs b/vendor/winit/src/platform_impl/linux/wayland/state.rs new file mode 100644 index 00000000..13ef99c2 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/state.rs @@ -0,0 +1,435 @@ +use std::cell::RefCell; +use std::sync::atomic::Ordering; +use std::sync::{Arc, Mutex}; + +use ahash::AHashMap; + +use sctk::reexports::calloop::LoopHandle; +use sctk::reexports::client::backend::ObjectId; +use sctk::reexports::client::globals::GlobalList; +use sctk::reexports::client::protocol::wl_output::WlOutput; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{Connection, Proxy, QueueHandle}; + +use sctk::compositor::{CompositorHandler, CompositorState}; +use sctk::output::{OutputHandler, OutputState}; +use sctk::registry::{ProvidesRegistryState, RegistryState}; +use sctk::seat::pointer::ThemedPointer; +use sctk::seat::SeatState; +use sctk::shell::xdg::window::{Window, WindowConfigure, WindowHandler}; +use sctk::shell::xdg::XdgShell; +use sctk::shell::WaylandSurface; +use sctk::shm::slot::SlotPool; +use sctk::shm::{Shm, ShmHandler}; +use sctk::subcompositor::SubcompositorState; + +use crate::platform_impl::wayland::event_loop::sink::EventSink; +use crate::platform_impl::wayland::output::MonitorHandle; +use crate::platform_impl::wayland::seat::{ + PointerConstraintsState, RelativePointerState, TextInputState, WinitPointerData, + WinitPointerDataExt, WinitSeatState, +}; +use crate::platform_impl::wayland::types::kwin_blur::KWinBlurManager; +use crate::platform_impl::wayland::types::wp_fractional_scaling::FractionalScalingManager; +use crate::platform_impl::wayland::types::wp_viewporter::ViewporterState; +use crate::platform_impl::wayland::types::xdg_activation::XdgActivationState; +use crate::platform_impl::wayland::window::{WindowRequests, WindowState}; +use crate::platform_impl::wayland::{WaylandError, WindowId}; +use crate::platform_impl::OsError; + +/// Winit's Wayland state. +pub struct WinitState { + /// The WlRegistry. + pub registry_state: RegistryState, + + /// The state of the WlOutput handling. + pub output_state: OutputState, + + /// The compositor state which is used to create new windows and regions. + pub compositor_state: Arc, + + /// The state of the subcompositor. + pub subcompositor_state: Option>, + + /// The seat state responsible for all sorts of input. + pub seat_state: SeatState, + + /// The shm for software buffers, such as cursors. + pub shm: Shm, + + /// The pool where custom cursors are allocated. + pub custom_cursor_pool: Arc>, + + /// The XDG shell that is used for windows. + pub xdg_shell: XdgShell, + + /// The currently present windows. + pub windows: RefCell>>>, + + /// The requests from the `Window` to EventLoop, such as close operations and redraw requests. + pub window_requests: RefCell>>, + + /// The events that were generated directly from the window. + pub window_events_sink: Arc>, + + /// The update for the `windows` coming from the compositor. + pub window_compositor_updates: Vec, + + /// Currently handled seats. + pub seats: AHashMap, + + /// Currently present cursor surfaces. + pub pointer_surfaces: AHashMap>>, + + /// The state of the text input on the client. + pub text_input_state: Option, + + /// Observed monitors. + pub monitors: Arc>>, + + /// Sink to accumulate window events from the compositor, which is latter dispatched in + /// event loop run. + pub events_sink: EventSink, + + /// Xdg activation. + pub xdg_activation: Option, + + /// Relative pointer. + pub relative_pointer: Option, + + /// Pointer constraints to handle pointer locking and confining. + pub pointer_constraints: Option>, + + /// Viewporter state on the given window. + pub viewporter_state: Option, + + /// Fractional scaling manager. + pub fractional_scaling_manager: Option, + + /// KWin blur manager. + pub kwin_blur_manager: Option, + + /// Loop handle to re-register event sources, such as keyboard repeat. + pub loop_handle: LoopHandle<'static, Self>, + + /// Whether we have dispatched events to the user thus we want to + /// send `AboutToWait` and normally wakeup the user. + pub dispatched_events: bool, +} + +impl WinitState { + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + loop_handle: LoopHandle<'static, WinitState>, + ) -> Result { + let registry_state = RegistryState::new(globals); + let compositor_state = + CompositorState::bind(globals, queue_handle).map_err(WaylandError::Bind)?; + let subcompositor_state = match SubcompositorState::bind( + compositor_state.wl_compositor().clone(), + globals, + queue_handle, + ) { + Ok(c) => Some(c), + Err(e) => { + tracing::warn!("Subcompositor protocol not available, ignoring CSD: {e:?}"); + None + }, + }; + + let output_state = OutputState::new(globals, queue_handle); + let monitors = output_state.outputs().map(MonitorHandle::new).collect(); + + let seat_state = SeatState::new(globals, queue_handle); + + let mut seats = AHashMap::default(); + for seat in seat_state.seats() { + seats.insert(seat.id(), WinitSeatState::new()); + } + + let (viewporter_state, fractional_scaling_manager) = + if let Ok(fsm) = FractionalScalingManager::new(globals, queue_handle) { + (ViewporterState::new(globals, queue_handle).ok(), Some(fsm)) + } else { + (None, None) + }; + + let shm = Shm::bind(globals, queue_handle).map_err(WaylandError::Bind)?; + let custom_cursor_pool = Arc::new(Mutex::new(SlotPool::new(2, &shm).unwrap())); + + Ok(Self { + registry_state, + compositor_state: Arc::new(compositor_state), + subcompositor_state: subcompositor_state.map(Arc::new), + output_state, + seat_state, + shm, + custom_cursor_pool, + + xdg_shell: XdgShell::bind(globals, queue_handle).map_err(WaylandError::Bind)?, + xdg_activation: XdgActivationState::bind(globals, queue_handle).ok(), + + windows: Default::default(), + window_requests: Default::default(), + window_compositor_updates: Vec::new(), + window_events_sink: Default::default(), + viewporter_state, + fractional_scaling_manager, + kwin_blur_manager: KWinBlurManager::new(globals, queue_handle).ok(), + + seats, + text_input_state: TextInputState::new(globals, queue_handle).ok(), + + relative_pointer: RelativePointerState::new(globals, queue_handle).ok(), + pointer_constraints: PointerConstraintsState::new(globals, queue_handle) + .map(Arc::new) + .ok(), + pointer_surfaces: Default::default(), + + monitors: Arc::new(Mutex::new(monitors)), + events_sink: EventSink::new(), + loop_handle, + // Make it true by default. + dispatched_events: true, + }) + } + + pub fn scale_factor_changed( + &mut self, + surface: &WlSurface, + scale_factor: f64, + is_legacy: bool, + ) { + // Check if the cursor surface. + let window_id = super::make_wid(surface); + + if let Some(window) = self.windows.get_mut().get(&window_id) { + // Don't update the scaling factor, when legacy method is used. + if is_legacy && self.fractional_scaling_manager.is_some() { + return; + } + + // The scale factor change is for the window. + let pos = if let Some(pos) = self + .window_compositor_updates + .iter() + .position(|update| update.window_id == window_id) + { + pos + } else { + self.window_compositor_updates.push(WindowCompositorUpdate::new(window_id)); + self.window_compositor_updates.len() - 1 + }; + + // Update the scale factor right away. + window.lock().unwrap().set_scale_factor(scale_factor); + self.window_compositor_updates[pos].scale_changed = true; + } else if let Some(pointer) = self.pointer_surfaces.get(&surface.id()) { + // Get the window, where the pointer resides right now. + let focused_window = match pointer.pointer().winit_data().focused_window() { + Some(focused_window) => focused_window, + None => return, + }; + + if let Some(window_state) = self.windows.get_mut().get(&focused_window) { + window_state.lock().unwrap().reload_cursor_style() + } + } + } + + pub fn queue_close(updates: &mut Vec, window_id: WindowId) { + let pos = if let Some(pos) = updates.iter().position(|update| update.window_id == window_id) + { + pos + } else { + updates.push(WindowCompositorUpdate::new(window_id)); + updates.len() - 1 + }; + + updates[pos].close_window = true; + } +} + +impl ShmHandler for WinitState { + fn shm_state(&mut self) -> &mut Shm { + &mut self.shm + } +} + +impl WindowHandler for WinitState { + fn request_close(&mut self, _: &Connection, _: &QueueHandle, window: &Window) { + let window_id = super::make_wid(window.wl_surface()); + Self::queue_close(&mut self.window_compositor_updates, window_id); + } + + fn configure( + &mut self, + _: &Connection, + _: &QueueHandle, + window: &Window, + configure: WindowConfigure, + _serial: u32, + ) { + let window_id = super::make_wid(window.wl_surface()); + + let pos = if let Some(pos) = + self.window_compositor_updates.iter().position(|update| update.window_id == window_id) + { + pos + } else { + self.window_compositor_updates.push(WindowCompositorUpdate::new(window_id)); + self.window_compositor_updates.len() - 1 + }; + + // Populate the configure to the window. + self.window_compositor_updates[pos].resized |= self + .windows + .get_mut() + .get_mut(&window_id) + .expect("got configure for dead window.") + .lock() + .unwrap() + .configure(configure, &self.shm, &self.subcompositor_state); + + // NOTE: configure demands wl_surface::commit, however winit doesn't commit on behalf of the + // users, since it can break a lot of things, thus it'll ask users to redraw instead. + self.window_requests + .get_mut() + .get(&window_id) + .unwrap() + .redraw_requested + .store(true, Ordering::Relaxed); + + // Manually mark that we've got an event, since configure may not generate a resize. + self.dispatched_events = true; + } +} + +impl OutputHandler for WinitState { + fn output_state(&mut self) -> &mut OutputState { + &mut self.output_state + } + + fn new_output(&mut self, _: &Connection, _: &QueueHandle, output: WlOutput) { + self.monitors.lock().unwrap().push(MonitorHandle::new(output)); + } + + fn update_output(&mut self, _: &Connection, _: &QueueHandle, updated: WlOutput) { + let mut monitors = self.monitors.lock().unwrap(); + let updated = MonitorHandle::new(updated); + if let Some(pos) = monitors.iter().position(|output| output == &updated) { + monitors[pos] = updated + } else { + monitors.push(updated) + } + } + + fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle, removed: WlOutput) { + let mut monitors = self.monitors.lock().unwrap(); + let removed = MonitorHandle::new(removed); + if let Some(pos) = monitors.iter().position(|output| output == &removed) { + monitors.remove(pos); + } + } +} + +impl CompositorHandler for WinitState { + fn transform_changed( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlSurface, + _: wayland_client::protocol::wl_output::Transform, + ) { + // TODO(kchibisov) we need to expose it somehow in winit. + } + + fn surface_enter( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlSurface, + _: &WlOutput, + ) { + } + + fn surface_leave( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlSurface, + _: &WlOutput, + ) { + } + + fn scale_factor_changed( + &mut self, + _: &Connection, + _: &QueueHandle, + surface: &WlSurface, + scale_factor: i32, + ) { + self.scale_factor_changed(surface, scale_factor as f64, true) + } + + fn frame(&mut self, _: &Connection, _: &QueueHandle, surface: &WlSurface, _: u32) { + let window_id = super::make_wid(surface); + let window = match self.windows.get_mut().get(&window_id) { + Some(window) => window, + None => return, + }; + + // In case we have a redraw requested we must indicate the wake up. + if self + .window_requests + .get_mut() + .get(&window_id) + .unwrap() + .redraw_requested + .load(Ordering::Relaxed) + { + self.dispatched_events = true; + } + + window.lock().unwrap().frame_callback_received(); + } +} + +impl ProvidesRegistryState for WinitState { + sctk::registry_handlers![OutputState, SeatState]; + + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } +} + +// The window update coming from the compositor. +#[derive(Debug, Clone, Copy)] +pub struct WindowCompositorUpdate { + /// The id of the window this updates belongs to. + pub window_id: WindowId, + + /// New window size. + pub resized: bool, + + /// New scale factor. + pub scale_changed: bool, + + /// Close the window. + pub close_window: bool, +} + +impl WindowCompositorUpdate { + fn new(window_id: WindowId) -> Self { + Self { window_id, resized: false, scale_changed: false, close_window: false } + } +} + +sctk::delegate_subcompositor!(WinitState); +sctk::delegate_compositor!(WinitState); +sctk::delegate_output!(WinitState); +sctk::delegate_registry!(WinitState); +sctk::delegate_shm!(WinitState); +sctk::delegate_xdg_shell!(WinitState); +sctk::delegate_xdg_window!(WinitState); diff --git a/vendor/winit/src/platform_impl/linux/wayland/types/cursor.rs b/vendor/winit/src/platform_impl/linux/wayland/types/cursor.rs new file mode 100644 index 00000000..c1a0c261 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/types/cursor.rs @@ -0,0 +1,59 @@ +use cursor_icon::CursorIcon; + +use sctk::reexports::client::protocol::wl_shm::Format; +use sctk::shm::slot::{Buffer, SlotPool}; + +use crate::cursor::CursorImage; + +#[derive(Debug)] +pub enum SelectedCursor { + Named(CursorIcon), + Custom(CustomCursor), +} + +impl Default for SelectedCursor { + fn default() -> Self { + Self::Named(Default::default()) + } +} + +#[derive(Debug)] +pub struct CustomCursor { + pub buffer: Buffer, + pub w: i32, + pub h: i32, + pub hotspot_x: i32, + pub hotspot_y: i32, +} + +impl CustomCursor { + pub(crate) fn new(pool: &mut SlotPool, image: &CursorImage) -> Self { + let (buffer, canvas) = pool + .create_buffer( + image.width as i32, + image.height as i32, + 4 * (image.width as i32), + Format::Argb8888, + ) + .unwrap(); + + for (canvas_chunk, rgba) in canvas.chunks_exact_mut(4).zip(image.rgba.chunks_exact(4)) { + // Alpha in buffer is premultiplied. + let alpha = rgba[3] as f32 / 255.; + let r = (rgba[0] as f32 * alpha) as u32; + let g = (rgba[1] as f32 * alpha) as u32; + let b = (rgba[2] as f32 * alpha) as u32; + let color = ((rgba[3] as u32) << 24) + (r << 16) + (g << 8) + b; + let array: &mut [u8; 4] = canvas_chunk.try_into().unwrap(); + *array = color.to_le_bytes(); + } + + CustomCursor { + buffer, + w: image.width as i32, + h: image.height as i32, + hotspot_x: image.hotspot_x as i32, + hotspot_y: image.hotspot_y as i32, + } + } +} diff --git a/vendor/winit/src/platform_impl/linux/wayland/types/kwin_blur.rs b/vendor/winit/src/platform_impl/linux/wayland/types/kwin_blur.rs new file mode 100644 index 00000000..83e82ad7 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/types/kwin_blur.rs @@ -0,0 +1,68 @@ +//! Handling of KDE-compatible blur. + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{delegate_dispatch, Connection, Dispatch, Proxy, QueueHandle}; +use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur; +use wayland_protocols_plasma::blur::client::org_kde_kwin_blur_manager::OrgKdeKwinBlurManager; + +use sctk::globals::GlobalData; + +use crate::platform_impl::wayland::state::WinitState; + +/// KWin blur manager. +#[derive(Debug, Clone)] +pub struct KWinBlurManager { + manager: OrgKdeKwinBlurManager, +} + +impl KWinBlurManager { + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { manager }) + } + + pub fn blur( + &self, + surface: &WlSurface, + queue_handle: &QueueHandle, + ) -> OrgKdeKwinBlur { + self.manager.create(surface, queue_handle, ()) + } + + pub fn unset(&self, surface: &WlSurface) { + self.manager.unset(surface) + } +} + +impl Dispatch for KWinBlurManager { + fn event( + _: &mut WinitState, + _: &OrgKdeKwinBlurManager, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + unreachable!("no events defined for org_kde_kwin_blur_manager"); + } +} + +impl Dispatch for KWinBlurManager { + fn event( + _: &mut WinitState, + _: &OrgKdeKwinBlur, + _: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + unreachable!("no events defined for org_kde_kwin_blur"); + } +} + +delegate_dispatch!(WinitState: [OrgKdeKwinBlurManager: GlobalData] => KWinBlurManager); +delegate_dispatch!(WinitState: [OrgKdeKwinBlur: ()] => KWinBlurManager); diff --git a/vendor/winit/src/platform_impl/linux/wayland/types/mod.rs b/vendor/winit/src/platform_impl/linux/wayland/types/mod.rs new file mode 100644 index 00000000..77e67f48 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/types/mod.rs @@ -0,0 +1,7 @@ +//! Wayland protocol implementation boilerplate. + +pub mod cursor; +pub mod kwin_blur; +pub mod wp_fractional_scaling; +pub mod wp_viewporter; +pub mod xdg_activation; diff --git a/vendor/winit/src/platform_impl/linux/wayland/types/wp_fractional_scaling.rs b/vendor/winit/src/platform_impl/linux/wayland/types/wp_fractional_scaling.rs new file mode 100644 index 00000000..2dfc9dbb --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/types/wp_fractional_scaling.rs @@ -0,0 +1,78 @@ +//! Handling of the fractional scaling. + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{delegate_dispatch, Connection, Dispatch, Proxy, QueueHandle}; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::{ + Event as FractionalScalingEvent, WpFractionalScaleV1, +}; + +use sctk::globals::GlobalData; + +use crate::platform_impl::wayland::state::WinitState; + +/// The scaling factor denominator. +const SCALE_DENOMINATOR: f64 = 120.; + +/// Fractional scaling manager. +#[derive(Debug)] +pub struct FractionalScalingManager { + manager: WpFractionalScaleManagerV1, +} + +pub struct FractionalScaling { + /// The surface used for scaling. + surface: WlSurface, +} + +impl FractionalScalingManager { + /// Create new viewporter. + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let manager = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { manager }) + } + + pub fn fractional_scaling( + &self, + surface: &WlSurface, + queue_handle: &QueueHandle, + ) -> WpFractionalScaleV1 { + let data = FractionalScaling { surface: surface.clone() }; + self.manager.get_fractional_scale(surface, queue_handle, data) + } +} + +impl Dispatch for FractionalScalingManager { + fn event( + _: &mut WinitState, + _: &WpFractionalScaleManagerV1, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + // No events. + } +} + +impl Dispatch for FractionalScalingManager { + fn event( + state: &mut WinitState, + _: &WpFractionalScaleV1, + event: ::Event, + data: &FractionalScaling, + _: &Connection, + _: &QueueHandle, + ) { + if let FractionalScalingEvent::PreferredScale { scale } = event { + state.scale_factor_changed(&data.surface, scale as f64 / SCALE_DENOMINATOR, false); + } + } +} + +delegate_dispatch!(WinitState: [WpFractionalScaleManagerV1: GlobalData] => FractionalScalingManager); +delegate_dispatch!(WinitState: [WpFractionalScaleV1: FractionalScaling] => FractionalScalingManager); diff --git a/vendor/winit/src/platform_impl/linux/wayland/types/wp_viewporter.rs b/vendor/winit/src/platform_impl/linux/wayland/types/wp_viewporter.rs new file mode 100644 index 00000000..90764825 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/types/wp_viewporter.rs @@ -0,0 +1,65 @@ +//! Handling of the wp-viewporter. + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{delegate_dispatch, Connection, Dispatch, Proxy, QueueHandle}; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewporter::WpViewporter; + +use sctk::globals::GlobalData; + +use crate::platform_impl::wayland::state::WinitState; + +/// Viewporter. +#[derive(Debug)] +pub struct ViewporterState { + viewporter: WpViewporter, +} + +impl ViewporterState { + /// Create new viewporter. + pub fn new( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let viewporter = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { viewporter }) + } + + /// Get the viewport for the given object. + pub fn get_viewport( + &self, + surface: &WlSurface, + queue_handle: &QueueHandle, + ) -> WpViewport { + self.viewporter.get_viewport(surface, queue_handle, GlobalData) + } +} + +impl Dispatch for ViewporterState { + fn event( + _: &mut WinitState, + _: &WpViewporter, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + // No events. + } +} +impl Dispatch for ViewporterState { + fn event( + _: &mut WinitState, + _: &WpViewport, + _: ::Event, + _: &GlobalData, + _: &Connection, + _: &QueueHandle, + ) { + // No events. + } +} + +delegate_dispatch!(WinitState: [WpViewporter: GlobalData] => ViewporterState); +delegate_dispatch!(WinitState: [WpViewport: GlobalData] => ViewporterState); diff --git a/vendor/winit/src/platform_impl/linux/wayland/types/xdg_activation.rs b/vendor/winit/src/platform_impl/linux/wayland/types/xdg_activation.rs new file mode 100644 index 00000000..9efc75da --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/types/xdg_activation.rs @@ -0,0 +1,103 @@ +//! Handling of xdg activation, which is used for user attention requests. + +use std::sync::atomic::AtomicBool; +use std::sync::Weak; + +use sctk::reexports::client::globals::{BindError, GlobalList}; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{delegate_dispatch, Connection, Dispatch, Proxy, QueueHandle}; +use sctk::reexports::protocols::xdg::activation::v1::client::xdg_activation_token_v1::{ + Event as ActivationTokenEvent, XdgActivationTokenV1, +}; +use sctk::reexports::protocols::xdg::activation::v1::client::xdg_activation_v1::XdgActivationV1; + +use sctk::globals::GlobalData; + +use crate::event_loop::AsyncRequestSerial; +use crate::platform_impl::wayland::state::WinitState; +use crate::platform_impl::WindowId; +use crate::window::ActivationToken; + +pub struct XdgActivationState { + xdg_activation: XdgActivationV1, +} + +impl XdgActivationState { + pub fn bind( + globals: &GlobalList, + queue_handle: &QueueHandle, + ) -> Result { + let xdg_activation = globals.bind(queue_handle, 1..=1, GlobalData)?; + Ok(Self { xdg_activation }) + } + + pub fn global(&self) -> &XdgActivationV1 { + &self.xdg_activation + } +} + +impl Dispatch for XdgActivationState { + fn event( + _state: &mut WinitState, + _proxy: &XdgActivationV1, + _event: ::Event, + _data: &GlobalData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + +impl Dispatch for XdgActivationState { + fn event( + state: &mut WinitState, + proxy: &XdgActivationTokenV1, + event: ::Event, + data: &XdgActivationTokenData, + _: &Connection, + _: &QueueHandle, + ) { + let token = match event { + ActivationTokenEvent::Done { token } => token, + _ => return, + }; + + let global = state + .xdg_activation + .as_ref() + .expect("got xdg_activation event without global.") + .global(); + + match data { + XdgActivationTokenData::Attention((surface, fence)) => { + global.activate(token, surface); + // Mark that no request attention is in process. + if let Some(attention_requested) = fence.upgrade() { + attention_requested.store(false, std::sync::atomic::Ordering::Relaxed); + } + }, + XdgActivationTokenData::Obtain((window_id, serial)) => { + state.events_sink.push_window_event( + crate::event::WindowEvent::ActivationTokenDone { + serial: *serial, + token: ActivationToken::from_raw(token), + }, + *window_id, + ); + }, + } + + proxy.destroy(); + } +} + +/// The data associated with the activation request. +pub enum XdgActivationTokenData { + /// Request user attention for the given surface. + Attention((WlSurface, Weak)), + /// Get a token to be passed outside of the winit. + Obtain((WindowId, AsyncRequestSerial)), +} + +delegate_dispatch!(WinitState: [ XdgActivationV1: GlobalData] => XdgActivationState); +delegate_dispatch!(WinitState: [ XdgActivationTokenV1: XdgActivationTokenData] => XdgActivationState); diff --git a/vendor/winit/src/platform_impl/linux/wayland/window/mod.rs b/vendor/winit/src/platform_impl/linux/wayland/window/mod.rs new file mode 100644 index 00000000..6d29a5a5 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/window/mod.rs @@ -0,0 +1,763 @@ +//! The Wayland window. + +use std::ffi::c_void; +use std::ptr::NonNull; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + +use sctk::reexports::client::protocol::wl_display::WlDisplay; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{Proxy, QueueHandle}; + +use sctk::compositor::{CompositorState, Region, SurfaceData}; +use sctk::reexports::protocols::xdg::activation::v1::client::xdg_activation_v1::XdgActivationV1; +use sctk::shell::xdg::window::{Window as SctkWindow, WindowDecorations}; +use sctk::shell::WaylandSurface; + +use tracing::warn; + +use crate::dpi::{LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::event::{Ime, WindowEvent}; +use crate::event_loop::AsyncRequestSerial; +use crate::platform_impl::{ + Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError, PlatformIcon, +}; +use crate::window::{ + Cursor, CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, WindowLevel, +}; + +use super::event_loop::sink::EventSink; +use super::output::MonitorHandle; +use super::state::WinitState; +use super::types::xdg_activation::XdgActivationTokenData; +use super::{ActiveEventLoop, WaylandError, WindowId}; + +pub(crate) mod state; + +pub use state::WindowState; + +/// The Wayland window. +pub struct Window { + /// Reference to the underlying SCTK window. + window: SctkWindow, + + /// Window id. + window_id: WindowId, + + /// The state of the window. + window_state: Arc>, + + /// Compositor to handle WlRegion stuff. + compositor: Arc, + + /// The wayland display used solely for raw window handle. + #[allow(dead_code)] + display: WlDisplay, + + /// Xdg activation to request user attention. + xdg_activation: Option, + + /// The state of the requested attention from the `xdg_activation`. + attention_requested: Arc, + + /// Handle to the main queue to perform requests. + queue_handle: QueueHandle, + + /// Window requests to the event loop. + window_requests: Arc, + + /// Observed monitors. + monitors: Arc>>, + + /// Source to wake-up the event-loop for window requests. + event_loop_awakener: calloop::ping::Ping, + + /// The event sink to deliver synthetic events. + window_events_sink: Arc>, +} + +impl Window { + pub(crate) fn new( + event_loop_window_target: &ActiveEventLoop, + attributes: WindowAttributes, + ) -> Result { + let queue_handle = event_loop_window_target.queue_handle.clone(); + let mut state = event_loop_window_target.state.borrow_mut(); + + let monitors = state.monitors.clone(); + + let surface = state.compositor_state.create_surface(&queue_handle); + let compositor = state.compositor_state.clone(); + let xdg_activation = + state.xdg_activation.as_ref().map(|activation_state| activation_state.global().clone()); + let display = event_loop_window_target.connection.display(); + + let size: Size = attributes.inner_size.unwrap_or(LogicalSize::new(800., 600.).into()); + + // We prefer server side decorations, however to not have decorations we ask for client + // side decorations instead. + let default_decorations = if attributes.decorations { + WindowDecorations::RequestServer + } else { + WindowDecorations::RequestClient + }; + + let window = + state.xdg_shell.create_window(surface.clone(), default_decorations, &queue_handle); + + let mut window_state = WindowState::new( + event_loop_window_target.connection.clone(), + &event_loop_window_target.queue_handle, + &state, + size, + window.clone(), + attributes.preferred_theme, + ); + + // Set transparency hint. + window_state.set_transparent(attributes.transparent); + + window_state.set_blur(attributes.blur); + + // Set the decorations hint. + window_state.set_decorate(attributes.decorations); + + // Set the app_id. + if let Some(name) = attributes.platform_specific.name.map(|name| name.general) { + window.set_app_id(name); + } + + // Set the window title. + window_state.set_title(attributes.title); + + // Set the min and max sizes. We must set the hints upon creating a window, so + // we use the default `1.` scaling... + let min_size = attributes.min_inner_size.map(|size| size.to_logical(1.)); + let max_size = attributes.max_inner_size.map(|size| size.to_logical(1.)); + window_state.set_min_inner_size(min_size); + window_state.set_max_inner_size(max_size); + + // Non-resizable implies that the min and max sizes are set to the same value. + window_state.set_resizable(attributes.resizable); + + // Set startup mode. + match attributes.fullscreen.map(Into::into) { + Some(Fullscreen::Exclusive(_)) => { + warn!("`Fullscreen::Exclusive` is ignored on Wayland"); + }, + #[cfg_attr(not(x11_platform), allow(clippy::bind_instead_of_map))] + Some(Fullscreen::Borderless(monitor)) => { + let output = monitor.and_then(|monitor| match monitor { + PlatformMonitorHandle::Wayland(monitor) => Some(monitor.proxy), + #[cfg(x11_platform)] + PlatformMonitorHandle::X(_) => None, + }); + + window.set_fullscreen(output.as_ref()) + }, + _ if attributes.maximized => window.set_maximized(), + _ => (), + }; + + match attributes.cursor { + Cursor::Icon(icon) => window_state.set_cursor(icon), + Cursor::Custom(cursor) => window_state.set_custom_cursor(cursor), + } + + // Apply resize increments. + if let Some(increments) = attributes.resize_increments { + let increments = increments.to_logical(window_state.scale_factor()); + window_state.set_resize_increments(Some(increments)); + } + + // Activate the window when the token is passed. + if let (Some(xdg_activation), Some(token)) = + (xdg_activation.as_ref(), attributes.platform_specific.activation_token) + { + xdg_activation.activate(token.token, &surface); + } + + // XXX Do initial commit. + window.commit(); + + // Add the window and window requests into the state. + let window_state = Arc::new(Mutex::new(window_state)); + let window_id = super::make_wid(&surface); + state.windows.get_mut().insert(window_id, window_state.clone()); + + let window_requests = WindowRequests { + redraw_requested: AtomicBool::new(true), + closed: AtomicBool::new(false), + }; + let window_requests = Arc::new(window_requests); + state.window_requests.get_mut().insert(window_id, window_requests.clone()); + + // Setup the event sync to insert `WindowEvents` right from the window. + let window_events_sink = state.window_events_sink.clone(); + + let mut wayland_source = event_loop_window_target.wayland_dispatcher.as_source_mut(); + let event_queue = wayland_source.queue(); + + // Do a roundtrip. + event_queue.roundtrip(&mut state).map_err(|error| { + os_error!(OsError::WaylandError(Arc::new(WaylandError::Dispatch(error)))) + })?; + + // XXX Wait for the initial configure to arrive. + while !window_state.lock().unwrap().is_configured() { + event_queue.blocking_dispatch(&mut state).map_err(|error| { + os_error!(OsError::WaylandError(Arc::new(WaylandError::Dispatch(error)))) + })?; + } + + // Wake-up event loop, so it'll send initial redraw requested. + let event_loop_awakener = event_loop_window_target.event_loop_awakener.clone(); + event_loop_awakener.ping(); + + Ok(Self { + window, + display, + monitors, + window_id, + compositor, + window_state, + queue_handle, + xdg_activation, + attention_requested: Arc::new(AtomicBool::new(false)), + event_loop_awakener, + window_requests, + window_events_sink, + }) + } + + pub(crate) fn xdg_toplevel(&self) -> Option> { + NonNull::new(self.window.xdg_toplevel().id().as_ptr().cast()) + } +} + +impl Window { + #[inline] + pub fn id(&self) -> WindowId { + self.window_id + } + + #[inline] + pub fn set_title(&self, title: impl ToString) { + let new_title = title.to_string(); + self.window_state.lock().unwrap().set_title(new_title); + } + + #[inline] + pub fn set_visible(&self, _visible: bool) { + // Not possible on Wayland. + } + + #[inline] + pub fn is_visible(&self) -> Option { + None + } + + #[inline] + pub fn outer_position(&self) -> Result, NotSupportedError> { + Err(NotSupportedError::new()) + } + + #[inline] + pub fn inner_position(&self) -> Result, NotSupportedError> { + Err(NotSupportedError::new()) + } + + #[inline] + pub fn set_outer_position(&self, _: Position) { + // Not possible on Wayland. + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + let window_state = self.window_state.lock().unwrap(); + let scale_factor = window_state.scale_factor(); + super::logical_to_physical_rounded(window_state.inner_size(), scale_factor) + } + + #[inline] + pub fn request_redraw(&self) { + // NOTE: try to not wake up the loop when the event was already scheduled and not yet + // processed by the loop, because if at this point the value was `true` it could only + // mean that the loop still haven't dispatched the value to the client and will do + // eventually, resetting it to `false`. + if self + .window_requests + .redraw_requested + .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + self.event_loop_awakener.ping(); + } + } + + #[inline] + pub fn pre_present_notify(&self) { + self.window_state.lock().unwrap().request_frame_callback(); + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + let window_state = self.window_state.lock().unwrap(); + let scale_factor = window_state.scale_factor(); + super::logical_to_physical_rounded(window_state.outer_size(), scale_factor) + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + let mut window_state = self.window_state.lock().unwrap(); + let new_size = window_state.request_inner_size(size); + self.request_redraw(); + Some(new_size) + } + + /// Set the minimum inner size for the window. + #[inline] + pub fn set_min_inner_size(&self, min_size: Option) { + let scale_factor = self.scale_factor(); + let min_size = min_size.map(|size| size.to_logical(scale_factor)); + self.window_state.lock().unwrap().set_min_inner_size(min_size); + // NOTE: Requires commit to be applied. + self.request_redraw(); + } + + /// Set the maximum inner size for the window. + #[inline] + pub fn set_max_inner_size(&self, max_size: Option) { + let scale_factor = self.scale_factor(); + let max_size = max_size.map(|size| size.to_logical(scale_factor)); + self.window_state.lock().unwrap().set_max_inner_size(max_size); + // NOTE: Requires commit to be applied. + self.request_redraw(); + } + + #[inline] + pub fn resize_increments(&self) -> Option> { + let window_state = self.window_state.lock().unwrap(); + let scale_factor = window_state.scale_factor(); + window_state + .resize_increments() + .map(|size| super::logical_to_physical_rounded(size, scale_factor)) + } + + #[inline] + pub fn set_resize_increments(&self, increments: Option) { + let mut window_state = self.window_state.lock().unwrap(); + let scale_factor = window_state.scale_factor(); + let increments = increments.map(|size| size.to_logical(scale_factor)); + window_state.set_resize_increments(increments); + } + + #[inline] + pub fn set_transparent(&self, transparent: bool) { + self.window_state.lock().unwrap().set_transparent(transparent); + } + + #[inline] + pub fn has_focus(&self) -> bool { + self.window_state.lock().unwrap().has_focus() + } + + #[inline] + pub fn is_minimized(&self) -> Option { + // XXX clients don't know whether they are minimized or not. + None + } + + #[inline] + pub fn show_window_menu(&self, position: Position) { + let scale_factor = self.scale_factor(); + let position = position.to_logical(scale_factor); + self.window_state.lock().unwrap().show_window_menu(position); + } + + #[inline] + pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { + self.window_state.lock().unwrap().drag_resize_window(direction) + } + + #[inline] + pub fn set_resizable(&self, resizable: bool) { + if self.window_state.lock().unwrap().set_resizable(resizable) { + // NOTE: Requires commit to be applied. + self.request_redraw(); + } + } + + #[inline] + pub fn is_resizable(&self) -> bool { + self.window_state.lock().unwrap().resizable() + } + + #[inline] + pub fn set_enabled_buttons(&self, _buttons: WindowButtons) { + // TODO(kchibisov) v5 of the xdg_shell allows that. + } + + #[inline] + pub fn enabled_buttons(&self) -> WindowButtons { + // TODO(kchibisov) v5 of the xdg_shell allows that. + WindowButtons::all() + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + self.window_state.lock().unwrap().scale_factor() + } + + #[inline] + pub fn set_blur(&self, blur: bool) { + self.window_state.lock().unwrap().set_blur(blur); + } + + #[inline] + pub fn set_decorations(&self, decorate: bool) { + self.window_state.lock().unwrap().set_decorate(decorate) + } + + #[inline] + pub fn is_decorated(&self) -> bool { + self.window_state.lock().unwrap().is_decorated() + } + + #[inline] + pub fn set_window_level(&self, _level: WindowLevel) {} + + #[inline] + pub(crate) fn set_window_icon(&self, _window_icon: Option) {} + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + // You can't unminimize the window on Wayland. + if !minimized { + warn!("Unminimizing is ignored on Wayland."); + return; + } + + self.window.set_minimized(); + } + + #[inline] + pub fn is_maximized(&self) -> bool { + self.window_state + .lock() + .unwrap() + .last_configure + .as_ref() + .map(|last_configure| last_configure.is_maximized()) + .unwrap_or_default() + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + if maximized { + self.window.set_maximized() + } else { + self.window.unset_maximized() + } + } + + #[inline] + pub(crate) fn fullscreen(&self) -> Option { + let is_fullscreen = self + .window_state + .lock() + .unwrap() + .last_configure + .as_ref() + .map(|last_configure| last_configure.is_fullscreen()) + .unwrap_or_default(); + + if is_fullscreen { + let current_monitor = self.current_monitor().map(PlatformMonitorHandle::Wayland); + Some(Fullscreen::Borderless(current_monitor)) + } else { + None + } + } + + #[inline] + pub(crate) fn set_fullscreen(&self, fullscreen: Option) { + match fullscreen { + Some(Fullscreen::Exclusive(_)) => { + warn!("`Fullscreen::Exclusive` is ignored on Wayland"); + }, + #[cfg_attr(not(x11_platform), allow(clippy::bind_instead_of_map))] + Some(Fullscreen::Borderless(monitor)) => { + let output = monitor.and_then(|monitor| match monitor { + PlatformMonitorHandle::Wayland(monitor) => Some(monitor.proxy), + #[cfg(x11_platform)] + PlatformMonitorHandle::X(_) => None, + }); + + self.window.set_fullscreen(output.as_ref()) + }, + None => self.window.unset_fullscreen(), + } + } + + #[inline] + pub fn set_cursor(&self, cursor: Cursor) { + let window_state = &mut self.window_state.lock().unwrap(); + + match cursor { + Cursor::Icon(icon) => window_state.set_cursor(icon), + Cursor::Custom(cursor) => window_state.set_custom_cursor(cursor), + } + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + self.window_state.lock().unwrap().set_cursor_visible(visible); + } + + pub fn request_user_attention(&self, request_type: Option) { + let xdg_activation = match self.xdg_activation.as_ref() { + Some(xdg_activation) => xdg_activation, + None => { + warn!("`request_user_attention` isn't supported"); + return; + }, + }; + + // Urgency is only removed by the compositor and there's no need to raise urgency when it + // was already raised. + if request_type.is_none() || self.attention_requested.load(Ordering::Relaxed) { + return; + } + + self.attention_requested.store(true, Ordering::Relaxed); + let surface = self.surface().clone(); + let data = XdgActivationTokenData::Attention(( + surface.clone(), + Arc::downgrade(&self.attention_requested), + )); + let xdg_activation_token = xdg_activation.get_activation_token(&self.queue_handle, data); + xdg_activation_token.set_surface(&surface); + xdg_activation_token.commit(); + } + + pub fn request_activation_token(&self) -> Result { + let xdg_activation = match self.xdg_activation.as_ref() { + Some(xdg_activation) => xdg_activation, + None => return Err(NotSupportedError::new()), + }; + + let serial = AsyncRequestSerial::get(); + + let data = XdgActivationTokenData::Obtain((self.window_id, serial)); + let xdg_activation_token = xdg_activation.get_activation_token(&self.queue_handle, data); + xdg_activation_token.set_surface(self.surface()); + xdg_activation_token.commit(); + + Ok(serial) + } + + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + self.window_state.lock().unwrap().set_cursor_grab(mode) + } + + #[inline] + pub fn set_cursor_position(&self, position: Position) -> Result<(), ExternalError> { + let scale_factor = self.scale_factor(); + let position = position.to_logical(scale_factor); + self.window_state + .lock() + .unwrap() + .set_cursor_position(position) + // Request redraw on success, since the state is double buffered. + .map(|_| self.request_redraw()) + } + + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + self.window_state.lock().unwrap().drag_window() + } + + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + let surface = self.window.wl_surface(); + + if hittest { + surface.set_input_region(None); + Ok(()) + } else { + let region = Region::new(&*self.compositor).map_err(|_| { + ExternalError::Os(os_error!(OsError::Misc("failed to set input region."))) + })?; + region.add(0, 0, 0, 0); + surface.set_input_region(Some(region.wl_region())); + Ok(()) + } + } + + #[inline] + pub fn set_ime_cursor_area(&self, position: Position, size: Size) { + let window_state = self.window_state.lock().unwrap(); + if window_state.ime_allowed() { + let scale_factor = window_state.scale_factor(); + let position = position.to_logical(scale_factor); + let size = size.to_logical(scale_factor); + window_state.set_ime_cursor_area(position, size); + } + } + + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + let mut window_state = self.window_state.lock().unwrap(); + + if window_state.ime_allowed() != allowed && window_state.set_ime_allowed(allowed) { + let event = WindowEvent::Ime(if allowed { Ime::Enabled } else { Ime::Disabled }); + self.window_events_sink.lock().unwrap().push_window_event(event, self.window_id); + self.event_loop_awakener.ping(); + } + } + + #[inline] + pub fn set_ime_purpose(&self, purpose: ImePurpose) { + self.window_state.lock().unwrap().set_ime_purpose(purpose); + } + + #[inline] + pub fn focus_window(&self) {} + + #[inline] + pub fn surface(&self) -> &WlSurface { + self.window.wl_surface() + } + + #[inline] + pub fn current_monitor(&self) -> Option { + let data = self.window.wl_surface().data::()?; + data.outputs().next().map(MonitorHandle::new) + } + + #[inline] + pub fn available_monitors(&self) -> Vec { + self.monitors.lock().unwrap().clone() + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + // XXX there's no such concept on Wayland. + None + } + + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut window_handle = rwh_04::WaylandHandle::empty(); + window_handle.surface = self.window.wl_surface().id().as_ptr() as *mut _; + window_handle.display = self.display.id().as_ptr() as *mut _; + rwh_04::RawWindowHandle::Wayland(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut window_handle = rwh_05::WaylandWindowHandle::empty(); + window_handle.surface = self.window.wl_surface().id().as_ptr() as *mut _; + rwh_05::RawWindowHandle::Wayland(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + let mut display_handle = rwh_05::WaylandDisplayHandle::empty(); + display_handle.display = self.display.id().as_ptr() as *mut _; + rwh_05::RawDisplayHandle::Wayland(display_handle) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> Result { + Ok(rwh_06::WaylandWindowHandle::new({ + let ptr = self.window.wl_surface().id().as_ptr(); + std::ptr::NonNull::new(ptr as *mut _).expect("wl_surface will never be null") + }) + .into()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::WaylandDisplayHandle::new({ + let ptr = self.display.id().as_ptr(); + std::ptr::NonNull::new(ptr as *mut _).expect("wl_proxy should never be null") + }) + .into()) + } + + #[inline] + pub fn set_theme(&self, theme: Option) { + self.window_state.lock().unwrap().set_theme(theme) + } + + #[inline] + pub fn theme(&self) -> Option { + self.window_state.lock().unwrap().theme() + } + + pub fn set_content_protected(&self, _protected: bool) {} + + #[inline] + pub fn title(&self) -> String { + self.window_state.lock().unwrap().title().to_owned() + } +} + +impl Drop for Window { + fn drop(&mut self) { + self.window_requests.closed.store(true, Ordering::Relaxed); + self.event_loop_awakener.ping(); + } +} + +/// The request from the window to the event loop. +#[derive(Debug)] +pub struct WindowRequests { + /// The window was closed. + pub closed: AtomicBool, + + /// Redraw Requested. + pub redraw_requested: AtomicBool, +} + +impl WindowRequests { + pub fn take_closed(&self) -> bool { + self.closed.swap(false, Ordering::Relaxed) + } + + pub fn take_redraw_requested(&self) -> bool { + self.redraw_requested.swap(false, Ordering::Relaxed) + } +} + +impl TryFrom<&str> for Theme { + type Error = (); + + /// ``` + /// use winit::window::Theme; + /// + /// assert_eq!("dark".try_into(), Ok(Theme::Dark)); + /// assert_eq!("lIghT".try_into(), Ok(Theme::Light)); + /// ``` + fn try_from(theme: &str) -> Result { + if theme.eq_ignore_ascii_case("dark") { + Ok(Self::Dark) + } else if theme.eq_ignore_ascii_case("light") { + Ok(Self::Light) + } else { + Err(()) + } + } +} diff --git a/vendor/winit/src/platform_impl/linux/wayland/window/state.rs b/vendor/winit/src/platform_impl/linux/wayland/window/state.rs new file mode 100644 index 00000000..1ef7a065 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/wayland/window/state.rs @@ -0,0 +1,1220 @@ +//! The state of the window, which is shared with the event-loop. + +use std::num::NonZeroU32; +use std::sync::{Arc, Mutex, Weak}; +use std::time::Duration; + +use ahash::HashSet; +use tracing::{info, warn}; + +use sctk::reexports::client::backend::ObjectId; +use sctk::reexports::client::protocol::wl_seat::WlSeat; +use sctk::reexports::client::protocol::wl_shm::WlShm; +use sctk::reexports::client::protocol::wl_surface::WlSurface; +use sctk::reexports::client::{Connection, Proxy, QueueHandle}; +use sctk::reexports::csd_frame::{ + DecorationsFrame, FrameAction, FrameClick, ResizeEdge, WindowState as XdgWindowState, +}; +use sctk::reexports::protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1::WpFractionalScaleV1; +use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::ZwpTextInputV3; +use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport; +use sctk::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge as XdgResizeEdge; + +use sctk::compositor::{CompositorState, Region, SurfaceData, SurfaceDataExt}; +use sctk::seat::pointer::{PointerDataExt, ThemedPointer}; +use sctk::shell::xdg::window::{DecorationMode, Window, WindowConfigure}; +use sctk::shell::xdg::XdgSurface; +use sctk::shell::WaylandSurface; +use sctk::shm::slot::SlotPool; +use sctk::shm::Shm; +use sctk::subcompositor::SubcompositorState; +use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur; + +use crate::cursor::CustomCursor as RootCustomCursor; +use crate::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Size}; +use crate::error::{ExternalError, NotSupportedError}; +use crate::platform_impl::wayland::logical_to_physical_rounded; +use crate::platform_impl::wayland::types::cursor::{CustomCursor, SelectedCursor}; +use crate::platform_impl::wayland::types::kwin_blur::KWinBlurManager; +use crate::platform_impl::{PlatformCustomCursor, WindowId}; +use crate::window::{CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme}; + +use crate::platform_impl::wayland::seat::{ + PointerConstraintsState, WinitPointerData, WinitPointerDataExt, ZwpTextInputV3Ext, +}; +use crate::platform_impl::wayland::state::{WindowCompositorUpdate, WinitState}; + +#[cfg(feature = "sctk-adwaita")] +pub type WinitFrame = sctk_adwaita::AdwaitaFrame; +#[cfg(not(feature = "sctk-adwaita"))] +pub type WinitFrame = sctk::shell::xdg::fallback_frame::FallbackFrame; + +// Minimum window inner size. +const MIN_WINDOW_SIZE: LogicalSize = LogicalSize::new(2, 1); + +/// The state of the window which is being updated from the [`WinitState`]. +pub struct WindowState { + /// The connection to Wayland server. + pub connection: Connection, + + /// The `Shm` to set cursor. + pub shm: WlShm, + + // A shared pool where to allocate custom cursors. + custom_cursor_pool: Arc>, + + /// The last received configure. + pub last_configure: Option, + + /// The pointers observed on the window. + pub pointers: Vec>>, + + selected_cursor: SelectedCursor, + + /// Whether the cursor is visible. + pub cursor_visible: bool, + + /// Pointer constraints to lock/confine pointer. + pub pointer_constraints: Option>, + + /// Queue handle. + pub queue_handle: QueueHandle, + + /// Theme variant. + theme: Option, + + /// The current window title. + title: String, + + /// Whether the frame is resizable. + resizable: bool, + + // NOTE: we can't use simple counter, since it's racy when seat getting destroyed and new + // is created, since add/removed stuff could be delivered a bit out of order. + /// Seats that has keyboard focus on that window. + seat_focus: HashSet, + + /// The scale factor of the window. + scale_factor: f64, + + /// Whether the window is transparent. + transparent: bool, + + /// The state of the compositor to create WlRegions. + compositor: Arc, + + /// The current cursor grabbing mode. + cursor_grab_mode: GrabState, + + /// Whether the IME input is allowed for that window. + ime_allowed: bool, + + /// The current IME purpose. + ime_purpose: ImePurpose, + + /// The text inputs observed on the window. + text_inputs: Vec, + + /// The inner size of the window, as in without client side decorations. + size: LogicalSize, + + /// Whether the CSD fail to create, so we don't try to create them on each iteration. + csd_fails: bool, + + /// Whether we should decorate the frame. + decorate: bool, + + /// Min size. + min_inner_size: LogicalSize, + max_inner_size: Option>, + resize_increments: Option>, + + /// The size of the window when no states were applied to it. The primary use for it + /// is to fallback to original window size, before it was maximized, if the compositor + /// sends `None` for the new size in the configure. + stateless_size: LogicalSize, + + /// Initial window size provided by the user. Removed on the first + /// configure. + initial_size: Option, + + /// The state of the frame callback. + frame_callback_state: FrameCallbackState, + + viewport: Option, + fractional_scale: Option, + blur: Option, + blur_manager: Option, + + /// Whether the client side decorations have pending move operations. + /// + /// The value is the serial of the event triggered moved. + has_pending_move: Option, + + /// The underlying SCTK window. + pub window: Window, + + // NOTE: The spec says that destroying parent(`window` in our case), will unmap the + // subsurfaces. Thus to achieve atomic unmap of the client, drop the decorations + // frame after the `window` is dropped. To achieve that we rely on rust's struct + // field drop order guarantees. + /// The window frame, which is created from the configure request. + frame: Option, +} + +impl WindowState { + /// Create new window state. + pub fn new( + connection: Connection, + queue_handle: &QueueHandle, + winit_state: &WinitState, + initial_size: Size, + window: Window, + theme: Option, + ) -> Self { + let compositor = winit_state.compositor_state.clone(); + let pointer_constraints = winit_state.pointer_constraints.clone(); + let viewport = winit_state + .viewporter_state + .as_ref() + .map(|state| state.get_viewport(window.wl_surface(), queue_handle)); + let fractional_scale = winit_state + .fractional_scaling_manager + .as_ref() + .map(|fsm| fsm.fractional_scaling(window.wl_surface(), queue_handle)); + + Self { + blur: None, + blur_manager: winit_state.kwin_blur_manager.clone(), + compositor, + connection, + csd_fails: false, + cursor_grab_mode: GrabState::new(), + selected_cursor: Default::default(), + cursor_visible: true, + decorate: true, + fractional_scale, + frame: None, + frame_callback_state: FrameCallbackState::None, + seat_focus: Default::default(), + has_pending_move: None, + ime_allowed: false, + ime_purpose: ImePurpose::Normal, + last_configure: None, + max_inner_size: None, + min_inner_size: MIN_WINDOW_SIZE, + resize_increments: None, + pointer_constraints, + pointers: Default::default(), + queue_handle: queue_handle.clone(), + resizable: true, + scale_factor: 1., + shm: winit_state.shm.wl_shm().clone(), + custom_cursor_pool: winit_state.custom_cursor_pool.clone(), + size: initial_size.to_logical(1.), + stateless_size: initial_size.to_logical(1.), + initial_size: Some(initial_size), + text_inputs: Vec::new(), + theme, + title: String::default(), + transparent: false, + viewport, + window, + } + } + + /// Apply closure on the given pointer. + fn apply_on_pointer, &WinitPointerData)>( + &self, + mut callback: F, + ) { + self.pointers.iter().filter_map(Weak::upgrade).for_each(|pointer| { + let data = pointer.pointer().winit_data(); + callback(pointer.as_ref(), data); + }) + } + + /// Get the current state of the frame callback. + pub fn frame_callback_state(&self) -> FrameCallbackState { + self.frame_callback_state + } + + /// The frame callback was received, but not yet sent to the user. + pub fn frame_callback_received(&mut self) { + self.frame_callback_state = FrameCallbackState::Received; + } + + /// Reset the frame callbacks state. + pub fn frame_callback_reset(&mut self) { + self.frame_callback_state = FrameCallbackState::None; + } + + /// Request a frame callback if we don't have one for this window in flight. + pub fn request_frame_callback(&mut self) { + let surface = self.window.wl_surface(); + match self.frame_callback_state { + FrameCallbackState::None | FrameCallbackState::Received => { + self.frame_callback_state = FrameCallbackState::Requested; + surface.frame(&self.queue_handle, surface.clone()); + }, + FrameCallbackState::Requested => (), + } + } + + pub fn configure( + &mut self, + configure: WindowConfigure, + shm: &Shm, + subcompositor: &Option>, + ) -> bool { + // NOTE: when using fractional scaling or wl_compositor@v6 the scaling + // should be delivered before the first configure, thus apply it to + // properly scale the physical sizes provided by the users. + if let Some(initial_size) = self.initial_size.take() { + self.size = initial_size.to_logical(self.scale_factor()); + self.stateless_size = self.size; + } + + if let Some(subcompositor) = subcompositor.as_ref().filter(|_| { + configure.decoration_mode == DecorationMode::Client + && self.frame.is_none() + && !self.csd_fails + }) { + match WinitFrame::new( + &self.window, + shm, + #[cfg(feature = "sctk-adwaita")] + self.compositor.clone(), + subcompositor.clone(), + self.queue_handle.clone(), + #[cfg(feature = "sctk-adwaita")] + into_sctk_adwaita_config(self.theme), + ) { + Ok(mut frame) => { + frame.set_title(&self.title); + frame.set_scaling_factor(self.scale_factor); + // Hide the frame if we were asked to not decorate. + frame.set_hidden(!self.decorate); + self.frame = Some(frame); + }, + Err(err) => { + warn!("Failed to create client side decorations frame: {err}"); + self.csd_fails = true; + }, + } + } else if configure.decoration_mode == DecorationMode::Server { + // Drop the frame for server side decorations to save resources. + self.frame = None; + } + + let stateless = Self::is_stateless(&configure); + + let (mut new_size, constrain) = if let Some(frame) = self.frame.as_mut() { + // Configure the window states. + frame.update_state(configure.state); + + match configure.new_size { + (Some(width), Some(height)) => { + let (width, height) = frame.subtract_borders(width, height); + let width = width.map(|w| w.get()).unwrap_or(1); + let height = height.map(|h| h.get()).unwrap_or(1); + ((width, height).into(), false) + }, + (..) if stateless => (self.stateless_size, true), + _ => (self.size, true), + } + } else { + match configure.new_size { + (Some(width), Some(height)) => ((width.get(), height.get()).into(), false), + _ if stateless => (self.stateless_size, true), + _ => (self.size, true), + } + }; + + // Apply configure bounds only when compositor let the user decide what size to pick. + if constrain { + let bounds = self.inner_size_bounds(&configure); + new_size.width = + bounds.0.map(|bound_w| new_size.width.min(bound_w.get())).unwrap_or(new_size.width); + new_size.height = bounds + .1 + .map(|bound_h| new_size.height.min(bound_h.get())) + .unwrap_or(new_size.height); + } + + // Apply size increments. + // + // We conditionally apply increments to avoid conflicts with the compositor's layout rules: + // 1. If the window is floating (constrain == true), we snap to increments to ensure the + // app's grid alignment. + // 2. If the user is interactively resizing (is_resizing), we snap the size to provide + // feedback. + // + // However, we MUST NOT snap if the compositor enforces a specific size (constrain == false, + // or states like Maximized/Tiled). Snapping in these cases (e.g. corner tiling) would + // shrink the window below the allocated area, creating visible gaps between valid + // windows or screen edges. + if (constrain || configure.is_resizing()) + && !configure.is_maximized() + && !configure.is_fullscreen() + && !configure.is_tiled() + { + if let Some(increments) = self.resize_increments { + // We use min size as a base size for the increments, similar to how X11 does it. + // + // This ensures that we can always reach the min size and the increments are + // calculated from it. + let (delta_width, delta_height) = ( + new_size.width.saturating_sub(self.min_inner_size.width), + new_size.height.saturating_sub(self.min_inner_size.height), + ); + + let width = + self.min_inner_size.width + (delta_width / increments.width) * increments.width; + let height = self.min_inner_size.height + + (delta_height / increments.height) * increments.height; + + new_size = (width, height).into(); + } + } + + let new_state = configure.state; + let old_state = self.last_configure.as_ref().map(|configure| configure.state); + + let state_change_requires_resize = old_state + .map(|old_state| { + !old_state + .symmetric_difference(new_state) + .difference(XdgWindowState::ACTIVATED | XdgWindowState::SUSPENDED) + .is_empty() + }) + // NOTE: `None` is present for the initial configure, thus we must always resize. + .unwrap_or(true); + + // NOTE: Set the configure before doing a resize, since we query it during it. + self.last_configure = Some(configure); + + if state_change_requires_resize || new_size != self.inner_size() { + self.resize(new_size); + true + } else { + false + } + } + + /// Compute the bounds for the inner size of the surface. + fn inner_size_bounds( + &self, + configure: &WindowConfigure, + ) -> (Option, Option) { + let configure_bounds = match configure.suggested_bounds { + Some((width, height)) => (NonZeroU32::new(width), NonZeroU32::new(height)), + None => (None, None), + }; + + if let Some(frame) = self.frame.as_ref() { + let (width, height) = frame.subtract_borders( + configure_bounds.0.unwrap_or(NonZeroU32::new(1).unwrap()), + configure_bounds.1.unwrap_or(NonZeroU32::new(1).unwrap()), + ); + (configure_bounds.0.and(width), configure_bounds.1.and(height)) + } else { + configure_bounds + } + } + + #[inline] + fn is_stateless(configure: &WindowConfigure) -> bool { + !(configure.is_maximized() || configure.is_fullscreen() || configure.is_tiled()) + } + + /// Start interacting drag resize. + pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { + let xdg_toplevel = self.window.xdg_toplevel(); + + // TODO(kchibisov) handle touch serials. + self.apply_on_pointer(|_, data| { + let serial = data.latest_button_serial(); + let seat = data.seat(); + xdg_toplevel.resize(seat, serial, direction.into()); + }); + + Ok(()) + } + + /// Start the window drag. + pub fn drag_window(&self) -> Result<(), ExternalError> { + let xdg_toplevel = self.window.xdg_toplevel(); + // TODO(kchibisov) handle touch serials. + self.apply_on_pointer(|_, data| { + let serial = data.latest_button_serial(); + let seat = data.seat(); + xdg_toplevel._move(seat, serial); + }); + + Ok(()) + } + + /// Tells whether the window should be closed. + #[allow(clippy::too_many_arguments)] + pub fn frame_click( + &mut self, + click: FrameClick, + pressed: bool, + seat: &WlSeat, + serial: u32, + timestamp: Duration, + window_id: WindowId, + updates: &mut Vec, + ) -> Option { + match self.frame.as_mut()?.on_click(timestamp, click, pressed)? { + FrameAction::Minimize => self.window.set_minimized(), + FrameAction::Maximize => self.window.set_maximized(), + FrameAction::UnMaximize => self.window.unset_maximized(), + FrameAction::Close => WinitState::queue_close(updates, window_id), + FrameAction::Move => self.has_pending_move = Some(serial), + FrameAction::Resize(edge) => { + let edge = match edge { + ResizeEdge::None => XdgResizeEdge::None, + ResizeEdge::Top => XdgResizeEdge::Top, + ResizeEdge::Bottom => XdgResizeEdge::Bottom, + ResizeEdge::Left => XdgResizeEdge::Left, + ResizeEdge::TopLeft => XdgResizeEdge::TopLeft, + ResizeEdge::BottomLeft => XdgResizeEdge::BottomLeft, + ResizeEdge::Right => XdgResizeEdge::Right, + ResizeEdge::TopRight => XdgResizeEdge::TopRight, + ResizeEdge::BottomRight => XdgResizeEdge::BottomRight, + _ => return None, + }; + self.window.resize(seat, serial, edge); + }, + FrameAction::ShowMenu(x, y) => self.window.show_window_menu(seat, serial, (x, y)), + _ => (), + }; + + Some(false) + } + + pub fn frame_point_left(&mut self) { + if let Some(frame) = self.frame.as_mut() { + frame.click_point_left(); + } + } + + // Move the point over decorations. + pub fn frame_point_moved( + &mut self, + seat: &WlSeat, + surface: &WlSurface, + timestamp: Duration, + x: f64, + y: f64, + ) -> Option { + // Take the serial if we had any, so it doesn't stick around. + let serial = self.has_pending_move.take(); + + if let Some(frame) = self.frame.as_mut() { + let cursor = frame.click_point_moved(timestamp, &surface.id(), x, y); + // If we have a cursor change, that means that cursor is over the decorations, + // so try to apply move. + if let Some(serial) = cursor.is_some().then_some(serial).flatten() { + self.window.move_(seat, serial); + None + } else { + cursor + } + } else { + None + } + } + + /// Get the stored resizable state. + #[inline] + pub fn resizable(&self) -> bool { + self.resizable + } + + /// Set the resizable state on the window. + /// + /// Returns `true` when the state was applied. + #[inline] + pub fn set_resizable(&mut self, resizable: bool) -> bool { + if self.resizable == resizable { + return false; + } + + self.resizable = resizable; + if resizable { + // Restore min/max sizes of the window. + self.reload_min_max_hints(); + } else { + self.set_min_inner_size(Some(self.size)); + self.set_max_inner_size(Some(self.size)); + } + + // Reload the state on the frame as well. + if let Some(frame) = self.frame.as_mut() { + frame.set_resizable(resizable); + } + + true + } + + /// Whether the window is focused by any seat. + #[inline] + pub fn has_focus(&self) -> bool { + !self.seat_focus.is_empty() + } + + /// Whether the IME is allowed. + #[inline] + pub fn ime_allowed(&self) -> bool { + self.ime_allowed + } + + /// Get the size of the window. + #[inline] + pub fn inner_size(&self) -> LogicalSize { + self.size + } + + /// Whether the window received initial configure event from the compositor. + #[inline] + pub fn is_configured(&self) -> bool { + self.last_configure.is_some() + } + + #[inline] + pub fn is_decorated(&mut self) -> bool { + let csd = self + .last_configure + .as_ref() + .map(|configure| configure.decoration_mode == DecorationMode::Client) + .unwrap_or(false); + if let Some(frame) = csd.then_some(self.frame.as_ref()).flatten() { + !frame.is_hidden() + } else { + // Server side decorations. + true + } + } + + /// Get the outer size of the window. + #[inline] + pub fn outer_size(&self) -> LogicalSize { + self.frame + .as_ref() + .map(|frame| frame.add_borders(self.size.width, self.size.height).into()) + .unwrap_or(self.size) + } + + /// Register pointer on the top-level. + pub fn pointer_entered(&mut self, added: Weak>) { + self.pointers.push(added); + self.reload_cursor_style(); + + let mode = self.cursor_grab_mode.user_grab_mode; + let _ = self.set_cursor_grab_inner(mode); + } + + /// Pointer has left the top-level. + pub fn pointer_left(&mut self, removed: Weak>) { + let mut new_pointers = Vec::new(); + for pointer in self.pointers.drain(..) { + if let Some(pointer) = pointer.upgrade() { + if pointer.pointer() != removed.upgrade().unwrap().pointer() { + new_pointers.push(Arc::downgrade(&pointer)); + } + } + } + + self.pointers = new_pointers; + } + + /// Refresh the decorations frame if it's present returning whether the client should redraw. + pub fn refresh_frame(&mut self) -> bool { + if let Some(frame) = self.frame.as_mut() { + if !frame.is_hidden() && frame.is_dirty() { + return frame.draw(); + } + } + + false + } + + /// Reload the cursor style on the given window. + pub fn reload_cursor_style(&mut self) { + if self.cursor_visible { + match &self.selected_cursor { + SelectedCursor::Named(icon) => self.set_cursor(*icon), + SelectedCursor::Custom(cursor) => self.apply_custom_cursor(cursor), + } + } else { + self.set_cursor_visible(self.cursor_visible); + } + } + + /// Reissue the transparency hint to the compositor. + pub fn reload_transparency_hint(&self) { + let surface = self.window.wl_surface(); + + if self.transparent { + surface.set_opaque_region(None); + } else if let Ok(region) = Region::new(&*self.compositor) { + region.add(0, 0, i32::MAX, i32::MAX); + surface.set_opaque_region(Some(region.wl_region())); + } else { + warn!("Failed to mark window opaque."); + } + } + + /// Try to resize the window when the user can do so. + pub fn request_inner_size(&mut self, inner_size: Size) -> PhysicalSize { + if self.last_configure.as_ref().map(Self::is_stateless).unwrap_or(true) { + self.resize(inner_size.to_logical(self.scale_factor())) + } + + logical_to_physical_rounded(self.inner_size(), self.scale_factor()) + } + + /// Resize the window to the new inner size. + fn resize(&mut self, inner_size: LogicalSize) { + self.size = inner_size; + + // Update the stateless size. + if Some(true) == self.last_configure.as_ref().map(Self::is_stateless) { + self.stateless_size = inner_size; + } + + // Update the inner frame. + let ((x, y), outer_size) = if let Some(frame) = self.frame.as_mut() { + // Resize only visible frame. + if !frame.is_hidden() { + frame.resize( + NonZeroU32::new(self.size.width).unwrap(), + NonZeroU32::new(self.size.height).unwrap(), + ); + } + + (frame.location(), frame.add_borders(self.size.width, self.size.height).into()) + } else { + ((0, 0), self.size) + }; + + // Reload the hint. + self.reload_transparency_hint(); + + // Set the window geometry. + self.window.xdg_surface().set_window_geometry( + x, + y, + outer_size.width as i32, + outer_size.height as i32, + ); + + // Update the target viewport, this is used if and only if fractional scaling is in use. + if let Some(viewport) = self.viewport.as_ref() { + // Set inner size without the borders. + viewport.set_destination(self.size.width as _, self.size.height as _); + } + } + + /// Get the scale factor of the window. + #[inline] + pub fn scale_factor(&self) -> f64 { + self.scale_factor + } + + /// Set the cursor icon. + pub fn set_cursor(&mut self, cursor_icon: CursorIcon) { + self.selected_cursor = SelectedCursor::Named(cursor_icon); + + if !self.cursor_visible { + return; + } + + self.apply_on_pointer(|pointer, _| { + if pointer.set_cursor(&self.connection, cursor_icon).is_err() { + warn!("Failed to set cursor to {:?}", cursor_icon); + } + }) + } + + /// Set the custom cursor icon. + pub(crate) fn set_custom_cursor(&mut self, cursor: RootCustomCursor) { + let cursor = match cursor { + RootCustomCursor { inner: PlatformCustomCursor::Wayland(cursor) } => cursor.0, + #[cfg(x11_platform)] + RootCustomCursor { inner: PlatformCustomCursor::X(_) } => { + tracing::error!("passed a X11 cursor to Wayland backend"); + return; + }, + }; + + let cursor = { + let mut pool = self.custom_cursor_pool.lock().unwrap(); + CustomCursor::new(&mut pool, &cursor) + }; + + if self.cursor_visible { + self.apply_custom_cursor(&cursor); + } + + self.selected_cursor = SelectedCursor::Custom(cursor); + } + + /// Set the resize increments of the window. + pub fn set_resize_increments(&mut self, increments: Option>) { + self.resize_increments = increments; + // NOTE: We don't update the window size here, because it will be done on the next resize + // or configure event. + } + + /// Get the resize increments of the window. + pub fn resize_increments(&self) -> Option> { + self.resize_increments + } + + fn apply_custom_cursor(&self, cursor: &CustomCursor) { + self.apply_on_pointer(|pointer, data| { + let surface = pointer.surface(); + + let scale = if let Some(viewport) = data.viewport() { + let scale = self.scale_factor(); + let size = PhysicalSize::new(cursor.w, cursor.h).to_logical(scale); + viewport.set_destination(size.width, size.height); + scale + } else { + let scale = surface.data::().unwrap().surface_data().scale_factor(); + surface.set_buffer_scale(scale); + scale as f64 + }; + + surface.attach(Some(cursor.buffer.wl_buffer()), 0, 0); + if surface.version() >= 4 { + surface.damage_buffer(0, 0, cursor.w, cursor.h); + } else { + let size = PhysicalSize::new(cursor.w, cursor.h).to_logical(scale); + surface.damage(0, 0, size.width, size.height); + } + surface.commit(); + + let serial = pointer + .pointer() + .data::() + .and_then(|data| data.pointer_data().latest_enter_serial()) + .unwrap(); + + let hotspot = + PhysicalPosition::new(cursor.hotspot_x, cursor.hotspot_y).to_logical(scale); + pointer.pointer().set_cursor(serial, Some(surface), hotspot.x, hotspot.y); + }); + } + + /// Set maximum inner window size. + pub fn set_min_inner_size(&mut self, size: Option>) { + // Ensure that the window has the right minimum size. + let mut size = size.unwrap_or(MIN_WINDOW_SIZE); + size.width = size.width.max(MIN_WINDOW_SIZE.width); + size.height = size.height.max(MIN_WINDOW_SIZE.height); + + // Add the borders. + let size = self + .frame + .as_ref() + .map(|frame| frame.add_borders(size.width, size.height).into()) + .unwrap_or(size); + + self.min_inner_size = size; + self.window.set_min_size(Some(size.into())); + } + + /// Set maximum inner window size. + pub fn set_max_inner_size(&mut self, size: Option>) { + let size = size.map(|size| { + self.frame + .as_ref() + .map(|frame| frame.add_borders(size.width, size.height).into()) + .unwrap_or(size) + }); + + self.max_inner_size = size; + self.window.set_max_size(size.map(Into::into)); + } + + /// Set the CSD theme. + pub fn set_theme(&mut self, theme: Option) { + self.theme = theme; + #[cfg(feature = "sctk-adwaita")] + if let Some(frame) = self.frame.as_mut() { + frame.set_config(into_sctk_adwaita_config(theme)) + } + } + + /// The current theme for CSD decorations. + #[inline] + pub fn theme(&self) -> Option { + self.theme + } + + /// Set the cursor grabbing state on the top-level. + pub fn set_cursor_grab(&mut self, mode: CursorGrabMode) -> Result<(), ExternalError> { + if self.cursor_grab_mode.user_grab_mode == mode { + return Ok(()); + } + + self.set_cursor_grab_inner(mode)?; + // Update user grab on success. + self.cursor_grab_mode.user_grab_mode = mode; + Ok(()) + } + + /// Reload the hints for minimum and maximum sizes. + pub fn reload_min_max_hints(&mut self) { + self.set_min_inner_size(Some(self.min_inner_size)); + self.set_max_inner_size(self.max_inner_size); + } + + /// Set the grabbing state on the surface. + fn set_cursor_grab_inner(&mut self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let pointer_constraints = match self.pointer_constraints.as_ref() { + Some(pointer_constraints) => pointer_constraints, + None if mode == CursorGrabMode::None => return Ok(()), + None => return Err(ExternalError::NotSupported(NotSupportedError::new())), + }; + + let mut unset_old = false; + match self.cursor_grab_mode.current_grab_mode { + CursorGrabMode::None => unset_old = true, + CursorGrabMode::Confined => self.apply_on_pointer(|_, data| { + data.unconfine_pointer(); + unset_old = true; + }), + CursorGrabMode::Locked => { + self.apply_on_pointer(|_, data| { + data.unlock_pointer(); + unset_old = true; + }); + }, + } + + // In case we haven't unset the old mode, it means that we don't have a cursor above + // the window, thus just wait for it to re-appear. + if !unset_old { + return Ok(()); + } + + let mut set_mode = false; + let surface = self.window.wl_surface(); + match mode { + CursorGrabMode::Locked => self.apply_on_pointer(|pointer, data| { + let pointer = pointer.pointer(); + data.lock_pointer(pointer_constraints, surface, pointer, &self.queue_handle); + set_mode = true; + }), + CursorGrabMode::Confined => self.apply_on_pointer(|pointer, data| { + let pointer = pointer.pointer(); + data.confine_pointer(pointer_constraints, surface, pointer, &self.queue_handle); + set_mode = true; + }), + CursorGrabMode::None => { + // Current lock/confine was already removed. + set_mode = true; + }, + } + + // Replace the current grab mode after we've ensure that it got updated. + if set_mode { + self.cursor_grab_mode.current_grab_mode = mode; + } + + Ok(()) + } + + pub fn show_window_menu(&self, position: LogicalPosition) { + // TODO(kchibisov) handle touch serials. + self.apply_on_pointer(|_, data| { + let serial = data.latest_button_serial(); + let seat = data.seat(); + self.window.show_window_menu(seat, serial, position.into()); + }); + } + + /// Set the position of the cursor. + pub fn set_cursor_position(&self, position: LogicalPosition) -> Result<(), ExternalError> { + if self.pointer_constraints.is_none() { + return Err(ExternalError::NotSupported(NotSupportedError::new())); + } + + // Position can be set only for locked cursor. + if self.cursor_grab_mode.current_grab_mode != CursorGrabMode::Locked { + return Err(ExternalError::Os(os_error!(crate::platform_impl::OsError::Misc( + "cursor position can be set only for locked cursor." + )))); + } + + self.apply_on_pointer(|_, data| { + data.set_locked_cursor_position(position.x, position.y); + }); + + Ok(()) + } + + /// Set the visibility state of the cursor. + pub fn set_cursor_visible(&mut self, cursor_visible: bool) { + self.cursor_visible = cursor_visible; + + if self.cursor_visible { + match &self.selected_cursor { + SelectedCursor::Named(icon) => self.set_cursor(*icon), + SelectedCursor::Custom(cursor) => self.apply_custom_cursor(cursor), + } + } else { + for pointer in self.pointers.iter().filter_map(|pointer| pointer.upgrade()) { + let latest_enter_serial = pointer.pointer().winit_data().latest_enter_serial(); + + pointer.pointer().set_cursor(latest_enter_serial, None, 0, 0); + } + } + } + + /// Whether show or hide client side decorations. + #[inline] + pub fn set_decorate(&mut self, decorate: bool) { + if decorate == self.decorate { + return; + } + + self.decorate = decorate; + + match self.last_configure.as_ref().map(|configure| configure.decoration_mode) { + Some(DecorationMode::Server) if !self.decorate => { + // To disable decorations we should request client and hide the frame. + self.window.request_decoration_mode(Some(DecorationMode::Client)) + }, + _ if self.decorate => self.window.request_decoration_mode(Some(DecorationMode::Server)), + _ => (), + } + + if let Some(frame) = self.frame.as_mut() { + frame.set_hidden(!decorate); + // Force the resize. + self.resize(self.size); + } + } + + /// Add seat focus for the window. + #[inline] + pub fn add_seat_focus(&mut self, seat: ObjectId) { + self.seat_focus.insert(seat); + } + + /// Remove seat focus from the window. + #[inline] + pub fn remove_seat_focus(&mut self, seat: &ObjectId) { + self.seat_focus.remove(seat); + } + + /// Returns `true` if the requested state was applied. + pub fn set_ime_allowed(&mut self, allowed: bool) -> bool { + self.ime_allowed = allowed; + + let mut applied = false; + for text_input in &self.text_inputs { + applied = true; + if allowed { + text_input.enable(); + text_input.set_content_type_by_purpose(self.ime_purpose); + } else { + text_input.disable(); + } + text_input.commit(); + } + + applied + } + + /// Set the IME position. + pub fn set_ime_cursor_area(&self, position: LogicalPosition, size: LogicalSize) { + // FIXME: This won't fly unless user will have a way to request IME window per seat, since + // the ime windows will be overlapping, but winit doesn't expose API to specify for + // which seat we're setting IME position. + let (x, y) = (position.x as i32, position.y as i32); + let (width, height) = (size.width as i32, size.height as i32); + for text_input in self.text_inputs.iter() { + text_input.set_cursor_rectangle(x, y, width, height); + text_input.commit(); + } + } + + /// Set the IME purpose. + pub fn set_ime_purpose(&mut self, purpose: ImePurpose) { + self.ime_purpose = purpose; + + for text_input in &self.text_inputs { + text_input.set_content_type_by_purpose(purpose); + text_input.commit(); + } + } + + /// Get the IME purpose. + pub fn ime_purpose(&self) -> ImePurpose { + self.ime_purpose + } + + /// Set the scale factor for the given window. + #[inline] + pub fn set_scale_factor(&mut self, scale_factor: f64) { + self.scale_factor = scale_factor; + + // NOTE: When fractional scaling is not used update the buffer scale. + if self.fractional_scale.is_none() { + let _ = self.window.set_buffer_scale(self.scale_factor as _); + } + + if let Some(frame) = self.frame.as_mut() { + frame.set_scaling_factor(scale_factor); + } + } + + /// Make window background blurred + #[inline] + pub fn set_blur(&mut self, blurred: bool) { + if blurred && self.blur.is_none() { + if let Some(blur_manager) = self.blur_manager.as_ref() { + let blur = blur_manager.blur(self.window.wl_surface(), &self.queue_handle); + blur.commit(); + self.blur = Some(blur); + } else { + info!("Blur manager unavailable, unable to change blur") + } + } else if !blurred && self.blur.is_some() { + self.blur_manager.as_ref().unwrap().unset(self.window.wl_surface()); + self.blur.take().unwrap().release(); + } + } + + /// Set the window title to a new value. + /// + /// This will automatically truncate the title to something meaningful. + pub fn set_title(&mut self, mut title: String) { + // Truncate the title to at most 1024 bytes, so that it does not blow up the protocol + // messages + if title.len() > 1024 { + let mut new_len = 1024; + while !title.is_char_boundary(new_len) { + new_len -= 1; + } + title.truncate(new_len); + } + + // Update the CSD title. + if let Some(frame) = self.frame.as_mut() { + frame.set_title(&title); + } + + self.window.set_title(&title); + self.title = title; + } + + /// Mark the window as transparent. + #[inline] + pub fn set_transparent(&mut self, transparent: bool) { + self.transparent = transparent; + self.reload_transparency_hint(); + } + + /// Register text input on the top-level. + #[inline] + pub fn text_input_entered(&mut self, text_input: &ZwpTextInputV3) { + if !self.text_inputs.iter().any(|t| t == text_input) { + self.text_inputs.push(text_input.clone()); + } + } + + /// The text input left the top-level. + #[inline] + pub fn text_input_left(&mut self, text_input: &ZwpTextInputV3) { + if let Some(position) = self.text_inputs.iter().position(|t| t == text_input) { + self.text_inputs.remove(position); + } + } + + /// Get the cached title. + #[inline] + pub fn title(&self) -> &str { + &self.title + } +} + +impl Drop for WindowState { + fn drop(&mut self) { + if let Some(blur) = self.blur.take() { + blur.release(); + } + + if let Some(fs) = self.fractional_scale.take() { + fs.destroy(); + } + + if let Some(viewport) = self.viewport.take() { + viewport.destroy(); + } + + // NOTE: the wl_surface used by the window is being cleaned up when + // dropping SCTK `Window`. + } +} + +/// The state of the cursor grabs. +#[derive(Clone, Copy)] +struct GrabState { + /// The grab mode requested by the user. + user_grab_mode: CursorGrabMode, + + /// The current grab mode. + current_grab_mode: CursorGrabMode, +} + +impl GrabState { + fn new() -> Self { + Self { user_grab_mode: CursorGrabMode::None, current_grab_mode: CursorGrabMode::None } + } +} + +/// The state of the frame callback. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum FrameCallbackState { + /// No frame callback was requested. + #[default] + None, + /// The frame callback was requested, but not yet arrived, the redraw events are throttled. + Requested, + /// The callback was marked as done, and user could receive redraw requested + Received, +} + +impl From for XdgResizeEdge { + fn from(value: ResizeDirection) -> Self { + match value { + ResizeDirection::North => XdgResizeEdge::Top, + ResizeDirection::West => XdgResizeEdge::Left, + ResizeDirection::NorthWest => XdgResizeEdge::TopLeft, + ResizeDirection::NorthEast => XdgResizeEdge::TopRight, + ResizeDirection::East => XdgResizeEdge::Right, + ResizeDirection::SouthWest => XdgResizeEdge::BottomLeft, + ResizeDirection::SouthEast => XdgResizeEdge::BottomRight, + ResizeDirection::South => XdgResizeEdge::Bottom, + } + } +} + +// NOTE: Rust doesn't allow `From>`. +#[cfg(feature = "sctk-adwaita")] +fn into_sctk_adwaita_config(theme: Option) -> sctk_adwaita::FrameConfig { + match theme { + Some(Theme::Light) => sctk_adwaita::FrameConfig::light(), + Some(Theme::Dark) => sctk_adwaita::FrameConfig::dark(), + None => sctk_adwaita::FrameConfig::auto(), + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/activation.rs b/vendor/winit/src/platform_impl/linux/x11/activation.rs new file mode 100644 index 00000000..8f4aa796 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/activation.rs @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! X11 activation handling. +//! +//! X11 has a "startup notification" specification similar to Wayland's, see this URL: +//! + +use super::atoms::*; +use super::{VoidCookie, X11Error, XConnection}; + +use std::ffi::CString; +use std::fmt::Write; + +use x11rb::protocol::xproto::{self, ConnectionExt as _}; + +impl XConnection { + /// "Request" a new activation token from the server. + pub(crate) fn request_activation_token(&self, window_title: &str) -> Result { + // The specification recommends the format "hostname+pid+"_TIME"+current time" + let uname = rustix::system::uname(); + let pid = rustix::process::getpid(); + let time = self.timestamp(); + + let activation_token = format!( + "{}{}_TIME{}", + uname.nodename().to_str().unwrap_or("winit"), + pid.as_raw_nonzero(), + time + ); + + // Set up the new startup notification. + let notification = { + let mut buffer = Vec::new(); + buffer.extend_from_slice(b"new: ID="); + quote_string(&activation_token, &mut buffer); + buffer.extend_from_slice(b" NAME="); + quote_string(window_title, &mut buffer); + buffer.extend_from_slice(b" SCREEN="); + push_display(&mut buffer, &self.default_screen_index()); + + CString::new(buffer) + .map_err(|err| X11Error::InvalidActivationToken(err.into_vec()))? + .into_bytes_with_nul() + }; + self.send_message(¬ification)?; + + Ok(activation_token) + } + + /// Finish launching a window with the given startup ID. + pub(crate) fn remove_activation_token( + &self, + window: xproto::Window, + startup_id: &str, + ) -> Result<(), X11Error> { + let atoms = self.atoms(); + + // Set the _NET_STARTUP_ID property on the window. + self.xcb_connection() + .change_property( + xproto::PropMode::REPLACE, + window, + atoms[_NET_STARTUP_ID], + xproto::AtomEnum::STRING, + 8, + startup_id.len().try_into().unwrap(), + startup_id.as_bytes(), + )? + .check()?; + + // Send the message indicating that the startup is over. + let message = { + const MESSAGE_ROOT: &str = "remove: ID="; + + let mut buffer = Vec::with_capacity( + MESSAGE_ROOT + .len() + .checked_add(startup_id.len()) + .and_then(|x| x.checked_add(1)) + .unwrap(), + ); + buffer.extend_from_slice(MESSAGE_ROOT.as_bytes()); + quote_string(startup_id, &mut buffer); + CString::new(buffer) + .map_err(|err| X11Error::InvalidActivationToken(err.into_vec()))? + .into_bytes_with_nul() + }; + + self.send_message(&message) + } + + /// Send a startup notification message to the window manager. + fn send_message(&self, message: &[u8]) -> Result<(), X11Error> { + let atoms = self.atoms(); + + // Create a new window to send the message over. + let screen = self.default_root(); + let window = xproto::WindowWrapper::create_window( + self.xcb_connection(), + screen.root_depth, + screen.root, + -100, + -100, + 1, + 1, + 0, + xproto::WindowClass::INPUT_OUTPUT, + screen.root_visual, + &xproto::CreateWindowAux::new().override_redirect(1).event_mask( + xproto::EventMask::STRUCTURE_NOTIFY | xproto::EventMask::PROPERTY_CHANGE, + ), + )?; + + // Serialize the messages in 20-byte chunks. + let mut message_type = atoms[_NET_STARTUP_INFO_BEGIN]; + message + .chunks(20) + .map(|chunk| { + let mut buffer = [0u8; 20]; + buffer[..chunk.len()].copy_from_slice(chunk); + let event = + xproto::ClientMessageEvent::new(8, window.window(), message_type, buffer); + + // Set the message type to the continuation atom for the next chunk. + message_type = atoms[_NET_STARTUP_INFO]; + + event + }) + .try_for_each(|event| { + // Send each event in order. + self.xcb_connection() + .send_event(false, screen.root, xproto::EventMask::PROPERTY_CHANGE, event) + .map(VoidCookie::ignore_error) + })?; + + Ok(()) + } +} + +/// Quote a literal string as per the startup notification specification. +fn quote_string(s: &str, target: &mut Vec) { + let total_len = s.len().checked_add(3).expect("quote string overflow"); + target.reserve(total_len); + + // Add the opening quote. + target.push(b'"'); + + // Iterate over the string split by literal quotes. + s.as_bytes().split(|&b| b == b'"').for_each(|part| { + // Add the part. + target.extend_from_slice(part); + + // Escape the quote. + target.push(b'\\'); + target.push(b'"'); + }); + + // Un-escape the last quote. + target.remove(target.len() - 2); +} + +/// Push a `Display` implementation to the buffer. +fn push_display(buffer: &mut Vec, display: &impl std::fmt::Display) { + struct Writer<'a> { + buffer: &'a mut Vec, + } + + impl std::fmt::Write for Writer<'_> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.buffer.extend_from_slice(s.as_bytes()); + Ok(()) + } + } + + write!(Writer { buffer }, "{display}").unwrap(); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn properly_escapes_x11_messages() { + let assert_eq = |input: &str, output: &[u8]| { + let mut buf = vec![]; + quote_string(input, &mut buf); + assert_eq!(buf, output); + }; + + assert_eq("", b"\"\""); + assert_eq("foo", b"\"foo\""); + assert_eq("foo\"bar", b"\"foo\\\"bar\""); + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/atoms.rs b/vendor/winit/src/platform_impl/linux/x11/atoms.rs new file mode 100644 index 00000000..c6d96c8f --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/atoms.rs @@ -0,0 +1,117 @@ +//! Collects every atom used by the platform implementation. + +use core::ops::Index; + +macro_rules! atom_manager { + ($($name:ident $(:$lit:literal)?),*) => { + x11rb::atom_manager! { + /// The atoms used by `winit` + pub Atoms: AtomsCookie { + $($name $(:$lit)?,)* + } + } + + /// Indices into the `Atoms` struct. + #[derive(Copy, Clone, Debug)] + #[allow(non_camel_case_types)] + pub enum AtomName { + $($name,)* + } + + impl AtomName { + pub(crate) fn atom_from( + self, + atoms: &Atoms + ) -> &x11rb::protocol::xproto::Atom { + match self { + $(AtomName::$name => &atoms.$name,)* + } + } + } + }; +} + +atom_manager! { + // General Use Atoms + CARD32, + UTF8_STRING, + WM_CHANGE_STATE, + WM_CLIENT_MACHINE, + WM_DELETE_WINDOW, + WM_PROTOCOLS, + WM_STATE, + XIM_SERVERS, + + // Assorted ICCCM Atoms + _NET_WM_ICON, + _NET_WM_MOVERESIZE, + _NET_WM_NAME, + _NET_WM_PID, + _NET_WM_PING, + _NET_WM_STATE, + _NET_WM_STATE_ABOVE, + _NET_WM_STATE_BELOW, + _NET_WM_STATE_FULLSCREEN, + _NET_WM_STATE_HIDDEN, + _NET_WM_STATE_MAXIMIZED_HORZ, + _NET_WM_STATE_MAXIMIZED_VERT, + _NET_WM_WINDOW_TYPE, + + // Activation atoms. + _NET_STARTUP_INFO_BEGIN, + _NET_STARTUP_INFO, + _NET_STARTUP_ID, + + // WM window types. + _NET_WM_WINDOW_TYPE_DESKTOP, + _NET_WM_WINDOW_TYPE_DOCK, + _NET_WM_WINDOW_TYPE_TOOLBAR, + _NET_WM_WINDOW_TYPE_MENU, + _NET_WM_WINDOW_TYPE_UTILITY, + _NET_WM_WINDOW_TYPE_SPLASH, + _NET_WM_WINDOW_TYPE_DIALOG, + _NET_WM_WINDOW_TYPE_DROPDOWN_MENU, + _NET_WM_WINDOW_TYPE_POPUP_MENU, + _NET_WM_WINDOW_TYPE_TOOLTIP, + _NET_WM_WINDOW_TYPE_NOTIFICATION, + _NET_WM_WINDOW_TYPE_COMBO, + _NET_WM_WINDOW_TYPE_DND, + _NET_WM_WINDOW_TYPE_NORMAL, + + // Drag-N-Drop Atoms + XdndAware, + XdndEnter, + XdndLeave, + XdndDrop, + XdndPosition, + XdndStatus, + XdndActionPrivate, + XdndSelection, + XdndFinished, + XdndTypeList, + TextUriList: b"text/uri-list", + None: b"None", + + // Miscellaneous Atoms + _GTK_THEME_VARIANT, + _MOTIF_WM_HINTS, + _NET_ACTIVE_WINDOW, + _NET_CLIENT_LIST, + _NET_FRAME_EXTENTS, + _NET_SUPPORTED, + _NET_SUPPORTING_WM_CHECK, + _XEMBED, + _XSETTINGS_SETTINGS +} + +impl Index for Atoms { + type Output = x11rb::protocol::xproto::Atom; + + fn index(&self, index: AtomName) -> &Self::Output { + index.atom_from(self) + } +} + +pub(crate) use AtomName::*; +// Make sure `None` is still defined. +pub(crate) use core::option::Option::None; diff --git a/vendor/winit/src/platform_impl/linux/x11/dnd.rs b/vendor/winit/src/platform_impl/linux/x11/dnd.rs new file mode 100644 index 00000000..691e40af --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/dnd.rs @@ -0,0 +1,174 @@ +use std::io; +use std::os::raw::*; +use std::path::{Path, PathBuf}; +use std::str::Utf8Error; +use std::sync::Arc; + +use percent_encoding::percent_decode; +use x11rb::protocol::xproto::{self, ConnectionExt}; + +use super::atoms::AtomName::None as DndNone; +use super::atoms::*; +use super::{util, CookieResultExt, X11Error, XConnection}; + +#[derive(Debug, Clone, Copy)] +pub enum DndState { + Accepted, + Rejected, +} + +#[derive(Debug)] +pub enum DndDataParseError { + EmptyData, + InvalidUtf8(#[allow(dead_code)] Utf8Error), + HostnameSpecified(#[allow(dead_code)] String), + UnexpectedProtocol(#[allow(dead_code)] String), + UnresolvablePath(#[allow(dead_code)] io::Error), +} + +impl From for DndDataParseError { + fn from(e: Utf8Error) -> Self { + DndDataParseError::InvalidUtf8(e) + } +} + +impl From for DndDataParseError { + fn from(e: io::Error) -> Self { + DndDataParseError::UnresolvablePath(e) + } +} + +pub struct Dnd { + xconn: Arc, + // Populated by XdndEnter event handler + pub version: Option, + pub type_list: Option>, + // Populated by XdndPosition event handler + pub source_window: Option, + // Populated by SelectionNotify event handler (triggered by XdndPosition event handler) + pub result: Option, DndDataParseError>>, +} + +impl Dnd { + pub fn new(xconn: Arc) -> Result { + Ok(Dnd { xconn, version: None, type_list: None, source_window: None, result: None }) + } + + pub fn reset(&mut self) { + self.version = None; + self.type_list = None; + self.source_window = None; + self.result = None; + } + + pub unsafe fn send_status( + &self, + this_window: xproto::Window, + target_window: xproto::Window, + state: DndState, + ) -> Result<(), X11Error> { + let atoms = self.xconn.atoms(); + let (accepted, action) = match state { + DndState::Accepted => (1, atoms[XdndActionPrivate]), + DndState::Rejected => (0, atoms[DndNone]), + }; + self.xconn + .send_client_msg(target_window, target_window, atoms[XdndStatus] as _, None, [ + this_window, + accepted, + 0, + 0, + action as _, + ])? + .ignore_error(); + + Ok(()) + } + + pub unsafe fn send_finished( + &self, + this_window: xproto::Window, + target_window: xproto::Window, + state: DndState, + ) -> Result<(), X11Error> { + let atoms = self.xconn.atoms(); + let (accepted, action) = match state { + DndState::Accepted => (1, atoms[XdndActionPrivate]), + DndState::Rejected => (0, atoms[DndNone]), + }; + self.xconn + .send_client_msg(target_window, target_window, atoms[XdndFinished] as _, None, [ + this_window, + accepted, + action as _, + 0, + 0, + ])? + .ignore_error(); + + Ok(()) + } + + pub unsafe fn get_type_list( + &self, + source_window: xproto::Window, + ) -> Result, util::GetPropertyError> { + let atoms = self.xconn.atoms(); + self.xconn.get_property( + source_window, + atoms[XdndTypeList], + xproto::Atom::from(xproto::AtomEnum::ATOM), + ) + } + + pub unsafe fn convert_selection(&self, window: xproto::Window, time: xproto::Timestamp) { + let atoms = self.xconn.atoms(); + self.xconn + .xcb_connection() + .convert_selection( + window, + atoms[XdndSelection], + atoms[TextUriList], + atoms[XdndSelection], + time, + ) + .expect_then_ignore_error("Failed to send XdndSelection event") + } + + pub unsafe fn read_data( + &self, + window: xproto::Window, + ) -> Result, util::GetPropertyError> { + let atoms = self.xconn.atoms(); + self.xconn.get_property(window, atoms[XdndSelection], atoms[TextUriList]) + } + + pub fn parse_data(&self, data: &mut [c_uchar]) -> Result, DndDataParseError> { + if !data.is_empty() { + let mut path_list = Vec::new(); + let decoded = percent_decode(data).decode_utf8()?.into_owned(); + for uri in decoded.split("\r\n").filter(|u| !u.is_empty()) { + // The format is specified as protocol://host/path + // However, it's typically simply protocol:///path + let path_str = if uri.starts_with("file://") { + let path_str = uri.replace("file://", ""); + if !path_str.starts_with('/') { + // A hostname is specified + // Supporting this case is beyond the scope of my mental health + return Err(DndDataParseError::HostnameSpecified(path_str)); + } + path_str + } else { + // Only the file protocol is supported + return Err(DndDataParseError::UnexpectedProtocol(uri.to_owned())); + }; + + let path = Path::new(&path_str).canonicalize()?; + path_list.push(path); + } + Ok(path_list) + } else { + Err(DndDataParseError::EmptyData) + } + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/event_processor.rs b/vendor/winit/src/platform_impl/linux/x11/event_processor.rs new file mode 100644 index 00000000..79f5c114 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/event_processor.rs @@ -0,0 +1,1883 @@ +use std::cell::{Cell, RefCell}; +use std::collections::{HashMap, VecDeque}; +use std::os::raw::{c_char, c_int, c_long, c_ulong}; +use std::slice; +use std::sync::{Arc, Mutex}; + +use x11_dl::xinput2::{ + self, XIDeviceEvent, XIEnterEvent, XIFocusInEvent, XIFocusOutEvent, XIHierarchyEvent, + XILeaveEvent, XIModifierState, XIRawEvent, +}; +use x11_dl::xlib::{ + self, Display as XDisplay, Window as XWindow, XAnyEvent, XClientMessageEvent, XConfigureEvent, + XDestroyWindowEvent, XEvent, XExposeEvent, XKeyEvent, XMapEvent, XPropertyEvent, + XReparentEvent, XSelectionEvent, XVisibilityEvent, XkbAnyEvent, XkbStateRec, +}; +use x11rb::protocol::xinput; +use x11rb::protocol::xkb::ID as XkbId; +use x11rb::protocol::xproto::{self, ConnectionExt as _, ModMask}; +use x11rb::x11_utils::{ExtensionInformation, Serialize}; +use xkbcommon_dl::xkb_mod_mask_t; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::event::{ + DeviceEvent, ElementState, Event, Ime, InnerSizeWriter, MouseButton, MouseScrollDelta, + RawKeyEvent, Touch, TouchPhase, WindowEvent, +}; +use crate::event_loop::ActiveEventLoop as RootAEL; +use crate::keyboard::ModifiersState; +use crate::platform_impl::common::xkb::{self, XkbState}; +use crate::platform_impl::platform::common::xkb::Context; +use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventReceiver, ImeRequest}; +use crate::platform_impl::platform::x11::ActiveEventLoop; +use crate::platform_impl::platform::ActiveEventLoop as PlatformActiveEventLoop; +use crate::platform_impl::x11::atoms::*; +use crate::platform_impl::x11::util::cookie::GenericEventCookie; +use crate::platform_impl::x11::{ + mkdid, mkwid, util, CookieResultExt, Device, DeviceId, DeviceInfo, Dnd, DndState, ImeReceiver, + ScrollOrientation, UnownedWindow, WindowId, +}; + +/// The maximum amount of X modifiers to replay. +pub const MAX_MOD_REPLAY_LEN: usize = 32; + +/// The X11 documentation states: "Keycodes lie in the inclusive range `[8, 255]`". +const KEYCODE_OFFSET: u8 = 8; + +pub struct EventProcessor { + pub dnd: Dnd, + pub ime_receiver: ImeReceiver, + pub ime_event_receiver: ImeEventReceiver, + pub randr_event_offset: u8, + pub devices: RefCell>, + pub xi2ext: ExtensionInformation, + pub xkbext: ExtensionInformation, + pub target: RootAEL, + pub xkb_context: Context, + // Number of touch events currently in progress + pub num_touch: u32, + // This is the last pressed key that is repeatable (if it hasn't been + // released). + // + // Used to detect key repeats. + pub held_key_press: Option, + pub first_touch: Option, + // Currently focused window belonging to this process + pub active_window: Option, + /// Latest modifiers we've sent for the user to trigger change in event. + pub modifiers: Cell, + // Track modifiers based on keycodes. NOTE: that serials generally don't work for tracking + // since they are not unique and could be duplicated in case of sequence of key events is + // delivered at near the same time. + pub xfiltered_modifiers: VecDeque, + pub xmodmap: util::ModifierKeymap, + pub is_composing: bool, +} + +impl EventProcessor { + pub fn process_event(&mut self, xev: &mut XEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + self.process_xevent(xev, &mut callback); + + let window_target = Self::window_target_mut(&mut self.target); + + // Handle IME requests. + while let Ok(request) = self.ime_receiver.try_recv() { + let ime = match window_target.ime.as_mut() { + Some(ime) => ime, + None => continue, + }; + let ime = ime.get_mut(); + match request { + ImeRequest::Position(window_id, x, y) => { + ime.send_xim_spot(window_id, x, y); + }, + ImeRequest::Allow(window_id, allowed) => { + ime.set_ime_allowed(window_id, allowed); + }, + } + } + + // Drain IME events. + while let Ok((window, event)) = self.ime_event_receiver.try_recv() { + let window_id = mkwid(window as xproto::Window); + let event = match event { + ImeEvent::Enabled => WindowEvent::Ime(Ime::Enabled), + ImeEvent::Start => { + self.is_composing = true; + WindowEvent::Ime(Ime::Preedit("".to_owned(), None)) + }, + ImeEvent::Update(text, position) if self.is_composing => { + WindowEvent::Ime(Ime::Preedit(text, Some((position, position)))) + }, + ImeEvent::End => { + self.is_composing = false; + // Issue empty preedit on `Done`. + WindowEvent::Ime(Ime::Preedit(String::new(), None)) + }, + ImeEvent::Disabled => { + self.is_composing = false; + WindowEvent::Ime(Ime::Disabled) + }, + _ => continue, + }; + + callback(&self.target, Event::WindowEvent { window_id, event }); + } + } + + /// XFilterEvent tells us when an event has been discarded by the input method. + /// Specifically, this involves all of the KeyPress events in compose/pre-edit sequences, + /// along with an extra copy of the KeyRelease events. This also prevents backspace and + /// arrow keys from being detected twice. + #[must_use] + fn filter_event(&mut self, xev: &mut XEvent) -> bool { + let wt = Self::window_target(&self.target); + unsafe { + (wt.xconn.xlib.XFilterEvent)(xev, { + let xev: &XAnyEvent = xev.as_ref(); + xev.window + }) == xlib::True + } + } + + fn process_xevent(&mut self, xev: &mut XEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let event_type = xev.get_type(); + + // If we have IME disabled, don't try to `filter_event`, since only IME can consume them + // and forward back. This is not desired for e.g. games since some IMEs may delay the input + // and game can toggle IME back when e.g. typing into some field where latency won't really + // matter. + let filtered = if event_type == xlib::KeyPress || event_type == xlib::KeyRelease { + let wt = Self::window_target(&self.target); + let ime = wt.ime.as_ref(); + let window = self.active_window.map(|window| window as XWindow); + let forward_to_ime = ime + .and_then(|ime| window.map(|window| ime.borrow().is_ime_allowed(window))) + .unwrap_or(false); + + let filtered = forward_to_ime && self.filter_event(xev); + if filtered { + let xev: &XKeyEvent = xev.as_ref(); + if self.xmodmap.is_modifier(xev.keycode as u8) { + // Don't grow the buffer past the `MAX_MOD_REPLAY_LEN`. This could happen + // when the modifiers are consumed entirely. + if self.xfiltered_modifiers.len() == MAX_MOD_REPLAY_LEN { + self.xfiltered_modifiers.pop_back(); + } + self.xfiltered_modifiers.push_front(xev.keycode as u8); + } + } + + filtered + } else { + self.filter_event(xev) + }; + + // Don't process event if it was filtered. + if filtered { + return; + } + + match event_type { + xlib::ClientMessage => self.client_message(xev.as_ref(), &mut callback), + xlib::SelectionNotify => self.selection_notify(xev.as_ref(), &mut callback), + xlib::ConfigureNotify => self.configure_notify(xev.as_ref(), &mut callback), + xlib::ReparentNotify => self.reparent_notify(xev.as_ref()), + xlib::MapNotify => self.map_notify(xev.as_ref(), &mut callback), + xlib::DestroyNotify => self.destroy_notify(xev.as_ref(), &mut callback), + xlib::PropertyNotify => self.property_notify(xev.as_ref(), &mut callback), + xlib::VisibilityNotify => self.visibility_notify(xev.as_ref(), &mut callback), + xlib::Expose => self.expose(xev.as_ref(), &mut callback), + // Note that in compose/pre-edit sequences, we'll always receive KeyRelease events. + ty @ xlib::KeyPress | ty @ xlib::KeyRelease => { + let state = if ty == xlib::KeyPress { + ElementState::Pressed + } else { + ElementState::Released + }; + + self.xinput_key_input(xev.as_mut(), state, &mut callback); + }, + xlib::GenericEvent => { + let wt = Self::window_target(&self.target); + let xev: GenericEventCookie = + match GenericEventCookie::from_event(wt.xconn.clone(), *xev) { + Some(xev) if xev.extension() == self.xi2ext.major_opcode => xev, + _ => return, + }; + + let evtype = xev.evtype(); + + match evtype { + ty @ xinput2::XI_ButtonPress | ty @ xinput2::XI_ButtonRelease => { + let state = if ty == xinput2::XI_ButtonPress { + ElementState::Pressed + } else { + ElementState::Released + }; + + let xev: &XIDeviceEvent = unsafe { xev.as_event() }; + self.update_mods_from_xinput2_event( + &xev.mods, + &xev.group, + false, + &mut callback, + ); + self.xinput2_button_input(xev, state, &mut callback); + }, + xinput2::XI_Motion => { + let xev: &XIDeviceEvent = unsafe { xev.as_event() }; + self.update_mods_from_xinput2_event( + &xev.mods, + &xev.group, + false, + &mut callback, + ); + self.xinput2_mouse_motion(xev, &mut callback); + }, + xinput2::XI_Enter => { + let xev: &XIEnterEvent = unsafe { xev.as_event() }; + self.xinput2_mouse_enter(xev, &mut callback); + }, + xinput2::XI_Leave => { + let xev: &XILeaveEvent = unsafe { xev.as_event() }; + self.update_mods_from_xinput2_event( + &xev.mods, + &xev.group, + false, + &mut callback, + ); + self.xinput2_mouse_left(xev, &mut callback); + }, + xinput2::XI_FocusIn => { + let xev: &XIFocusInEvent = unsafe { xev.as_event() }; + self.xinput2_focused(xev, &mut callback); + }, + xinput2::XI_FocusOut => { + let xev: &XIFocusOutEvent = unsafe { xev.as_event() }; + self.xinput2_unfocused(xev, &mut callback); + }, + xinput2::XI_TouchBegin | xinput2::XI_TouchUpdate | xinput2::XI_TouchEnd => { + let phase = match evtype { + xinput2::XI_TouchBegin => TouchPhase::Started, + xinput2::XI_TouchUpdate => TouchPhase::Moved, + xinput2::XI_TouchEnd => TouchPhase::Ended, + _ => unreachable!(), + }; + + let xev: &XIDeviceEvent = unsafe { xev.as_event() }; + self.xinput2_touch(xev, phase, &mut callback); + }, + xinput2::XI_RawButtonPress | xinput2::XI_RawButtonRelease => { + let state = match evtype { + xinput2::XI_RawButtonPress => ElementState::Pressed, + xinput2::XI_RawButtonRelease => ElementState::Released, + _ => unreachable!(), + }; + + let xev: &XIRawEvent = unsafe { xev.as_event() }; + self.xinput2_raw_button_input(xev, state, &mut callback); + }, + xinput2::XI_RawMotion => { + let xev: &XIRawEvent = unsafe { xev.as_event() }; + self.xinput2_raw_mouse_motion(xev, &mut callback); + }, + xinput2::XI_RawKeyPress | xinput2::XI_RawKeyRelease => { + let state = match evtype { + xinput2::XI_RawKeyPress => ElementState::Pressed, + xinput2::XI_RawKeyRelease => ElementState::Released, + _ => unreachable!(), + }; + + let xev: &xinput2::XIRawEvent = unsafe { xev.as_event() }; + self.xinput2_raw_key_input(xev, state, &mut callback); + }, + + xinput2::XI_HierarchyChanged => { + let xev: &XIHierarchyEvent = unsafe { xev.as_event() }; + self.xinput2_hierarchy_changed(xev, &mut callback); + }, + _ => {}, + } + }, + _ => { + if event_type == self.xkbext.first_event as _ { + let xev: &XkbAnyEvent = unsafe { &*(xev as *const _ as *const XkbAnyEvent) }; + self.xkb_event(xev, &mut callback); + } + if event_type == self.randr_event_offset as c_int { + self.process_dpi_change(&mut callback); + } + }, + } + } + + pub fn poll(&self) -> bool { + let window_target = Self::window_target(&self.target); + let result = unsafe { (window_target.xconn.xlib.XPending)(window_target.xconn.display) }; + + result != 0 + } + + pub unsafe fn poll_one_event(&mut self, event_ptr: *mut XEvent) -> bool { + let window_target = Self::window_target(&self.target); + // This function is used to poll and remove a single event + // from the Xlib event queue in a non-blocking, atomic way. + // XCheckIfEvent is non-blocking and removes events from queue. + // XNextEvent can't be used because it blocks while holding the + // global Xlib mutex. + // XPeekEvent does not remove events from the queue. + unsafe extern "C" fn predicate( + _display: *mut XDisplay, + _event: *mut XEvent, + _arg: *mut c_char, + ) -> c_int { + // This predicate always returns "true" (1) to accept all events + 1 + } + + let result = unsafe { + (window_target.xconn.xlib.XCheckIfEvent)( + window_target.xconn.display, + event_ptr, + Some(predicate), + std::ptr::null_mut(), + ) + }; + + result != 0 + } + + pub fn init_device(&self, device: xinput::DeviceId) { + let window_target = Self::window_target(&self.target); + let mut devices = self.devices.borrow_mut(); + if let Some(info) = DeviceInfo::get(&window_target.xconn, device as _) { + for info in info.iter() { + devices.insert(DeviceId(info.deviceid as _), Device::new(info)); + } + } + } + + pub fn with_window(&self, window_id: xproto::Window, callback: F) -> Option + where + F: Fn(&Arc) -> Ret, + { + let mut deleted = false; + let window_id = WindowId(window_id as _); + let window_target = Self::window_target(&self.target); + let result = window_target + .windows + .borrow() + .get(&window_id) + .and_then(|window| { + let arc = window.upgrade(); + deleted = arc.is_none(); + arc + }) + .map(|window| callback(&window)); + + if deleted { + // Garbage collection + window_target.windows.borrow_mut().remove(&window_id); + } + + result + } + + // NOTE: we avoid `self` to not borrow the entire `self` as not mut. + /// Get the platform window target. + pub fn window_target(window_target: &RootAEL) -> &ActiveEventLoop { + match &window_target.p { + PlatformActiveEventLoop::X(target) => target, + #[cfg(wayland_platform)] + _ => unreachable!(), + } + } + + /// Get the platform window target. + pub fn window_target_mut(window_target: &mut RootAEL) -> &mut ActiveEventLoop { + match &mut window_target.p { + PlatformActiveEventLoop::X(target) => target, + #[cfg(wayland_platform)] + _ => unreachable!(), + } + } + + fn client_message(&mut self, xev: &XClientMessageEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let atoms = wt.xconn.atoms(); + + let window = xev.window as xproto::Window; + let window_id = mkwid(window); + + if xev.data.get_long(0) as xproto::Atom == wt.wm_delete_window { + let event = Event::WindowEvent { window_id, event: WindowEvent::CloseRequested }; + callback(&self.target, event); + return; + } + + if xev.data.get_long(0) as xproto::Atom == wt.net_wm_ping { + let client_msg = xproto::ClientMessageEvent { + response_type: xproto::CLIENT_MESSAGE_EVENT, + format: xev.format as _, + sequence: xev.serial as _, + window: wt.root, + type_: xev.message_type as _, + data: xproto::ClientMessageData::from({ + let [a, b, c, d, e]: [c_long; 5] = xev.data.as_longs().try_into().unwrap(); + [a as u32, b as u32, c as u32, d as u32, e as u32] + }), + }; + + wt.xconn + .xcb_connection() + .send_event( + false, + wt.root, + xproto::EventMask::SUBSTRUCTURE_NOTIFY + | xproto::EventMask::SUBSTRUCTURE_REDIRECT, + client_msg.serialize(), + ) + .expect_then_ignore_error("Failed to send `ClientMessage` event."); + return; + } + + if xev.message_type == atoms[XdndEnter] as c_ulong { + let source_window = xev.data.get_long(0) as xproto::Window; + let flags = xev.data.get_long(1); + let version = flags >> 24; + self.dnd.version = Some(version); + let has_more_types = flags - (flags & (c_long::MAX - 1)) == 1; + if !has_more_types { + let type_list = vec![ + xev.data.get_long(2) as xproto::Atom, + xev.data.get_long(3) as xproto::Atom, + xev.data.get_long(4) as xproto::Atom, + ]; + self.dnd.type_list = Some(type_list); + } else if let Ok(more_types) = unsafe { self.dnd.get_type_list(source_window) } { + self.dnd.type_list = Some(more_types); + } + return; + } + + if xev.message_type == atoms[XdndPosition] as c_ulong { + // This event occurs every time the mouse moves while a file's being dragged + // over our window. We emit HoveredFile in response; while the macOS backend + // does that upon a drag entering, XDND doesn't have access to the actual drop + // data until this event. For parity with other platforms, we only emit + // `HoveredFile` the first time, though if winit's API is later extended to + // supply position updates with `HoveredFile` or another event, implementing + // that here would be trivial. + + let source_window = xev.data.get_long(0) as xproto::Window; + + // Equivalent to `(x << shift) | y` + // where `shift = mem::size_of::() * 8` + // Note that coordinates are in "desktop space", not "window space" + // (in X11 parlance, they're root window coordinates) + // let packed_coordinates = xev.data.get_long(2); + // let shift = mem::size_of::() * 8; + // let x = packed_coordinates >> shift; + // let y = packed_coordinates & !(x << shift); + + // By our own state flow, `version` should never be `None` at this point. + let version = self.dnd.version.unwrap_or(5); + + // Action is specified in versions 2 and up, though we don't need it anyway. + // let action = xev.data.get_long(4); + + let accepted = if let Some(ref type_list) = self.dnd.type_list { + type_list.contains(&atoms[TextUriList]) + } else { + false + }; + + if !accepted { + unsafe { + self.dnd + .send_status(window, source_window, DndState::Rejected) + .expect("Failed to send `XdndStatus` message."); + } + self.dnd.reset(); + return; + } + + self.dnd.source_window = Some(source_window); + if self.dnd.result.is_none() { + let time = if version >= 1 { + xev.data.get_long(3) as xproto::Timestamp + } else { + // In version 0, time isn't specified + x11rb::CURRENT_TIME + }; + + // Log this timestamp. + wt.xconn.set_timestamp(time); + + // This results in the `SelectionNotify` event below + unsafe { + self.dnd.convert_selection(window, time); + } + } + + unsafe { + self.dnd + .send_status(window, source_window, DndState::Accepted) + .expect("Failed to send `XdndStatus` message."); + } + return; + } + + if xev.message_type == atoms[XdndDrop] as c_ulong { + let (source_window, state) = if let Some(source_window) = self.dnd.source_window { + if let Some(Ok(ref path_list)) = self.dnd.result { + for path in path_list { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::DroppedFile(path.clone()), + }; + callback(&self.target, event); + } + } + (source_window, DndState::Accepted) + } else { + // `source_window` won't be part of our DND state if we already rejected the drop in + // our `XdndPosition` handler. + let source_window = xev.data.get_long(0) as xproto::Window; + (source_window, DndState::Rejected) + }; + + unsafe { + self.dnd + .send_finished(window, source_window, state) + .expect("Failed to send `XdndFinished` message."); + } + + self.dnd.reset(); + return; + } + + if xev.message_type == atoms[XdndLeave] as c_ulong { + self.dnd.reset(); + let event = Event::WindowEvent { window_id, event: WindowEvent::HoveredFileCancelled }; + callback(&self.target, event); + } + } + + fn selection_notify(&mut self, xev: &XSelectionEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let atoms = wt.xconn.atoms(); + + let window = xev.requestor as xproto::Window; + let window_id = mkwid(window); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + if xev.property != atoms[XdndSelection] as c_ulong { + return; + } + + // This is where we receive data from drag and drop + self.dnd.result = None; + if let Ok(mut data) = unsafe { self.dnd.read_data(window) } { + let parse_result = self.dnd.parse_data(&mut data); + if let Ok(ref path_list) = parse_result { + for path in path_list { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::HoveredFile(path.clone()), + }; + callback(&self.target, event); + } + } + self.dnd.result = Some(parse_result); + } + } + + fn configure_notify(&self, xev: &XConfigureEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + let xwindow = xev.window as xproto::Window; + let window_id = mkwid(xwindow); + + let window = match self.with_window(xwindow, Arc::clone) { + Some(window) => window, + None => return, + }; + + // So apparently... + // `XSendEvent` (synthetic `ConfigureNotify`) -> position relative to root + // `XConfigureNotify` (real `ConfigureNotify`) -> position relative to parent + // https://tronche.com/gui/x/icccm/sec-4.html#s-4.1.5 + // We don't want to send `Moved` when this is false, since then every `Resized` + // (whether the window moved or not) is accompanied by an extraneous `Moved` event + // that has a position relative to the parent window. + let is_synthetic = xev.send_event == xlib::True; + + // These are both in physical space. + let new_inner_size = (xev.width as u32, xev.height as u32); + let new_inner_position = (xev.x, xev.y); + + let (mut resized, moved) = { + let mut shared_state_lock = window.shared_state_lock(); + + let resized = util::maybe_change(&mut shared_state_lock.size, new_inner_size); + let moved = if is_synthetic { + util::maybe_change(&mut shared_state_lock.inner_position, new_inner_position) + } else { + // Detect when frame extents change. + // Since this isn't synthetic, as per the notes above, this position is relative to + // the parent window. + let rel_parent = new_inner_position; + if util::maybe_change(&mut shared_state_lock.inner_position_rel_parent, rel_parent) + { + // This ensures we process the next `Moved`. + shared_state_lock.inner_position = None; + // Extra insurance against stale frame extents. + shared_state_lock.frame_extents = None; + } + false + }; + (resized, moved) + }; + + let position = window.shared_state_lock().position; + + let new_outer_position = if let (Some(position), false) = (position, moved) { + position + } else { + let mut shared_state_lock = window.shared_state_lock(); + + // We need to convert client area position to window position. + let frame_extents = + shared_state_lock.frame_extents.as_ref().cloned().unwrap_or_else(|| { + let frame_extents = wt.xconn.get_frame_extents_heuristic(xwindow, wt.root); + shared_state_lock.frame_extents = Some(frame_extents.clone()); + frame_extents + }); + let outer = + frame_extents.inner_pos_to_outer(new_inner_position.0, new_inner_position.1); + shared_state_lock.position = Some(outer); + + // Unlock shared state to prevent deadlock in callback below + drop(shared_state_lock); + + if moved { + callback(&self.target, Event::WindowEvent { + window_id, + event: WindowEvent::Moved(outer.into()), + }); + } + outer + }; + + if is_synthetic { + let mut shared_state_lock = window.shared_state_lock(); + // If we don't use the existing adjusted value when available, then the user can screw + // up the resizing by dragging across monitors *without* dropping the + // window. + let (width, height) = + shared_state_lock.dpi_adjusted.unwrap_or((xev.width as u32, xev.height as u32)); + + let last_scale_factor = shared_state_lock.last_monitor.scale_factor; + let new_scale_factor = { + let window_rect = util::AaRect::new(new_outer_position, new_inner_size); + let monitor = wt + .xconn + .get_monitor_for_window(Some(window_rect)) + .expect("Failed to find monitor for window"); + + if monitor.is_dummy() { + // Avoid updating monitor using a dummy monitor handle + last_scale_factor + } else { + shared_state_lock.last_monitor = monitor.clone(); + monitor.scale_factor + } + }; + if last_scale_factor != new_scale_factor { + let (new_width, new_height) = window.adjust_for_dpi( + last_scale_factor, + new_scale_factor, + width, + height, + &shared_state_lock, + ); + + let old_inner_size = PhysicalSize::new(width, height); + let new_inner_size = PhysicalSize::new(new_width, new_height); + + // Unlock shared state to prevent deadlock in callback below + drop(shared_state_lock); + + let inner_size = Arc::new(Mutex::new(new_inner_size)); + callback(&self.target, Event::WindowEvent { + window_id, + event: WindowEvent::ScaleFactorChanged { + scale_factor: new_scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&inner_size)), + }, + }); + + let new_inner_size = *inner_size.lock().unwrap(); + drop(inner_size); + + if new_inner_size != old_inner_size { + window.request_inner_size_physical(new_inner_size.width, new_inner_size.height); + window.shared_state_lock().dpi_adjusted = Some(new_inner_size.into()); + // if the DPI factor changed, force a resize event to ensure the logical + // size is computed with the right DPI factor + resized = true; + } + } + } + + // NOTE: Ensure that the lock is dropped before handling the resized and + // sending the event back to user. + let hittest = { + let mut shared_state_lock = window.shared_state_lock(); + let hittest = shared_state_lock.cursor_hittest; + + // This is a hack to ensure that the DPI adjusted resize is actually + // applied on all WMs. KWin doesn't need this, but Xfwm does. The hack + // should not be run on other WMs, since tiling WMs constrain the window + // size, making the resize fail. This would cause an endless stream of + // XResizeWindow requests, making Xorg, the winit client, and the WM + // consume 100% of CPU. + if let Some(adjusted_size) = shared_state_lock.dpi_adjusted { + if new_inner_size == adjusted_size || !util::wm_name_is_one_of(&["Xfwm4"]) { + // When this finally happens, the event will not be synthetic. + shared_state_lock.dpi_adjusted = None; + } else { + // Unlock shared state to prevent deadlock in callback below + drop(shared_state_lock); + window.request_inner_size_physical(adjusted_size.0, adjusted_size.1); + } + } + + hittest + }; + + // Reload hittest. + if hittest.unwrap_or(false) { + let _ = window.set_cursor_hittest(true); + } + + if resized { + callback(&self.target, Event::WindowEvent { + window_id, + event: WindowEvent::Resized(new_inner_size.into()), + }); + } + } + + /// This is generally a reliable way to detect when the window manager's been + /// replaced, though this event is only fired by reparenting window managers + /// (which is almost all of them). Failing to correctly update WM info doesn't + /// really have much impact, since on the WMs affected (xmonad, dwm, etc.) the only + /// effect is that we waste some time trying to query unsupported properties. + fn reparent_notify(&self, xev: &XReparentEvent) { + let wt = Self::window_target(&self.target); + + wt.xconn.update_cached_wm_info(wt.root); + + self.with_window(xev.window as xproto::Window, |window| { + window.invalidate_cached_frame_extents(); + }); + } + + fn map_notify(&self, xev: &XMapEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let window = xev.window as xproto::Window; + let window_id = mkwid(window); + + // NOTE: Re-issue the focus state when mapping the window. + // + // The purpose of it is to deliver initial focused state of the newly created + // window, given that we can't rely on `CreateNotify`, due to it being not + // sent. + let focus = self.with_window(window, |window| window.has_focus()).unwrap_or_default(); + let event = Event::WindowEvent { window_id, event: WindowEvent::Focused(focus) }; + + callback(&self.target, event); + } + + fn destroy_notify(&self, xev: &XDestroyWindowEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + let window = xev.window as xproto::Window; + let window_id = mkwid(window); + + // In the event that the window's been destroyed without being dropped first, we + // cleanup again here. + wt.windows.borrow_mut().remove(&WindowId(window as _)); + + // Since all XIM stuff needs to happen from the same thread, we destroy the input + // context here instead of when dropping the window. + if let Some(ime) = wt.ime.as_ref() { + ime.borrow_mut() + .remove_context(window as XWindow) + .expect("Failed to destroy input context"); + } + + callback(&self.target, Event::WindowEvent { window_id, event: WindowEvent::Destroyed }); + } + + fn property_notify(&mut self, xev: &XPropertyEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let atoms = wt.x_connection().atoms(); + let atom = xev.atom as xproto::Atom; + + if atom == xproto::Atom::from(xproto::AtomEnum::RESOURCE_MANAGER) + || atom == atoms[_XSETTINGS_SETTINGS] + { + self.process_dpi_change(&mut callback); + } + } + + fn visibility_notify(&self, xev: &XVisibilityEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let xwindow = xev.window as xproto::Window; + + let event = Event::WindowEvent { + window_id: mkwid(xwindow), + event: WindowEvent::Occluded(xev.state == xlib::VisibilityFullyObscured), + }; + callback(&self.target, event); + + self.with_window(xwindow, |window| { + window.visibility_notify(); + }); + } + + fn expose(&self, xev: &XExposeEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + // Multiple Expose events may be received for subareas of a window. + // We issue `RedrawRequested` only for the last event of such a series. + if xev.count == 0 { + let window = xev.window as xproto::Window; + let window_id = mkwid(window); + + let event = Event::WindowEvent { window_id, event: WindowEvent::RedrawRequested }; + + callback(&self.target, event); + } + } + + fn xinput_key_input( + &mut self, + xev: &mut XKeyEvent, + state: ElementState, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let window = match self.active_window { + Some(window) => window, + None => return, + }; + + let window_id = mkwid(window); + let device_id = mkdid(util::VIRTUAL_CORE_KEYBOARD); + + let keycode = xev.keycode as _; + + // Update state to track key repeats and determine whether this key was a repeat. + // + // Note, when a key is held before focusing on this window the first + // (non-synthetic) event will not be flagged as a repeat (also note that the + // synthetic press event that is generated before this when the window gains focus + // will also not be flagged as a repeat). + // + // Only keys that can repeat should change the held_key_press state since a + // continuously held repeatable key may continue repeating after the press of a + // non-repeatable key. + let key_repeats = + self.xkb_context.keymap_mut().map(|k| k.key_repeats(keycode)).unwrap_or(false); + let repeat = if key_repeats { + let is_latest_held = self.held_key_press == Some(keycode); + + if state == ElementState::Pressed { + self.held_key_press = Some(keycode); + is_latest_held + } else { + // Check that the released key is the latest repeatable key that has been + // pressed, since repeats will continue for the latest key press if a + // different previously pressed key is released. + if is_latest_held { + self.held_key_press = None; + } + false + } + } else { + false + }; + + // NOTE: When the modifier was captured by the XFilterEvents the modifiers for the modifier + // itself are out of sync due to XkbState being delivered before XKeyEvent, since it's + // being replayed by the XIM, thus we should replay ourselves. + let replay = if let Some(position) = + self.xfiltered_modifiers.iter().rev().position(|&s| s == xev.keycode as u8) + { + // We don't have to replay modifiers pressed before the current event if some events + // were not forwarded to us, since their state is irrelevant. + self.xfiltered_modifiers.resize(self.xfiltered_modifiers.len() - 1 - position, 0); + true + } else { + false + }; + + // Always update the modifiers when we're not replaying. + if !replay { + self.update_mods_from_core_event(window_id, xev.state as u16, &mut callback); + } + + if keycode != 0 && !self.is_composing { + // Don't alter the modifiers state from replaying. + if replay { + self.send_synthic_modifier_from_core(window_id, xev.state as u16, &mut callback); + } + + if let Some(mut key_processor) = self.xkb_context.key_context() { + let event = key_processor.process_key_event(keycode, state, repeat); + let event = Event::WindowEvent { + window_id, + event: WindowEvent::KeyboardInput { device_id, event, is_synthetic: false }, + }; + callback(&self.target, event); + } + + // Restore the client's modifiers state after replay. + if replay { + self.send_modifiers(window_id, self.modifiers.get(), true, &mut callback); + } + + return; + } + + let wt = Self::window_target(&self.target); + + if let Some(ic) = + wt.ime.as_ref().and_then(|ime| ime.borrow().get_context(window as XWindow)) + { + let written = wt.xconn.lookup_utf8(ic, xev); + if !written.is_empty() { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + }; + callback(&self.target, event); + + let event = + Event::WindowEvent { window_id, event: WindowEvent::Ime(Ime::Commit(written)) }; + + self.is_composing = false; + callback(&self.target, event); + } + } + } + + fn send_synthic_modifier_from_core( + &mut self, + window_id: crate::window::WindowId, + state: u16, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let keymap = match self.xkb_context.keymap_mut() { + Some(keymap) => keymap, + None => return, + }; + + let wt = Self::window_target(&self.target); + let xcb = wt.xconn.xcb_connection().get_raw_xcb_connection(); + + // Use synthetic state since we're replaying the modifier. The user modifier state + // will be restored later. + let mut xkb_state = match XkbState::new_x11(xcb, keymap) { + Some(xkb_state) => xkb_state, + None => return, + }; + + let mask = self.xkb_mod_mask_from_core(state); + xkb_state.update_modifiers(mask, 0, 0, 0, 0, Self::core_keyboard_group(state)); + let mods: ModifiersState = xkb_state.modifiers().into(); + + let event = + Event::WindowEvent { window_id, event: WindowEvent::ModifiersChanged(mods.into()) }; + + callback(&self.target, event); + } + + fn xinput2_button_input( + &self, + event: &XIDeviceEvent, + state: ElementState, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let window_id = mkwid(event.event as xproto::Window); + let device_id = mkdid(event.deviceid as xinput::DeviceId); + + // Set the timestamp. + wt.xconn.set_timestamp(event.time as xproto::Timestamp); + + // Deliver multi-touch events instead of emulated mouse events. + if (event.flags & xinput2::XIPointerEmulated) != 0 { + return; + } + + let event = match event.detail as u32 { + xlib::Button1 => { + WindowEvent::MouseInput { device_id, state, button: MouseButton::Left } + }, + xlib::Button2 => { + WindowEvent::MouseInput { device_id, state, button: MouseButton::Middle } + }, + + xlib::Button3 => { + WindowEvent::MouseInput { device_id, state, button: MouseButton::Right } + }, + + // Suppress emulated scroll wheel clicks, since we handle the real motion events for + // those. In practice, even clicky scroll wheels appear to be reported by + // evdev (and XInput2 in turn) as axis motion, so we don't otherwise + // special-case these button presses. + 4..=7 => WindowEvent::MouseWheel { + device_id, + delta: match event.detail { + 4 => MouseScrollDelta::LineDelta(0.0, 1.0), + 5 => MouseScrollDelta::LineDelta(0.0, -1.0), + 6 => MouseScrollDelta::LineDelta(1.0, 0.0), + 7 => MouseScrollDelta::LineDelta(-1.0, 0.0), + _ => unreachable!(), + }, + phase: TouchPhase::Moved, + }, + 8 => WindowEvent::MouseInput { device_id, state, button: MouseButton::Back }, + + 9 => WindowEvent::MouseInput { device_id, state, button: MouseButton::Forward }, + x => WindowEvent::MouseInput { device_id, state, button: MouseButton::Other(x as u16) }, + }; + + let event = Event::WindowEvent { window_id, event }; + callback(&self.target, event); + } + + fn xinput2_mouse_motion(&self, event: &XIDeviceEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(event.time as xproto::Timestamp); + + let device_id = mkdid(event.deviceid as xinput::DeviceId); + let window = event.event as xproto::Window; + let window_id = mkwid(window); + let new_cursor_pos = (event.event_x, event.event_y); + + let cursor_moved = self.with_window(window, |window| { + let mut shared_state_lock = window.shared_state_lock(); + util::maybe_change(&mut shared_state_lock.cursor_pos, new_cursor_pos) + }); + + if cursor_moved == Some(true) { + let position = PhysicalPosition::new(event.event_x, event.event_y); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CursorMoved { device_id, position }, + }; + callback(&self.target, event); + } else if cursor_moved.is_none() { + return; + } + + // More gymnastics, for self.devices + let mask = unsafe { + slice::from_raw_parts(event.valuators.mask, event.valuators.mask_len as usize) + }; + let mut devices = self.devices.borrow_mut(); + let physical_device = match devices.get_mut(&DeviceId(event.sourceid as xinput::DeviceId)) { + Some(device) => device, + None => return, + }; + + let mut events = Vec::new(); + let mut value = event.valuators.values; + for i in 0..event.valuators.mask_len * 8 { + if !xinput2::XIMaskIsSet(mask, i) { + continue; + } + + let x = unsafe { *value }; + + let event = if let Some(&mut (_, ref mut info)) = + physical_device.scroll_axes.iter_mut().find(|&&mut (axis, _)| axis == i as _) + { + let delta = (x - info.position) / info.increment; + info.position = x; + // X11 vertical scroll coordinates are opposite to winit's + let delta = match info.orientation { + ScrollOrientation::Horizontal => { + MouseScrollDelta::LineDelta(-delta as f32, 0.0) + }, + ScrollOrientation::Vertical => MouseScrollDelta::LineDelta(0.0, -delta as f32), + }; + + WindowEvent::MouseWheel { device_id, delta, phase: TouchPhase::Moved } + } else { + WindowEvent::AxisMotion { device_id, axis: i as u32, value: unsafe { *value } } + }; + + events.push(Event::WindowEvent { window_id, event }); + + value = unsafe { value.offset(1) }; + } + + for event in events { + callback(&self.target, event); + } + } + + fn xinput2_mouse_enter(&self, event: &XIEnterEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(event.time as xproto::Timestamp); + + let window = event.event as xproto::Window; + let window_id = mkwid(window); + let device_id = mkdid(event.deviceid as xinput::DeviceId); + + if let Some(all_info) = DeviceInfo::get(&wt.xconn, super::ALL_DEVICES.into()) { + let mut devices = self.devices.borrow_mut(); + for device_info in all_info.iter() { + // The second expression is need for resetting to work correctly on i3, and + // presumably some other WMs. On those, `XI_Enter` doesn't include the physical + // device ID, so both `sourceid` and `deviceid` are the virtual device. + if device_info.deviceid == event.sourceid + || device_info.attachment == event.sourceid + { + let device_id = DeviceId(device_info.deviceid as _); + if let Some(device) = devices.get_mut(&device_id) { + device.reset_scroll_position(device_info); + } + } + } + } + + if self.window_exists(window) { + let position = PhysicalPosition::new(event.event_x, event.event_y); + + let event = + Event::WindowEvent { window_id, event: WindowEvent::CursorEntered { device_id } }; + callback(&self.target, event); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CursorMoved { device_id, position }, + }; + callback(&self.target, event); + } + } + + fn xinput2_mouse_left(&self, event: &XILeaveEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let window = event.event as xproto::Window; + + // Set the timestamp. + wt.xconn.set_timestamp(event.time as xproto::Timestamp); + + // Leave, FocusIn, and FocusOut can be received by a window that's already + // been destroyed, which the user presumably doesn't want to deal with. + if self.window_exists(window) { + let event = Event::WindowEvent { + window_id: mkwid(window), + event: WindowEvent::CursorLeft { + device_id: mkdid(event.deviceid as xinput::DeviceId), + }, + }; + callback(&self.target, event); + } + } + + fn xinput2_focused(&mut self, xev: &XIFocusInEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let window = xev.event as xproto::Window; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + if let Some(ime) = wt.ime.as_ref() { + ime.borrow_mut().focus(xev.event).expect("Failed to focus input context"); + } + + if self.active_window == Some(window) { + return; + } + + self.active_window = Some(window); + + wt.update_listen_device_events(true); + + let window_id = mkwid(window); + let position = PhysicalPosition::new(xev.event_x, xev.event_y); + + if let Some(window) = self.with_window(window, Arc::clone) { + window.shared_state_lock().has_focus = true; + } + + let event = Event::WindowEvent { window_id, event: WindowEvent::Focused(true) }; + callback(&self.target, event); + + // Issue key press events for all pressed keys + Self::handle_pressed_keys( + &self.target, + window_id, + ElementState::Pressed, + &mut self.xkb_context, + &mut callback, + ); + + self.update_mods_from_query(window_id, &mut callback); + + // The deviceid for this event is for a keyboard instead of a pointer, + // so we have to do a little extra work. + let pointer_id = self + .devices + .borrow() + .get(&DeviceId(xev.deviceid as xinput::DeviceId)) + .map(|device| device.attachment) + .unwrap_or(2); + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CursorMoved { device_id: mkdid(pointer_id as _), position }, + }; + callback(&self.target, event); + } + + fn xinput2_unfocused(&mut self, xev: &XIFocusOutEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + let window = xev.event as xproto::Window; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + if !self.window_exists(window) { + return; + } + + if let Some(ime) = wt.ime.as_ref() { + ime.borrow_mut().unfocus(xev.event).expect("Failed to unfocus input context"); + } + + if self.active_window.take() == Some(window) { + let window_id = mkwid(window); + + wt.update_listen_device_events(false); + + // Clear the modifiers when unfocusing the window. + if let Some(xkb_state) = self.xkb_context.state_mut() { + xkb_state.update_modifiers(0, 0, 0, 0, 0, 0); + let mods = xkb_state.modifiers(); + self.send_modifiers(window_id, mods.into(), true, &mut callback); + } + + // Issue key release events for all pressed keys + Self::handle_pressed_keys( + &self.target, + window_id, + ElementState::Released, + &mut self.xkb_context, + &mut callback, + ); + + // Clear this so detecting key repeats is consistently handled when the + // window regains focus. + self.held_key_press = None; + + if let Some(window) = self.with_window(window, Arc::clone) { + window.shared_state_lock().has_focus = false; + } + + let event = Event::WindowEvent { window_id, event: WindowEvent::Focused(false) }; + callback(&self.target, event) + } + } + + fn xinput2_touch( + &mut self, + xev: &XIDeviceEvent, + phase: TouchPhase, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let window = xev.event as xproto::Window; + if self.window_exists(window) { + let window_id = mkwid(window); + let id = xev.detail as u64; + let location = PhysicalPosition::new(xev.event_x, xev.event_y); + + // Mouse cursor position changes when touch events are received. + // Only the first concurrently active touch ID moves the mouse cursor. + if is_first_touch(&mut self.first_touch, &mut self.num_touch, id, phase) { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::CursorMoved { + device_id: mkdid(util::VIRTUAL_CORE_POINTER), + position: location.cast(), + }, + }; + callback(&self.target, event); + } + + let event = Event::WindowEvent { + window_id, + event: WindowEvent::Touch(Touch { + device_id: mkdid(xev.deviceid as xinput::DeviceId), + phase, + location, + force: None, // TODO + id, + }), + }; + callback(&self.target, event) + } + } + + fn xinput2_raw_button_input( + &self, + xev: &XIRawEvent, + state: ElementState, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + if xev.flags & xinput2::XIPointerEmulated == 0 { + let event = Event::DeviceEvent { + device_id: mkdid(xev.deviceid as xinput::DeviceId), + event: DeviceEvent::Button { state, button: xev.detail as u32 }, + }; + callback(&self.target, event); + } + } + + fn xinput2_raw_mouse_motion(&self, xev: &XIRawEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let did = mkdid(xev.deviceid as xinput::DeviceId); + + let mask = + unsafe { slice::from_raw_parts(xev.valuators.mask, xev.valuators.mask_len as usize) }; + let mut value = xev.raw_values; + let mut mouse_delta = util::Delta::default(); + let mut scroll_delta = util::Delta::default(); + for i in 0..xev.valuators.mask_len * 8 { + if !xinput2::XIMaskIsSet(mask, i) { + continue; + } + let x = unsafe { value.read_unaligned() }; + + // We assume that every XInput2 device with analog axes is a pointing device emitting + // relative coordinates. + match i { + 0 => mouse_delta.set_x(x), + 1 => mouse_delta.set_y(x), + 2 => scroll_delta.set_x(x as f32), + 3 => scroll_delta.set_y(x as f32), + _ => {}, + } + + let event = Event::DeviceEvent { + device_id: did, + event: DeviceEvent::Motion { axis: i as u32, value: x }, + }; + callback(&self.target, event); + + value = unsafe { value.offset(1) }; + } + + if let Some(mouse_delta) = mouse_delta.consume() { + let event = Event::DeviceEvent { + device_id: did, + event: DeviceEvent::MouseMotion { delta: mouse_delta }, + }; + callback(&self.target, event); + } + + if let Some(scroll_delta) = scroll_delta.consume() { + let event = Event::DeviceEvent { + device_id: did, + event: DeviceEvent::MouseWheel { + delta: MouseScrollDelta::LineDelta(scroll_delta.0, scroll_delta.1), + }, + }; + callback(&self.target, event); + } + } + + fn xinput2_raw_key_input( + &mut self, + xev: &XIRawEvent, + state: ElementState, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let device_id = mkdid(xev.sourceid as xinput::DeviceId); + let keycode = xev.detail as u32; + if keycode < KEYCODE_OFFSET as u32 { + return; + } + let physical_key = xkb::raw_keycode_to_physicalkey(keycode); + + callback(&self.target, Event::DeviceEvent { + device_id, + event: DeviceEvent::Key(RawKeyEvent { physical_key, state }), + }); + } + + fn xinput2_hierarchy_changed(&mut self, xev: &XIHierarchyEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + let infos = unsafe { slice::from_raw_parts(xev.info, xev.num_info as usize) }; + for info in infos { + if 0 != info.flags & (xinput2::XISlaveAdded | xinput2::XIMasterAdded) { + self.init_device(info.deviceid as xinput::DeviceId); + callback(&self.target, Event::DeviceEvent { + device_id: mkdid(info.deviceid as xinput::DeviceId), + event: DeviceEvent::Added, + }); + } else if 0 != info.flags & (xinput2::XISlaveRemoved | xinput2::XIMasterRemoved) { + callback(&self.target, Event::DeviceEvent { + device_id: mkdid(info.deviceid as xinput::DeviceId), + event: DeviceEvent::Removed, + }); + let mut devices = self.devices.borrow_mut(); + devices.remove(&DeviceId(info.deviceid as xinput::DeviceId)); + } + } + } + + fn xkb_event(&mut self, xev: &XkbAnyEvent, mut callback: F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + match xev.xkb_type { + xlib::XkbNewKeyboardNotify => { + let xev = unsafe { &*(xev as *const _ as *const xlib::XkbNewKeyboardNotifyEvent) }; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + let keycodes_changed_flag = 0x1; + let geometry_changed_flag = 0x1 << 1; + + let keycodes_changed = util::has_flag(xev.changed, keycodes_changed_flag); + let geometry_changed = util::has_flag(xev.changed, geometry_changed_flag); + + if xev.device == self.xkb_context.core_keyboard_id + && (keycodes_changed || geometry_changed) + { + let xcb = wt.xconn.xcb_connection().get_raw_xcb_connection(); + self.xkb_context.set_keymap_from_x11(xcb); + self.xmodmap.reload_from_x_connection(&wt.xconn); + + let window_id = match self.active_window.map(super::mkwid) { + Some(window_id) => window_id, + None => return, + }; + + if let Some(state) = self.xkb_context.state_mut() { + let mods = state.modifiers().into(); + self.send_modifiers(window_id, mods, true, &mut callback); + } + } + }, + xlib::XkbMapNotify => { + let xcb = wt.xconn.xcb_connection().get_raw_xcb_connection(); + self.xkb_context.set_keymap_from_x11(xcb); + self.xmodmap.reload_from_x_connection(&wt.xconn); + let window_id = match self.active_window.map(super::mkwid) { + Some(window_id) => window_id, + None => return, + }; + + if let Some(state) = self.xkb_context.state_mut() { + let mods = state.modifiers().into(); + self.send_modifiers(window_id, mods, true, &mut callback); + } + }, + xlib::XkbStateNotify => { + let xev = unsafe { &*(xev as *const _ as *const xlib::XkbStateNotifyEvent) }; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + + if let Some(state) = self.xkb_context.state_mut() { + state.update_modifiers( + xev.base_mods, + xev.latched_mods, + xev.locked_mods, + xev.base_group as u32, + xev.latched_group as u32, + xev.locked_group as u32, + ); + + let window_id = match self.active_window.map(super::mkwid) { + Some(window_id) => window_id, + None => return, + }; + + let mods = state.modifiers().into(); + self.send_modifiers(window_id, mods, true, &mut callback); + } + }, + _ => {}, + } + } + + pub fn update_mods_from_xinput2_event( + &mut self, + mods: &XIModifierState, + group: &XIModifierState, + force: bool, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + if let Some(state) = self.xkb_context.state_mut() { + state.update_modifiers( + mods.base as u32, + mods.latched as u32, + mods.locked as u32, + group.base as u32, + group.latched as u32, + group.locked as u32, + ); + + // NOTE: we use active window since generally sub windows don't have keyboard input, + // and winit assumes that unfocused window doesn't have modifiers. + let window_id = match self.active_window.map(super::mkwid) { + Some(window_id) => window_id, + None => return, + }; + + let mods = state.modifiers(); + self.send_modifiers(window_id, mods.into(), force, &mut callback); + } + } + + fn update_mods_from_query( + &mut self, + window_id: crate::window::WindowId, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + + let xkb_state = match self.xkb_context.state_mut() { + Some(xkb_state) => xkb_state, + None => return, + }; + + unsafe { + let mut state: XkbStateRec = std::mem::zeroed(); + if (wt.xconn.xlib.XkbGetState)(wt.xconn.display, XkbId::USE_CORE_KBD.into(), &mut state) + == xlib::True + { + xkb_state.update_modifiers( + state.base_mods as u32, + state.latched_mods as u32, + state.locked_mods as u32, + state.base_group as u32, + state.latched_group as u32, + state.locked_group as u32, + ); + } + } + + let mods = xkb_state.modifiers(); + self.send_modifiers(window_id, mods.into(), true, &mut callback) + } + + pub fn update_mods_from_core_event( + &mut self, + window_id: crate::window::WindowId, + state: u16, + mut callback: F, + ) where + F: FnMut(&RootAEL, Event), + { + let xkb_mask = self.xkb_mod_mask_from_core(state); + let xkb_state = match self.xkb_context.state_mut() { + Some(xkb_state) => xkb_state, + None => return, + }; + + // NOTE: this is inspired by Qt impl. + let mut depressed = xkb_state.depressed_modifiers() & xkb_mask; + let latched = xkb_state.latched_modifiers() & xkb_mask; + let locked = xkb_state.locked_modifiers() & xkb_mask; + // Set modifiers in depressed if they don't appear in any of the final masks. + depressed |= !(depressed | latched | locked) & xkb_mask; + + xkb_state.update_modifiers( + depressed, + latched, + locked, + 0, + 0, + Self::core_keyboard_group(state), + ); + + let mods = xkb_state.modifiers(); + self.send_modifiers(window_id, mods.into(), false, &mut callback); + } + + // Bits 13 and 14 report the state keyboard group. + pub fn core_keyboard_group(state: u16) -> u32 { + ((state >> 13) & 3) as u32 + } + + pub fn xkb_mod_mask_from_core(&mut self, state: u16) -> xkb_mod_mask_t { + let mods_indices = match self.xkb_context.keymap_mut() { + Some(keymap) => keymap.mods_indices(), + None => return 0, + }; + + // Build the XKB modifiers from the regular state. + let mut depressed = 0u32; + if let Some(shift) = mods_indices.shift.filter(|_| ModMask::SHIFT.intersects(state)) { + depressed |= 1 << shift; + } + if let Some(caps) = mods_indices.caps.filter(|_| ModMask::LOCK.intersects(state)) { + depressed |= 1 << caps; + } + if let Some(ctrl) = mods_indices.ctrl.filter(|_| ModMask::CONTROL.intersects(state)) { + depressed |= 1 << ctrl; + } + if let Some(alt) = mods_indices.alt.filter(|_| ModMask::M1.intersects(state)) { + depressed |= 1 << alt; + } + if let Some(num) = mods_indices.num.filter(|_| ModMask::M2.intersects(state)) { + depressed |= 1 << num; + } + if let Some(mod3) = mods_indices.mod3.filter(|_| ModMask::M3.intersects(state)) { + depressed |= 1 << mod3; + } + if let Some(logo) = mods_indices.logo.filter(|_| ModMask::M4.intersects(state)) { + depressed |= 1 << logo; + } + if let Some(mod5) = mods_indices.mod5.filter(|_| ModMask::M5.intersects(state)) { + depressed |= 1 << mod5; + } + + depressed + } + + /// Send modifiers for the active window. + /// + /// The event won't be sent when the `modifiers` match the previously `sent` modifiers value, + /// unless `force` is passed. The `force` should be passed when the active window changes. + fn send_modifiers)>( + &self, + window_id: crate::window::WindowId, + modifiers: ModifiersState, + force: bool, + callback: &mut F, + ) { + // NOTE: Always update the modifiers to account for case when they've changed + // and forced was `true`. + if self.modifiers.replace(modifiers) != modifiers || force { + let event = Event::WindowEvent { + window_id, + event: WindowEvent::ModifiersChanged(self.modifiers.get().into()), + }; + callback(&self.target, event); + } + } + + fn handle_pressed_keys( + target: &RootAEL, + window_id: crate::window::WindowId, + state: ElementState, + xkb_context: &mut Context, + callback: &mut F, + ) where + F: FnMut(&RootAEL, Event), + { + let device_id = mkdid(util::VIRTUAL_CORE_KEYBOARD); + + // Update modifiers state and emit key events based on which keys are currently pressed. + let window_target = Self::window_target(target); + let xcb = window_target.xconn.xcb_connection().get_raw_xcb_connection(); + + let keymap = match xkb_context.keymap_mut() { + Some(keymap) => keymap, + None => return, + }; + + // Send the keys using the synthetic state to not alter the main state. + let mut xkb_state = match XkbState::new_x11(xcb, keymap) { + Some(xkb_state) => xkb_state, + None => return, + }; + let mut key_processor = match xkb_context.key_context_with_state(&mut xkb_state) { + Some(key_processor) => key_processor, + None => return, + }; + + for keycode in + window_target.xconn.query_keymap().into_iter().filter(|k| *k >= KEYCODE_OFFSET) + { + let event = key_processor.process_key_event(keycode as u32, state, false); + let event = Event::WindowEvent { + window_id, + event: WindowEvent::KeyboardInput { device_id, event, is_synthetic: true }, + }; + callback(target, event); + } + } + + fn process_dpi_change(&self, callback: &mut F) + where + F: FnMut(&RootAEL, Event), + { + let wt = Self::window_target(&self.target); + wt.xconn.reload_database().expect("failed to reload Xft database"); + + // In the future, it would be quite easy to emit monitor hotplug events. + let prev_list = { + let prev_list = wt.xconn.invalidate_cached_monitor_list(); + match prev_list { + Some(prev_list) => prev_list, + None => return, + } + }; + + let new_list = wt.xconn.available_monitors().expect("Failed to get monitor list"); + for new_monitor in new_list { + // Previous list may be empty, in case of disconnecting and + // reconnecting the only one monitor. We still need to emit events in + // this case. + let maybe_prev_scale_factor = prev_list + .iter() + .find(|prev_monitor| prev_monitor.name == new_monitor.name) + .map(|prev_monitor| prev_monitor.scale_factor); + if Some(new_monitor.scale_factor) != maybe_prev_scale_factor { + for window in wt.windows.borrow().iter().filter_map(|(_, w)| w.upgrade()) { + window.refresh_dpi_for_monitor(&new_monitor, maybe_prev_scale_factor, |event| { + callback(&self.target, event); + }) + } + } + } + } + + fn window_exists(&self, window_id: xproto::Window) -> bool { + self.with_window(window_id, |_| ()).is_some() + } +} + +fn is_first_touch(first: &mut Option, num: &mut u32, id: u64, phase: TouchPhase) -> bool { + match phase { + TouchPhase::Started => { + if *num == 0 { + *first = Some(id); + } + *num += 1; + }, + TouchPhase::Cancelled | TouchPhase::Ended => { + if *first == Some(id) { + *first = None; + } + *num = num.saturating_sub(1); + }, + _ => (), + } + + *first == Some(id) +} diff --git a/vendor/winit/src/platform_impl/linux/x11/ffi.rs b/vendor/winit/src/platform_impl/linux/x11/ffi.rs new file mode 100644 index 00000000..57bd78e9 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/ffi.rs @@ -0,0 +1,5 @@ +pub use x11_dl::error::OpenError; +pub use x11_dl::xcursor::*; +pub use x11_dl::xinput2::*; +pub use x11_dl::xlib::*; +pub use x11_dl::xlib_xcb::*; diff --git a/vendor/winit/src/platform_impl/linux/x11/ime/callbacks.rs b/vendor/winit/src/platform_impl/linux/x11/ime/callbacks.rs new file mode 100644 index 00000000..fa0fa5c6 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/ime/callbacks.rs @@ -0,0 +1,206 @@ +use std::collections::HashMap; +use std::os::raw::c_char; +use std::ptr; +use std::sync::Arc; + +use super::{ffi, XConnection, XError}; + +use super::context::{ImeContext, ImeContextCreationError}; +use super::inner::{close_im, ImeInner}; +use super::input_method::PotentialInputMethods; + +pub(crate) unsafe fn xim_set_callback( + xconn: &Arc, + xim: ffi::XIM, + field: *const c_char, + callback: *mut ffi::XIMCallback, +) -> Result<(), XError> { + // It's advisable to wrap variadic FFI functions in our own functions, as we want to minimize + // access that isn't type-checked. + unsafe { (xconn.xlib.XSetIMValues)(xim, field, callback, ptr::null_mut::<()>()) }; + xconn.check_errors() +} + +// Set a callback for when an input method matching the current locale modifiers becomes +// available. Note that this has nothing to do with what input methods are open or able to be +// opened, and simply uses the modifiers that are set when the callback is set. +// * This is called per locale modifier, not per input method opened with that locale modifier. +// * Trying to set this for multiple locale modifiers causes problems, i.e. one of the rebuilt input +// contexts would always silently fail to use the input method. +pub(crate) unsafe fn set_instantiate_callback( + xconn: &Arc, + client_data: ffi::XPointer, +) -> Result<(), XError> { + unsafe { + (xconn.xlib.XRegisterIMInstantiateCallback)( + xconn.display, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + Some(xim_instantiate_callback), + client_data, + ) + }; + xconn.check_errors() +} + +pub(crate) unsafe fn unset_instantiate_callback( + xconn: &Arc, + client_data: ffi::XPointer, +) -> Result<(), XError> { + unsafe { + (xconn.xlib.XUnregisterIMInstantiateCallback)( + xconn.display, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + Some(xim_instantiate_callback), + client_data, + ) + }; + xconn.check_errors() +} + +pub(crate) unsafe fn set_destroy_callback( + xconn: &Arc, + im: ffi::XIM, + inner: &ImeInner, +) -> Result<(), XError> { + unsafe { + xim_set_callback( + xconn, + im, + ffi::XNDestroyCallback_0.as_ptr() as *const _, + &inner.destroy_callback as *const _ as *mut _, + ) + } +} + +#[derive(Debug)] +#[allow(clippy::enum_variant_names)] +enum ReplaceImError { + // Boxed to prevent large error type + MethodOpenFailed(#[allow(dead_code)] Box), + ContextCreationFailed(#[allow(dead_code)] ImeContextCreationError), + SetDestroyCallbackFailed(#[allow(dead_code)] XError), +} + +// Attempt to replace current IM (which may or may not be presently valid) with a new one. This +// includes replacing all existing input contexts and free'ing resources as necessary. This only +// modifies existing state if all operations succeed. +unsafe fn replace_im(inner: *mut ImeInner) -> Result<(), ReplaceImError> { + let xconn = unsafe { &(*inner).xconn }; + + let (new_im, is_fallback) = { + let new_im = unsafe { (*inner).potential_input_methods.open_im(xconn, None) }; + let is_fallback = new_im.is_fallback(); + ( + new_im.ok().ok_or_else(|| { + ReplaceImError::MethodOpenFailed(Box::new(unsafe { + (*inner).potential_input_methods.clone() + })) + })?, + is_fallback, + ) + }; + + // It's important to always set a destroy callback, since there's otherwise potential for us + // to try to use or free a resource that's already been destroyed on the server. + { + let result = unsafe { set_destroy_callback(xconn, new_im.im, &*inner) }; + if result.is_err() { + let _ = unsafe { close_im(xconn, new_im.im) }; + } + result + } + .map_err(ReplaceImError::SetDestroyCallbackFailed)?; + + let mut new_contexts = HashMap::new(); + for (window, old_context) in unsafe { (*inner).contexts.iter() } { + let spot = old_context.as_ref().map(|old_context| old_context.ic_spot); + + // Check if the IME was allowed on that context. + let is_allowed = + old_context.as_ref().map(|old_context| old_context.is_allowed()).unwrap_or_default(); + + let new_context = { + let result = unsafe { + ImeContext::new( + xconn, + &new_im, + *window, + spot, + (*inner).event_sender.clone(), + is_allowed, + ) + }; + if result.is_err() { + let _ = unsafe { close_im(xconn, new_im.im) }; + } + result.map_err(ReplaceImError::ContextCreationFailed)? + }; + new_contexts.insert(*window, Some(new_context)); + } + + // If we've made it this far, everything succeeded. + unsafe { + let _ = (*inner).destroy_all_contexts_if_necessary(); + let _ = (*inner).close_im_if_necessary(); + (*inner).im = Some(new_im); + (*inner).contexts = new_contexts; + (*inner).is_destroyed = false; + (*inner).is_fallback = is_fallback; + } + Ok(()) +} + +pub unsafe extern "C" fn xim_instantiate_callback( + _display: *mut ffi::Display, + client_data: ffi::XPointer, + // This field is unsupplied. + _call_data: ffi::XPointer, +) { + let inner: *mut ImeInner = client_data as _; + if !inner.is_null() { + let xconn = unsafe { &(*inner).xconn }; + match unsafe { replace_im(inner) } { + Ok(()) => unsafe { + let _ = unset_instantiate_callback(xconn, client_data); + (*inner).is_fallback = false; + }, + Err(err) => unsafe { + if (*inner).is_destroyed { + // We have no usable input methods! + panic!("Failed to reopen input method: {err:?}"); + } + }, + } + } +} + +// This callback is triggered when the input method is closed on the server end. When this +// happens, XCloseIM/XDestroyIC doesn't need to be called, as the resources have already been +// free'd (attempting to do so causes our connection to freeze). +pub unsafe extern "C" fn xim_destroy_callback( + _xim: ffi::XIM, + client_data: ffi::XPointer, + // This field is unsupplied. + _call_data: ffi::XPointer, +) { + let inner: *mut ImeInner = client_data as _; + if !inner.is_null() { + unsafe { (*inner).is_destroyed = true }; + let xconn = unsafe { &(*inner).xconn }; + if unsafe { !(*inner).is_fallback } { + let _ = unsafe { set_instantiate_callback(xconn, client_data) }; + // Attempt to open fallback input method. + match unsafe { replace_im(inner) } { + Ok(()) => unsafe { (*inner).is_fallback = true }, + Err(err) => { + // We have no usable input methods! + panic!("Failed to open fallback input method: {err:?}"); + }, + } + } + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/ime/context.rs b/vendor/winit/src/platform_impl/linux/x11/ime/context.rs new file mode 100644 index 00000000..2c6c075e --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/ime/context.rs @@ -0,0 +1,376 @@ +use std::ffi::CStr; +use std::os::raw::c_short; +use std::sync::Arc; +use std::{mem, ptr}; + +use x11_dl::xlib::{XIMCallback, XIMPreeditCaretCallbackStruct, XIMPreeditDrawCallbackStruct}; + +use super::{ffi, util, XConnection, XError}; +use crate::platform_impl::platform::x11::ime::input_method::{InputMethod, Style, XIMStyle}; +use crate::platform_impl::platform::x11::ime::{ImeEvent, ImeEventSender}; + +/// IME creation error. +#[derive(Debug)] +pub enum ImeContextCreationError { + /// Got the error from Xlib. + XError(XError), + + /// Got null pointer from Xlib but without exact reason. + Null, +} + +/// The callback used by XIM preedit functions. +type XIMProcNonnull = unsafe extern "C" fn(ffi::XIM, ffi::XPointer, ffi::XPointer); + +/// Wrapper for creating XIM callbacks. +#[inline] +fn create_xim_callback(client_data: ffi::XPointer, callback: XIMProcNonnull) -> ffi::XIMCallback { + XIMCallback { client_data, callback: Some(callback) } +} + +/// The server started preedit. +extern "C" fn preedit_start_callback( + _xim: ffi::XIM, + client_data: ffi::XPointer, + _call_data: ffi::XPointer, +) -> i32 { + let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; + + client_data.text.clear(); + client_data.cursor_pos = 0; + client_data + .event_sender + .send((client_data.window, ImeEvent::Start)) + .expect("failed to send preedit start event"); + -1 +} + +/// Done callback is used when the preedit should be hidden. +extern "C" fn preedit_done_callback( + _xim: ffi::XIM, + client_data: ffi::XPointer, + _call_data: ffi::XPointer, +) { + let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; + + // Drop text buffer and reset cursor position on done. + client_data.text = Vec::new(); + client_data.cursor_pos = 0; + + client_data + .event_sender + .send((client_data.window, ImeEvent::End)) + .expect("failed to send preedit end event"); +} + +fn calc_byte_position(text: &[char], pos: usize) -> usize { + text.iter().take(pos).fold(0, |byte_pos, text| byte_pos + text.len_utf8()) +} + +/// Preedit text information to be drawn inline by the client. +extern "C" fn preedit_draw_callback( + _xim: ffi::XIM, + client_data: ffi::XPointer, + call_data: ffi::XPointer, +) { + let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; + let call_data = unsafe { &mut *(call_data as *mut XIMPreeditDrawCallbackStruct) }; + client_data.cursor_pos = call_data.caret as usize; + + let chg_range = + call_data.chg_first as usize..(call_data.chg_first + call_data.chg_length) as usize; + if chg_range.start > client_data.text.len() || chg_range.end > client_data.text.len() { + tracing::warn!( + "invalid chg range: buffer length={}, but chg_first={} chg_length={}", + client_data.text.len(), + call_data.chg_first, + call_data.chg_length + ); + return; + } + + // NULL indicate text deletion + let mut new_chars = if call_data.text.is_null() { + Vec::new() + } else { + let xim_text = unsafe { &mut *(call_data.text) }; + if xim_text.encoding_is_wchar > 0 { + return; + } + + let new_text = unsafe { xim_text.string.multi_byte }; + + if new_text.is_null() { + return; + } + + let new_text = unsafe { CStr::from_ptr(new_text) }; + + String::from(new_text.to_str().expect("Invalid UTF-8 String from IME")).chars().collect() + }; + let mut old_text_tail = client_data.text.split_off(chg_range.end); + client_data.text.truncate(chg_range.start); + client_data.text.append(&mut new_chars); + client_data.text.append(&mut old_text_tail); + let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos); + + client_data + .event_sender + .send(( + client_data.window, + ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos), + )) + .expect("failed to send preedit update event"); +} + +/// Handling of cursor movements in preedit text. +extern "C" fn preedit_caret_callback( + _xim: ffi::XIM, + client_data: ffi::XPointer, + call_data: ffi::XPointer, +) { + let client_data = unsafe { &mut *(client_data as *mut ImeContextClientData) }; + let call_data = unsafe { &mut *(call_data as *mut XIMPreeditCaretCallbackStruct) }; + + if call_data.direction == ffi::XIMCaretDirection::XIMAbsolutePosition { + client_data.cursor_pos = call_data.position as usize; + let cursor_byte_pos = calc_byte_position(&client_data.text, client_data.cursor_pos); + + client_data + .event_sender + .send(( + client_data.window, + ImeEvent::Update(client_data.text.iter().collect(), cursor_byte_pos), + )) + .expect("failed to send preedit update event"); + } +} + +/// Struct to simplify callback creation and latter passing into Xlib XIM. +struct PreeditCallbacks { + start_callback: ffi::XIMCallback, + done_callback: ffi::XIMCallback, + draw_callback: ffi::XIMCallback, + caret_callback: ffi::XIMCallback, +} + +impl PreeditCallbacks { + pub fn new(client_data: ffi::XPointer) -> PreeditCallbacks { + let start_callback = create_xim_callback(client_data, unsafe { + mem::transmute::( + preedit_start_callback as *const () as usize, + ) + }); + let done_callback = create_xim_callback(client_data, preedit_done_callback); + let caret_callback = create_xim_callback(client_data, preedit_caret_callback); + let draw_callback = create_xim_callback(client_data, preedit_draw_callback); + + PreeditCallbacks { start_callback, done_callback, caret_callback, draw_callback } + } +} + +struct ImeContextClientData { + window: ffi::Window, + event_sender: ImeEventSender, + text: Vec, + cursor_pos: usize, +} + +// XXX: this struct doesn't destroy its XIC resource when dropped. +// This is intentional, as it doesn't have enough information to know whether or not the context +// still exists on the server. Since `ImeInner` has that awareness, destruction must be handled +// through `ImeInner`. +pub struct ImeContext { + pub(crate) ic: ffi::XIC, + pub(crate) ic_spot: ffi::XPoint, + pub(crate) allowed: bool, + // Since the data is passed shared between X11 XIM callbacks, but couldn't be directly free + // from there we keep the pointer to automatically deallocate it. + _client_data: Box, +} + +impl ImeContext { + pub(crate) unsafe fn new( + xconn: &Arc, + im: &InputMethod, + window: ffi::Window, + ic_spot: Option, + event_sender: ImeEventSender, + allowed: bool, + ) -> Result { + let client_data = Box::into_raw(Box::new(ImeContextClientData { + window, + event_sender, + text: Vec::new(), + cursor_pos: 0, + })); + + let style = if allowed { im.preedit_style } else { im.none_style }; + + let ic = match style as _ { + Style::Preedit(style) => unsafe { + ImeContext::create_preedit_ic( + xconn, + im.im, + style, + window, + client_data as ffi::XPointer, + ) + }, + Style::Nothing(style) => unsafe { + ImeContext::create_nothing_ic(xconn, im.im, style, window) + }, + Style::None(style) => unsafe { + ImeContext::create_none_ic(xconn, im.im, style, window) + }, + } + .ok_or(ImeContextCreationError::Null)?; + + xconn.check_errors().map_err(ImeContextCreationError::XError)?; + + let mut context = ImeContext { + ic, + ic_spot: ffi::XPoint { x: 0, y: 0 }, + allowed, + _client_data: unsafe { Box::from_raw(client_data) }, + }; + + // Set the spot location, if it's present. + if let Some(ic_spot) = ic_spot { + context.set_spot(xconn, ic_spot.x, ic_spot.y) + } + + Ok(context) + } + + unsafe fn create_none_ic( + xconn: &Arc, + im: ffi::XIM, + style: XIMStyle, + window: ffi::Window, + ) -> Option { + let ic = unsafe { + (xconn.xlib.XCreateIC)( + im, + ffi::XNInputStyle_0.as_ptr() as *const _, + style, + ffi::XNClientWindow_0.as_ptr() as *const _, + window, + ptr::null_mut::<()>(), + ) + }; + + (!ic.is_null()).then_some(ic) + } + + unsafe fn create_preedit_ic( + xconn: &Arc, + im: ffi::XIM, + style: XIMStyle, + window: ffi::Window, + client_data: ffi::XPointer, + ) -> Option { + let preedit_callbacks = PreeditCallbacks::new(client_data); + let preedit_attr = util::memory::XSmartPointer::new(xconn, unsafe { + (xconn.xlib.XVaCreateNestedList)( + 0, + ffi::XNPreeditStartCallback_0.as_ptr() as *const _, + &(preedit_callbacks.start_callback) as *const _, + ffi::XNPreeditDoneCallback_0.as_ptr() as *const _, + &(preedit_callbacks.done_callback) as *const _, + ffi::XNPreeditCaretCallback_0.as_ptr() as *const _, + &(preedit_callbacks.caret_callback) as *const _, + ffi::XNPreeditDrawCallback_0.as_ptr() as *const _, + &(preedit_callbacks.draw_callback) as *const _, + ptr::null_mut::<()>(), + ) + }) + .expect("XVaCreateNestedList returned NULL"); + + let ic = unsafe { + (xconn.xlib.XCreateIC)( + im, + ffi::XNInputStyle_0.as_ptr() as *const _, + style, + ffi::XNClientWindow_0.as_ptr() as *const _, + window, + ffi::XNPreeditAttributes_0.as_ptr() as *const _, + preedit_attr.ptr, + ptr::null_mut::<()>(), + ) + }; + + (!ic.is_null()).then_some(ic) + } + + unsafe fn create_nothing_ic( + xconn: &Arc, + im: ffi::XIM, + style: XIMStyle, + window: ffi::Window, + ) -> Option { + let ic = unsafe { + (xconn.xlib.XCreateIC)( + im, + ffi::XNInputStyle_0.as_ptr() as *const _, + style, + ffi::XNClientWindow_0.as_ptr() as *const _, + window, + ptr::null_mut::<()>(), + ) + }; + + (!ic.is_null()).then_some(ic) + } + + pub(crate) fn focus(&self, xconn: &Arc) -> Result<(), XError> { + unsafe { + (xconn.xlib.XSetICFocus)(self.ic); + } + xconn.check_errors() + } + + pub(crate) fn unfocus(&self, xconn: &Arc) -> Result<(), XError> { + unsafe { + (xconn.xlib.XUnsetICFocus)(self.ic); + } + xconn.check_errors() + } + + pub fn is_allowed(&self) -> bool { + self.allowed + } + + // Set the spot for preedit text. Setting spot isn't working with libX11 when preedit callbacks + // are being used. Certain IMEs do show selection window, but it's placed in bottom left of the + // window and couldn't be changed. + // + // For me see: https://bugs.freedesktop.org/show_bug.cgi?id=1580. + pub(crate) fn set_spot(&mut self, xconn: &Arc, x: c_short, y: c_short) { + if !self.is_allowed() || self.ic_spot.x == x && self.ic_spot.y == y { + return; + } + + self.ic_spot = ffi::XPoint { x, y }; + + unsafe { + let preedit_attr = util::memory::XSmartPointer::new( + xconn, + (xconn.xlib.XVaCreateNestedList)( + 0, + ffi::XNSpotLocation_0.as_ptr(), + &self.ic_spot, + ptr::null_mut::<()>(), + ), + ) + .expect("XVaCreateNestedList returned NULL"); + + (xconn.xlib.XSetICValues)( + self.ic, + ffi::XNPreeditAttributes_0.as_ptr() as *const _, + preedit_attr.ptr, + ptr::null_mut::<()>(), + ); + } + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/ime/inner.rs b/vendor/winit/src/platform_impl/linux/x11/ime/inner.rs new file mode 100644 index 00000000..da1ccf40 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/ime/inner.rs @@ -0,0 +1,74 @@ +use std::collections::HashMap; +use std::mem; +use std::sync::Arc; + +use super::{ffi, XConnection, XError}; + +use super::context::ImeContext; +use super::input_method::{InputMethod, PotentialInputMethods}; +use crate::platform_impl::platform::x11::ime::ImeEventSender; + +pub(crate) unsafe fn close_im(xconn: &Arc, im: ffi::XIM) -> Result<(), XError> { + unsafe { (xconn.xlib.XCloseIM)(im) }; + xconn.check_errors() +} + +pub(crate) unsafe fn destroy_ic(xconn: &Arc, ic: ffi::XIC) -> Result<(), XError> { + unsafe { (xconn.xlib.XDestroyIC)(ic) }; + xconn.check_errors() +} + +pub(crate) struct ImeInner { + pub xconn: Arc, + pub im: Option, + pub potential_input_methods: PotentialInputMethods, + pub contexts: HashMap>, + // WARNING: this is initially zeroed! + pub destroy_callback: ffi::XIMCallback, + pub event_sender: ImeEventSender, + // Indicates whether or not the input method was destroyed on the server end + // (i.e. if ibus/fcitx/etc. was terminated/restarted) + pub is_destroyed: bool, + pub is_fallback: bool, +} + +impl ImeInner { + pub(crate) fn new( + xconn: Arc, + potential_input_methods: PotentialInputMethods, + event_sender: ImeEventSender, + ) -> Self { + ImeInner { + xconn, + im: None, + potential_input_methods, + contexts: HashMap::new(), + destroy_callback: unsafe { mem::zeroed() }, + event_sender, + is_destroyed: false, + is_fallback: false, + } + } + + pub unsafe fn close_im_if_necessary(&self) -> Result { + match self.im.as_ref() { + Some(im) if !self.is_destroyed => unsafe { close_im(&self.xconn, im.im).map(|_| true) }, + _ => Ok(false), + } + } + + pub unsafe fn destroy_ic_if_necessary(&self, ic: ffi::XIC) -> Result { + if !self.is_destroyed { + unsafe { destroy_ic(&self.xconn, ic) }.map(|_| true) + } else { + Ok(false) + } + } + + pub unsafe fn destroy_all_contexts_if_necessary(&self) -> Result { + for context in self.contexts.values().flatten() { + unsafe { self.destroy_ic_if_necessary(context.ic)? }; + } + Ok(!self.is_destroyed) + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/ime/input_method.rs b/vendor/winit/src/platform_impl/linux/x11/ime/input_method.rs new file mode 100644 index 00000000..e1f2fbd2 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/ime/input_method.rs @@ -0,0 +1,345 @@ +use std::ffi::{CStr, CString, IntoStringError}; +use std::os::raw::{c_char, c_ulong, c_ushort}; +use std::sync::{Arc, Mutex}; +use std::{env, fmt, ptr}; + +use super::super::atoms::*; +use super::{ffi, util, XConnection, XError}; +use x11rb::protocol::xproto; + +static GLOBAL_LOCK: Mutex<()> = Mutex::new(()); + +unsafe fn open_im(xconn: &Arc, locale_modifiers: &CStr) -> Option { + let _lock = GLOBAL_LOCK.lock(); + + // XSetLocaleModifiers returns... + // * The current locale modifiers if it's given a NULL pointer. + // * The new locale modifiers if we succeeded in setting them. + // * NULL if the locale modifiers string is malformed or if the current locale is not supported + // by Xlib. + unsafe { (xconn.xlib.XSetLocaleModifiers)(locale_modifiers.as_ptr()) }; + + let im = unsafe { + (xconn.xlib.XOpenIM)(xconn.display, ptr::null_mut(), ptr::null_mut(), ptr::null_mut()) + }; + + if im.is_null() { + None + } else { + Some(im) + } +} + +#[derive(Debug)] +pub struct InputMethod { + pub im: ffi::XIM, + pub preedit_style: Style, + pub none_style: Style, + _name: String, +} + +impl InputMethod { + fn new(xconn: &Arc, im: ffi::XIM, name: String) -> Option { + let mut styles: *mut XIMStyles = std::ptr::null_mut(); + + // Query the styles supported by the XIM. + unsafe { + if !(xconn.xlib.XGetIMValues)( + im, + ffi::XNQueryInputStyle_0.as_ptr() as *const _, + (&mut styles) as *mut _, + std::ptr::null_mut::<()>(), + ) + .is_null() + { + return None; + } + } + + let mut preedit_style = None; + let mut none_style = None; + + unsafe { + std::slice::from_raw_parts((*styles).supported_styles, (*styles).count_styles as _) + .iter() + .for_each(|style| match *style { + XIM_PREEDIT_STYLE => { + preedit_style = Some(Style::Preedit(*style)); + }, + XIM_NOTHING_STYLE if preedit_style.is_none() => { + preedit_style = Some(Style::Nothing(*style)) + }, + XIM_NONE_STYLE => none_style = Some(Style::None(*style)), + _ => (), + }); + + (xconn.xlib.XFree)(styles.cast()); + }; + + if preedit_style.is_none() && none_style.is_none() { + return None; + } + + let preedit_style = preedit_style.unwrap_or_else(|| none_style.unwrap()); + let none_style = none_style.unwrap_or(preedit_style); + + Some(InputMethod { im, _name: name, preedit_style, none_style }) + } +} + +const XIM_PREEDIT_STYLE: XIMStyle = (ffi::XIMPreeditCallbacks | ffi::XIMStatusNothing) as XIMStyle; +const XIM_NOTHING_STYLE: XIMStyle = (ffi::XIMPreeditNothing | ffi::XIMStatusNothing) as XIMStyle; +const XIM_NONE_STYLE: XIMStyle = (ffi::XIMPreeditNone | ffi::XIMStatusNone) as XIMStyle; + +/// Style of the IME context. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Style { + /// Preedit callbacks. + Preedit(XIMStyle), + + /// Nothing. + Nothing(XIMStyle), + + /// No IME. + None(XIMStyle), +} + +impl Default for Style { + fn default() -> Self { + Style::None(XIM_NONE_STYLE) + } +} + +#[repr(C)] +#[derive(Debug)] +struct XIMStyles { + count_styles: c_ushort, + supported_styles: *const XIMStyle, +} + +pub(crate) type XIMStyle = c_ulong; + +#[derive(Debug)] +pub enum InputMethodResult { + /// Input method used locale modifier from `XMODIFIERS` environment variable. + XModifiers(InputMethod), + /// Input method used internal fallback locale modifier. + Fallback(InputMethod), + /// Input method could not be opened using any locale modifier tried. + Failure, +} + +impl InputMethodResult { + pub fn is_fallback(&self) -> bool { + matches!(self, InputMethodResult::Fallback(_)) + } + + pub fn ok(self) -> Option { + use self::InputMethodResult::*; + match self { + XModifiers(im) | Fallback(im) => Some(im), + Failure => None, + } + } +} + +#[derive(Debug, Clone)] +enum GetXimServersError { + XError(#[allow(dead_code)] XError), + GetPropertyError(#[allow(dead_code)] util::GetPropertyError), + InvalidUtf8(#[allow(dead_code)] IntoStringError), +} + +impl From for GetXimServersError { + fn from(error: util::GetPropertyError) -> Self { + GetXimServersError::GetPropertyError(error) + } +} + +// The root window has a property named XIM_SERVERS, which contains a list of atoms representing +// the available XIM servers. For instance, if you're using ibus, it would contain an atom named +// "@server=ibus". It's possible for this property to contain multiple atoms, though presumably +// rare. Note that we replace "@server=" with "@im=" in order to match the format of locale +// modifiers, since we don't want a user who's looking at logs to ask "am I supposed to set +// XMODIFIERS to `@server=ibus`?!?" +unsafe fn get_xim_servers(xconn: &Arc) -> Result, GetXimServersError> { + let atoms = xconn.atoms(); + let servers_atom = atoms[XIM_SERVERS]; + + let root = unsafe { (xconn.xlib.XDefaultRootWindow)(xconn.display) }; + + let mut atoms: Vec = xconn + .get_property::( + root as xproto::Window, + servers_atom, + xproto::Atom::from(xproto::AtomEnum::ATOM), + ) + .map_err(GetXimServersError::GetPropertyError)? + .into_iter() + .map(|atom| atom as _) + .collect::>(); + + let mut names: Vec<*const c_char> = Vec::with_capacity(atoms.len()); + unsafe { + (xconn.xlib.XGetAtomNames)( + xconn.display, + atoms.as_mut_ptr(), + atoms.len() as _, + names.as_mut_ptr() as _, + ) + }; + unsafe { names.set_len(atoms.len()) }; + + let mut formatted_names = Vec::with_capacity(names.len()); + for name in names { + let string = unsafe { CStr::from_ptr(name) } + .to_owned() + .into_string() + .map_err(GetXimServersError::InvalidUtf8)?; + unsafe { (xconn.xlib.XFree)(name as _) }; + formatted_names.push(string.replace("@server=", "@im=")); + } + xconn.check_errors().map_err(GetXimServersError::XError)?; + Ok(formatted_names) +} + +#[derive(Clone)] +struct InputMethodName { + c_string: CString, + string: String, +} + +impl InputMethodName { + pub fn from_string(string: String) -> Self { + let c_string = CString::new(string.clone()) + .expect("String used to construct CString contained null byte"); + InputMethodName { c_string, string } + } + + pub fn from_str(string: &str) -> Self { + let c_string = + CString::new(string).expect("String used to construct CString contained null byte"); + InputMethodName { c_string, string: string.to_owned() } + } +} + +impl fmt::Debug for InputMethodName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.string.fmt(f) + } +} + +#[derive(Debug, Clone)] +struct PotentialInputMethod { + name: InputMethodName, + successful: Option, +} + +impl PotentialInputMethod { + pub fn from_string(string: String) -> Self { + PotentialInputMethod { name: InputMethodName::from_string(string), successful: None } + } + + pub fn from_str(string: &str) -> Self { + PotentialInputMethod { name: InputMethodName::from_str(string), successful: None } + } + + pub fn reset(&mut self) { + self.successful = None; + } + + pub fn open_im(&mut self, xconn: &Arc) -> Option { + let im = unsafe { open_im(xconn, &self.name.c_string) }; + self.successful = Some(im.is_some()); + im.and_then(|im| InputMethod::new(xconn, im, self.name.string.clone())) + } +} + +// By logging this struct, you get a sequential listing of every locale modifier tried, where it +// came from, and if it succeeded. +#[derive(Debug, Clone)] +pub(crate) struct PotentialInputMethods { + // On correctly configured systems, the XMODIFIERS environment variable tells us everything we + // need to know. + xmodifiers: Option, + // We have some standard options at our disposal that should ostensibly always work. For users + // who only need compose sequences, this ensures that the program launches without a hitch + // For users who need more sophisticated IME features, this is more or less a silent failure. + // Logging features should be added in the future to allow both audiences to be effectively + // served. + fallbacks: [PotentialInputMethod; 2], + // For diagnostic purposes, we include the list of XIM servers that the server reports as + // being available. + _xim_servers: Result, GetXimServersError>, +} + +impl PotentialInputMethods { + pub fn new(xconn: &Arc) -> Self { + let xmodifiers = env::var("XMODIFIERS").ok().map(PotentialInputMethod::from_string); + PotentialInputMethods { + // Since passing "" to XSetLocaleModifiers results in it defaulting to the value of + // XMODIFIERS, it's worth noting what happens if XMODIFIERS is also "". If simply + // running the program with `XMODIFIERS="" cargo run`, then assuming XMODIFIERS is + // defined in the profile (or parent environment) then that parent XMODIFIERS is used. + // If that XMODIFIERS value is also "" (i.e. if you ran `export XMODIFIERS=""`), then + // XSetLocaleModifiers uses the default local input method. Note that defining + // XMODIFIERS as "" is different from XMODIFIERS not being defined at all, since in + // that case, we get `None` and end up skipping ahead to the next method. + xmodifiers, + fallbacks: [ + // This is a standard input method that supports compose sequences, which should + // always be available. `@im=none` appears to mean the same thing. + PotentialInputMethod::from_str("@im=local"), + // This explicitly specifies to use the implementation-dependent default, though + // that seems to be equivalent to just using the local input method. + PotentialInputMethod::from_str("@im="), + ], + // The XIM_SERVERS property can have surprising values. For instance, when I exited + // ibus to run fcitx, it retained the value denoting ibus. Even more surprising is + // that the fcitx input method could only be successfully opened using "@im=ibus". + // Presumably due to this quirk, it's actually possible to alternate between ibus and + // fcitx in a running application. + _xim_servers: unsafe { get_xim_servers(xconn) }, + } + } + + // This resets the `successful` field of every potential input method, ensuring we have + // accurate information when this struct is re-used by the destruction/instantiation callbacks. + fn reset(&mut self) { + if let Some(ref mut input_method) = self.xmodifiers { + input_method.reset(); + } + + for input_method in &mut self.fallbacks { + input_method.reset(); + } + } + + pub fn open_im( + &mut self, + xconn: &Arc, + callback: Option<&dyn Fn()>, + ) -> InputMethodResult { + use self::InputMethodResult::*; + + self.reset(); + + if let Some(ref mut input_method) = self.xmodifiers { + let im = input_method.open_im(xconn); + if let Some(im) = im { + return XModifiers(im); + } else if let Some(ref callback) = callback { + callback(); + } + } + + for input_method in &mut self.fallbacks { + let im = input_method.open_im(xconn); + if let Some(im) = im { + return Fallback(im); + } + } + + Failure + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/ime/mod.rs b/vendor/winit/src/platform_impl/linux/x11/ime/mod.rs new file mode 100644 index 00000000..0a419c8d --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/ime/mod.rs @@ -0,0 +1,233 @@ +// Important: all XIM calls need to happen from the same thread! + +mod callbacks; +mod context; +mod inner; +mod input_method; + +use std::sync::mpsc::{Receiver, Sender}; +use std::sync::Arc; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use self::callbacks::*; +use self::context::ImeContext; +pub use self::context::ImeContextCreationError; +use self::inner::{close_im, ImeInner}; +use self::input_method::PotentialInputMethods; +use super::{ffi, util, XConnection, XError}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum ImeEvent { + Enabled, + Start, + Update(String, usize), + End, + Disabled, +} + +pub type ImeReceiver = Receiver; +pub type ImeSender = Sender; +pub type ImeEventReceiver = Receiver<(ffi::Window, ImeEvent)>; +pub type ImeEventSender = Sender<(ffi::Window, ImeEvent)>; + +/// Request to control XIM handler from the window. +pub enum ImeRequest { + /// Set IME spot position for given `window_id`. + Position(ffi::Window, i16, i16), + + /// Allow IME input for the given `window_id`. + Allow(ffi::Window, bool), +} + +#[derive(Debug)] +pub(crate) enum ImeCreationError { + // Boxed to prevent large error type + OpenFailure(Box), + SetDestroyCallbackFailed(#[allow(dead_code)] XError), +} + +pub(crate) struct Ime { + xconn: Arc, + // The actual meat of this struct is boxed away, since it needs to have a fixed location in + // memory so we can pass a pointer to it around. + inner: Box, +} + +impl Ime { + pub fn new( + xconn: Arc, + event_sender: ImeEventSender, + ) -> Result { + let potential_input_methods = PotentialInputMethods::new(&xconn); + + let (mut inner, client_data) = { + let mut inner = Box::new(ImeInner::new(xconn, potential_input_methods, event_sender)); + let inner_ptr = Box::into_raw(inner); + let client_data = inner_ptr as _; + let destroy_callback = + ffi::XIMCallback { client_data, callback: Some(xim_destroy_callback) }; + inner = unsafe { Box::from_raw(inner_ptr) }; + inner.destroy_callback = destroy_callback; + (inner, client_data) + }; + + let xconn = Arc::clone(&inner.xconn); + + let input_method = inner.potential_input_methods.open_im( + &xconn, + Some(&|| { + let _ = unsafe { set_instantiate_callback(&xconn, client_data) }; + }), + ); + + let is_fallback = input_method.is_fallback(); + if let Some(input_method) = input_method.ok() { + inner.is_fallback = is_fallback; + unsafe { + let result = set_destroy_callback(&xconn, input_method.im, &inner) + .map_err(ImeCreationError::SetDestroyCallbackFailed); + if result.is_err() { + let _ = close_im(&xconn, input_method.im); + } + result?; + } + inner.im = Some(input_method); + Ok(Ime { xconn, inner }) + } else { + Err(ImeCreationError::OpenFailure(Box::new(inner.potential_input_methods))) + } + } + + pub fn is_destroyed(&self) -> bool { + self.inner.is_destroyed + } + + // This pattern is used for various methods here: + // Ok(_) indicates that nothing went wrong internally + // Ok(true) indicates that the action was actually performed + // Ok(false) indicates that the action is not presently applicable + pub fn create_context( + &mut self, + window: ffi::Window, + with_ime: bool, + ) -> Result { + let context = if self.is_destroyed() { + // Create empty entry in map, so that when IME is rebuilt, this window has a context. + None + } else { + let im = self.inner.im.as_ref().unwrap(); + + let context = unsafe { + ImeContext::new( + &self.inner.xconn, + im, + window, + None, + self.inner.event_sender.clone(), + with_ime, + )? + }; + + let event = if context.is_allowed() { ImeEvent::Enabled } else { ImeEvent::Disabled }; + self.inner.event_sender.send((window, event)).expect("Failed to send enabled event"); + + Some(context) + }; + + self.inner.contexts.insert(window, context); + Ok(!self.is_destroyed()) + } + + pub fn get_context(&self, window: ffi::Window) -> Option { + if self.is_destroyed() { + return None; + } + if let Some(Some(context)) = self.inner.contexts.get(&window) { + Some(context.ic) + } else { + None + } + } + + pub fn remove_context(&mut self, window: ffi::Window) -> Result { + if let Some(Some(context)) = self.inner.contexts.remove(&window) { + unsafe { + self.inner.destroy_ic_if_necessary(context.ic)?; + } + Ok(true) + } else { + Ok(false) + } + } + + pub fn focus(&mut self, window: ffi::Window) -> Result { + if self.is_destroyed() { + return Ok(false); + } + if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) { + context.focus(&self.xconn).map(|_| true) + } else { + Ok(false) + } + } + + pub fn unfocus(&mut self, window: ffi::Window) -> Result { + if self.is_destroyed() { + return Ok(false); + } + if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) { + context.unfocus(&self.xconn).map(|_| true) + } else { + Ok(false) + } + } + + pub fn send_xim_spot(&mut self, window: ffi::Window, x: i16, y: i16) { + if self.is_destroyed() { + return; + } + if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) { + context.set_spot(&self.xconn, x as _, y as _); + } + } + + pub fn set_ime_allowed(&mut self, window: ffi::Window, allowed: bool) { + if self.is_destroyed() { + return; + } + + if let Some(&mut Some(ref mut context)) = self.inner.contexts.get_mut(&window) { + if allowed == context.is_allowed() { + return; + } + } + + // Remove context for that window. + let _ = self.remove_context(window); + + // Create new context supporting IME input. + let _ = self.create_context(window, allowed); + } + + pub fn is_ime_allowed(&self, window: ffi::Window) -> bool { + if self.is_destroyed() { + false + } else if let Some(Some(context)) = self.inner.contexts.get(&window) { + context.is_allowed() + } else { + false + } + } +} + +impl Drop for Ime { + fn drop(&mut self) { + unsafe { + let _ = self.inner.destroy_all_contexts_if_necessary(); + let _ = self.inner.close_im_if_necessary(); + } + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/mod.rs b/vendor/winit/src/platform_impl/linux/x11/mod.rs new file mode 100644 index 00000000..f29b314a --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/mod.rs @@ -0,0 +1,1064 @@ +use std::cell::{Cell, RefCell}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::ffi::CStr; +use std::marker::PhantomData; +use std::mem::MaybeUninit; +use std::ops::Deref; +use std::os::raw::*; +use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; +use std::sync::mpsc::{self, Receiver, Sender, TryRecvError}; +use std::sync::{Arc, Weak}; +use std::time::{Duration, Instant}; +use std::{fmt, ptr, slice, str}; + +use calloop::generic::Generic; +use calloop::ping::Ping; +use calloop::{EventLoop as Loop, Readiness}; +use libc::{setlocale, LC_CTYPE}; +use tracing::warn; + +use x11rb::connection::RequestConnection; +use x11rb::errors::{ConnectError, ConnectionError, IdsExhausted, ReplyError}; +use x11rb::protocol::xinput::{self, ConnectionExt as _}; +use x11rb::protocol::xkb; +use x11rb::protocol::xproto::{self, ConnectionExt as _}; +use x11rb::x11_utils::X11Error as LogicalError; +use x11rb::xcb_ffi::ReplyOrIdError; + +use crate::error::{EventLoopError, OsError as RootOsError}; +use crate::event::{Event, StartCause, WindowEvent}; +use crate::event_loop::{ActiveEventLoop as RootAEL, ControlFlow, DeviceEvents, EventLoopClosed}; +use crate::platform::pump_events::PumpStatus; +use crate::platform_impl::common::xkb::Context; +use crate::platform_impl::platform::{min_timeout, WindowId}; +use crate::platform_impl::{ + ActiveEventLoop as PlatformActiveEventLoop, OsError, PlatformCustomCursor, +}; +use crate::window::{CustomCursor as RootCustomCursor, CustomCursorSource, WindowAttributes}; + +mod activation; +mod atoms; +mod dnd; +mod event_processor; +pub mod ffi; +mod ime; +mod monitor; +mod util; +mod window; +mod xdisplay; +mod xsettings; + +pub use util::CustomCursor; + +use atoms::*; +use dnd::{Dnd, DndState}; +use event_processor::{EventProcessor, MAX_MOD_REPLAY_LEN}; +use ime::{Ime, ImeCreationError, ImeReceiver, ImeRequest, ImeSender}; +pub(crate) use monitor::{MonitorHandle, VideoModeHandle}; +use window::UnownedWindow; +pub(crate) use xdisplay::{XConnection, XError, XNotSupported}; + +// Xinput constants not defined in x11rb +const ALL_DEVICES: u16 = 0; +const ALL_MASTER_DEVICES: u16 = 1; +const ICONIC_STATE: u32 = 3; + +/// The underlying x11rb connection that we are using. +type X11rbConnection = x11rb::xcb_ffi::XCBConnection; + +type X11Source = Generic>; + +struct WakeSender { + sender: Sender, + waker: Ping, +} + +impl Clone for WakeSender { + fn clone(&self) -> Self { + Self { sender: self.sender.clone(), waker: self.waker.clone() } + } +} + +impl WakeSender { + pub fn send(&self, t: T) -> Result<(), EventLoopClosed> { + let res = self.sender.send(t).map_err(|e| EventLoopClosed(e.0)); + if res.is_ok() { + self.waker.ping(); + } + res + } +} + +struct PeekableReceiver { + recv: Receiver, + first: Option, +} + +impl PeekableReceiver { + pub fn from_recv(recv: Receiver) -> Self { + Self { recv, first: None } + } + + pub fn has_incoming(&mut self) -> bool { + if self.first.is_some() { + return true; + } + + match self.recv.try_recv() { + Ok(v) => { + self.first = Some(v); + true + }, + Err(TryRecvError::Empty) => false, + Err(TryRecvError::Disconnected) => { + warn!("Channel was disconnected when checking incoming"); + false + }, + } + } + + pub fn try_recv(&mut self) -> Result { + if let Some(first) = self.first.take() { + return Ok(first); + } + self.recv.try_recv() + } +} + +pub struct ActiveEventLoop { + xconn: Arc, + wm_delete_window: xproto::Atom, + net_wm_ping: xproto::Atom, + ime_sender: ImeSender, + control_flow: Cell, + exit: Cell>, + root: xproto::Window, + ime: Option>, + windows: RefCell>>, + redraw_sender: WakeSender, + activation_sender: WakeSender, + device_events: Cell, +} + +pub struct EventLoop { + loop_running: bool, + event_loop: Loop<'static, EventLoopState>, + waker: calloop::ping::Ping, + event_processor: EventProcessor, + redraw_receiver: PeekableReceiver, + user_receiver: PeekableReceiver, + activation_receiver: PeekableReceiver, + user_sender: Sender, + + /// The current state of the event loop. + state: EventLoopState, +} + +type ActivationToken = (WindowId, crate::event_loop::AsyncRequestSerial); + +struct EventLoopState { + /// The latest readiness state for the x11 file descriptor + x11_readiness: Readiness, +} + +pub struct EventLoopProxy { + user_sender: WakeSender, +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + EventLoopProxy { user_sender: self.user_sender.clone() } + } +} + +impl EventLoop { + pub(crate) fn new(xconn: Arc) -> EventLoop { + let root = xconn.default_root().root; + let atoms = xconn.atoms(); + + let wm_delete_window = atoms[WM_DELETE_WINDOW]; + let net_wm_ping = atoms[_NET_WM_PING]; + + let dnd = Dnd::new(Arc::clone(&xconn)) + .expect("Failed to call XInternAtoms when initializing drag and drop"); + + let (ime_sender, ime_receiver) = mpsc::channel(); + let (ime_event_sender, ime_event_receiver) = mpsc::channel(); + // Input methods will open successfully without setting the locale, but it won't be + // possible to actually commit pre-edit sequences. + unsafe { + // Remember default locale to restore it if target locale is unsupported + // by Xlib + let default_locale = setlocale(LC_CTYPE, ptr::null()); + setlocale(LC_CTYPE, b"\0".as_ptr() as *const _); + + // Check if set locale is supported by Xlib. + // If not, calls to some Xlib functions like `XSetLocaleModifiers` + // will fail. + let locale_supported = (xconn.xlib.XSupportsLocale)() == 1; + if !locale_supported { + let unsupported_locale = setlocale(LC_CTYPE, ptr::null()); + warn!( + "Unsupported locale \"{}\". Restoring default locale \"{}\".", + CStr::from_ptr(unsupported_locale).to_string_lossy(), + CStr::from_ptr(default_locale).to_string_lossy() + ); + // Restore default locale + setlocale(LC_CTYPE, default_locale); + } + } + + let ime = Ime::new(Arc::clone(&xconn), ime_event_sender); + if let Err(ImeCreationError::OpenFailure(state)) = ime.as_ref() { + warn!("Failed to open input method: {state:#?}"); + } else if let Err(err) = ime.as_ref() { + warn!("Failed to set input method destruction callback: {err:?}"); + } + + let ime = ime.ok().map(RefCell::new); + + let randr_event_offset = + xconn.select_xrandr_input(root).expect("Failed to query XRandR extension"); + + let xi2ext = xconn + .xcb_connection() + .extension_information(xinput::X11_EXTENSION_NAME) + .expect("Failed to query XInput extension") + .expect("X server missing XInput extension"); + let xkbext = xconn + .xcb_connection() + .extension_information(xkb::X11_EXTENSION_NAME) + .expect("Failed to query XKB extension") + .expect("X server missing XKB extension"); + + // Check for XInput2 support. + xconn + .xcb_connection() + .xinput_xi_query_version(2, 3) + .expect("Failed to send XInput2 query version request") + .reply() + .expect("Error while checking for XInput2 query version reply"); + + xconn.update_cached_wm_info(root); + + // Create an event loop. + let event_loop = + Loop::::try_new().expect("Failed to initialize the event loop"); + let handle = event_loop.handle(); + + // Create the X11 event dispatcher. + let source = X11Source::new( + // SAFETY: xcb owns the FD and outlives the source. + unsafe { BorrowedFd::borrow_raw(xconn.xcb_connection().as_raw_fd()) }, + calloop::Interest::READ, + calloop::Mode::Level, + ); + handle + .insert_source(source, |readiness, _, state| { + state.x11_readiness = readiness; + Ok(calloop::PostAction::Continue) + }) + .expect("Failed to register the X11 event dispatcher"); + + let (waker, waker_source) = + calloop::ping::make_ping().expect("Failed to create event loop waker"); + event_loop + .handle() + .insert_source(waker_source, move |_, _, _| { + // No extra handling is required, we just need to wake-up. + }) + .expect("Failed to register the event loop waker source"); + + // Create a channel for handling redraw requests. + let (redraw_sender, redraw_channel) = mpsc::channel(); + + // Create a channel for sending activation tokens. + let (activation_token_sender, activation_token_channel) = mpsc::channel(); + + // Create a channel for sending user events. + let (user_sender, user_channel) = mpsc::channel(); + + let xkb_context = + Context::from_x11_xkb(xconn.xcb_connection().get_raw_xcb_connection()).unwrap(); + + let mut xmodmap = util::ModifierKeymap::new(); + xmodmap.reload_from_x_connection(&xconn); + + let window_target = ActiveEventLoop { + ime, + root, + control_flow: Cell::new(ControlFlow::default()), + exit: Cell::new(None), + windows: Default::default(), + ime_sender, + xconn, + wm_delete_window, + net_wm_ping, + redraw_sender: WakeSender { + sender: redraw_sender, // not used again so no clone + waker: waker.clone(), + }, + activation_sender: WakeSender { + sender: activation_token_sender, // not used again so no clone + waker: waker.clone(), + }, + device_events: Default::default(), + }; + + // Set initial device event filter. + window_target.update_listen_device_events(true); + + let root_window_target = + RootAEL { p: PlatformActiveEventLoop::X(window_target), _marker: PhantomData }; + + let event_processor = EventProcessor { + target: root_window_target, + dnd, + devices: Default::default(), + randr_event_offset, + ime_receiver, + ime_event_receiver, + xi2ext, + xfiltered_modifiers: VecDeque::with_capacity(MAX_MOD_REPLAY_LEN), + xmodmap, + xkbext, + xkb_context, + num_touch: 0, + held_key_press: None, + first_touch: None, + active_window: None, + modifiers: Default::default(), + is_composing: false, + }; + + // Register for device hotplug events + // (The request buffer is flushed during `init_device`) + let xconn = &EventProcessor::window_target(&event_processor.target).xconn; + + xconn + .select_xinput_events( + root, + ALL_DEVICES, + x11rb::protocol::xinput::XIEventMask::HIERARCHY, + ) + .expect_then_ignore_error("Failed to register for XInput2 device hotplug events"); + + xconn + .select_xkb_events( + 0x100, // Use the "core keyboard device" + xkb::EventType::NEW_KEYBOARD_NOTIFY + | xkb::EventType::MAP_NOTIFY + | xkb::EventType::STATE_NOTIFY, + ) + .unwrap(); + + event_processor.init_device(ALL_DEVICES); + + EventLoop { + loop_running: false, + event_loop, + waker, + event_processor, + redraw_receiver: PeekableReceiver::from_recv(redraw_channel), + activation_receiver: PeekableReceiver::from_recv(activation_token_channel), + user_receiver: PeekableReceiver::from_recv(user_channel), + user_sender, + state: EventLoopState { x11_readiness: Readiness::EMPTY }, + } + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { + user_sender: WakeSender { sender: self.user_sender.clone(), waker: self.waker.clone() }, + } + } + + pub(crate) fn window_target(&self) -> &RootAEL { + &self.event_processor.target + } + + pub fn run_on_demand(&mut self, mut event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &RootAEL), + { + let exit = loop { + match self.pump_events(None, &mut event_handler) { + PumpStatus::Exit(0) => { + break Ok(()); + }, + PumpStatus::Exit(code) => { + break Err(EventLoopError::ExitFailure(code)); + }, + _ => { + continue; + }, + } + }; + + // Applications aren't allowed to carry windows between separate + // `run_on_demand` calls but if they have only just dropped their + // windows we need to make sure those last requests are sent to the + // X Server. + let wt = EventProcessor::window_target(&self.event_processor.target); + wt.x_connection().sync_with_server().map_err(|x_err| { + EventLoopError::Os(os_error!(OsError::XError(Arc::new(X11Error::Xlib(x_err))))) + })?; + + exit + } + + pub fn pump_events(&mut self, timeout: Option, mut callback: F) -> PumpStatus + where + F: FnMut(Event, &RootAEL), + { + if !self.loop_running { + self.loop_running = true; + + // run the initial loop iteration + self.single_iteration(&mut callback, StartCause::Init); + } + + // Consider the possibility that the `StartCause::Init` iteration could + // request to Exit. + if !self.exiting() { + self.poll_events_with_timeout(timeout, &mut callback); + } + if let Some(code) = self.exit_code() { + self.loop_running = false; + + callback(Event::LoopExiting, self.window_target()); + + PumpStatus::Exit(code) + } else { + PumpStatus::Continue + } + } + + fn has_pending(&mut self) -> bool { + self.event_processor.poll() + || self.user_receiver.has_incoming() + || self.redraw_receiver.has_incoming() + } + + pub fn poll_events_with_timeout(&mut self, mut timeout: Option, mut callback: F) + where + F: FnMut(Event, &RootAEL), + { + let start = Instant::now(); + + let has_pending = self.has_pending(); + + timeout = if has_pending { + // If we already have work to do then we don't want to block on the next poll. + Some(Duration::ZERO) + } else { + let control_flow_timeout = match self.control_flow() { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Duration::ZERO), + ControlFlow::WaitUntil(wait_deadline) => { + Some(wait_deadline.saturating_duration_since(start)) + }, + }; + + min_timeout(control_flow_timeout, timeout) + }; + + self.state.x11_readiness = Readiness::EMPTY; + if let Err(error) = + self.event_loop.dispatch(timeout, &mut self.state).map_err(std::io::Error::from) + { + tracing::error!("Failed to poll for events: {error:?}"); + let exit_code = error.raw_os_error().unwrap_or(1); + self.set_exit_code(exit_code); + return; + } + + // NB: `StartCause::Init` is handled as a special case and doesn't need + // to be considered here + let cause = match self.control_flow() { + ControlFlow::Poll => StartCause::Poll, + ControlFlow::Wait => StartCause::WaitCancelled { start, requested_resume: None }, + ControlFlow::WaitUntil(deadline) => { + if Instant::now() < deadline { + StartCause::WaitCancelled { start, requested_resume: Some(deadline) } + } else { + StartCause::ResumeTimeReached { start, requested_resume: deadline } + } + }, + }; + + // False positive / spurious wake ups could lead to us spamming + // redundant iterations of the event loop with no new events to + // dispatch. + // + // If there's no readable event source then we just double check if we + // have any pending `_receiver` events and if not we return without + // running a loop iteration. + // If we don't have any pending `_receiver` + if !self.has_pending() + && !matches!(&cause, StartCause::ResumeTimeReached { .. } | StartCause::Poll) + && timeout.is_none() + { + return; + } + + self.single_iteration(&mut callback, cause); + } + + fn single_iteration(&mut self, callback: &mut F, cause: StartCause) + where + F: FnMut(Event, &RootAEL), + { + callback(Event::NewEvents(cause), &self.event_processor.target); + + // NB: For consistency all platforms must emit a 'resumed' event even though X11 + // applications don't themselves have a formal suspend/resume lifecycle. + if cause == StartCause::Init { + callback(Event::Resumed, &self.event_processor.target); + } + + // Process all pending events + self.drain_events(callback); + + // Empty activation tokens. + while let Ok((window_id, serial)) = self.activation_receiver.try_recv() { + let token = self.event_processor.with_window(window_id.0 as xproto::Window, |window| { + window.generate_activation_token() + }); + + match token { + Some(Ok(token)) => { + let event = Event::WindowEvent { + window_id: crate::window::WindowId(window_id), + event: WindowEvent::ActivationTokenDone { + serial, + token: crate::window::ActivationToken::from_raw(token), + }, + }; + callback(event, &self.event_processor.target) + }, + Some(Err(e)) => { + tracing::error!("Failed to get activation token: {}", e); + }, + None => {}, + } + } + + // Empty the user event buffer + { + while let Ok(event) = self.user_receiver.try_recv() { + callback(Event::UserEvent(event), &self.event_processor.target); + } + } + + // Empty the redraw requests + { + let mut windows = HashSet::new(); + + while let Ok(window_id) = self.redraw_receiver.try_recv() { + windows.insert(window_id); + } + + for window_id in windows { + let window_id = crate::window::WindowId(window_id); + callback( + Event::WindowEvent { window_id, event: WindowEvent::RedrawRequested }, + &self.event_processor.target, + ); + } + } + + // This is always the last event we dispatch before poll again + { + callback(Event::AboutToWait, &self.event_processor.target); + } + } + + fn drain_events(&mut self, callback: &mut F) + where + F: FnMut(Event, &RootAEL), + { + let mut xev = MaybeUninit::uninit(); + + while unsafe { self.event_processor.poll_one_event(xev.as_mut_ptr()) } { + let mut xev = unsafe { xev.assume_init() }; + self.event_processor.process_event(&mut xev, |window_target, event| { + if let Event::WindowEvent { + window_id: crate::window::WindowId(wid), + event: WindowEvent::RedrawRequested, + } = event + { + let window_target = EventProcessor::window_target(window_target); + window_target.redraw_sender.send(wid).unwrap(); + } else { + callback(event, window_target); + } + }); + } + } + + fn control_flow(&self) -> ControlFlow { + let window_target = EventProcessor::window_target(&self.event_processor.target); + window_target.control_flow() + } + + fn exiting(&self) -> bool { + let window_target = EventProcessor::window_target(&self.event_processor.target); + window_target.exiting() + } + + fn set_exit_code(&self, code: i32) { + let window_target = EventProcessor::window_target(&self.event_processor.target); + window_target.set_exit_code(code); + } + + fn exit_code(&self) -> Option { + let window_target = EventProcessor::window_target(&self.event_processor.target); + window_target.exit_code() + } +} + +impl AsFd for EventLoop { + fn as_fd(&self) -> BorrowedFd<'_> { + self.event_loop.as_fd() + } +} + +impl AsRawFd for EventLoop { + fn as_raw_fd(&self) -> RawFd { + self.event_loop.as_raw_fd() + } +} + +impl ActiveEventLoop { + /// Returns the `XConnection` of this events loop. + #[inline] + pub(crate) fn x_connection(&self) -> &Arc { + &self.xconn + } + + pub fn available_monitors(&self) -> impl Iterator { + self.xconn.available_monitors().into_iter().flatten() + } + + pub fn primary_monitor(&self) -> Option { + self.xconn.primary_monitor().ok() + } + + pub(crate) fn create_custom_cursor(&self, cursor: CustomCursorSource) -> RootCustomCursor { + RootCustomCursor { inner: PlatformCustomCursor::X(CustomCursor::new(self, cursor.inner)) } + } + + pub fn listen_device_events(&self, allowed: DeviceEvents) { + self.device_events.set(allowed); + } + + /// Update the device event based on window focus. + pub fn update_listen_device_events(&self, focus: bool) { + let device_events = self.device_events.get() == DeviceEvents::Always + || (focus && self.device_events.get() == DeviceEvents::WhenFocused); + + let mut mask = xinput::XIEventMask::from(0u32); + if device_events { + mask = xinput::XIEventMask::RAW_MOTION + | xinput::XIEventMask::RAW_BUTTON_PRESS + | xinput::XIEventMask::RAW_BUTTON_RELEASE + | xinput::XIEventMask::RAW_KEY_PRESS + | xinput::XIEventMask::RAW_KEY_RELEASE; + } + + self.xconn + .select_xinput_events(self.root, ALL_MASTER_DEVICES, mask) + .expect_then_ignore_error("Failed to update device event filter"); + } + + #[cfg(feature = "rwh_05")] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + let mut display_handle = rwh_05::XlibDisplayHandle::empty(); + display_handle.display = self.xconn.display as *mut _; + display_handle.screen = self.xconn.default_screen_index() as c_int; + display_handle.into() + } + + #[cfg(feature = "rwh_06")] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + let display_handle = rwh_06::XlibDisplayHandle::new( + // SAFETY: display will never be null + Some( + std::ptr::NonNull::new(self.xconn.display as *mut _) + .expect("X11 display should never be null"), + ), + self.xconn.default_screen_index() as c_int, + ); + Ok(display_handle.into()) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.control_flow.set(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub(crate) fn exit(&self) { + self.exit.set(Some(0)) + } + + pub(crate) fn clear_exit(&self) { + self.exit.set(None) + } + + pub(crate) fn exiting(&self) -> bool { + self.exit.get().is_some() + } + + pub(crate) fn set_exit_code(&self, code: i32) { + self.exit.set(Some(code)) + } + + pub(crate) fn exit_code(&self) -> Option { + self.exit.get() + } +} + +impl EventLoopProxy { + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.user_sender.send(event).map_err(|e| EventLoopClosed(e.0)) + } +} + +struct DeviceInfo<'a> { + xconn: &'a XConnection, + info: *const ffi::XIDeviceInfo, + count: usize, +} + +impl<'a> DeviceInfo<'a> { + fn get(xconn: &'a XConnection, device: c_int) -> Option { + unsafe { + let mut count = 0; + let info = (xconn.xinput2.XIQueryDevice)(xconn.display, device, &mut count); + xconn.check_errors().ok()?; + + if info.is_null() || count == 0 { + None + } else { + Some(DeviceInfo { xconn, info, count: count as usize }) + } + } + } +} + +impl Drop for DeviceInfo<'_> { + fn drop(&mut self) { + assert!(!self.info.is_null()); + unsafe { (self.xconn.xinput2.XIFreeDeviceInfo)(self.info as *mut _) }; + } +} + +impl Deref for DeviceInfo<'_> { + type Target = [ffi::XIDeviceInfo]; + + fn deref(&self) -> &Self::Target { + unsafe { slice::from_raw_parts(self.info, self.count) } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId(xinput::DeviceId); + +impl DeviceId { + #[allow(unused)] + pub const fn dummy() -> Self { + DeviceId(0) + } +} + +pub(crate) struct Window(Arc); + +impl Deref for Window { + type Target = UnownedWindow; + + #[inline] + fn deref(&self) -> &UnownedWindow { + &self.0 + } +} + +impl Window { + pub(crate) fn new( + event_loop: &ActiveEventLoop, + attribs: WindowAttributes, + ) -> Result { + let window = Arc::new(UnownedWindow::new(event_loop, attribs)?); + event_loop.windows.borrow_mut().insert(window.id(), Arc::downgrade(&window)); + Ok(Window(window)) + } +} + +impl Drop for Window { + fn drop(&mut self) { + let window = self.deref(); + let xconn = &window.xconn; + + if let Ok(c) = xconn.xcb_connection().destroy_window(window.id().0 as xproto::Window) { + c.ignore_error(); + } + } +} + +/// Generic sum error type for X11 errors. +#[derive(Debug)] +pub enum X11Error { + /// An error from the Xlib library. + Xlib(XError), + + /// An error that occurred while trying to connect to the X server. + Connect(ConnectError), + + /// An error that occurred over the connection medium. + Connection(ConnectionError), + + /// An error that occurred logically on the X11 end. + X11(LogicalError), + + /// The XID range has been exhausted. + XidsExhausted(IdsExhausted), + + /// Got `null` from an Xlib function without a reason. + UnexpectedNull(&'static str), + + /// Got an invalid activation token. + InvalidActivationToken(Vec), + + /// An extension that we rely on is not available. + MissingExtension(&'static str), + + /// Could not find a matching X11 visual for this visualid + NoSuchVisual(xproto::Visualid), + + /// Unable to parse xsettings. + XsettingsParse(xsettings::ParserError), + + /// Failed to get property. + GetProperty(util::GetPropertyError), +} + +impl fmt::Display for X11Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + X11Error::Xlib(e) => write!(f, "Xlib error: {e}"), + X11Error::Connect(e) => write!(f, "X11 connection error: {e}"), + X11Error::Connection(e) => write!(f, "X11 connection error: {e}"), + X11Error::XidsExhausted(e) => write!(f, "XID range exhausted: {e}"), + X11Error::GetProperty(e) => write!(f, "Failed to get X property {e}"), + X11Error::X11(e) => write!(f, "X11 error: {e:?}"), + X11Error::UnexpectedNull(s) => write!(f, "Xlib function returned null: {s}"), + X11Error::InvalidActivationToken(s) => write!( + f, + "Invalid activation token: {}", + std::str::from_utf8(s).unwrap_or("") + ), + X11Error::MissingExtension(s) => write!(f, "Missing X11 extension: {s}"), + X11Error::NoSuchVisual(visualid) => { + write!(f, "Could not find a matching X11 visual for ID `{visualid:x}`") + }, + X11Error::XsettingsParse(err) => { + write!(f, "Failed to parse xsettings: {err:?}") + }, + } + } +} + +impl std::error::Error for X11Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + X11Error::Xlib(e) => Some(e), + X11Error::Connect(e) => Some(e), + X11Error::Connection(e) => Some(e), + X11Error::XidsExhausted(e) => Some(e), + _ => None, + } + } +} + +impl From for X11Error { + fn from(e: XError) -> Self { + X11Error::Xlib(e) + } +} + +impl From for X11Error { + fn from(e: ConnectError) -> Self { + X11Error::Connect(e) + } +} + +impl From for X11Error { + fn from(e: ConnectionError) -> Self { + X11Error::Connection(e) + } +} + +impl From for X11Error { + fn from(e: LogicalError) -> Self { + X11Error::X11(e) + } +} + +impl From for X11Error { + fn from(value: ReplyError) -> Self { + match value { + ReplyError::ConnectionError(e) => e.into(), + ReplyError::X11Error(e) => e.into(), + } + } +} + +impl From for X11Error { + fn from(value: ime::ImeContextCreationError) -> Self { + match value { + ime::ImeContextCreationError::XError(e) => e.into(), + ime::ImeContextCreationError::Null => Self::UnexpectedNull("XOpenIM"), + } + } +} + +impl From for X11Error { + fn from(value: ReplyOrIdError) -> Self { + match value { + ReplyOrIdError::ConnectionError(e) => e.into(), + ReplyOrIdError::X11Error(e) => e.into(), + ReplyOrIdError::IdsExhausted => Self::XidsExhausted(IdsExhausted), + } + } +} + +impl From for X11Error { + fn from(value: xsettings::ParserError) -> Self { + Self::XsettingsParse(value) + } +} + +impl From for X11Error { + fn from(value: util::GetPropertyError) -> Self { + Self::GetProperty(value) + } +} + +/// Type alias for a void cookie. +type VoidCookie<'a> = x11rb::cookie::VoidCookie<'a, X11rbConnection>; + +/// Extension trait for `Result`. +trait CookieResultExt { + /// Unwrap the send error and ignore the result. + fn expect_then_ignore_error(self, msg: &str); +} + +impl CookieResultExt for Result, E> { + fn expect_then_ignore_error(self, msg: &str) { + self.expect(msg).ignore_error() + } +} + +fn mkwid(w: xproto::Window) -> crate::window::WindowId { + crate::window::WindowId(crate::platform_impl::platform::WindowId(w as _)) +} +fn mkdid(w: xinput::DeviceId) -> crate::event::DeviceId { + crate::event::DeviceId(crate::platform_impl::DeviceId::X(DeviceId(w))) +} + +#[derive(Debug)] +pub struct Device { + _name: String, + scroll_axes: Vec<(i32, ScrollAxis)>, + // For master devices, this is the paired device (pointer <-> keyboard). + // For slave devices, this is the master. + attachment: c_int, +} + +#[derive(Debug, Copy, Clone)] +struct ScrollAxis { + increment: f64, + orientation: ScrollOrientation, + position: f64, +} + +#[derive(Debug, Copy, Clone)] +enum ScrollOrientation { + Vertical, + Horizontal, +} + +impl Device { + fn new(info: &ffi::XIDeviceInfo) -> Self { + let name = unsafe { CStr::from_ptr(info.name).to_string_lossy() }; + let mut scroll_axes = Vec::new(); + + if Device::physical_device(info) { + // Identify scroll axes + for &class_ptr in Device::classes(info) { + let ty = unsafe { (*class_ptr)._type }; + if ty == ffi::XIScrollClass { + let info = unsafe { &*(class_ptr as *const ffi::XIScrollClassInfo) }; + scroll_axes.push((info.number, ScrollAxis { + increment: info.increment, + orientation: match info.scroll_type { + ffi::XIScrollTypeHorizontal => ScrollOrientation::Horizontal, + ffi::XIScrollTypeVertical => ScrollOrientation::Vertical, + _ => unreachable!(), + }, + position: 0.0, + })); + } + } + } + + let mut device = + Device { _name: name.into_owned(), scroll_axes, attachment: info.attachment }; + device.reset_scroll_position(info); + device + } + + fn reset_scroll_position(&mut self, info: &ffi::XIDeviceInfo) { + if Device::physical_device(info) { + for &class_ptr in Device::classes(info) { + let ty = unsafe { (*class_ptr)._type }; + if ty == ffi::XIValuatorClass { + let info = unsafe { &*(class_ptr as *const ffi::XIValuatorClassInfo) }; + if let Some(&mut (_, ref mut axis)) = + self.scroll_axes.iter_mut().find(|&&mut (axis, _)| axis == info.number) + { + axis.position = info.value; + } + } + } + } + } + + #[inline] + fn physical_device(info: &ffi::XIDeviceInfo) -> bool { + info._use == ffi::XISlaveKeyboard + || info._use == ffi::XISlavePointer + || info._use == ffi::XIFloatingSlave + } + + #[inline] + fn classes(info: &ffi::XIDeviceInfo) -> &[*const ffi::XIAnyClassInfo] { + unsafe { + slice::from_raw_parts( + info.classes as *const *const ffi::XIAnyClassInfo, + info.num_classes as usize, + ) + } + } +} + +/// Convert the raw X11 representation for a 32-bit floating point to a double. +#[inline] +fn xinput_fp1616_to_float(fp: xinput::Fp1616) -> f64 { + (fp as f64) / ((1 << 16) as f64) +} diff --git a/vendor/winit/src/platform_impl/linux/x11/monitor.rs b/vendor/winit/src/platform_impl/linux/x11/monitor.rs new file mode 100644 index 00000000..1964bc93 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/monitor.rs @@ -0,0 +1,355 @@ +use super::{util, X11Error, XConnection}; +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::platform_impl::VideoModeHandle as PlatformVideoModeHandle; +use x11rb::connection::RequestConnection; +use x11rb::protocol::randr::{self, ConnectionExt as _}; +use x11rb::protocol::xproto; + +// Used for testing. This should always be committed as false. +const DISABLE_MONITOR_LIST_CACHING: bool = false; + +impl XConnection { + pub fn invalidate_cached_monitor_list(&self) -> Option> { + // We update this lazily. + self.monitor_handles.lock().unwrap().take() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct VideoModeHandle { + pub(crate) size: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate_millihertz: u32, + pub(crate) native_mode: randr::Mode, + pub(crate) monitor: Option, +} + +impl VideoModeHandle { + #[inline] + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + #[inline] + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + #[inline] + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + #[inline] + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone().unwrap() + } +} + +#[derive(Debug, Clone)] +pub struct MonitorHandle { + /// The actual id + pub(crate) id: randr::Crtc, + /// The name of the monitor + pub(crate) name: String, + /// The size of the monitor + dimensions: (u32, u32), + /// The position of the monitor in the X screen + position: (i32, i32), + /// If the monitor is the primary one + primary: bool, + /// The refresh rate used by monitor. + refresh_rate_millihertz: Option, + /// The DPI scale factor + pub(crate) scale_factor: f64, + /// Used to determine which windows are on this monitor + pub(crate) rect: util::AaRect, + /// Supported video modes on this monitor + video_modes: Vec, +} + +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for MonitorHandle {} + +impl PartialOrd for MonitorHandle { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for MonitorHandle { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.id.cmp(&other.id) + } +} + +impl std::hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +#[inline] +pub fn mode_refresh_rate_millihertz(mode: &randr::ModeInfo) -> Option { + if mode.dot_clock > 0 && mode.htotal > 0 && mode.vtotal > 0 { + #[allow(clippy::unnecessary_cast)] + Some((mode.dot_clock as u64 * 1000 / (mode.htotal as u64 * mode.vtotal as u64)) as u32) + } else { + None + } +} + +impl MonitorHandle { + fn new( + xconn: &XConnection, + resources: &ScreenResources, + id: randr::Crtc, + crtc: &randr::GetCrtcInfoReply, + primary: bool, + ) -> Option { + let (name, scale_factor, video_modes) = xconn.get_output_info(resources, crtc)?; + let dimensions = (crtc.width as u32, crtc.height as u32); + let position = (crtc.x as i32, crtc.y as i32); + + // Get the refresh rate of the current video mode. + let current_mode = crtc.mode; + let screen_modes = resources.modes(); + let refresh_rate_millihertz = screen_modes + .iter() + .find(|mode| mode.id == current_mode) + .and_then(mode_refresh_rate_millihertz); + + let rect = util::AaRect::new(position, dimensions); + + Some(MonitorHandle { + id, + name, + refresh_rate_millihertz, + scale_factor, + dimensions, + position, + primary, + rect, + video_modes, + }) + } + + pub fn dummy() -> Self { + MonitorHandle { + id: 0, + name: "".into(), + scale_factor: 1.0, + dimensions: (1, 1), + position: (0, 0), + refresh_rate_millihertz: None, + primary: true, + rect: util::AaRect::new((0, 0), (1, 1)), + video_modes: Vec::new(), + } + } + + pub(crate) fn is_dummy(&self) -> bool { + // Zero is an invalid XID value; no real monitor will have it + self.id == 0 + } + + pub fn name(&self) -> Option { + Some(self.name.clone()) + } + + #[inline] + pub fn native_identifier(&self) -> u32 { + self.id as _ + } + + pub fn size(&self) -> PhysicalSize { + self.dimensions.into() + } + + pub fn position(&self) -> PhysicalPosition { + self.position.into() + } + + pub fn refresh_rate_millihertz(&self) -> Option { + self.refresh_rate_millihertz + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + self.scale_factor + } + + #[inline] + pub fn video_modes(&self) -> impl Iterator { + let monitor = self.clone(); + self.video_modes.clone().into_iter().map(move |mut x| { + x.monitor = Some(monitor.clone()); + PlatformVideoModeHandle::X(x) + }) + } +} + +impl XConnection { + pub fn get_monitor_for_window( + &self, + window_rect: Option, + ) -> Result { + let monitors = self.available_monitors()?; + + if monitors.is_empty() { + // Return a dummy monitor to avoid panicking + return Ok(MonitorHandle::dummy()); + } + + let default = monitors.first().unwrap(); + + let window_rect = match window_rect { + Some(rect) => rect, + None => return Ok(default.to_owned()), + }; + + let mut largest_overlap = 0; + let mut matched_monitor = default; + for monitor in &monitors { + let overlapping_area = window_rect.get_overlapping_area(&monitor.rect); + if overlapping_area > largest_overlap { + largest_overlap = overlapping_area; + matched_monitor = monitor; + } + } + + Ok(matched_monitor.to_owned()) + } + + fn query_monitor_list(&self) -> Result, X11Error> { + let root = self.default_root(); + let resources = + ScreenResources::from_connection(self.xcb_connection(), root, self.randr_version())?; + + // Pipeline all of the get-crtc requests. + let mut crtc_cookies = Vec::with_capacity(resources.crtcs().len()); + for &crtc in resources.crtcs() { + crtc_cookies + .push(self.xcb_connection().randr_get_crtc_info(crtc, x11rb::CURRENT_TIME)?); + } + + // Do this here so we do all of our requests in one shot. + let primary = self.xcb_connection().randr_get_output_primary(root.root)?.reply()?.output; + + let mut crtc_infos = Vec::with_capacity(crtc_cookies.len()); + for cookie in crtc_cookies { + let reply = cookie.reply()?; + crtc_infos.push(reply); + } + + let mut has_primary = false; + let mut available_monitors = Vec::with_capacity(resources.crtcs().len()); + for (crtc_id, crtc) in resources.crtcs().iter().zip(crtc_infos.iter()) { + if crtc.width == 0 || crtc.height == 0 || crtc.outputs.is_empty() { + continue; + } + + let is_primary = crtc.outputs[0] == primary; + has_primary |= is_primary; + let monitor = MonitorHandle::new(self, &resources, *crtc_id, crtc, is_primary); + available_monitors.extend(monitor); + } + + // If we don't have a primary monitor, just pick one ourselves! + if !has_primary { + if let Some(ref mut fallback) = available_monitors.first_mut() { + // Setting this here will come in handy if we ever add an `is_primary` method. + fallback.primary = true; + } + } + + Ok(available_monitors) + } + + pub fn available_monitors(&self) -> Result, X11Error> { + let mut monitors_lock = self.monitor_handles.lock().unwrap(); + match *monitors_lock { + Some(ref monitors) => Ok(monitors.clone()), + None => { + let monitors = self.query_monitor_list()?; + if !DISABLE_MONITOR_LIST_CACHING { + *monitors_lock = Some(monitors.clone()); + } + Ok(monitors) + }, + } + } + + #[inline] + pub fn primary_monitor(&self) -> Result { + Ok(self + .available_monitors()? + .into_iter() + .find(|monitor| monitor.primary) + .unwrap_or_else(MonitorHandle::dummy)) + } + + pub fn select_xrandr_input(&self, root: xproto::Window) -> Result { + use randr::NotifyMask; + + // Get extension info. + let info = self + .xcb_connection() + .extension_information(randr::X11_EXTENSION_NAME)? + .ok_or(X11Error::MissingExtension(randr::X11_EXTENSION_NAME))?; + + // Select input data. + let event_mask = + NotifyMask::CRTC_CHANGE | NotifyMask::OUTPUT_PROPERTY | NotifyMask::SCREEN_CHANGE; + self.xcb_connection().randr_select_input(root, event_mask)?; + + Ok(info.first_event) + } +} + +pub struct ScreenResources { + /// List of attached modes. + modes: Vec, + + /// List of attached CRTCs. + crtcs: Vec, +} + +impl ScreenResources { + pub(crate) fn modes(&self) -> &[randr::ModeInfo] { + &self.modes + } + + pub(crate) fn crtcs(&self) -> &[randr::Crtc] { + &self.crtcs + } + + pub(crate) fn from_connection( + conn: &impl x11rb::connection::Connection, + root: &x11rb::protocol::xproto::Screen, + (major_version, minor_version): (u32, u32), + ) -> Result { + if (major_version == 1 && minor_version >= 3) || major_version > 1 { + let reply = conn.randr_get_screen_resources_current(root.root)?.reply()?; + Ok(Self::from_get_screen_resources_current_reply(reply)) + } else { + let reply = conn.randr_get_screen_resources(root.root)?.reply()?; + Ok(Self::from_get_screen_resources_reply(reply)) + } + } + + pub(crate) fn from_get_screen_resources_reply(reply: randr::GetScreenResourcesReply) -> Self { + Self { modes: reply.modes, crtcs: reply.crtcs } + } + + pub(crate) fn from_get_screen_resources_current_reply( + reply: randr::GetScreenResourcesCurrentReply, + ) -> Self { + Self { modes: reply.modes, crtcs: reply.crtcs } + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/tests/xsettings.dat b/vendor/winit/src/platform_impl/linux/x11/tests/xsettings.dat new file mode 100644 index 00000000..e04eeb14 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/tests/xsettings.dat @@ -0,0 +1 @@ +0x6c,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x22,0x00,0x00,0x00,0x00,0x00,0x0b,0x00,0x58,0x66,0x74,0x2f,0x48,0x69,0x6e,0x74,0x69,0x6e,0x67,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x14,0x00,0x47,0x74,0x6b,0x2f,0x44,0x69,0x61,0x6c,0x6f,0x67,0x73,0x55,0x73,0x65,0x48,0x65,0x61,0x64,0x65,0x72,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x0c,0x00,0x47,0x74,0x6b,0x2f,0x46,0x6f,0x6e,0x74,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x0b,0x00,0x00,0x00,0x4e,0x6f,0x74,0x6f,0x20,0x53,0x61,0x6e,0x73,0x20,0x39,0x00,0x01,0x00,0x0d,0x00,0x58,0x66,0x74,0x2f,0x4c,0x63,0x64,0x66,0x69,0x6c,0x74,0x65,0x72,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0a,0x00,0x00,0x00,0x6c,0x63,0x64,0x64,0x65,0x66,0x61,0x75,0x6c,0x74,0x00,0x00,0x01,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x4b,0x65,0x79,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x0d,0x00,0x58,0x66,0x74,0x2f,0x48,0x69,0x6e,0x74,0x53,0x74,0x79,0x6c,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0a,0x00,0x00,0x00,0x68,0x69,0x6e,0x74,0x73,0x6c,0x69,0x67,0x68,0x74,0x00,0x00,0x01,0x00,0x11,0x00,0x4e,0x65,0x74,0x2f,0x49,0x63,0x6f,0x6e,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x14,0x00,0x00,0x00,0x65,0x6c,0x65,0x6d,0x65,0x6e,0x74,0x61,0x72,0x79,0x2d,0x78,0x66,0x63,0x65,0x2d,0x64,0x61,0x72,0x6b,0x00,0x00,0x0d,0x00,0x58,0x66,0x74,0x2f,0x41,0x6e,0x74,0x69,0x61,0x6c,0x69,0x61,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x00,0x08,0x00,0x58,0x66,0x74,0x2f,0x52,0x47,0x42,0x41,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x72,0x67,0x62,0x00,0x00,0x00,0x13,0x00,0x4e,0x65,0x74,0x2f,0x43,0x75,0x72,0x73,0x6f,0x72,0x42,0x6c,0x69,0x6e,0x6b,0x54,0x69,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0xb0,0x04,0x00,0x00,0x00,0x00,0x13,0x00,0x47,0x74,0x6b,0x2f,0x43,0x75,0x72,0x73,0x6f,0x72,0x54,0x68,0x65,0x6d,0x65,0x53,0x69,0x7a,0x65,0x00,0x00,0x00,0x00,0x00,0x18,0x00,0x00,0x00,0x01,0x00,0x15,0x00,0x4e,0x65,0x74,0x2f,0x46,0x61,0x6c,0x6c,0x62,0x61,0x63,0x6b,0x49,0x63,0x6f,0x6e,0x54,0x68,0x65,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x67,0x6e,0x6f,0x6d,0x65,0x00,0x00,0x00,0x01,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x54,0x6f,0x6f,0x6c,0x62,0x61,0x72,0x53,0x74,0x79,0x6c,0x65,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x69,0x63,0x6f,0x6e,0x73,0x00,0x00,0x00,0x01,0x00,0x12,0x00,0x4e,0x65,0x74,0x2f,0x53,0x6f,0x75,0x6e,0x64,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0x00,0x00,0x00,0x64,0x65,0x66,0x61,0x75,0x6c,0x74,0x00,0x00,0x00,0x15,0x00,0x4e,0x65,0x74,0x2f,0x45,0x6e,0x61,0x62,0x6c,0x65,0x45,0x76,0x65,0x6e,0x74,0x53,0x6f,0x75,0x6e,0x64,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0f,0x00,0x4e,0x65,0x74,0x2f,0x43,0x75,0x72,0x73,0x6f,0x72,0x42,0x6c,0x69,0x6e,0x6b,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x43,0x6f,0x6c,0x6f,0x72,0x50,0x61,0x6c,0x65,0x74,0x74,0x65,0x00,0x00,0x00,0x00,0x94,0x00,0x00,0x00,0x62,0x6c,0x61,0x63,0x6b,0x3a,0x77,0x68,0x69,0x74,0x65,0x3a,0x67,0x72,0x61,0x79,0x35,0x30,0x3a,0x72,0x65,0x64,0x3a,0x70,0x75,0x72,0x70,0x6c,0x65,0x3a,0x62,0x6c,0x75,0x65,0x3a,0x6c,0x69,0x67,0x68,0x74,0x20,0x62,0x6c,0x75,0x65,0x3a,0x67,0x72,0x65,0x65,0x6e,0x3a,0x79,0x65,0x6c,0x6c,0x6f,0x77,0x3a,0x6f,0x72,0x61,0x6e,0x67,0x65,0x3a,0x6c,0x61,0x76,0x65,0x6e,0x64,0x65,0x72,0x3a,0x62,0x72,0x6f,0x77,0x6e,0x3a,0x67,0x6f,0x6c,0x64,0x65,0x6e,0x72,0x6f,0x64,0x34,0x3a,0x64,0x6f,0x64,0x67,0x65,0x72,0x20,0x62,0x6c,0x75,0x65,0x3a,0x70,0x69,0x6e,0x6b,0x3a,0x6c,0x69,0x67,0x68,0x74,0x20,0x67,0x72,0x65,0x65,0x6e,0x3a,0x67,0x72,0x61,0x79,0x31,0x30,0x3a,0x67,0x72,0x61,0x79,0x33,0x30,0x3a,0x67,0x72,0x61,0x79,0x37,0x35,0x3a,0x67,0x72,0x61,0x79,0x39,0x30,0x00,0x00,0x13,0x00,0x4e,0x65,0x74,0x2f,0x44,0x6f,0x75,0x62,0x6c,0x65,0x43,0x6c,0x69,0x63,0x6b,0x54,0x69,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x90,0x01,0x00,0x00,0x00,0x00,0x13,0x00,0x47,0x74,0x6b,0x2f,0x43,0x61,0x6e,0x43,0x68,0x61,0x6e,0x67,0x65,0x41,0x63,0x63,0x65,0x6c,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x4d,0x65,0x6e,0x75,0x42,0x61,0x72,0x41,0x63,0x63,0x65,0x6c,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x46,0x31,0x30,0x00,0x01,0x00,0x0d,0x00,0x4e,0x65,0x74,0x2f,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x00,0x00,0x47,0x72,0x65,0x79,0x62,0x69,0x72,0x64,0x01,0x00,0x17,0x00,0x47,0x74,0x6b,0x2f,0x54,0x69,0x74,0x6c,0x65,0x62,0x61,0x72,0x4d,0x69,0x64,0x64,0x6c,0x65,0x43,0x6c,0x69,0x63,0x6b,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x6c,0x6f,0x77,0x65,0x72,0x00,0x00,0x00,0x00,0x00,0x10,0x00,0x47,0x74,0x6b,0x2f,0x42,0x75,0x74,0x74,0x6f,0x6e,0x49,0x6d,0x61,0x67,0x65,0x73,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x17,0x00,0x4e,0x65,0x74,0x2f,0x44,0x6f,0x75,0x62,0x6c,0x65,0x43,0x6c,0x69,0x63,0x6b,0x44,0x69,0x73,0x74,0x61,0x6e,0x63,0x65,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x01,0x00,0x15,0x00,0x47,0x74,0x6b,0x2f,0x4d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x46,0x6f,0x6e,0x74,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0c,0x00,0x00,0x00,0x4d,0x6f,0x6e,0x6f,0x73,0x70,0x61,0x63,0x65,0x20,0x31,0x30,0x00,0x00,0x07,0x00,0x58,0x66,0x74,0x2f,0x44,0x50,0x49,0x00,0x02,0x00,0x00,0x00,0x00,0x80,0x01,0x00,0x01,0x00,0x13,0x00,0x47,0x74,0x6b,0x2f,0x43,0x75,0x72,0x73,0x6f,0x72,0x54,0x68,0x65,0x6d,0x65,0x4e,0x61,0x6d,0x65,0x00,0x00,0x00,0x00,0x00,0x09,0x00,0x00,0x00,0x44,0x4d,0x5a,0x2d,0x57,0x68,0x69,0x74,0x65,0x00,0x00,0x00,0x00,0x00,0x13,0x00,0x47,0x74,0x6b,0x2f,0x54,0x6f,0x6f,0x6c,0x62,0x61,0x72,0x49,0x63,0x6f,0x6e,0x53,0x69,0x7a,0x65,0x00,0x00,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x14,0x00,0x4e,0x65,0x74,0x2f,0x44,0x6e,0x64,0x44,0x72,0x61,0x67,0x54,0x68,0x72,0x65,0x73,0x68,0x6f,0x6c,0x64,0x00,0x00,0x00,0x00,0x08,0x00,0x00,0x00,0x01,0x00,0x14,0x00,0x47,0x74,0x6b,0x2f,0x44,0x65,0x63,0x6f,0x72,0x61,0x74,0x69,0x6f,0x6e,0x4c,0x61,0x79,0x6f,0x75,0x74,0x00,0x00,0x00,0x00,0x1c,0x00,0x00,0x00,0x6d,0x65,0x6e,0x75,0x3a,0x6d,0x69,0x6e,0x69,0x6d,0x69,0x7a,0x65,0x2c,0x6d,0x61,0x78,0x69,0x6d,0x69,0x7a,0x65,0x2c,0x63,0x6c,0x6f,0x73,0x65,0x00,0x00,0x1d,0x00,0x4e,0x65,0x74,0x2f,0x45,0x6e,0x61,0x62,0x6c,0x65,0x49,0x6e,0x70,0x75,0x74,0x46,0x65,0x65,0x64,0x62,0x61,0x63,0x6b,0x53,0x6f,0x75,0x6e,0x64,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x17,0x00,0x47,0x64,0x6b,0x2f,0x57,0x69,0x6e,0x64,0x6f,0x77,0x53,0x63,0x61,0x6c,0x69,0x6e,0x67,0x46,0x61,0x63,0x74,0x6f,0x72,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x00,0x0d,0x00,0x47,0x74,0x6b,0x2f,0x49,0x63,0x6f,0x6e,0x53,0x69,0x7a,0x65,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10,0x00,0x00,0x00,0x67,0x74,0x6b,0x2d,0x62,0x75,0x74,0x74,0x6f,0x6e,0x3d,0x31,0x36,0x2c,0x31,0x36,0x00,0x00,0x0e,0x00,0x47,0x74,0x6b,0x2f,0x4d,0x65,0x6e,0x75,0x49,0x6d,0x61,0x67,0x65,0x73,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00 diff --git a/vendor/winit/src/platform_impl/linux/x11/util/client_msg.rs b/vendor/winit/src/platform_impl/linux/x11/util/client_msg.rs new file mode 100644 index 00000000..cf551776 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/util/client_msg.rs @@ -0,0 +1,31 @@ +use super::*; +use x11rb::x11_utils::Serialize; + +impl XConnection { + pub fn send_client_msg( + &self, + window: xproto::Window, // The window this is "about"; not necessarily this window + target_window: xproto::Window, // The window we're sending to + message_type: xproto::Atom, + event_mask: Option, + data: impl Into, + ) -> Result, X11Error> { + let event = xproto::ClientMessageEvent { + response_type: xproto::CLIENT_MESSAGE_EVENT, + window, + format: 32, + data: data.into(), + sequence: 0, + type_: message_type, + }; + + self.xcb_connection() + .send_event( + false, + target_window, + event_mask.unwrap_or(xproto::EventMask::NO_EVENT), + event.serialize(), + ) + .map_err(Into::into) + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/util/cookie.rs b/vendor/winit/src/platform_impl/linux/x11/util/cookie.rs new file mode 100644 index 00000000..ccdfa8cd --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/util/cookie.rs @@ -0,0 +1,55 @@ +use std::ffi::c_int; +use std::sync::Arc; + +use x11_dl::xlib::{self, XEvent, XGenericEventCookie}; + +use crate::platform_impl::x11::XConnection; + +/// XEvents of type GenericEvent store their actual data in an XGenericEventCookie data structure. +/// This is a wrapper to extract the cookie from a GenericEvent XEvent and release the cookie data +/// once it has been processed +pub struct GenericEventCookie { + cookie: XGenericEventCookie, + xconn: Arc, +} + +impl GenericEventCookie { + pub fn from_event(xconn: Arc, event: XEvent) -> Option { + unsafe { + let mut cookie: XGenericEventCookie = From::from(event); + if (xconn.xlib.XGetEventData)(xconn.display, &mut cookie) == xlib::True { + Some(GenericEventCookie { cookie, xconn }) + } else { + None + } + } + } + + #[inline] + pub fn extension(&self) -> u8 { + self.cookie.extension as u8 + } + + #[inline] + pub fn evtype(&self) -> c_int { + self.cookie.evtype + } + + /// Borrow inner event data as `&T`. + /// + /// ## SAFETY + /// + /// The caller must ensure that the event has the `T` inside of it. + #[inline] + pub unsafe fn as_event(&self) -> &T { + unsafe { &*(self.cookie.data as *const _) } + } +} + +impl Drop for GenericEventCookie { + fn drop(&mut self) { + unsafe { + (self.xconn.xlib.XFreeEventData)(self.xconn.display, &mut self.cookie); + } + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/util/cursor.rs b/vendor/winit/src/platform_impl/linux/x11/util/cursor.rs new file mode 100644 index 00000000..169748d1 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/util/cursor.rs @@ -0,0 +1,174 @@ +use std::ffi::CString; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; +use std::{iter, slice}; + +use x11rb::connection::Connection; + +use crate::platform_impl::PlatformCustomCursorSource; +use crate::window::CursorIcon; + +use super::super::ActiveEventLoop; +use super::*; + +impl XConnection { + pub fn set_cursor_icon(&self, window: xproto::Window, cursor: Option) { + let cursor = *self + .cursor_cache + .lock() + .unwrap() + .entry(cursor) + .or_insert_with(|| self.get_cursor(cursor)); + + self.update_cursor(window, cursor).expect("Failed to set cursor"); + } + + pub(crate) fn set_custom_cursor(&self, window: xproto::Window, cursor: &CustomCursor) { + self.update_cursor(window, cursor.inner.cursor).expect("Failed to set cursor"); + } + + fn create_empty_cursor(&self) -> ffi::Cursor { + let data = 0; + let pixmap = unsafe { + let screen = (self.xlib.XDefaultScreen)(self.display); + let window = (self.xlib.XRootWindow)(self.display, screen); + (self.xlib.XCreateBitmapFromData)(self.display, window, &data, 1, 1) + }; + + if pixmap == 0 { + panic!("failed to allocate pixmap for cursor"); + } + + unsafe { + // We don't care about this color, since it only fills bytes + // in the pixmap which are not 0 in the mask. + let mut dummy_color = MaybeUninit::uninit(); + let cursor = (self.xlib.XCreatePixmapCursor)( + self.display, + pixmap, + pixmap, + dummy_color.as_mut_ptr(), + dummy_color.as_mut_ptr(), + 0, + 0, + ); + (self.xlib.XFreePixmap)(self.display, pixmap); + + cursor + } + } + + fn get_cursor(&self, cursor: Option) -> ffi::Cursor { + let cursor = match cursor { + Some(cursor) => cursor, + None => return self.create_empty_cursor(), + }; + + let mut xcursor = 0; + for &name in iter::once(&cursor.name()).chain(cursor.alt_names().iter()) { + let name = CString::new(name).unwrap(); + xcursor = unsafe { + (self.xcursor.XcursorLibraryLoadCursor)( + self.display, + name.as_ptr() as *const c_char, + ) + }; + + if xcursor != 0 { + break; + } + } + + xcursor + } + + fn update_cursor(&self, window: xproto::Window, cursor: ffi::Cursor) -> Result<(), X11Error> { + self.xcb_connection() + .change_window_attributes( + window, + &xproto::ChangeWindowAttributesAux::new().cursor(cursor as xproto::Cursor), + )? + .ignore_error(); + + self.xcb_connection().flush()?; + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SelectedCursor { + Custom(CustomCursor), + Named(CursorIcon), +} + +#[derive(Debug, Clone)] +pub struct CustomCursor { + inner: Arc, +} + +impl Hash for CustomCursor { + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.inner).hash(state); + } +} + +impl PartialEq for CustomCursor { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.inner, &other.inner) + } +} + +impl Eq for CustomCursor {} + +impl CustomCursor { + pub(crate) fn new( + event_loop: &ActiveEventLoop, + cursor: PlatformCustomCursorSource, + ) -> CustomCursor { + unsafe { + let ximage = (event_loop.xconn.xcursor.XcursorImageCreate)( + cursor.0.width as i32, + cursor.0.height as i32, + ); + if ximage.is_null() { + panic!("failed to allocate cursor image"); + } + (*ximage).xhot = cursor.0.hotspot_x as u32; + (*ximage).yhot = cursor.0.hotspot_y as u32; + (*ximage).delay = 0; + + let dst = slice::from_raw_parts_mut((*ximage).pixels, cursor.0.rgba.len() / 4); + for (dst, chunk) in dst.iter_mut().zip(cursor.0.rgba.chunks_exact(4)) { + *dst = (chunk[0] as u32) << 16 + | (chunk[1] as u32) << 8 + | (chunk[2] as u32) + | (chunk[3] as u32) << 24; + } + + let cursor = + (event_loop.xconn.xcursor.XcursorImageLoadCursor)(event_loop.xconn.display, ximage); + (event_loop.xconn.xcursor.XcursorImageDestroy)(ximage); + Self { inner: Arc::new(CustomCursorInner { xconn: event_loop.xconn.clone(), cursor }) } + } + } +} + +#[derive(Debug)] +struct CustomCursorInner { + xconn: Arc, + cursor: ffi::Cursor, +} + +impl Drop for CustomCursorInner { + fn drop(&mut self) { + unsafe { + (self.xconn.xlib.XFreeCursor)(self.xconn.display, self.cursor); + } + } +} + +impl Default for SelectedCursor { + fn default() -> Self { + SelectedCursor::Named(Default::default()) + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/util/geometry.rs b/vendor/winit/src/platform_impl/linux/x11/util/geometry.rs new file mode 100644 index 00000000..63ec5647 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/util/geometry.rs @@ -0,0 +1,265 @@ +use std::cmp; + +use super::*; + +// Friendly neighborhood axis-aligned rectangle +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AaRect { + x: i64, + y: i64, + width: i64, + height: i64, +} + +impl AaRect { + pub fn new((x, y): (i32, i32), (width, height): (u32, u32)) -> Self { + let (x, y) = (x as i64, y as i64); + let (width, height) = (width as i64, height as i64); + AaRect { x, y, width, height } + } + + pub fn contains_point(&self, x: i64, y: i64) -> bool { + x >= self.x && x <= self.x + self.width && y >= self.y && y <= self.y + self.height + } + + pub fn get_overlapping_area(&self, other: &Self) -> i64 { + let x_overlap = cmp::max( + 0, + cmp::min(self.x + self.width, other.x + other.width) - cmp::max(self.x, other.x), + ); + let y_overlap = cmp::max( + 0, + cmp::min(self.y + self.height, other.y + other.height) - cmp::max(self.y, other.y), + ); + x_overlap * y_overlap + } +} + +#[derive(Debug, Clone)] +pub struct FrameExtents { + pub left: u32, + pub right: u32, + pub top: u32, + pub bottom: u32, +} + +impl FrameExtents { + pub fn new(left: u32, right: u32, top: u32, bottom: u32) -> Self { + FrameExtents { left, right, top, bottom } + } + + pub fn from_border(border: u32) -> Self { + Self::new(border, border, border, border) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FrameExtentsHeuristicPath { + Supported, + UnsupportedNested, + UnsupportedBordered, +} + +#[derive(Debug, Clone)] +pub struct FrameExtentsHeuristic { + pub frame_extents: FrameExtents, + pub heuristic_path: FrameExtentsHeuristicPath, +} + +impl FrameExtentsHeuristic { + pub fn inner_pos_to_outer(&self, x: i32, y: i32) -> (i32, i32) { + use self::FrameExtentsHeuristicPath::*; + if self.heuristic_path != UnsupportedBordered { + (x - self.frame_extents.left as i32, y - self.frame_extents.top as i32) + } else { + (x, y) + } + } + + pub fn inner_size_to_outer(&self, width: u32, height: u32) -> (u32, u32) { + ( + width.saturating_add( + self.frame_extents.left.saturating_add(self.frame_extents.right) as _ + ), + height.saturating_add( + self.frame_extents.top.saturating_add(self.frame_extents.bottom) as _ + ), + ) + } +} + +impl XConnection { + // This is adequate for inner_position + pub fn translate_coords( + &self, + window: xproto::Window, + root: xproto::Window, + ) -> Result { + self.xcb_connection().translate_coordinates(window, root, 0, 0)?.reply().map_err(Into::into) + } + + // This is adequate for inner_size + pub fn get_geometry( + &self, + window: xproto::Window, + ) -> Result { + self.xcb_connection().get_geometry(window)?.reply().map_err(Into::into) + } + + fn get_frame_extents(&self, window: xproto::Window) -> Option { + let atoms = self.atoms(); + let extents_atom = atoms[_NET_FRAME_EXTENTS]; + + if !hint_is_supported(extents_atom) { + return None; + } + + // Of the WMs tested, xmonad, i3, dwm, IceWM (1.3.x and earlier), and blackbox don't + // support this. As this is part of EWMH (Extended Window Manager Hints), it's likely to + // be unsupported by many smaller WMs. + let extents: Option> = self + .get_property(window, extents_atom, xproto::Atom::from(xproto::AtomEnum::CARDINAL)) + .ok(); + + extents.and_then(|extents| { + if extents.len() >= 4 { + Some(FrameExtents { + left: extents[0], + right: extents[1], + top: extents[2], + bottom: extents[3], + }) + } else { + None + } + }) + } + + pub fn is_top_level(&self, window: xproto::Window, root: xproto::Window) -> Option { + let atoms = self.atoms(); + let client_list_atom = atoms[_NET_CLIENT_LIST]; + + if !hint_is_supported(client_list_atom) { + return None; + } + + let client_list: Option> = self + .get_property(root, client_list_atom, xproto::Atom::from(xproto::AtomEnum::WINDOW)) + .ok(); + + client_list.map(|client_list| client_list.contains(&(window as xproto::Window))) + } + + fn get_parent_window(&self, window: xproto::Window) -> Result { + let parent = self.xcb_connection().query_tree(window)?.reply()?.parent; + Ok(parent) + } + + fn climb_hierarchy( + &self, + window: xproto::Window, + root: xproto::Window, + ) -> Result { + let mut outer_window = window; + loop { + let candidate = self.get_parent_window(outer_window)?; + if candidate == root { + break; + } + outer_window = candidate; + } + Ok(outer_window) + } + + pub fn get_frame_extents_heuristic( + &self, + window: xproto::Window, + root: xproto::Window, + ) -> FrameExtentsHeuristic { + use self::FrameExtentsHeuristicPath::*; + + // Position relative to root window. + // With rare exceptions, this is the position of a nested window. Cases where the window + // isn't nested are outlined in the comments throughout this function, but in addition to + // that, fullscreen windows often aren't nested. + let (inner_y_rel_root, child) = { + let coords = self + .translate_coords(window, root) + .expect("Failed to translate window coordinates"); + (coords.dst_y, coords.child) + }; + + let (width, height, border) = { + let inner_geometry = + self.get_geometry(window).expect("Failed to get inner window geometry"); + (inner_geometry.width, inner_geometry.height, inner_geometry.border_width) + }; + + // The first condition is only false for un-nested windows, but isn't always false for + // un-nested windows. Mutter/Muffin/Budgie and Marco present a mysterious discrepancy: + // when y is on the range [0, 2] and if the window has been unfocused since being + // undecorated (or was undecorated upon construction), the first condition is true, + // requiring us to rely on the second condition. + let nested = !(window == child || self.is_top_level(child, root) == Some(true)); + + // Hopefully the WM supports EWMH, allowing us to get exact info on the window frames. + if let Some(mut frame_extents) = self.get_frame_extents(window) { + // Mutter/Muffin/Budgie and Marco preserve their decorated frame extents when + // decorations are disabled, but since the window becomes un-nested, it's easy to + // catch. + if !nested { + frame_extents = FrameExtents::new(0, 0, 0, 0); + } + + // The difference between the nested window's position and the outermost window's + // position is equivalent to the frame size. In most scenarios, this is equivalent to + // manually climbing the hierarchy as is done in the case below. Here's a list of + // known discrepancies: + // * Mutter/Muffin/Budgie gives decorated windows a margin of 9px (only 7px on top) in + // addition to a 1px semi-transparent border. The margin can be easily observed by + // using a screenshot tool to get a screenshot of a selected window, and is presumably + // used for drawing drop shadows. Getting window geometry information via + // hierarchy-climbing results in this margin being included in both the position and + // outer size, so a window positioned at (0, 0) would be reported as having a position + // (-10, -8). + // * Compiz has a drop shadow margin just like Mutter/Muffin/Budgie, though it's 10px on + // all sides, and there's no additional border. + // * Enlightenment otherwise gets a y position equivalent to inner_y_rel_root. Without + // decorations, there's no difference. This is presumably related to Enlightenment's + // fairly unique concept of window position; it interprets positions given to + // XMoveWindow as a client area position rather than a position of the overall window. + + FrameExtentsHeuristic { frame_extents, heuristic_path: Supported } + } else if nested { + // If the position value we have is for a nested window used as the client area, we'll + // just climb up the hierarchy and get the geometry of the outermost window we're + // nested in. + let outer_window = + self.climb_hierarchy(window, root).expect("Failed to climb window hierarchy"); + let (outer_y, outer_width, outer_height) = { + let outer_geometry = + self.get_geometry(outer_window).expect("Failed to get outer window geometry"); + (outer_geometry.y, outer_geometry.width, outer_geometry.height) + }; + + // Since we have the geometry of the outermost window and the geometry of the client + // area, we can figure out what's in between. + let diff_x = outer_width.saturating_sub(width) as u32; + let diff_y = outer_height.saturating_sub(height) as u32; + let offset_y = inner_y_rel_root.saturating_sub(outer_y) as u32; + + let left = diff_x / 2; + let right = left; + let top = offset_y; + let bottom = diff_y.saturating_sub(offset_y); + + let frame_extents = FrameExtents::new(left, right, top, bottom); + FrameExtentsHeuristic { frame_extents, heuristic_path: UnsupportedNested } + } else { + // This is the case for xmonad and dwm, AKA the only WMs tested that supplied a + // border value. This is convenient, since we can use it to get an accurate frame. + let frame_extents = FrameExtents::from_border(border.into()); + FrameExtentsHeuristic { frame_extents, heuristic_path: UnsupportedBordered } + } + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/util/hint.rs b/vendor/winit/src/platform_impl/linux/x11/util/hint.rs new file mode 100644 index 00000000..a31872d5 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/util/hint.rs @@ -0,0 +1,169 @@ +use crate::platform::x11::WindowType; +use std::sync::Arc; + +use super::*; + +#[derive(Debug)] +#[allow(dead_code)] +pub enum StateOperation { + Remove = 0, // _NET_WM_STATE_REMOVE + Add = 1, // _NET_WM_STATE_ADD + Toggle = 2, // _NET_WM_STATE_TOGGLE +} + +impl From for StateOperation { + fn from(op: bool) -> Self { + if op { + StateOperation::Add + } else { + StateOperation::Remove + } + } +} + +impl WindowType { + pub(crate) fn as_atom(&self, xconn: &Arc) -> xproto::Atom { + use self::WindowType::*; + let atom_name = match *self { + Desktop => _NET_WM_WINDOW_TYPE_DESKTOP, + Dock => _NET_WM_WINDOW_TYPE_DOCK, + Toolbar => _NET_WM_WINDOW_TYPE_TOOLBAR, + Menu => _NET_WM_WINDOW_TYPE_MENU, + Utility => _NET_WM_WINDOW_TYPE_UTILITY, + Splash => _NET_WM_WINDOW_TYPE_SPLASH, + Dialog => _NET_WM_WINDOW_TYPE_DIALOG, + DropdownMenu => _NET_WM_WINDOW_TYPE_DROPDOWN_MENU, + PopupMenu => _NET_WM_WINDOW_TYPE_POPUP_MENU, + Tooltip => _NET_WM_WINDOW_TYPE_TOOLTIP, + Notification => _NET_WM_WINDOW_TYPE_NOTIFICATION, + Combo => _NET_WM_WINDOW_TYPE_COMBO, + Dnd => _NET_WM_WINDOW_TYPE_DND, + Normal => _NET_WM_WINDOW_TYPE_NORMAL, + }; + + let atoms = xconn.atoms(); + atoms[atom_name] + } +} + +pub struct MotifHints { + hints: MwmHints, +} + +struct MwmHints { + flags: u32, + functions: u32, + decorations: u32, + input_mode: u32, + status: u32, +} + +#[allow(dead_code)] +mod mwm { + // Motif WM hints are obsolete, but still widely supported. + // https://stackoverflow.com/a/1909708 + pub const MWM_HINTS_FUNCTIONS: u32 = 1 << 0; + pub const MWM_HINTS_DECORATIONS: u32 = 1 << 1; + + pub const MWM_FUNC_ALL: u32 = 1 << 0; + pub const MWM_FUNC_RESIZE: u32 = 1 << 1; + pub const MWM_FUNC_MOVE: u32 = 1 << 2; + pub const MWM_FUNC_MINIMIZE: u32 = 1 << 3; + pub const MWM_FUNC_MAXIMIZE: u32 = 1 << 4; + pub const MWM_FUNC_CLOSE: u32 = 1 << 5; +} + +impl MotifHints { + pub fn new() -> MotifHints { + MotifHints { + hints: MwmHints { flags: 0, functions: 0, decorations: 0, input_mode: 0, status: 0 }, + } + } + + pub fn set_decorations(&mut self, decorations: bool) { + self.hints.flags |= mwm::MWM_HINTS_DECORATIONS; + self.hints.decorations = decorations as u32; + } + + pub fn set_maximizable(&mut self, maximizable: bool) { + if maximizable { + self.add_func(mwm::MWM_FUNC_MAXIMIZE); + } else { + self.remove_func(mwm::MWM_FUNC_MAXIMIZE); + } + } + + fn add_func(&mut self, func: u32) { + if self.hints.flags & mwm::MWM_HINTS_FUNCTIONS != 0 { + if self.hints.functions & mwm::MWM_FUNC_ALL != 0 { + self.hints.functions &= !func; + } else { + self.hints.functions |= func; + } + } + } + + fn remove_func(&mut self, func: u32) { + if self.hints.flags & mwm::MWM_HINTS_FUNCTIONS == 0 { + self.hints.flags |= mwm::MWM_HINTS_FUNCTIONS; + self.hints.functions = mwm::MWM_FUNC_ALL; + } + + if self.hints.functions & mwm::MWM_FUNC_ALL != 0 { + self.hints.functions |= func; + } else { + self.hints.functions &= !func; + } + } +} + +impl Default for MotifHints { + fn default() -> Self { + Self::new() + } +} + +impl XConnection { + pub fn get_motif_hints(&self, window: xproto::Window) -> MotifHints { + let atoms = self.atoms(); + let motif_hints = atoms[_MOTIF_WM_HINTS]; + + let mut hints = MotifHints::new(); + + if let Ok(props) = self.get_property::(window, motif_hints, motif_hints) { + hints.hints.flags = props.first().cloned().unwrap_or(0); + hints.hints.functions = props.get(1).cloned().unwrap_or(0); + hints.hints.decorations = props.get(2).cloned().unwrap_or(0); + hints.hints.input_mode = props.get(3).cloned().unwrap_or(0); + hints.hints.status = props.get(4).cloned().unwrap_or(0); + } + + hints + } + + #[allow(clippy::unnecessary_cast)] + pub fn set_motif_hints( + &self, + window: xproto::Window, + hints: &MotifHints, + ) -> Result, X11Error> { + let atoms = self.atoms(); + let motif_hints = atoms[_MOTIF_WM_HINTS]; + + let hints_data: [u32; 5] = [ + hints.hints.flags as u32, + hints.hints.functions as u32, + hints.hints.decorations as u32, + hints.hints.input_mode as u32, + hints.hints.status as u32, + ]; + + self.change_property( + window, + motif_hints, + motif_hints, + xproto::PropMode::REPLACE, + &hints_data, + ) + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/util/icon.rs b/vendor/winit/src/platform_impl/linux/x11/util/icon.rs new file mode 100644 index 00000000..07b5fee6 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/util/icon.rs @@ -0,0 +1,36 @@ +#![allow(clippy::assertions_on_constants)] + +use super::*; +use crate::icon::{Pixel, RgbaIcon, PIXEL_SIZE}; + +impl Pixel { + pub fn to_packed_argb(&self) -> Cardinal { + let mut cardinal = 0; + assert!(CARDINAL_SIZE >= PIXEL_SIZE); + let as_bytes = &mut cardinal as *mut _ as *mut u8; + unsafe { + *as_bytes.offset(0) = self.b; + *as_bytes.offset(1) = self.g; + *as_bytes.offset(2) = self.r; + *as_bytes.offset(3) = self.a; + } + cardinal + } +} + +impl RgbaIcon { + pub(crate) fn to_cardinals(&self) -> Vec { + assert_eq!(self.rgba.len() % PIXEL_SIZE, 0); + let pixel_count = self.rgba.len() / PIXEL_SIZE; + assert_eq!(pixel_count, (self.width * self.height) as usize); + let mut data = Vec::with_capacity(pixel_count); + data.push(self.width as Cardinal); + data.push(self.height as Cardinal); + let pixels = self.rgba.as_ptr() as *const Pixel; + for pixel_index in 0..pixel_count { + let pixel = unsafe { &*pixels.add(pixel_index) }; + data.push(pixel.to_packed_argb()); + } + data + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/util/input.rs b/vendor/winit/src/platform_impl/linux/x11/util/input.rs new file mode 100644 index 00000000..37d21d11 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/util/input.rs @@ -0,0 +1,106 @@ +use std::{slice, str}; +use x11rb::protocol::xinput::{self, ConnectionExt as _}; +use x11rb::protocol::xkb; + +use super::*; + +pub const VIRTUAL_CORE_POINTER: u16 = 2; +pub const VIRTUAL_CORE_KEYBOARD: u16 = 3; + +// A base buffer size of 1kB uses a negligible amount of RAM while preventing us from having to +// re-allocate (and make another round-trip) in the *vast* majority of cases. +// To test if `lookup_utf8` works correctly, set this to 1. +const TEXT_BUFFER_SIZE: usize = 1024; + +impl XConnection { + pub fn select_xinput_events( + &self, + window: xproto::Window, + device_id: u16, + mask: xinput::XIEventMask, + ) -> Result, X11Error> { + self.xcb_connection() + .xinput_xi_select_events(window, &[xinput::EventMask { + deviceid: device_id, + mask: vec![mask], + }]) + .map_err(Into::into) + } + + pub fn select_xkb_events( + &self, + device_id: xkb::DeviceSpec, + mask: xkb::EventType, + ) -> Result { + let mask = u16::from(mask) as _; + let status = + unsafe { (self.xlib.XkbSelectEvents)(self.display, device_id as _, mask, mask) }; + + if status == ffi::True { + self.flush_requests()?; + Ok(true) + } else { + tracing::error!("Could not select XKB events: The XKB extension is not initialized!"); + Ok(false) + } + } + + pub fn query_pointer( + &self, + window: xproto::Window, + device_id: u16, + ) -> Result { + self.xcb_connection() + .xinput_xi_query_pointer(window, device_id)? + .reply() + .map_err(Into::into) + } + + fn lookup_utf8_inner( + &self, + ic: ffi::XIC, + key_event: &mut ffi::XKeyEvent, + buffer: *mut u8, + size: usize, + ) -> (ffi::KeySym, ffi::Status, c_int) { + let mut keysym: ffi::KeySym = 0; + let mut status: ffi::Status = 0; + let count = unsafe { + (self.xlib.Xutf8LookupString)( + ic, + key_event, + buffer as *mut c_char, + size as c_int, + &mut keysym, + &mut status, + ) + }; + (keysym, status, count) + } + + pub fn lookup_utf8(&self, ic: ffi::XIC, key_event: &mut ffi::XKeyEvent) -> String { + // `assume_init` is safe here because the array consists of `MaybeUninit` values, + // which do not require initialization. + let mut buffer: [MaybeUninit; TEXT_BUFFER_SIZE] = + unsafe { MaybeUninit::uninit().assume_init() }; + // If the buffer overflows, we'll make a new one on the heap. + let mut vec; + + let (_, status, count) = + self.lookup_utf8_inner(ic, key_event, buffer.as_mut_ptr() as *mut u8, buffer.len()); + + let bytes = if status == ffi::XBufferOverflow { + vec = Vec::with_capacity(count as usize); + let (_, _, new_count) = + self.lookup_utf8_inner(ic, key_event, vec.as_mut_ptr(), vec.capacity()); + debug_assert_eq!(count, new_count); + + unsafe { vec.set_len(count as usize) }; + &vec[..count as usize] + } else { + unsafe { slice::from_raw_parts(buffer.as_ptr() as *const u8, count as usize) } + }; + + str::from_utf8(bytes).unwrap_or("").to_string() + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/util/keys.rs b/vendor/winit/src/platform_impl/linux/x11/util/keys.rs new file mode 100644 index 00000000..a6ad291c --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/util/keys.rs @@ -0,0 +1,75 @@ +use std::iter::Enumerate; +use std::slice::Iter; + +use super::*; + +pub struct Keymap { + keys: [u8; 32], +} + +pub struct KeymapIter<'a> { + iter: Enumerate>, + index: usize, + item: Option, +} + +impl Keymap { + pub fn iter(&self) -> KeymapIter<'_> { + KeymapIter { iter: self.keys.iter().enumerate(), index: 0, item: None } + } +} + +impl<'a> IntoIterator for &'a Keymap { + type IntoIter = KeymapIter<'a>; + type Item = ffi::KeyCode; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl Iterator for KeymapIter<'_> { + type Item = ffi::KeyCode; + + fn next(&mut self) -> Option { + if self.item.is_none() { + for (index, &item) in self.iter.by_ref() { + if item != 0 { + self.index = index; + self.item = Some(item); + break; + } + } + } + + self.item.take().map(|item| { + debug_assert!(item != 0); + + let bit = first_bit(item); + + if item != bit { + // Remove the first bit; save the rest for further iterations + self.item = Some(item ^ bit); + } + + let shift = bit.trailing_zeros() + (self.index * 8) as u32; + shift as ffi::KeyCode + }) + } +} + +impl XConnection { + pub fn query_keymap(&self) -> Keymap { + let mut keys = [0; 32]; + + unsafe { + (self.xlib.XQueryKeymap)(self.display, keys.as_mut_ptr() as *mut c_char); + } + + Keymap { keys } + } +} + +fn first_bit(b: u8) -> u8 { + 1 << b.trailing_zeros() +} diff --git a/vendor/winit/src/platform_impl/linux/x11/util/memory.rs b/vendor/winit/src/platform_impl/linux/x11/util/memory.rs new file mode 100644 index 00000000..4e052a75 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/util/memory.rs @@ -0,0 +1,42 @@ +use std::ops::{Deref, DerefMut}; + +use super::*; + +pub(crate) struct XSmartPointer<'a, T> { + xconn: &'a XConnection, + pub ptr: *mut T, +} + +impl<'a, T> XSmartPointer<'a, T> { + // You're responsible for only passing things to this that should be XFree'd. + // Returns None if ptr is null. + pub fn new(xconn: &'a XConnection, ptr: *mut T) -> Option { + if !ptr.is_null() { + Some(XSmartPointer { xconn, ptr }) + } else { + None + } + } +} + +impl Deref for XSmartPointer<'_, T> { + type Target = T; + + fn deref(&self) -> &T { + unsafe { &*self.ptr } + } +} + +impl DerefMut for XSmartPointer<'_, T> { + fn deref_mut(&mut self) -> &mut T { + unsafe { &mut *self.ptr } + } +} + +impl Drop for XSmartPointer<'_, T> { + fn drop(&mut self) { + unsafe { + (self.xconn.xlib.XFree)(self.ptr as *mut _); + } + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/util/mod.rs b/vendor/winit/src/platform_impl/linux/x11/util/mod.rs new file mode 100644 index 00000000..55e6c2dd --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/util/mod.rs @@ -0,0 +1,77 @@ +// Welcome to the util module, where we try to keep you from shooting yourself in the foot. +// *results may vary + +use std::mem::{self, MaybeUninit}; +use std::ops::BitAnd; +use std::os::raw::*; + +mod client_msg; +pub mod cookie; +mod cursor; +mod geometry; +mod hint; +mod icon; +mod input; +pub mod keys; +pub(crate) mod memory; +mod mouse; +mod randr; +mod window_property; +mod wm; +mod xmodmap; + +pub use self::cursor::*; +pub use self::geometry::*; +pub use self::hint::*; +pub use self::input::*; +pub use self::mouse::*; +pub use self::window_property::*; +pub use self::wm::*; +pub use self::xmodmap::ModifierKeymap; + +use super::atoms::*; +use super::{ffi, VoidCookie, X11Error, XConnection, XError}; +use x11rb::protocol::xproto::{self, ConnectionExt as _}; + +pub fn maybe_change(field: &mut Option, value: T) -> bool { + let wrapped = Some(value); + if *field != wrapped { + *field = wrapped; + true + } else { + false + } +} + +pub fn has_flag(bitset: T, flag: T) -> bool +where + T: Copy + PartialEq + BitAnd, +{ + bitset & flag == flag +} + +impl XConnection { + // This is important, so pay attention! + // Xlib has an output buffer, and tries to hide the async nature of X from you. + // This buffer contains the requests you make, and is flushed under various circumstances: + // 1. `XPending`, `XNextEvent`, and `XWindowEvent` flush "as needed" + // 2. `XFlush` explicitly flushes + // 3. `XSync` flushes and blocks until all requests are responded to + // 4. Calls that have a return dependent on a response (i.e. `XGetWindowProperty`) sync + // internally. When in doubt, check the X11 source; if a function calls `_XReply`, it flushes + // and waits. + // All util functions that abstract an async function will return a `Flusher`. + pub fn flush_requests(&self) -> Result<(), XError> { + unsafe { (self.xlib.XFlush)(self.display) }; + // println!("XFlush"); + // This isn't necessarily a useful time to check for errors (since our request hasn't + // necessarily been processed yet) + self.check_errors() + } + + pub fn sync_with_server(&self) -> Result<(), XError> { + unsafe { (self.xlib.XSync)(self.display, ffi::False) }; + // println!("XSync"); + self.check_errors() + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/util/modifiers.rs b/vendor/winit/src/platform_impl/linux/x11/util/modifiers.rs new file mode 100644 index 00000000..bb157e44 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/util/modifiers.rs @@ -0,0 +1,187 @@ +use std::{collections::HashMap, slice}; + +use super::*; + +use crate::event::{ElementState, ModifiersState}; + +// Offsets within XModifierKeymap to each set of keycodes. +// We are only interested in Shift, Control, Alt, and Logo. +// +// There are 8 sets total. The order of keycode sets is: +// Shift, Lock, Control, Mod1 (Alt), Mod2, Mod3, Mod4 (Logo), Mod5 +// +// https://tronche.com/gui/x/xlib/input/XSetModifierMapping.html +const SHIFT_OFFSET: usize = 0; +const CONTROL_OFFSET: usize = 2; +const ALT_OFFSET: usize = 3; +const LOGO_OFFSET: usize = 6; +const NUM_MODS: usize = 8; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Modifier { + Alt, + Ctrl, + Shift, + Logo, +} + +#[derive(Debug, Default)] +pub(crate) struct ModifierKeymap { + // Maps keycodes to modifiers + keys: HashMap, +} + +#[derive(Clone, Debug, Default)] +pub(crate) struct ModifierKeyState { + // Contains currently pressed modifier keys and their corresponding modifiers + keys: HashMap, + state: ModifiersState, +} + +impl ModifierKeymap { + pub fn new() -> ModifierKeymap { + ModifierKeymap::default() + } + + pub fn get_modifier(&self, keycode: ffi::KeyCode) -> Option { + self.keys.get(&keycode).cloned() + } + + pub fn reset_from_x_connection(&mut self, xconn: &XConnection) { + { + let keymap = xconn.xcb_connection().get_modifier_mapping().expect("get_modifier_mapping failed").reply().expect("get_modifier_mapping failed"); + + if keymap.is_null() { + panic!("failed to allocate XModifierKeymap"); + } + + self.reset_from_x_keymap(&*keymap); + + (xconn.xlib.XFreeModifiermap)(keymap); + } + } + + pub fn reset_from_x_keymap(&mut self, keymap: &ffi::XModifierKeymap) { + let keys_per_mod = keymap.max_keypermod as usize; + + let keys = unsafe { + slice::from_raw_parts(keymap.modifiermap as *const _, keys_per_mod * NUM_MODS) + }; + + self.keys.clear(); + + self.read_x_keys(keys, SHIFT_OFFSET, keys_per_mod, Modifier::Shift); + self.read_x_keys(keys, CONTROL_OFFSET, keys_per_mod, Modifier::Ctrl); + self.read_x_keys(keys, ALT_OFFSET, keys_per_mod, Modifier::Alt); + self.read_x_keys(keys, LOGO_OFFSET, keys_per_mod, Modifier::Logo); + } + + fn read_x_keys( + &mut self, + keys: &[ffi::KeyCode], + offset: usize, + keys_per_mod: usize, + modifier: Modifier, + ) { + let start = offset * keys_per_mod; + let end = start + keys_per_mod; + + for &keycode in &keys[start..end] { + if keycode != 0 { + self.keys.insert(keycode, modifier); + } + } + } +} + +impl ModifierKeyState { + pub fn update_keymap(&mut self, mods: &ModifierKeymap) { + self.keys.retain(|k, v| { + if let Some(m) = mods.get_modifier(*k) { + *v = m; + true + } else { + false + } + }); + + self.reset_state(); + } + + pub fn update_state( + &mut self, + state: &ModifiersState, + except: Option, + ) -> Option { + let mut new_state = *state; + + match except { + Some(Modifier::Alt) => new_state.set(ModifiersState::ALT, self.state.alt()), + Some(Modifier::Ctrl) => new_state.set(ModifiersState::CTRL, self.state.ctrl()), + Some(Modifier::Shift) => new_state.set(ModifiersState::SHIFT, self.state.shift()), + Some(Modifier::Logo) => new_state.set(ModifiersState::LOGO, self.state.logo()), + None => (), + } + + if self.state == new_state { + None + } else { + self.keys.retain(|_k, v| get_modifier(&new_state, *v)); + self.state = new_state; + Some(new_state) + } + } + + pub fn modifiers(&self) -> ModifiersState { + self.state + } + + pub fn key_event(&mut self, state: ElementState, keycode: ffi::KeyCode, modifier: Modifier) { + match state { + ElementState::Pressed => self.key_press(keycode, modifier), + ElementState::Released => self.key_release(keycode), + } + } + + pub fn key_press(&mut self, keycode: ffi::KeyCode, modifier: Modifier) { + self.keys.insert(keycode, modifier); + + set_modifier(&mut self.state, modifier, true); + } + + pub fn key_release(&mut self, keycode: ffi::KeyCode) { + if let Some(modifier) = self.keys.remove(&keycode) { + if !self.keys.values().any(|&m| m == modifier) { + set_modifier(&mut self.state, modifier, false); + } + } + } + + fn reset_state(&mut self) { + let mut new_state = ModifiersState::default(); + + for &m in self.keys.values() { + set_modifier(&mut new_state, m, true); + } + + self.state = new_state; + } +} + +fn get_modifier(state: &ModifiersState, modifier: Modifier) -> bool { + match modifier { + Modifier::Alt => state.alt(), + Modifier::Ctrl => state.ctrl(), + Modifier::Shift => state.shift(), + Modifier::Logo => state.logo(), + } +} + +fn set_modifier(state: &mut ModifiersState, modifier: Modifier, value: bool) { + match modifier { + Modifier::Alt => state.set(ModifiersState::ALT, value), + Modifier::Ctrl => state.set(ModifiersState::CTRL, value), + Modifier::Shift => state.set(ModifiersState::SHIFT, value), + Modifier::Logo => state.set(ModifiersState::LOGO, value), + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/util/mouse.rs b/vendor/winit/src/platform_impl/linux/x11/util/mouse.rs new file mode 100644 index 00000000..e66a76c3 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/util/mouse.rs @@ -0,0 +1,49 @@ +//! Utilities for handling mouse events. + +/// Recorded mouse delta designed to filter out noise. +pub struct Delta { + x: T, + y: T, +} + +impl Default for Delta { + fn default() -> Self { + Self { x: Default::default(), y: Default::default() } + } +} + +impl Delta { + pub(crate) fn set_x(&mut self, x: T) { + self.x = x; + } + + pub(crate) fn set_y(&mut self, y: T) { + self.y = y; + } +} + +macro_rules! consume { + ($this:expr, $ty:ty) => {{ + let this = $this; + let (x, y) = match (this.x.abs() < <$ty>::EPSILON, this.y.abs() < <$ty>::EPSILON) { + (true, true) => return None, + (false, true) => (this.x, 0.0), + (true, false) => (0.0, this.y), + (false, false) => (this.x, this.y), + }; + + Some((x, y)) + }}; +} + +impl Delta { + pub(crate) fn consume(self) -> Option<(f32, f32)> { + consume!(self, f32) + } +} + +impl Delta { + pub(crate) fn consume(self) -> Option<(f64, f64)> { + consume!(self, f64) + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/util/randr.rs b/vendor/winit/src/platform_impl/linux/x11/util/randr.rs new file mode 100644 index 00000000..d10c97e5 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/util/randr.rs @@ -0,0 +1,186 @@ +use std::str::FromStr; +use std::{env, str}; + +use super::*; +use crate::dpi::validate_scale_factor; +use crate::platform_impl::platform::x11::{monitor, VideoModeHandle}; + +use tracing::warn; +use x11rb::protocol::randr::{self, ConnectionExt as _}; + +/// Represents values of `WINIT_HIDPI_FACTOR`. +pub enum EnvVarDPI { + Randr, + Scale(f64), + NotSet, +} + +pub fn calc_dpi_factor( + (width_px, height_px): (u32, u32), + (width_mm, height_mm): (u64, u64), +) -> f64 { + // See http://xpra.org/trac/ticket/728 for more information. + if width_mm == 0 || height_mm == 0 { + warn!("XRandR reported that the display's 0mm in size, which is certifiably insane"); + return 1.0; + } + + let ppmm = ((width_px as f64 * height_px as f64) / (width_mm as f64 * height_mm as f64)).sqrt(); + // Quantize 1/12 step size + let dpi_factor = ((ppmm * (12.0 * 25.4 / 96.0)).round() / 12.0).max(1.0); + assert!(validate_scale_factor(dpi_factor)); + if dpi_factor <= 20. { + dpi_factor + } else { + 1. + } +} + +impl XConnection { + // Retrieve DPI from Xft.dpi property + pub fn get_xft_dpi(&self) -> Option { + // Try to get it from XSETTINGS first. + if let Some(xsettings_screen) = self.xsettings_screen() { + match self.xsettings_dpi(xsettings_screen) { + Ok(Some(dpi)) => return Some(dpi), + Ok(None) => {}, + Err(err) => { + tracing::warn!("failed to fetch XSettings: {err}"); + }, + } + } + + self.database().get_string("Xft.dpi", "").and_then(|s| f64::from_str(s).ok()) + } + + pub fn get_output_info( + &self, + resources: &monitor::ScreenResources, + crtc: &randr::GetCrtcInfoReply, + ) -> Option<(String, f64, Vec)> { + let output_info = match self + .xcb_connection() + .randr_get_output_info(crtc.outputs[0], x11rb::CURRENT_TIME) + .map_err(X11Error::from) + .and_then(|r| r.reply().map_err(X11Error::from)) + { + Ok(output_info) => output_info, + Err(err) => { + warn!("Failed to get output info: {:?}", err); + return None; + }, + }; + + let bit_depth = self.default_root().root_depth; + let output_modes = &output_info.modes; + let resource_modes = resources.modes(); + + let modes = resource_modes + .iter() + // XRROutputInfo contains an array of mode ids that correspond to + // modes in the array in XRRScreenResources + .filter(|x| output_modes.contains(&x.id)) + .map(|mode| { + VideoModeHandle { + size: (mode.width.into(), mode.height.into()), + refresh_rate_millihertz: monitor::mode_refresh_rate_millihertz(mode) + .unwrap_or(0), + bit_depth: bit_depth as u16, + native_mode: mode.id, + // This is populated in `MonitorHandle::video_modes` as the + // video mode is returned to the user + monitor: None, + } + }) + .collect(); + + let name = match str::from_utf8(&output_info.name) { + Ok(name) => name.to_owned(), + Err(err) => { + warn!("Failed to get output name: {:?}", err); + return None; + }, + }; + // Override DPI if `WINIT_X11_SCALE_FACTOR` variable is set + let deprecated_dpi_override = env::var("WINIT_HIDPI_FACTOR").ok(); + if deprecated_dpi_override.is_some() { + warn!( + "The WINIT_HIDPI_FACTOR environment variable is deprecated; use \ + WINIT_X11_SCALE_FACTOR" + ) + } + let dpi_env = env::var("WINIT_X11_SCALE_FACTOR").ok().map_or_else( + || EnvVarDPI::NotSet, + |var| { + if var.to_lowercase() == "randr" { + EnvVarDPI::Randr + } else if let Ok(dpi) = f64::from_str(&var) { + EnvVarDPI::Scale(dpi) + } else if var.is_empty() { + EnvVarDPI::NotSet + } else { + panic!( + "`WINIT_X11_SCALE_FACTOR` invalid; DPI factors must be either normal \ + floats greater than 0, or `randr`. Got `{var}`" + ); + } + }, + ); + + let scale_factor = match dpi_env { + EnvVarDPI::Randr => calc_dpi_factor( + (crtc.width.into(), crtc.height.into()), + (output_info.mm_width as _, output_info.mm_height as _), + ), + EnvVarDPI::Scale(dpi_override) => { + if !validate_scale_factor(dpi_override) { + panic!( + "`WINIT_X11_SCALE_FACTOR` invalid; DPI factors must be either normal \ + floats greater than 0, or `randr`. Got `{dpi_override}`", + ); + } + dpi_override + }, + EnvVarDPI::NotSet => { + if let Some(dpi) = self.get_xft_dpi() { + dpi / 96. + } else { + calc_dpi_factor( + (crtc.width.into(), crtc.height.into()), + (output_info.mm_width as _, output_info.mm_height as _), + ) + } + }, + }; + + Some((name, scale_factor, modes)) + } + + pub fn set_crtc_config( + &self, + crtc_id: randr::Crtc, + mode_id: randr::Mode, + ) -> Result<(), X11Error> { + let crtc = + self.xcb_connection().randr_get_crtc_info(crtc_id, x11rb::CURRENT_TIME)?.reply()?; + + self.xcb_connection() + .randr_set_crtc_config( + crtc_id, + crtc.timestamp, + x11rb::CURRENT_TIME, + crtc.x, + crtc.y, + mode_id, + crtc.rotation, + &crtc.outputs, + )? + .reply() + .map(|_| ()) + .map_err(Into::into) + } + + pub fn get_crtc_mode(&self, crtc_id: randr::Crtc) -> Result { + Ok(self.xcb_connection().randr_get_crtc_info(crtc_id, x11rb::CURRENT_TIME)?.reply()?.mode) + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/util/window_property.rs b/vendor/winit/src/platform_impl/linux/x11/util/window_property.rs new file mode 100644 index 00000000..0f4ca165 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/util/window_property.rs @@ -0,0 +1,196 @@ +use std::error::Error; +use std::fmt; +use std::sync::Arc; + +use bytemuck::{NoUninit, Pod}; + +use x11rb::connection::Connection; +use x11rb::errors::ReplyError; + +use super::*; + +pub const CARDINAL_SIZE: usize = mem::size_of::(); + +pub type Cardinal = u32; + +#[derive(Debug, Clone)] +pub enum GetPropertyError { + X11rbError(Arc), + TypeMismatch(xproto::Atom), + FormatMismatch(c_int), +} + +impl GetPropertyError { + pub fn is_actual_property_type(&self, t: xproto::Atom) -> bool { + if let GetPropertyError::TypeMismatch(actual_type) = *self { + actual_type == t + } else { + false + } + } +} + +impl> From for GetPropertyError { + fn from(e: T) -> Self { + Self::X11rbError(Arc::new(e.into())) + } +} + +impl fmt::Display for GetPropertyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GetPropertyError::X11rbError(err) => err.fmt(f), + GetPropertyError::TypeMismatch(err) => write!(f, "type mismatch: {err}"), + GetPropertyError::FormatMismatch(err) => write!(f, "format mismatch: {err}"), + } + } +} + +impl Error for GetPropertyError {} + +// Number of 32-bit chunks to retrieve per iteration of get_property's inner loop. +// To test if `get_property` works correctly, set this to 1. +const PROPERTY_BUFFER_SIZE: u32 = 1024; // 4k of RAM ought to be enough for anyone! + +impl XConnection { + pub fn get_property( + &self, + window: xproto::Window, + property: xproto::Atom, + property_type: xproto::Atom, + ) -> Result, GetPropertyError> { + let mut iter = PropIterator::new(self.xcb_connection(), window, property, property_type); + let mut data = vec![]; + + loop { + if !iter.next_window(&mut data)? { + break; + } + } + + Ok(data) + } + + pub fn change_property<'a, T: NoUninit>( + &'a self, + window: xproto::Window, + property: xproto::Atom, + property_type: xproto::Atom, + mode: xproto::PropMode, + new_value: &[T], + ) -> Result, X11Error> { + assert!([1usize, 2, 4].contains(&mem::size_of::())); + self.xcb_connection() + .change_property( + mode, + window, + property, + property_type, + (mem::size_of::() * 8) as u8, + new_value.len().try_into().expect("too many items for property"), + bytemuck::cast_slice::(new_value), + ) + .map_err(Into::into) + } +} + +/// An iterator over the "windows" of the property that we are fetching. +struct PropIterator<'a, C: ?Sized, T> { + /// Handle to the connection. + conn: &'a C, + + /// The window that we're fetching the property from. + window: xproto::Window, + + /// The property that we're fetching. + property: xproto::Atom, + + /// The type of the property that we're fetching. + property_type: xproto::Atom, + + /// The offset of the next window, in 32-bit chunks. + offset: u32, + + /// The format of the type. + format: u8, + + /// Keep a reference to `T`. + _phantom: std::marker::PhantomData, +} + +impl<'a, C: Connection + ?Sized, T: Pod> PropIterator<'a, C, T> { + /// Create a new property iterator. + fn new( + conn: &'a C, + window: xproto::Window, + property: xproto::Atom, + property_type: xproto::Atom, + ) -> Self { + let format = match mem::size_of::() { + 1 => 8, + 2 => 16, + 4 => 32, + _ => unreachable!(), + }; + + Self { + conn, + window, + property, + property_type, + offset: 0, + format, + _phantom: Default::default(), + } + } + + /// Get the next window and append it to `data`. + /// + /// Returns whether there are more windows to fetch. + fn next_window(&mut self, data: &mut Vec) -> Result { + // Send the request and wait for the reply. + let reply = self + .conn + .get_property( + false, + self.window, + self.property, + self.property_type, + self.offset, + PROPERTY_BUFFER_SIZE, + )? + .reply()?; + + // Make sure that the reply is of the correct type. + if reply.type_ != self.property_type { + return Err(GetPropertyError::TypeMismatch(reply.type_)); + } + + // Make sure that the reply is of the correct format. + if reply.format != self.format { + return Err(GetPropertyError::FormatMismatch(reply.format.into())); + } + + // Append the data to the output. + if mem::size_of::() == 1 && mem::align_of::() == 1 { + // We can just do a bytewise append. + data.extend_from_slice(bytemuck::cast_slice(&reply.value)); + } else { + // Rust's borrowing and types system makes this a bit tricky. + // + // We need to make sure that the data is properly aligned. Unfortunately the best + // safe way to do this is to copy the data to another buffer and then append. + // + // TODO(notgull): It may be worth it to use `unsafe` to copy directly from + // `reply.value` to `data`; check if this is faster. Use benchmarks! + let old_len = data.len(); + let added_len = reply.value.len() / mem::size_of::(); + data.resize(old_len + added_len, T::zeroed()); + bytemuck::cast_slice_mut::(&mut data[old_len..]).copy_from_slice(&reply.value); + } + + // Check `bytes_after` to see if there are more windows to fetch. + self.offset += PROPERTY_BUFFER_SIZE; + Ok(reply.bytes_after != 0) + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/util/wm.rs b/vendor/winit/src/platform_impl/linux/x11/util/wm.rs new file mode 100644 index 00000000..be70b835 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/util/wm.rs @@ -0,0 +1,137 @@ +use std::sync::Mutex; + +use super::*; + +// https://specifications.freedesktop.org/wm-spec/latest/ar01s04.html#idm46075117309248 +pub const MOVERESIZE_TOPLEFT: isize = 0; +pub const MOVERESIZE_TOP: isize = 1; +pub const MOVERESIZE_TOPRIGHT: isize = 2; +pub const MOVERESIZE_RIGHT: isize = 3; +pub const MOVERESIZE_BOTTOMRIGHT: isize = 4; +pub const MOVERESIZE_BOTTOM: isize = 5; +pub const MOVERESIZE_BOTTOMLEFT: isize = 6; +pub const MOVERESIZE_LEFT: isize = 7; +pub const MOVERESIZE_MOVE: isize = 8; + +// This info is global to the window manager. +static SUPPORTED_HINTS: Mutex> = Mutex::new(Vec::new()); +static WM_NAME: Mutex> = Mutex::new(None); + +pub fn hint_is_supported(hint: xproto::Atom) -> bool { + (*SUPPORTED_HINTS.lock().unwrap()).contains(&hint) +} + +pub fn wm_name_is_one_of(names: &[&str]) -> bool { + if let Some(ref name) = *WM_NAME.lock().unwrap() { + names.contains(&name.as_str()) + } else { + false + } +} + +impl XConnection { + pub fn update_cached_wm_info(&self, root: xproto::Window) { + *SUPPORTED_HINTS.lock().unwrap() = self.get_supported_hints(root); + *WM_NAME.lock().unwrap() = self.get_wm_name(root); + } + + fn get_supported_hints(&self, root: xproto::Window) -> Vec { + let atoms = self.atoms(); + let supported_atom = atoms[_NET_SUPPORTED]; + self.get_property(root, supported_atom, xproto::Atom::from(xproto::AtomEnum::ATOM)) + .unwrap_or_else(|_| Vec::with_capacity(0)) + } + + #[allow(clippy::useless_conversion)] + fn get_wm_name(&self, root: xproto::Window) -> Option { + let atoms = self.atoms(); + let check_atom = atoms[_NET_SUPPORTING_WM_CHECK]; + let wm_name_atom = atoms[_NET_WM_NAME]; + + // Mutter/Muffin/Budgie doesn't have _NET_SUPPORTING_WM_CHECK in its _NET_SUPPORTED, despite + // it working and being supported. This has been reported upstream, but due to the + // inavailability of time machines, we'll just try to get _NET_SUPPORTING_WM_CHECK + // regardless of whether or not the WM claims to support it. + // + // Blackbox 0.70 also incorrectly reports not supporting this, though that appears to be + // fixed in 0.72. + // if !supported_hints.contains(&check_atom) { + // return None; + // } + + // IceWM (1.3.x and earlier) doesn't report supporting _NET_WM_NAME, but will nonetheless + // provide us with a value for it. Note that the unofficial 1.4 fork of IceWM works fine. + // if !supported_hints.contains(&wm_name_atom) { + // return None; + // } + + // Of the WMs tested, only xmonad and dwm fail to provide a WM name. + + // Querying this property on the root window will give us the ID of a child window created + // by the WM. + let root_window_wm_check = { + let result = self.get_property::( + root, + check_atom, + xproto::Atom::from(xproto::AtomEnum::WINDOW), + ); + + let wm_check = result.ok().and_then(|wm_check| wm_check.first().cloned()); + + wm_check? + }; + + // Querying the same property on the child window we were given, we should get this child + // window's ID again. + let child_window_wm_check = { + let result = self.get_property::( + root_window_wm_check.into(), + check_atom, + xproto::Atom::from(xproto::AtomEnum::WINDOW), + ); + + let wm_check = result.ok().and_then(|wm_check| wm_check.first().cloned()); + + wm_check? + }; + + // These values should be the same. + if root_window_wm_check != child_window_wm_check { + return None; + } + + // All of that work gives us a window ID that we can get the WM name from. + let wm_name = { + let atoms = self.atoms(); + let utf8_string_atom = atoms[UTF8_STRING]; + + let result = + self.get_property(root_window_wm_check.into(), wm_name_atom, utf8_string_atom); + + // IceWM requires this. IceWM was also the only WM tested that returns a null-terminated + // string. For more fun trivia, IceWM is also unique in including version and uname + // information in this string (this means you'll have to be careful if you want to match + // against it, though). + // The unofficial 1.4 fork of IceWM still includes the extra details, but properly + // returns a UTF8 string that isn't null-terminated. + let no_utf8 = if let Err(ref err) = result { + err.is_actual_property_type(xproto::Atom::from(xproto::AtomEnum::STRING)) + } else { + false + }; + + if no_utf8 { + self.get_property( + root_window_wm_check.into(), + wm_name_atom, + xproto::Atom::from(xproto::AtomEnum::STRING), + ) + } else { + result + } + } + .ok(); + + wm_name.and_then(|wm_name| String::from_utf8(wm_name).ok()) + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/util/xmodmap.rs b/vendor/winit/src/platform_impl/linux/x11/util/xmodmap.rs new file mode 100644 index 00000000..565fd729 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/util/xmodmap.rs @@ -0,0 +1,56 @@ +use std::collections::HashSet; +use std::slice; + +use x11_dl::xlib::{KeyCode as XKeyCode, XModifierKeymap}; + +// Offsets within XModifierKeymap to each set of keycodes. +// We are only interested in Shift, Control, Alt, and Logo. +// +// There are 8 sets total. The order of keycode sets is: +// Shift, Lock, Control, Mod1 (Alt), Mod2, Mod3, Mod4 (Logo), Mod5 +// +// https://tronche.com/gui/x/xlib/input/XSetModifierMapping.html +const NUM_MODS: usize = 8; + +/// Track which keys are modifiers, so we can properly replay them when they were filtered. +#[derive(Debug, Default)] +pub struct ModifierKeymap { + // Maps keycodes to modifiers + modifiers: HashSet, +} + +impl ModifierKeymap { + pub fn new() -> ModifierKeymap { + ModifierKeymap::default() + } + + pub fn is_modifier(&self, keycode: XKeyCode) -> bool { + self.modifiers.contains(&keycode) + } + + pub fn reload_from_x_connection(&mut self, xconn: &super::XConnection) { + unsafe { + let keymap = (xconn.xlib.XGetModifierMapping)(xconn.display); + + if keymap.is_null() { + return; + } + + self.reset_from_x_keymap(&*keymap); + + (xconn.xlib.XFreeModifiermap)(keymap); + } + } + + fn reset_from_x_keymap(&mut self, keymap: &XModifierKeymap) { + let keys_per_mod = keymap.max_keypermod as usize; + + let keys = unsafe { + slice::from_raw_parts(keymap.modifiermap as *const _, keys_per_mod * NUM_MODS) + }; + self.modifiers.clear(); + for key in keys { + self.modifiers.insert(*key); + } + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/window.rs b/vendor/winit/src/platform_impl/linux/x11/window.rs new file mode 100644 index 00000000..7e15d5f2 --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/window.rs @@ -0,0 +1,1938 @@ +use std::ffi::CString; +use std::mem::replace; +use std::os::raw::*; +use std::path::Path; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::{cmp, env}; + +use tracing::{debug, info, warn}; +use x11rb::connection::Connection; +use x11rb::properties::{WmHints, WmSizeHints, WmSizeHintsSpecification}; +use x11rb::protocol::shape::{ConnectionExt as ShapeExt, SK, SO}; +use x11rb::protocol::xproto::{self, ClipOrdering, ConnectionExt as _, Rectangle}; +use x11rb::protocol::{randr, xinput}; + +use crate::cursor::{Cursor, CustomCursor as RootCustomCursor}; +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::event::{Event, InnerSizeWriter, WindowEvent}; +use crate::event_loop::AsyncRequestSerial; +use crate::platform::x11::WindowType; +use crate::platform_impl::x11::atoms::*; +use crate::platform_impl::x11::{ + xinput_fp1616_to_float, MonitorHandle as X11MonitorHandle, WakeSender, X11Error, +}; +use crate::platform_impl::{ + Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError, PlatformCustomCursor, + PlatformIcon, VideoModeHandle as PlatformVideoModeHandle, +}; +use crate::window::{ + CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes, + WindowButtons, WindowLevel, +}; + +use super::util::{self, SelectedCursor}; +use super::{ + ffi, ActiveEventLoop, CookieResultExt, ImeRequest, ImeSender, VoidCookie, WindowId, XConnection, +}; + +#[derive(Debug)] +pub struct SharedState { + pub cursor_pos: Option<(f64, f64)>, + pub size: Option<(u32, u32)>, + pub position: Option<(i32, i32)>, + pub inner_position: Option<(i32, i32)>, + pub inner_position_rel_parent: Option<(i32, i32)>, + pub is_resizable: bool, + pub is_decorated: bool, + pub last_monitor: X11MonitorHandle, + pub dpi_adjusted: Option<(u32, u32)>, + pub(crate) fullscreen: Option, + // Set when application calls `set_fullscreen` when window is not visible + pub(crate) desired_fullscreen: Option>, + // Used to restore position after exiting fullscreen + pub restore_position: Option<(i32, i32)>, + // Used to restore video mode after exiting fullscreen + pub desktop_video_mode: Option<(randr::Crtc, randr::Mode)>, + pub frame_extents: Option, + pub min_inner_size: Option, + pub max_inner_size: Option, + pub resize_increments: Option, + pub base_size: Option, + pub visibility: Visibility, + pub has_focus: bool, + // Use `Option` to not apply hittest logic when it was never requested. + pub cursor_hittest: Option, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Visibility { + No, + Yes, + // Waiting for VisibilityNotify + YesWait, +} + +impl SharedState { + fn new(last_monitor: X11MonitorHandle, window_attributes: &WindowAttributes) -> Mutex { + let visibility = + if window_attributes.visible { Visibility::YesWait } else { Visibility::No }; + + Mutex::new(SharedState { + last_monitor, + visibility, + + is_resizable: window_attributes.resizable, + is_decorated: window_attributes.decorations, + cursor_pos: None, + size: None, + position: None, + inner_position: None, + inner_position_rel_parent: None, + dpi_adjusted: None, + fullscreen: None, + desired_fullscreen: None, + restore_position: None, + desktop_video_mode: None, + frame_extents: None, + min_inner_size: None, + max_inner_size: None, + resize_increments: None, + base_size: None, + has_focus: false, + cursor_hittest: None, + }) + } +} + +unsafe impl Send for UnownedWindow {} +unsafe impl Sync for UnownedWindow {} + +pub struct UnownedWindow { + pub(crate) xconn: Arc, // never changes + xwindow: xproto::Window, // never changes + #[allow(dead_code)] + visual: u32, // never changes + root: xproto::Window, // never changes + #[allow(dead_code)] + screen_id: i32, // never changes + selected_cursor: Mutex, + cursor_grabbed_mode: Mutex, + #[allow(clippy::mutex_atomic)] + cursor_visible: Mutex, + ime_sender: Mutex, + pub shared_state: Mutex, + redraw_sender: WakeSender, + activation_sender: WakeSender, +} + +macro_rules! leap { + ($e:expr) => { + match $e { + Ok(x) => x, + Err(err) => return Err(os_error!(OsError::XError(X11Error::from(err).into()))), + } + }; +} + +impl UnownedWindow { + #[allow(clippy::unnecessary_cast)] + pub(crate) fn new( + event_loop: &ActiveEventLoop, + window_attrs: WindowAttributes, + ) -> Result { + let xconn = &event_loop.xconn; + let atoms = xconn.atoms(); + + let screen_id = match window_attrs.platform_specific.x11.screen_id { + Some(id) => id, + None => xconn.default_screen_index() as c_int, + }; + + let screen = { + let screen_id_usize = usize::try_from(screen_id) + .map_err(|_| os_error!(OsError::Misc("screen id must be non-negative")))?; + xconn.xcb_connection().setup().roots.get(screen_id_usize).ok_or(os_error!( + OsError::Misc("requested screen id not present in server's response") + ))? + }; + + #[cfg(feature = "rwh_06")] + let root = match window_attrs.parent_window.as_ref().map(|handle| handle.0) { + Some(rwh_06::RawWindowHandle::Xlib(handle)) => handle.window as xproto::Window, + Some(rwh_06::RawWindowHandle::Xcb(handle)) => handle.window.get(), + Some(raw) => unreachable!("Invalid raw window handle {raw:?} on X11"), + None => screen.root, + }; + #[cfg(not(feature = "rwh_06"))] + let root = event_loop.root; + + let mut monitors = leap!(xconn.available_monitors()); + let guessed_monitor = if monitors.is_empty() { + X11MonitorHandle::dummy() + } else { + xconn + .query_pointer(root, util::VIRTUAL_CORE_POINTER) + .ok() + .and_then(|pointer_state| { + let (x, y) = (pointer_state.root_x as i64, pointer_state.root_y as i64); + + for i in 0..monitors.len() { + if monitors[i].rect.contains_point(x, y) { + return Some(monitors.swap_remove(i)); + } + } + + None + }) + .unwrap_or_else(|| monitors.swap_remove(0)) + }; + let scale_factor = guessed_monitor.scale_factor(); + + info!("Guessed window scale factor: {}", scale_factor); + + let max_inner_size: Option<(u32, u32)> = + window_attrs.max_inner_size.map(|size| size.to_physical::(scale_factor).into()); + let min_inner_size: Option<(u32, u32)> = + window_attrs.min_inner_size.map(|size| size.to_physical::(scale_factor).into()); + + let position = + window_attrs.position.map(|position| position.to_physical::(scale_factor)); + + let dimensions = { + // x11 only applies constraints when the window is actively resized + // by the user, so we have to manually apply the initial constraints + let mut dimensions: (u32, u32) = window_attrs + .inner_size + .map(|size| size.to_physical::(scale_factor)) + .or_else(|| Some((800, 600).into())) + .map(Into::into) + .unwrap(); + if let Some(max) = max_inner_size { + dimensions.0 = cmp::min(dimensions.0, max.0); + dimensions.1 = cmp::min(dimensions.1, max.1); + } + if let Some(min) = min_inner_size { + dimensions.0 = cmp::max(dimensions.0, min.0); + dimensions.1 = cmp::max(dimensions.1, min.1); + } + debug!("Calculated physical dimensions: {}x{}", dimensions.0, dimensions.1); + dimensions + }; + + // An iterator over the visuals matching screen id combined with their depths. + let mut all_visuals = screen + .allowed_depths + .iter() + .flat_map(|depth| depth.visuals.iter().map(move |visual| (visual, depth.depth))); + + // creating + let (visualtype, depth, require_colormap) = + match window_attrs.platform_specific.x11.visual_id { + Some(vi) => { + // Find this specific visual. + let (visualtype, depth) = + all_visuals.find(|(visual, _)| visual.visual_id == vi).ok_or_else( + || os_error!(OsError::XError(X11Error::NoSuchVisual(vi).into())), + )?; + + (Some(visualtype), depth, true) + }, + None if window_attrs.transparent => { + // Find a suitable visual, true color with 32 bits of depth. + all_visuals + .find_map(|(visual, depth)| { + (depth == 32 && visual.class == xproto::VisualClass::TRUE_COLOR) + .then_some((Some(visual), depth, true)) + }) + .unwrap_or_else(|| { + debug!( + "Could not set transparency, because XMatchVisualInfo returned \ + zero for the required parameters" + ); + (None as _, x11rb::COPY_FROM_PARENT as _, false) + }) + }, + _ => (None, x11rb::COPY_FROM_PARENT as _, false), + }; + let mut visual = visualtype.map_or(x11rb::COPY_FROM_PARENT, |v| v.visual_id); + + let window_attributes = { + use xproto::EventMask; + + let mut aux = xproto::CreateWindowAux::new(); + let event_mask = EventMask::EXPOSURE + | EventMask::STRUCTURE_NOTIFY + | EventMask::VISIBILITY_CHANGE + | EventMask::KEY_PRESS + | EventMask::KEY_RELEASE + | EventMask::KEYMAP_STATE + | EventMask::BUTTON_PRESS + | EventMask::BUTTON_RELEASE + | EventMask::POINTER_MOTION + | EventMask::PROPERTY_CHANGE; + + aux = aux.event_mask(event_mask).border_pixel(0); + + if window_attrs.platform_specific.x11.override_redirect { + aux = aux.override_redirect(true as u32); + } + + // Add a colormap if needed. + let colormap_visual = match window_attrs.platform_specific.x11.visual_id { + Some(vi) => Some(vi), + None if require_colormap => Some(visual), + _ => None, + }; + + if let Some(visual) = colormap_visual { + let colormap = leap!(xconn.xcb_connection().generate_id()); + leap!(xconn.xcb_connection().create_colormap( + xproto::ColormapAlloc::NONE, + colormap, + root, + visual, + )); + aux = aux.colormap(colormap); + } else { + aux = aux.colormap(0); + } + + aux + }; + + // Figure out the window's parent. + let parent = window_attrs.platform_specific.x11.embed_window.unwrap_or(root); + + // finally creating the window + let xwindow = { + let (x, y) = position.map_or((0, 0), Into::into); + let wid = leap!(xconn.xcb_connection().generate_id()); + let result = xconn.xcb_connection().create_window( + depth, + wid, + parent, + x, + y, + dimensions.0.try_into().unwrap(), + dimensions.1.try_into().unwrap(), + 0, + xproto::WindowClass::INPUT_OUTPUT, + visual, + &window_attributes, + ); + leap!(leap!(result).check()); + + wid + }; + + // The COPY_FROM_PARENT is a special value for the visual used to copy + // the visual from the parent window, thus we have to query the visual + // we've got when we built the window above. + if visual == x11rb::COPY_FROM_PARENT { + visual = leap!(leap!(xconn + .xcb_connection() + .get_window_attributes(xwindow as xproto::Window)) + .reply()) + .visual; + } + + #[allow(clippy::mutex_atomic)] + let mut window = UnownedWindow { + xconn: Arc::clone(xconn), + xwindow: xwindow as xproto::Window, + visual, + root, + screen_id, + selected_cursor: Default::default(), + cursor_grabbed_mode: Mutex::new(CursorGrabMode::None), + cursor_visible: Mutex::new(true), + ime_sender: Mutex::new(event_loop.ime_sender.clone()), + shared_state: SharedState::new(guessed_monitor, &window_attrs), + redraw_sender: event_loop.redraw_sender.clone(), + activation_sender: event_loop.activation_sender.clone(), + }; + + // Title must be set before mapping. Some tiling window managers (i.e. i3) use the window + // title to determine placement/etc., so doing this after mapping would cause the WM to + // act on the wrong title state. + leap!(window.set_title_inner(&window_attrs.title)).ignore_error(); + leap!(window.set_decorations_inner(window_attrs.decorations)).ignore_error(); + + if let Some(theme) = window_attrs.preferred_theme { + leap!(window.set_theme_inner(Some(theme))).ignore_error(); + } + + // Embed the window if needed. + if window_attrs.platform_specific.x11.embed_window.is_some() { + window.embed_window()?; + } + + { + // Enable drag and drop (TODO: extend API to make this toggleable) + { + let dnd_aware_atom = atoms[XdndAware]; + let version = &[5u32]; // Latest version; hasn't changed since 2002 + leap!(xconn.change_property( + window.xwindow, + dnd_aware_atom, + u32::from(xproto::AtomEnum::ATOM), + xproto::PropMode::REPLACE, + version, + )) + .ignore_error(); + } + + // WM_CLASS must be set *before* mapping the window, as per ICCCM! + { + let (instance, class) = if let Some(name) = window_attrs.platform_specific.name { + (name.instance, name.general) + } else { + let class = env::args_os() + .next() + .as_ref() + // Default to the name of the binary (via argv[0]) + .and_then(|path| Path::new(path).file_name()) + .and_then(|bin_name| bin_name.to_str()) + .map(|bin_name| bin_name.to_owned()) + .unwrap_or_else(|| window_attrs.title.clone()); + // This environment variable is extraordinarily unlikely to actually be used... + let instance = env::var("RESOURCE_NAME").ok().unwrap_or_else(|| class.clone()); + (instance, class) + }; + + let class = format!("{instance}\0{class}\0"); + leap!(xconn.change_property( + window.xwindow, + xproto::Atom::from(xproto::AtomEnum::WM_CLASS), + xproto::Atom::from(xproto::AtomEnum::STRING), + xproto::PropMode::REPLACE, + class.as_bytes(), + )) + .ignore_error(); + } + + if let Some(flusher) = leap!(window.set_pid()) { + flusher.ignore_error() + } + + leap!(window.set_window_types(window_attrs.platform_specific.x11.x11_window_types)) + .ignore_error(); + + // Set size hints. + let mut min_inner_size = + window_attrs.min_inner_size.map(|size| size.to_physical::(scale_factor)); + let mut max_inner_size = + window_attrs.max_inner_size.map(|size| size.to_physical::(scale_factor)); + + if !window_attrs.resizable { + if util::wm_name_is_one_of(&["Xfwm4"]) { + warn!("To avoid a WM bug, disabling resizing has no effect on Xfwm4"); + } else { + max_inner_size = Some(dimensions.into()); + min_inner_size = Some(dimensions.into()); + } + } + + let shared_state = window.shared_state.get_mut().unwrap(); + shared_state.min_inner_size = min_inner_size.map(Into::into); + shared_state.max_inner_size = max_inner_size.map(Into::into); + shared_state.resize_increments = window_attrs.resize_increments; + shared_state.base_size = window_attrs.platform_specific.x11.base_size; + + let normal_hints = WmSizeHints { + position: position.map(|PhysicalPosition { x, y }| { + (WmSizeHintsSpecification::UserSpecified, x, y) + }), + size: Some(( + WmSizeHintsSpecification::UserSpecified, + cast_dimension_to_hint(dimensions.0), + cast_dimension_to_hint(dimensions.1), + )), + max_size: max_inner_size.map(cast_physical_size_to_hint), + min_size: min_inner_size.map(cast_physical_size_to_hint), + size_increment: window_attrs + .resize_increments + .map(|size| cast_size_to_hint(size, scale_factor)), + base_size: window_attrs + .platform_specific + .x11 + .base_size + .map(|size| cast_size_to_hint(size, scale_factor)), + aspect: None, + win_gravity: None, + }; + leap!(leap!(normal_hints.set( + xconn.xcb_connection(), + window.xwindow as xproto::Window, + xproto::AtomEnum::WM_NORMAL_HINTS, + )) + .check()); + + // Set window icons + if let Some(icon) = window_attrs.window_icon { + leap!(window.set_icon_inner(icon.inner)).ignore_error(); + } + + // Opt into handling window close + let result = xconn.xcb_connection().change_property( + xproto::PropMode::REPLACE, + window.xwindow, + atoms[WM_PROTOCOLS], + xproto::AtomEnum::ATOM, + 32, + 2, + bytemuck::cast_slice::(&[ + atoms[WM_DELETE_WINDOW], + atoms[_NET_WM_PING], + ]), + ); + leap!(result).ignore_error(); + + // Select XInput2 events + let mask = xinput::XIEventMask::MOTION + | xinput::XIEventMask::BUTTON_PRESS + | xinput::XIEventMask::BUTTON_RELEASE + | xinput::XIEventMask::ENTER + | xinput::XIEventMask::LEAVE + | xinput::XIEventMask::FOCUS_IN + | xinput::XIEventMask::FOCUS_OUT + | xinput::XIEventMask::TOUCH_BEGIN + | xinput::XIEventMask::TOUCH_UPDATE + | xinput::XIEventMask::TOUCH_END; + leap!(xconn.select_xinput_events(window.xwindow, super::ALL_MASTER_DEVICES, mask)) + .ignore_error(); + + // Set visibility (map window) + if window_attrs.visible { + leap!(xconn.xcb_connection().map_window(window.xwindow)).ignore_error(); + leap!(xconn.xcb_connection().configure_window( + xwindow, + &xproto::ConfigureWindowAux::new().stack_mode(xproto::StackMode::ABOVE) + )) + .ignore_error(); + } + + // Attempt to make keyboard input repeat detectable + unsafe { + let mut supported_ptr = ffi::False; + (xconn.xlib.XkbSetDetectableAutoRepeat)( + xconn.display, + ffi::True, + &mut supported_ptr, + ); + if supported_ptr == ffi::False { + return Err(os_error!(OsError::Misc("`XkbSetDetectableAutoRepeat` failed"))); + } + } + + // Try to create input context for the window. + if let Some(ime) = event_loop.ime.as_ref() { + let result = ime.borrow_mut().create_context(window.xwindow as ffi::Window, false); + leap!(result); + } + + // These properties must be set after mapping + if window_attrs.maximized { + leap!(window.set_maximized_inner(window_attrs.maximized)).ignore_error(); + } + if window_attrs.fullscreen.is_some() { + if let Some(flusher) = + leap!(window + .set_fullscreen_inner(window_attrs.fullscreen.clone().map(Into::into))) + { + flusher.ignore_error() + } + + if let Some(PhysicalPosition { x, y }) = position { + let shared_state = window.shared_state.get_mut().unwrap(); + + shared_state.restore_position = Some((x, y)); + } + } + + leap!(window.set_window_level_inner(window_attrs.window_level)).ignore_error(); + } + + window.set_cursor(window_attrs.cursor); + + // Remove the startup notification if we have one. + if let Some(startup) = window_attrs.platform_specific.activation_token.as_ref() { + leap!(xconn.remove_activation_token(xwindow, &startup.token)); + } + + // We never want to give the user a broken window, since by then, it's too late to handle. + let window = leap!(xconn.sync_with_server().map(|_| window)); + + Ok(window) + } + + /// Embed this window into a parent window. + pub(super) fn embed_window(&self) -> Result<(), RootOsError> { + let atoms = self.xconn.atoms(); + leap!(leap!(self.xconn.change_property( + self.xwindow, + atoms[_XEMBED], + atoms[_XEMBED], + xproto::PropMode::REPLACE, + &[0u32, 1u32], + )) + .check()); + + Ok(()) + } + + pub(super) fn shared_state_lock(&self) -> MutexGuard<'_, SharedState> { + self.shared_state.lock().unwrap() + } + + fn set_pid(&self) -> Result>, X11Error> { + let atoms = self.xconn.atoms(); + let pid_atom = atoms[_NET_WM_PID]; + let client_machine_atom = atoms[WM_CLIENT_MACHINE]; + + // Get the hostname and the PID. + let uname = rustix::system::uname(); + let pid = rustix::process::getpid(); + + self.xconn + .change_property( + self.xwindow, + pid_atom, + xproto::Atom::from(xproto::AtomEnum::CARDINAL), + xproto::PropMode::REPLACE, + &[pid.as_raw_nonzero().get() as util::Cardinal], + )? + .ignore_error(); + let flusher = self.xconn.change_property( + self.xwindow, + client_machine_atom, + xproto::Atom::from(xproto::AtomEnum::STRING), + xproto::PropMode::REPLACE, + uname.nodename().to_bytes(), + ); + flusher.map(Some) + } + + fn set_window_types(&self, window_types: Vec) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let hint_atom = atoms[_NET_WM_WINDOW_TYPE]; + let atoms: Vec<_> = window_types.iter().map(|t| t.as_atom(&self.xconn)).collect(); + + self.xconn.change_property( + self.xwindow, + hint_atom, + xproto::Atom::from(xproto::AtomEnum::ATOM), + xproto::PropMode::REPLACE, + &atoms, + ) + } + + pub fn set_theme_inner(&self, theme: Option) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let hint_atom = atoms[_GTK_THEME_VARIANT]; + let utf8_atom = atoms[UTF8_STRING]; + let variant = match theme { + Some(Theme::Dark) => "dark", + Some(Theme::Light) => "light", + None => "dark", + }; + let variant = CString::new(variant).expect("`_GTK_THEME_VARIANT` contained null byte"); + self.xconn.change_property( + self.xwindow, + hint_atom, + utf8_atom, + xproto::PropMode::REPLACE, + variant.as_bytes(), + ) + } + + #[inline] + pub fn set_theme(&self, theme: Option) { + self.set_theme_inner(theme).expect("Failed to change window theme").ignore_error(); + + self.xconn.flush_requests().expect("Failed to change window theme"); + } + + fn set_netwm( + &self, + operation: util::StateOperation, + properties: (u32, u32, u32, u32), + ) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let state_atom = atoms[_NET_WM_STATE]; + self.xconn.send_client_msg( + self.xwindow, + self.root, + state_atom, + Some(xproto::EventMask::SUBSTRUCTURE_REDIRECT | xproto::EventMask::SUBSTRUCTURE_NOTIFY), + [operation as u32, properties.0, properties.1, properties.2, properties.3], + ) + } + + fn set_fullscreen_hint(&self, fullscreen: bool) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let fullscreen_atom = atoms[_NET_WM_STATE_FULLSCREEN]; + let flusher = self.set_netwm(fullscreen.into(), (fullscreen_atom, 0, 0, 0)); + + if fullscreen { + // Ensure that the fullscreen window receives input focus to prevent + // locking up the user's display. + self.xconn + .xcb_connection() + .set_input_focus(xproto::InputFocus::PARENT, self.xwindow, x11rb::CURRENT_TIME)? + .ignore_error(); + } + + flusher + } + + fn set_fullscreen_inner( + &self, + fullscreen: Option, + ) -> Result>, X11Error> { + let mut shared_state_lock = self.shared_state_lock(); + + match shared_state_lock.visibility { + // Setting fullscreen on a window that is not visible will generate an error. + Visibility::No | Visibility::YesWait => { + shared_state_lock.desired_fullscreen = Some(fullscreen); + return Ok(None); + }, + Visibility::Yes => (), + } + + let old_fullscreen = shared_state_lock.fullscreen.clone(); + if old_fullscreen == fullscreen { + return Ok(None); + } + shared_state_lock.fullscreen.clone_from(&fullscreen); + + match (&old_fullscreen, &fullscreen) { + // Store the desktop video mode before entering exclusive + // fullscreen, so we can restore it upon exit, as XRandR does not + // provide a mechanism to set this per app-session or restore this + // to the desktop video mode as macOS and Windows do + (&None, &Some(Fullscreen::Exclusive(PlatformVideoModeHandle::X(ref video_mode)))) + | ( + &Some(Fullscreen::Borderless(_)), + &Some(Fullscreen::Exclusive(PlatformVideoModeHandle::X(ref video_mode))), + ) => { + let monitor = video_mode.monitor.as_ref().unwrap(); + shared_state_lock.desktop_video_mode = Some(( + monitor.id, + self.xconn.get_crtc_mode(monitor.id).expect("Failed to get desktop video mode"), + )); + }, + // Restore desktop video mode upon exiting exclusive fullscreen + (&Some(Fullscreen::Exclusive(_)), &None) + | (&Some(Fullscreen::Exclusive(_)), &Some(Fullscreen::Borderless(_))) => { + let (monitor_id, mode_id) = shared_state_lock.desktop_video_mode.take().unwrap(); + self.xconn + .set_crtc_config(monitor_id, mode_id) + .expect("failed to restore desktop video mode"); + }, + _ => (), + } + + drop(shared_state_lock); + + match fullscreen { + None => { + let flusher = self.set_fullscreen_hint(false); + let mut shared_state_lock = self.shared_state_lock(); + if let Some(position) = shared_state_lock.restore_position.take() { + drop(shared_state_lock); + self.set_position_inner(position.0, position.1) + .expect_then_ignore_error("Failed to restore window position"); + } + flusher.map(Some) + }, + Some(fullscreen) => { + let (video_mode, monitor) = match fullscreen { + Fullscreen::Exclusive(PlatformVideoModeHandle::X(ref video_mode)) => { + (Some(video_mode), video_mode.monitor.clone().unwrap()) + }, + Fullscreen::Borderless(Some(PlatformMonitorHandle::X(monitor))) => { + (None, monitor) + }, + Fullscreen::Borderless(None) => { + (None, self.shared_state_lock().last_monitor.clone()) + }, + #[cfg(wayland_platform)] + _ => unreachable!(), + }; + + // Don't set fullscreen on an invalid dummy monitor handle + if monitor.is_dummy() { + return Ok(None); + } + + if let Some(video_mode) = video_mode { + // FIXME: this is actually not correct if we're setting the + // video mode to a resolution higher than the current + // desktop resolution, because XRandR does not automatically + // reposition the monitors to the right and below this + // monitor. + // + // What ends up happening is we will get the fullscreen + // window showing up on those monitors as well, because + // their virtual position now overlaps with the monitor that + // we just made larger.. + // + // It'd be quite a bit of work to handle this correctly (and + // nobody else seems to bother doing this correctly either), + // so we're just leaving this broken. Fixing this would + // involve storing all CRTCs upon entering fullscreen, + // restoring them upon exit, and after entering fullscreen, + // repositioning displays to the right and below this + // display. I think there would still be edge cases that are + // difficult or impossible to handle correctly, e.g. what if + // a new monitor was plugged in while in fullscreen? + // + // I think we might just want to disallow setting the video + // mode higher than the current desktop video mode (I'm sure + // this will make someone unhappy, but it's very unusual for + // games to want to do this anyway). + self.xconn + .set_crtc_config(monitor.id, video_mode.native_mode) + .expect("failed to set video mode"); + } + + let window_position = self.outer_position_physical(); + self.shared_state_lock().restore_position = Some(window_position); + let monitor_origin: (i32, i32) = monitor.position().into(); + self.set_position_inner(monitor_origin.0, monitor_origin.1) + .expect_then_ignore_error("Failed to set window position"); + self.set_fullscreen_hint(true).map(Some) + }, + } + } + + #[inline] + pub(crate) fn fullscreen(&self) -> Option { + let shared_state = self.shared_state_lock(); + + shared_state.desired_fullscreen.clone().unwrap_or_else(|| shared_state.fullscreen.clone()) + } + + #[inline] + pub(crate) fn set_fullscreen(&self, fullscreen: Option) { + if let Some(flusher) = + self.set_fullscreen_inner(fullscreen).expect("Failed to change window fullscreen state") + { + flusher.check().expect("Failed to change window fullscreen state"); + self.invalidate_cached_frame_extents(); + } + } + + // Called by EventProcessor when a VisibilityNotify event is received + pub(crate) fn visibility_notify(&self) { + let mut shared_state = self.shared_state_lock(); + + match shared_state.visibility { + Visibility::No => self + .xconn + .xcb_connection() + .unmap_window(self.xwindow) + .expect_then_ignore_error("Failed to unmap window"), + Visibility::Yes => (), + Visibility::YesWait => { + shared_state.visibility = Visibility::Yes; + + if let Some(fullscreen) = shared_state.desired_fullscreen.take() { + drop(shared_state); + self.set_fullscreen(fullscreen); + } + }, + } + } + + pub fn current_monitor(&self) -> Option { + Some(self.shared_state_lock().last_monitor.clone()) + } + + pub fn available_monitors(&self) -> Vec { + self.xconn.available_monitors().expect("Failed to get available monitors") + } + + pub fn primary_monitor(&self) -> Option { + Some(self.xconn.primary_monitor().expect("Failed to get primary monitor")) + } + + #[inline] + pub fn is_minimized(&self) -> Option { + let atoms = self.xconn.atoms(); + let state_atom = atoms[_NET_WM_STATE]; + let state = self.xconn.get_property( + self.xwindow, + state_atom, + xproto::Atom::from(xproto::AtomEnum::ATOM), + ); + let hidden_atom = atoms[_NET_WM_STATE_HIDDEN]; + + Some(match state { + Ok(atoms) => { + atoms.iter().any(|atom: &xproto::Atom| *atom as xproto::Atom == hidden_atom) + }, + _ => false, + }) + } + + /// Refresh the API for the given monitor. + #[inline] + pub(super) fn refresh_dpi_for_monitor( + &self, + new_monitor: &X11MonitorHandle, + maybe_prev_scale_factor: Option, + mut callback: impl FnMut(Event), + ) { + // Check if the self is on this monitor + let monitor = self.shared_state_lock().last_monitor.clone(); + if monitor.name == new_monitor.name { + let (width, height) = self.inner_size_physical(); + let (new_width, new_height) = self.adjust_for_dpi( + // If we couldn't determine the previous scale + // factor (e.g., because all monitors were closed + // before), just pick whatever the current monitor + // has set as a baseline. + maybe_prev_scale_factor.unwrap_or(monitor.scale_factor), + new_monitor.scale_factor, + width, + height, + &self.shared_state_lock(), + ); + + let window_id = crate::window::WindowId(self.id()); + let old_inner_size = PhysicalSize::new(width, height); + let inner_size = Arc::new(Mutex::new(PhysicalSize::new(new_width, new_height))); + callback(Event::WindowEvent { + window_id, + event: WindowEvent::ScaleFactorChanged { + scale_factor: new_monitor.scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&inner_size)), + }, + }); + + let new_inner_size = *inner_size.lock().unwrap(); + drop(inner_size); + + if new_inner_size != old_inner_size { + let (new_width, new_height) = new_inner_size.into(); + self.request_inner_size_physical(new_width, new_height); + } + } + } + + fn set_minimized_inner(&self, minimized: bool) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + + if minimized { + let root_window = self.xconn.default_root().root; + + self.xconn.send_client_msg( + self.xwindow, + root_window, + atoms[WM_CHANGE_STATE], + Some( + xproto::EventMask::SUBSTRUCTURE_REDIRECT + | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + ), + [3u32, 0, 0, 0, 0], + ) + } else { + self.xconn.send_client_msg( + self.xwindow, + self.root, + atoms[_NET_ACTIVE_WINDOW], + Some( + xproto::EventMask::SUBSTRUCTURE_REDIRECT + | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + ), + [1, x11rb::CURRENT_TIME, 0, 0, 0], + ) + } + } + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + self.set_minimized_inner(minimized) + .expect_then_ignore_error("Failed to change window minimization"); + + self.xconn.flush_requests().expect("Failed to change window minimization"); + } + + #[inline] + pub fn is_maximized(&self) -> bool { + let atoms = self.xconn.atoms(); + let state_atom = atoms[_NET_WM_STATE]; + let state = self.xconn.get_property( + self.xwindow, + state_atom, + xproto::Atom::from(xproto::AtomEnum::ATOM), + ); + let horz_atom = atoms[_NET_WM_STATE_MAXIMIZED_HORZ]; + let vert_atom = atoms[_NET_WM_STATE_MAXIMIZED_VERT]; + match state { + Ok(atoms) => { + let horz_maximized = atoms.contains(&horz_atom); + let vert_maximized = atoms.contains(&vert_atom); + horz_maximized && vert_maximized + }, + _ => false, + } + } + + fn set_maximized_inner(&self, maximized: bool) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let horz_atom = atoms[_NET_WM_STATE_MAXIMIZED_HORZ]; + let vert_atom = atoms[_NET_WM_STATE_MAXIMIZED_VERT]; + + self.set_netwm(maximized.into(), (horz_atom, vert_atom, 0, 0)) + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + self.set_maximized_inner(maximized) + .expect_then_ignore_error("Failed to change window maximization"); + self.xconn.flush_requests().expect("Failed to change window maximization"); + self.invalidate_cached_frame_extents(); + } + + fn set_title_inner(&self, title: &str) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + + let title = CString::new(title).expect("Window title contained null byte"); + self.xconn + .change_property( + self.xwindow, + xproto::Atom::from(xproto::AtomEnum::WM_NAME), + xproto::Atom::from(xproto::AtomEnum::STRING), + xproto::PropMode::REPLACE, + title.as_bytes(), + )? + .ignore_error(); + self.xconn.change_property( + self.xwindow, + atoms[_NET_WM_NAME], + atoms[UTF8_STRING], + xproto::PropMode::REPLACE, + title.as_bytes(), + ) + } + + #[inline] + pub fn set_title(&self, title: &str) { + self.set_title_inner(title).expect_then_ignore_error("Failed to set window title"); + + self.xconn.flush_requests().expect("Failed to set window title"); + } + + #[inline] + pub fn set_transparent(&self, _transparent: bool) {} + + #[inline] + pub fn set_blur(&self, _blur: bool) {} + + fn set_decorations_inner(&self, decorations: bool) -> Result, X11Error> { + self.shared_state_lock().is_decorated = decorations; + let mut hints = self.xconn.get_motif_hints(self.xwindow); + + hints.set_decorations(decorations); + + self.xconn.set_motif_hints(self.xwindow, &hints) + } + + #[inline] + pub fn set_decorations(&self, decorations: bool) { + self.set_decorations_inner(decorations) + .expect_then_ignore_error("Failed to set decoration state"); + self.xconn.flush_requests().expect("Failed to set decoration state"); + self.invalidate_cached_frame_extents(); + } + + #[inline] + pub fn is_decorated(&self) -> bool { + self.shared_state_lock().is_decorated + } + + fn set_maximizable_inner(&self, maximizable: bool) -> Result, X11Error> { + let mut hints = self.xconn.get_motif_hints(self.xwindow); + + hints.set_maximizable(maximizable); + + self.xconn.set_motif_hints(self.xwindow, &hints) + } + + fn toggle_atom(&self, atom_name: AtomName, enable: bool) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let atom = atoms[atom_name]; + self.set_netwm(enable.into(), (atom, 0, 0, 0)) + } + + fn set_window_level_inner(&self, level: WindowLevel) -> Result, X11Error> { + self.toggle_atom(_NET_WM_STATE_ABOVE, level == WindowLevel::AlwaysOnTop)?.ignore_error(); + self.toggle_atom(_NET_WM_STATE_BELOW, level == WindowLevel::AlwaysOnBottom) + } + + #[inline] + pub fn set_window_level(&self, level: WindowLevel) { + self.set_window_level_inner(level) + .expect_then_ignore_error("Failed to set window-level state"); + self.xconn.flush_requests().expect("Failed to set window-level state"); + } + + fn set_icon_inner(&self, icon: PlatformIcon) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let icon_atom = atoms[_NET_WM_ICON]; + let data = icon.to_cardinals(); + self.xconn.change_property( + self.xwindow, + icon_atom, + xproto::Atom::from(xproto::AtomEnum::CARDINAL), + xproto::PropMode::REPLACE, + data.as_slice(), + ) + } + + fn unset_icon_inner(&self) -> Result, X11Error> { + let atoms = self.xconn.atoms(); + let icon_atom = atoms[_NET_WM_ICON]; + let empty_data: [util::Cardinal; 0] = []; + self.xconn.change_property( + self.xwindow, + icon_atom, + xproto::Atom::from(xproto::AtomEnum::CARDINAL), + xproto::PropMode::REPLACE, + &empty_data, + ) + } + + #[inline] + pub(crate) fn set_window_icon(&self, icon: Option) { + match icon { + Some(icon) => self.set_icon_inner(icon), + None => self.unset_icon_inner(), + } + .expect_then_ignore_error("Failed to set icons"); + + self.xconn.flush_requests().expect("Failed to set icons"); + } + + #[inline] + pub fn set_visible(&self, visible: bool) { + let mut shared_state = self.shared_state_lock(); + + match (visible, shared_state.visibility) { + (true, Visibility::Yes) | (true, Visibility::YesWait) | (false, Visibility::No) => { + return + }, + _ => (), + } + + if visible { + self.xconn + .xcb_connection() + .map_window(self.xwindow) + .expect_then_ignore_error("Failed to call `xcb_map_window`"); + self.xconn + .xcb_connection() + .configure_window( + self.xwindow, + &xproto::ConfigureWindowAux::new().stack_mode(xproto::StackMode::ABOVE), + ) + .expect_then_ignore_error("Failed to call `xcb_configure_window`"); + self.xconn.flush_requests().expect("Failed to call XMapRaised"); + shared_state.visibility = Visibility::YesWait; + } else { + self.xconn + .xcb_connection() + .unmap_window(self.xwindow) + .expect_then_ignore_error("Failed to call `xcb_unmap_window`"); + self.xconn.flush_requests().expect("Failed to call XUnmapWindow"); + shared_state.visibility = Visibility::No; + } + } + + #[inline] + pub fn is_visible(&self) -> Option { + Some(self.shared_state_lock().visibility == Visibility::Yes) + } + + fn update_cached_frame_extents(&self) { + let extents = self.xconn.get_frame_extents_heuristic(self.xwindow, self.root); + self.shared_state_lock().frame_extents = Some(extents); + } + + pub(crate) fn invalidate_cached_frame_extents(&self) { + self.shared_state_lock().frame_extents.take(); + } + + pub(crate) fn outer_position_physical(&self) -> (i32, i32) { + let extents = self.shared_state_lock().frame_extents.clone(); + if let Some(extents) = extents { + let (x, y) = self.inner_position_physical(); + extents.inner_pos_to_outer(x, y) + } else { + self.update_cached_frame_extents(); + self.outer_position_physical() + } + } + + #[inline] + pub fn outer_position(&self) -> Result, NotSupportedError> { + let extents = self.shared_state_lock().frame_extents.clone(); + if let Some(extents) = extents { + let (x, y) = self.inner_position_physical(); + Ok(extents.inner_pos_to_outer(x, y).into()) + } else { + self.update_cached_frame_extents(); + self.outer_position() + } + } + + pub(crate) fn inner_position_physical(&self) -> (i32, i32) { + // This should be okay to unwrap since the only error XTranslateCoordinates can return + // is BadWindow, and if the window handle is bad we have bigger problems. + self.xconn + .translate_coords(self.xwindow, self.root) + .map(|coords| (coords.dst_x.into(), coords.dst_y.into())) + .unwrap() + } + + #[inline] + pub fn inner_position(&self) -> Result, NotSupportedError> { + Ok(self.inner_position_physical().into()) + } + + pub(crate) fn set_position_inner( + &self, + mut x: i32, + mut y: i32, + ) -> Result, X11Error> { + // There are a few WMs that set client area position rather than window position, so + // we'll translate for consistency. + if util::wm_name_is_one_of(&["Enlightenment", "FVWM"]) { + let extents = self.shared_state_lock().frame_extents.clone(); + if let Some(extents) = extents { + x += cast_dimension_to_hint(extents.frame_extents.left); + y += cast_dimension_to_hint(extents.frame_extents.top); + } else { + self.update_cached_frame_extents(); + return self.set_position_inner(x, y); + } + } + + self.xconn + .xcb_connection() + .configure_window(self.xwindow, &xproto::ConfigureWindowAux::new().x(x).y(y)) + .map_err(Into::into) + } + + pub(crate) fn set_position_physical(&self, x: i32, y: i32) { + self.set_position_inner(x, y).expect_then_ignore_error("Failed to call `XMoveWindow`"); + } + + #[inline] + pub fn set_outer_position(&self, position: Position) { + let (x, y) = position.to_physical::(self.scale_factor()).into(); + self.set_position_physical(x, y); + } + + pub(crate) fn inner_size_physical(&self) -> (u32, u32) { + // This should be okay to unwrap since the only error XGetGeometry can return + // is BadWindow, and if the window handle is bad we have bigger problems. + self.xconn + .get_geometry(self.xwindow) + .map(|geo| (geo.width.into(), geo.height.into())) + .unwrap() + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + self.inner_size_physical().into() + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + let extents = self.shared_state_lock().frame_extents.clone(); + if let Some(extents) = extents { + let (width, height) = self.inner_size_physical(); + extents.inner_size_to_outer(width, height).into() + } else { + self.update_cached_frame_extents(); + self.outer_size() + } + } + + pub(crate) fn request_inner_size_physical(&self, width: u32, height: u32) { + self.xconn + .xcb_connection() + .configure_window( + self.xwindow, + &xproto::ConfigureWindowAux::new().width(width).height(height), + ) + .expect_then_ignore_error("Failed to call `xcb_configure_window`"); + self.xconn.flush_requests().expect("Failed to call XResizeWindow"); + // cursor_hittest needs to be reapplied after each window resize. + if self.shared_state_lock().cursor_hittest.unwrap_or(false) { + let _ = self.set_cursor_hittest(true); + } + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + let scale_factor = self.scale_factor(); + let size = size.to_physical::(scale_factor).into(); + if !self.shared_state_lock().is_resizable { + self.update_normal_hints(|normal_hints| { + normal_hints.min_size = Some(size); + normal_hints.max_size = Some(size); + }) + .expect("Failed to call `XSetWMNormalHints`"); + } + self.request_inner_size_physical(size.0 as u32, size.1 as u32); + + None + } + + fn update_normal_hints(&self, callback: F) -> Result<(), X11Error> + where + F: FnOnce(&mut WmSizeHints), + { + let mut normal_hints = WmSizeHints::get( + self.xconn.xcb_connection(), + self.xwindow as xproto::Window, + xproto::AtomEnum::WM_NORMAL_HINTS, + )? + .reply()? + .unwrap_or_default(); + callback(&mut normal_hints); + normal_hints + .set( + self.xconn.xcb_connection(), + self.xwindow as xproto::Window, + xproto::AtomEnum::WM_NORMAL_HINTS, + )? + .ignore_error(); + Ok(()) + } + + pub(crate) fn set_min_inner_size_physical(&self, dimensions: Option<(u32, u32)>) { + self.update_normal_hints(|normal_hints| { + normal_hints.min_size = + dimensions.map(|(w, h)| (cast_dimension_to_hint(w), cast_dimension_to_hint(h))) + }) + .expect("Failed to call `XSetWMNormalHints`"); + } + + #[inline] + pub fn set_min_inner_size(&self, dimensions: Option) { + self.shared_state_lock().min_inner_size = dimensions; + let physical_dimensions = + dimensions.map(|dimensions| dimensions.to_physical::(self.scale_factor()).into()); + self.set_min_inner_size_physical(physical_dimensions); + } + + pub(crate) fn set_max_inner_size_physical(&self, dimensions: Option<(u32, u32)>) { + self.update_normal_hints(|normal_hints| { + normal_hints.max_size = + dimensions.map(|(w, h)| (cast_dimension_to_hint(w), cast_dimension_to_hint(h))) + }) + .expect("Failed to call `XSetWMNormalHints`"); + } + + #[inline] + pub fn set_max_inner_size(&self, dimensions: Option) { + self.shared_state_lock().max_inner_size = dimensions; + let physical_dimensions = + dimensions.map(|dimensions| dimensions.to_physical::(self.scale_factor()).into()); + self.set_max_inner_size_physical(physical_dimensions); + } + + #[inline] + pub fn resize_increments(&self) -> Option> { + WmSizeHints::get( + self.xconn.xcb_connection(), + self.xwindow as xproto::Window, + xproto::AtomEnum::WM_NORMAL_HINTS, + ) + .ok() + .and_then(|cookie| cookie.reply().ok()) + .flatten() + .and_then(|hints| hints.size_increment) + .map(|(width, height)| (width as u32, height as u32).into()) + } + + #[inline] + pub fn set_resize_increments(&self, increments: Option) { + self.shared_state_lock().resize_increments = increments; + let physical_increments = + increments.map(|increments| cast_size_to_hint(increments, self.scale_factor())); + self.update_normal_hints(|hints| hints.size_increment = physical_increments) + .expect("Failed to call `XSetWMNormalHints`"); + } + + pub(crate) fn adjust_for_dpi( + &self, + old_scale_factor: f64, + new_scale_factor: f64, + width: u32, + height: u32, + shared_state: &SharedState, + ) -> (u32, u32) { + let scale_factor = new_scale_factor / old_scale_factor; + self.update_normal_hints(|normal_hints| { + let dpi_adjuster = |size: Size| -> (i32, i32) { cast_size_to_hint(size, scale_factor) }; + let max_size = shared_state.max_inner_size.map(dpi_adjuster); + let min_size = shared_state.min_inner_size.map(dpi_adjuster); + let resize_increments = shared_state.resize_increments.map(dpi_adjuster); + let base_size = shared_state.base_size.map(dpi_adjuster); + + normal_hints.max_size = max_size; + normal_hints.min_size = min_size; + normal_hints.size_increment = resize_increments; + normal_hints.base_size = base_size; + }) + .expect("Failed to update normal hints"); + + let new_width = (width as f64 * scale_factor).round() as u32; + let new_height = (height as f64 * scale_factor).round() as u32; + + (new_width, new_height) + } + + pub fn set_resizable(&self, resizable: bool) { + if util::wm_name_is_one_of(&["Xfwm4"]) { + // Making the window unresizable on Xfwm prevents further changes to `WM_NORMAL_HINTS` + // from being detected. This makes it impossible for resizing to be + // re-enabled, and also breaks DPI scaling. As such, we choose the lesser of + // two evils and do nothing. + warn!("To avoid a WM bug, disabling resizing has no effect on Xfwm4"); + return; + } + + let (min_size, max_size) = if resizable { + let shared_state_lock = self.shared_state_lock(); + (shared_state_lock.min_inner_size, shared_state_lock.max_inner_size) + } else { + let window_size = Some(Size::from(self.inner_size())); + (window_size, window_size) + }; + self.shared_state_lock().is_resizable = resizable; + + self.set_maximizable_inner(resizable) + .expect_then_ignore_error("Failed to call `XSetWMNormalHints`"); + + let scale_factor = self.scale_factor(); + let min_inner_size = min_size.map(|size| cast_size_to_hint(size, scale_factor)); + let max_inner_size = max_size.map(|size| cast_size_to_hint(size, scale_factor)); + self.update_normal_hints(|normal_hints| { + normal_hints.min_size = min_inner_size; + normal_hints.max_size = max_inner_size; + }) + .expect("Failed to call `XSetWMNormalHints`"); + } + + #[inline] + pub fn is_resizable(&self) -> bool { + self.shared_state_lock().is_resizable + } + + #[inline] + pub fn set_enabled_buttons(&self, _buttons: WindowButtons) {} + + #[inline] + pub fn enabled_buttons(&self) -> WindowButtons { + WindowButtons::all() + } + + #[allow(dead_code)] + #[inline] + pub fn xlib_display(&self) -> *mut c_void { + self.xconn.display as _ + } + + #[allow(dead_code)] + #[inline] + pub fn xlib_window(&self) -> c_ulong { + self.xwindow as ffi::Window + } + + #[inline] + pub fn set_cursor(&self, cursor: Cursor) { + match cursor { + Cursor::Icon(icon) => { + let old_cursor = replace( + &mut *self.selected_cursor.lock().unwrap(), + SelectedCursor::Named(icon), + ); + + #[allow(clippy::mutex_atomic)] + if SelectedCursor::Named(icon) != old_cursor && *self.cursor_visible.lock().unwrap() + { + self.xconn.set_cursor_icon(self.xwindow, Some(icon)); + } + }, + Cursor::Custom(RootCustomCursor { inner: PlatformCustomCursor::X(cursor) }) => { + #[allow(clippy::mutex_atomic)] + if *self.cursor_visible.lock().unwrap() { + self.xconn.set_custom_cursor(self.xwindow, &cursor); + } + + *self.selected_cursor.lock().unwrap() = SelectedCursor::Custom(cursor); + }, + #[cfg(wayland_platform)] + Cursor::Custom(RootCustomCursor { inner: PlatformCustomCursor::Wayland(_) }) => { + tracing::error!("passed a Wayland cursor to X11 backend") + }, + } + } + + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + // We don't support the locked cursor yet, so ignore it early on. + if mode == CursorGrabMode::Locked { + return Err(ExternalError::NotSupported(NotSupportedError::new())); + } + + let mut grabbed_lock = self.cursor_grabbed_mode.lock().unwrap(); + if mode == *grabbed_lock { + return Ok(()); + } + + // We ungrab before grabbing to prevent passive grabs from causing `AlreadyGrabbed`. + // Therefore, this is common to both codepaths. + self.xconn + .xcb_connection() + .ungrab_pointer(x11rb::CURRENT_TIME) + .expect_then_ignore_error("Failed to call `xcb_ungrab_pointer`"); + *grabbed_lock = CursorGrabMode::None; + + let result = match mode { + CursorGrabMode::None => self.xconn.flush_requests().map_err(|err| { + ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(err).into()))) + }), + CursorGrabMode::Confined => { + let result = self + .xconn + .xcb_connection() + .grab_pointer( + true as _, + self.xwindow, + xproto::EventMask::BUTTON_PRESS + | xproto::EventMask::BUTTON_RELEASE + | xproto::EventMask::ENTER_WINDOW + | xproto::EventMask::LEAVE_WINDOW + | xproto::EventMask::POINTER_MOTION + | xproto::EventMask::POINTER_MOTION_HINT + | xproto::EventMask::BUTTON1_MOTION + | xproto::EventMask::BUTTON2_MOTION + | xproto::EventMask::BUTTON3_MOTION + | xproto::EventMask::BUTTON4_MOTION + | xproto::EventMask::BUTTON5_MOTION + | xproto::EventMask::KEYMAP_STATE, + xproto::GrabMode::ASYNC, + xproto::GrabMode::ASYNC, + self.xwindow, + 0u32, + x11rb::CURRENT_TIME, + ) + .expect("Failed to call `grab_pointer`") + .reply() + .expect("Failed to receive reply from `grab_pointer`"); + + match result.status { + xproto::GrabStatus::SUCCESS => Ok(()), + xproto::GrabStatus::ALREADY_GRABBED => { + Err("Cursor could not be confined: already confined by another client") + }, + xproto::GrabStatus::INVALID_TIME => { + Err("Cursor could not be confined: invalid time") + }, + xproto::GrabStatus::NOT_VIEWABLE => { + Err("Cursor could not be confined: confine location not viewable") + }, + xproto::GrabStatus::FROZEN => { + Err("Cursor could not be confined: frozen by another client") + }, + _ => unreachable!(), + } + .map_err(|err| ExternalError::Os(os_error!(OsError::Misc(err)))) + }, + CursorGrabMode::Locked => return Ok(()), + }; + + if result.is_ok() { + *grabbed_lock = mode; + } + + result + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + #[allow(clippy::mutex_atomic)] + let mut visible_lock = self.cursor_visible.lock().unwrap(); + if visible == *visible_lock { + return; + } + let cursor = + if visible { Some((*self.selected_cursor.lock().unwrap()).clone()) } else { None }; + *visible_lock = visible; + drop(visible_lock); + match cursor { + Some(SelectedCursor::Custom(cursor)) => { + self.xconn.set_custom_cursor(self.xwindow, &cursor); + }, + Some(SelectedCursor::Named(cursor)) => { + self.xconn.set_cursor_icon(self.xwindow, Some(cursor)); + }, + None => { + self.xconn.set_cursor_icon(self.xwindow, None); + }, + } + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + self.shared_state_lock().last_monitor.scale_factor + } + + pub fn set_cursor_position_physical(&self, x: i32, y: i32) -> Result<(), ExternalError> { + { + self.xconn + .xcb_connection() + .warp_pointer(x11rb::NONE, self.xwindow, 0, 0, 0, 0, x as _, y as _) + .map_err(|e| { + ExternalError::Os(os_error!(OsError::XError(X11Error::from(e).into()))) + })?; + self.xconn.flush_requests().map_err(|e| { + ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(e).into()))) + }) + } + } + + #[inline] + pub fn set_cursor_position(&self, position: Position) -> Result<(), ExternalError> { + let (x, y) = position.to_physical::(self.scale_factor()).into(); + self.set_cursor_position_physical(x, y) + } + + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + // Implement cursor hittest for X11 by either setting an empty or full window input shape. + + // In X11, every window has two "shapes": + // * Bounding shape: defines the visible outline of the window. + // * Input shape: defines the region of the window that receives pointer/keyboard events. + // If the input shape is the full window rectangle, the window behaves normally. + // If the input shape is empty, the window is completely click‑through. + // Here, we implement hit test by mapping `hittest = true` to "restore a full input shape" + // and `hittest = false` to "clear the input shape" (empty list of rectangles). + let mut rectangles: Vec = Vec::new(); + if hittest { + let size = self.inner_size(); + rectangles.push(Rectangle { + x: 0, + y: 0, + width: size.width as u16, + height: size.height as u16, + }) + } + + self.xconn + .xcb_connection() + .shape_rectangles( + SO::SET, + SK::INPUT, + ClipOrdering::UNSORTED, + self.xwindow, + 0, + 0, + &rectangles, + ) + .map_err(|_e| ExternalError::Ignored)?; + self.shared_state_lock().cursor_hittest = Some(hittest); + Ok(()) + } + + /// Moves the window while it is being dragged. + pub fn drag_window(&self) -> Result<(), ExternalError> { + self.drag_initiate(util::MOVERESIZE_MOVE) + } + + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + + /// Resizes the window while it is being dragged. + pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { + self.drag_initiate(match direction { + ResizeDirection::East => util::MOVERESIZE_RIGHT, + ResizeDirection::North => util::MOVERESIZE_TOP, + ResizeDirection::NorthEast => util::MOVERESIZE_TOPRIGHT, + ResizeDirection::NorthWest => util::MOVERESIZE_TOPLEFT, + ResizeDirection::South => util::MOVERESIZE_BOTTOM, + ResizeDirection::SouthEast => util::MOVERESIZE_BOTTOMRIGHT, + ResizeDirection::SouthWest => util::MOVERESIZE_BOTTOMLEFT, + ResizeDirection::West => util::MOVERESIZE_LEFT, + }) + } + + /// Initiates a drag operation while the left mouse button is pressed. + fn drag_initiate(&self, action: isize) -> Result<(), ExternalError> { + let pointer = self + .xconn + .query_pointer(self.xwindow, util::VIRTUAL_CORE_POINTER) + .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err.into()))))?; + + let window = self.inner_position().map_err(ExternalError::NotSupported)?; + + let atoms = self.xconn.atoms(); + let message = atoms[_NET_WM_MOVERESIZE]; + + // we can't use `set_cursor_grab(false)` here because it doesn't run `XUngrabPointer` + // if the cursor isn't currently grabbed + let mut grabbed_lock = self.cursor_grabbed_mode.lock().unwrap(); + self.xconn + .xcb_connection() + .ungrab_pointer(x11rb::CURRENT_TIME) + .map_err(|err| { + ExternalError::Os(os_error!(OsError::XError(X11Error::from(err).into()))) + })? + .ignore_error(); + self.xconn.flush_requests().map_err(|err| { + ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(err).into()))) + })?; + *grabbed_lock = CursorGrabMode::None; + + // we keep the lock until we are done + self.xconn + .send_client_msg( + self.xwindow, + self.root, + message, + Some( + xproto::EventMask::SUBSTRUCTURE_REDIRECT + | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + ), + [ + (window.x + xinput_fp1616_to_float(pointer.win_x) as i32) as u32, + (window.y + xinput_fp1616_to_float(pointer.win_y) as i32) as u32, + action.try_into().unwrap(), + 1, // Button 1 + 1, + ], + ) + .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err.into()))))?; + + self.xconn.flush_requests().map_err(|err| { + ExternalError::Os(os_error!(OsError::XError(X11Error::Xlib(err).into()))) + }) + } + + #[inline] + pub fn set_ime_cursor_area(&self, spot: Position, _size: Size) { + let (x, y) = spot.to_physical::(self.scale_factor()).into(); + let _ = self.ime_sender.lock().unwrap().send(ImeRequest::Position( + self.xwindow as ffi::Window, + x, + y, + )); + } + + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + let _ = self + .ime_sender + .lock() + .unwrap() + .send(ImeRequest::Allow(self.xwindow as ffi::Window, allowed)); + } + + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + + #[inline] + pub fn focus_window(&self) { + let atoms = self.xconn.atoms(); + let state_atom = atoms[WM_STATE]; + let state_type_atom = atoms[CARD32]; + let is_minimized = if let Ok(state) = + self.xconn.get_property::(self.xwindow, state_atom, state_type_atom) + { + state.contains(&super::ICONIC_STATE) + } else { + false + }; + let is_visible = match self.shared_state_lock().visibility { + Visibility::Yes => true, + Visibility::YesWait | Visibility::No => false, + }; + + if is_visible && !is_minimized { + self.xconn + .send_client_msg( + self.xwindow, + self.root, + atoms[_NET_ACTIVE_WINDOW], + Some( + xproto::EventMask::SUBSTRUCTURE_REDIRECT + | xproto::EventMask::SUBSTRUCTURE_NOTIFY, + ), + [1, x11rb::CURRENT_TIME, 0, 0, 0], + ) + .expect_then_ignore_error("Failed to send client message"); + if let Err(e) = self.xconn.flush_requests() { + tracing::error!( + "`flush` returned an error when focusing the window. Error was: {}", + e + ); + } + } + } + + #[inline] + pub fn request_user_attention(&self, request_type: Option) { + let mut wm_hints = + WmHints::get(self.xconn.xcb_connection(), self.xwindow as xproto::Window) + .ok() + .and_then(|cookie| cookie.reply().ok()) + .flatten() + .unwrap_or_default(); + + wm_hints.urgent = request_type.is_some(); + wm_hints + .set(self.xconn.xcb_connection(), self.xwindow as xproto::Window) + .expect_then_ignore_error("Failed to set WM hints"); + } + + #[inline] + pub(crate) fn generate_activation_token(&self) -> Result { + // Get the title from the WM_NAME property. + let atoms = self.xconn.atoms(); + let title = { + let title_bytes = self + .xconn + .get_property(self.xwindow, atoms[_NET_WM_NAME], atoms[UTF8_STRING]) + .expect("Failed to get title"); + + String::from_utf8(title_bytes).expect("Bad title") + }; + + // Get the activation token and then put it in the event queue. + let token = self.xconn.request_activation_token(&title)?; + + Ok(token) + } + + #[inline] + pub fn request_activation_token(&self) -> Result { + let serial = AsyncRequestSerial::get(); + self.activation_sender + .send((self.id(), serial)) + .expect("activation token channel should never be closed"); + Ok(serial) + } + + #[inline] + pub fn id(&self) -> WindowId { + WindowId(self.xwindow as _) + } + + #[inline] + pub fn request_redraw(&self) { + self.redraw_sender.send(WindowId(self.xwindow as _)).unwrap(); + } + + #[inline] + pub fn pre_present_notify(&self) { + // TODO timer + } + + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut window_handle = rwh_04::XlibHandle::empty(); + window_handle.display = self.xlib_display(); + window_handle.window = self.xlib_window(); + window_handle.visual_id = self.visual as c_ulong; + rwh_04::RawWindowHandle::Xlib(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut window_handle = rwh_05::XlibWindowHandle::empty(); + window_handle.window = self.xlib_window(); + window_handle.visual_id = self.visual as c_ulong; + window_handle.into() + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + let mut display_handle = rwh_05::XlibDisplayHandle::empty(); + display_handle.display = self.xlib_display(); + display_handle.screen = self.screen_id; + display_handle.into() + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> Result { + let mut window_handle = rwh_06::XlibWindowHandle::new(self.xlib_window()); + window_handle.visual_id = self.visual as c_ulong; + Ok(window_handle.into()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::XlibDisplayHandle::new( + Some( + std::ptr::NonNull::new(self.xlib_display()) + .expect("display pointer should never be null"), + ), + self.screen_id, + ) + .into()) + } + + #[inline] + pub fn theme(&self) -> Option { + None + } + + pub fn set_content_protected(&self, _protected: bool) {} + + #[inline] + pub fn has_focus(&self) -> bool { + self.shared_state_lock().has_focus + } + + pub fn title(&self) -> String { + String::new() + } +} + +/// Cast a dimension value into a hinted dimension for `WmSizeHints`, clamping if too large. +fn cast_dimension_to_hint(val: u32) -> i32 { + val.try_into().unwrap_or(i32::MAX) +} + +/// Use the above strategy to cast a physical size into a hinted size. +fn cast_physical_size_to_hint(size: PhysicalSize) -> (i32, i32) { + let PhysicalSize { width, height } = size; + (cast_dimension_to_hint(width), cast_dimension_to_hint(height)) +} + +/// Use the above strategy to cast a size into a hinted size. +fn cast_size_to_hint(size: Size, scale_factor: f64) -> (i32, i32) { + match size { + Size::Physical(size) => cast_physical_size_to_hint(size), + Size::Logical(size) => size.to_physical::(scale_factor).into(), + } +} diff --git a/vendor/winit/src/platform_impl/linux/x11/xdisplay.rs b/vendor/winit/src/platform_impl/linux/x11/xdisplay.rs new file mode 100644 index 00000000..79da0d1f --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/xdisplay.rs @@ -0,0 +1,356 @@ +use std::collections::HashMap; +use std::error::Error; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard}; +use std::{fmt, ptr}; + +use crate::window::CursorIcon; + +use super::atoms::Atoms; +use super::ffi; +use super::monitor::MonitorHandle; +use x11rb::connection::Connection; +use x11rb::protocol::randr::ConnectionExt as _; +use x11rb::protocol::xproto::{self, ConnectionExt}; +use x11rb::resource_manager; +use x11rb::xcb_ffi::XCBConnection; + +/// A connection to an X server. +pub struct XConnection { + pub xlib: ffi::Xlib, + pub xcursor: ffi::Xcursor, + + // TODO(notgull): I'd like to remove this, but apparently Xlib and Xinput2 are tied together + // for some reason. + pub xinput2: ffi::XInput2, + + pub display: *mut ffi::Display, + + /// The manager for the XCB connection. + /// + /// The `Option` ensures that we can drop it before we close the `Display`. + xcb: Option, + + /// The atoms used by `winit`. + /// + /// This is a large structure, so I've elected to Box it to make accessing the fields of + /// this struct easier. Feel free to unbox it if you like kicking puppies. + atoms: Box, + + /// The index of the default screen. + default_screen: usize, + + /// The last timestamp received by this connection. + timestamp: AtomicU32, + + /// List of monitor handles. + pub monitor_handles: Mutex>>, + + /// The resource database. + database: RwLock, + + /// RandR version. + randr_version: (u32, u32), + + /// Atom for the XSettings screen. + xsettings_screen: Option, + + pub latest_error: Mutex>, + pub cursor_cache: Mutex, ffi::Cursor>>, +} + +unsafe impl Send for XConnection {} +unsafe impl Sync for XConnection {} + +pub type XErrorHandler = + Option std::os::raw::c_int>; + +impl XConnection { + pub fn new(error_handler: XErrorHandler) -> Result { + // opening the libraries + let xlib = ffi::Xlib::open()?; + let xcursor = ffi::Xcursor::open()?; + let xlib_xcb = ffi::Xlib_xcb::open()?; + let xinput2 = ffi::XInput2::open()?; + + unsafe { (xlib.XInitThreads)() }; + unsafe { (xlib.XSetErrorHandler)(error_handler) }; + + // calling XOpenDisplay + let display = unsafe { + let display = (xlib.XOpenDisplay)(ptr::null()); + if display.is_null() { + return Err(XNotSupported::XOpenDisplayFailed); + } + display + }; + + // Open the x11rb XCB connection. + let xcb = { + // Get a pointer to the underlying XCB connection + let xcb_connection = + unsafe { (xlib_xcb.XGetXCBConnection)(display as *mut ffi::Display) }; + assert!(!xcb_connection.is_null()); + + // Wrap the XCB connection in an x11rb XCB connection + let conn = + unsafe { XCBConnection::from_raw_xcb_connection(xcb_connection.cast(), false) }; + + conn.map_err(|e| XNotSupported::XcbConversionError(Arc::new(WrapConnectError(e))))? + }; + + // Get the default screen. + let default_screen = unsafe { (xlib.XDefaultScreen)(display) } as usize; + + // Load the database. + let database = resource_manager::new_from_default(&xcb) + .map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))?; + + // Load the RandR version. + let randr_version = xcb + .randr_query_version(1, 3) + .expect("failed to request XRandR version") + .reply() + .expect("failed to query XRandR version"); + + let xsettings_screen = Self::new_xsettings_screen(&xcb, default_screen); + if xsettings_screen.is_none() { + tracing::warn!("error setting XSETTINGS; Xft options won't reload automatically") + } + + // Fetch atoms. + let atoms = Atoms::new(&xcb) + .map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))? + .reply() + .map_err(|e| XNotSupported::XcbConversionError(Arc::new(e)))?; + + Ok(XConnection { + xlib, + xcursor, + xinput2, + display, + xcb: Some(xcb), + atoms: Box::new(atoms), + default_screen, + timestamp: AtomicU32::new(0), + latest_error: Mutex::new(None), + monitor_handles: Mutex::new(None), + database: RwLock::new(database), + cursor_cache: Default::default(), + randr_version: (randr_version.major_version, randr_version.minor_version), + xsettings_screen, + }) + } + + fn new_xsettings_screen(xcb: &XCBConnection, default_screen: usize) -> Option { + // Fetch the _XSETTINGS_S[screen number] atom. + let xsettings_screen = xcb + .intern_atom(false, format!("_XSETTINGS_S{default_screen}").as_bytes()) + .ok()? + .reply() + .ok()? + .atom; + + // Get PropertyNotify events from the XSETTINGS window. + // TODO: The XSETTINGS window here can change. In the future, listen for DestroyNotify on + // this window in order to accommodate for a changed window here. + let selector_window = xcb.get_selection_owner(xsettings_screen).ok()?.reply().ok()?.owner; + + xcb.change_window_attributes( + selector_window, + &xproto::ChangeWindowAttributesAux::new() + .event_mask(xproto::EventMask::PROPERTY_CHANGE), + ) + .ok()? + .check() + .ok()?; + + Some(xsettings_screen) + } + + /// Checks whether an error has been triggered by the previous function calls. + #[inline] + pub fn check_errors(&self) -> Result<(), XError> { + let error = self.latest_error.lock().unwrap().take(); + if let Some(error) = error { + Err(error) + } else { + Ok(()) + } + } + + #[inline] + pub fn randr_version(&self) -> (u32, u32) { + self.randr_version + } + + /// Get the underlying XCB connection. + #[inline] + pub fn xcb_connection(&self) -> &XCBConnection { + self.xcb.as_ref().expect("xcb_connection somehow called after drop?") + } + + /// Get the list of atoms. + #[inline] + pub fn atoms(&self) -> &Atoms { + &self.atoms + } + + /// Get the index of the default screen. + #[inline] + pub fn default_screen_index(&self) -> usize { + self.default_screen + } + + /// Get the default screen. + #[inline] + pub fn default_root(&self) -> &xproto::Screen { + &self.xcb_connection().setup().roots[self.default_screen] + } + + /// Get the resource database. + #[inline] + pub fn database(&self) -> RwLockReadGuard<'_, resource_manager::Database> { + self.database.read().unwrap_or_else(|e| e.into_inner()) + } + + /// Reload the resource database. + #[inline] + pub fn reload_database(&self) -> Result<(), super::X11Error> { + let database = resource_manager::new_from_default(self.xcb_connection())?; + *self.database.write().unwrap_or_else(|e| e.into_inner()) = database; + Ok(()) + } + + /// Get the latest timestamp. + #[inline] + pub fn timestamp(&self) -> u32 { + self.timestamp.load(Ordering::Relaxed) + } + + /// Set the last witnessed timestamp. + #[inline] + pub fn set_timestamp(&self, timestamp: u32) { + // Store the timestamp in the slot if it's greater than the last one. + let mut last_timestamp = self.timestamp.load(Ordering::Relaxed); + loop { + if (timestamp as i32).wrapping_sub(last_timestamp as i32) <= 0 { + break; + } + + match self.timestamp.compare_exchange( + last_timestamp, + timestamp, + Ordering::Relaxed, + Ordering::Relaxed, + ) { + Ok(_) => break, + Err(x) => last_timestamp = x, + } + } + } + + /// Get the atom for Xsettings. + #[inline] + pub fn xsettings_screen(&self) -> Option { + self.xsettings_screen + } +} + +impl fmt::Debug for XConnection { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.display.fmt(f) + } +} + +impl Drop for XConnection { + #[inline] + fn drop(&mut self) { + self.xcb = None; + unsafe { (self.xlib.XCloseDisplay)(self.display) }; + } +} + +/// Error triggered by xlib. +#[derive(Debug, Clone)] +pub struct XError { + pub description: String, + pub error_code: u8, + pub request_code: u8, + pub minor_code: u8, +} + +impl Error for XError {} + +impl fmt::Display for XError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!( + formatter, + "X error: {} (code: {}, request code: {}, minor code: {})", + self.description, self.error_code, self.request_code, self.minor_code + ) + } +} + +/// Error returned if this system doesn't have XLib or can't create an X connection. +#[derive(Clone, Debug)] +pub enum XNotSupported { + /// Failed to load one or several shared libraries. + LibraryOpenError(ffi::OpenError), + + /// Connecting to the X server with `XOpenDisplay` failed. + XOpenDisplayFailed, // TODO: add better message. + + /// We encountered an error while converting the connection to XCB. + XcbConversionError(Arc), +} + +impl From for XNotSupported { + #[inline] + fn from(err: ffi::OpenError) -> XNotSupported { + XNotSupported::LibraryOpenError(err) + } +} + +impl XNotSupported { + fn description(&self) -> &'static str { + match self { + XNotSupported::LibraryOpenError(_) => "Failed to load one of xlib's shared libraries", + XNotSupported::XOpenDisplayFailed => "Failed to open connection to X server", + XNotSupported::XcbConversionError(_) => "Failed to convert Xlib connection to XCB", + } + } +} + +impl Error for XNotSupported { + #[inline] + fn source(&self) -> Option<&(dyn Error + 'static)> { + match *self { + XNotSupported::LibraryOpenError(ref err) => Some(err), + XNotSupported::XcbConversionError(ref err) => Some(&**err), + _ => None, + } + } +} + +impl fmt::Display for XNotSupported { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + formatter.write_str(self.description()) + } +} + +/// A newtype wrapper around a `ConnectError` that can't be accessed by downstream libraries. +/// +/// Without this, `x11rb` would become a public dependency. +#[derive(Debug)] +struct WrapConnectError(x11rb::rust_connection::ConnectError); + +impl fmt::Display for WrapConnectError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl Error for WrapConnectError { + // We can't implement `source()` here or otherwise risk exposing `x11rb`. +} diff --git a/vendor/winit/src/platform_impl/linux/x11/xsettings.rs b/vendor/winit/src/platform_impl/linux/x11/xsettings.rs new file mode 100644 index 00000000..dd5b074f --- /dev/null +++ b/vendor/winit/src/platform_impl/linux/x11/xsettings.rs @@ -0,0 +1,325 @@ +//! Parser for the xsettings data format. +//! +//! Some of this code is referenced from [here]. +//! +//! [here]: https://github.com/derat/xsettingsd + +use std::iter; +use std::num::NonZeroUsize; + +use x11rb::protocol::xproto::{self, ConnectionExt}; + +use super::atoms::*; +use super::XConnection; + +type Result = core::result::Result; + +const DPI_NAME: &[u8] = b"Xft/DPI"; +const DPI_MULTIPLIER: f64 = 1024.0; +const LITTLE_ENDIAN: u8 = b'l'; +const BIG_ENDIAN: u8 = b'B'; + +impl XConnection { + /// Get the DPI from XSettings. + pub(crate) fn xsettings_dpi( + &self, + xsettings_screen: xproto::Atom, + ) -> core::result::Result, super::X11Error> { + let atoms = self.atoms(); + + // Get the current owner of the screen's settings. + let owner = self.xcb_connection().get_selection_owner(xsettings_screen)?.reply()?; + + // Read the _XSETTINGS_SETTINGS property. + let data: Vec = + self.get_property(owner.owner, atoms[_XSETTINGS_SETTINGS], atoms[_XSETTINGS_SETTINGS])?; + + // Parse the property. + let dpi_setting = read_settings(&data)? + .find(|res| res.as_ref().map_or(true, |s| s.name == DPI_NAME)) + .transpose()?; + if let Some(dpi_setting) = dpi_setting { + let base_dpi = match dpi_setting.data { + SettingData::Integer(dpi) => dpi as f64, + SettingData::String(_) => { + return Err(ParserError::BadType(SettingType::String).into()) + }, + SettingData::Color(_) => { + return Err(ParserError::BadType(SettingType::Color).into()) + }, + }; + + Ok(Some(base_dpi / DPI_MULTIPLIER)) + } else { + Ok(None) + } + } +} + +/// Read over the settings in the block of data. +fn read_settings(data: &[u8]) -> Result>> + '_> { + // Create a parser. This automatically parses the first 8 bytes for metadata. + let mut parser = Parser::new(data)?; + + // Read the total number of settings. + let total_settings = parser.i32()?; + + // Iterate over the settings. + let iter = iter::repeat_with(move || Setting::parse(&mut parser)).take(total_settings as usize); + Ok(iter) +} + +/// A setting in the settings list. +struct Setting<'a> { + /// The name of the setting. + name: &'a [u8], + + /// The data contained in the setting. + data: SettingData<'a>, +} + +/// The data contained in a setting. +enum SettingData<'a> { + Integer(i32), + String(#[allow(dead_code)] &'a [u8]), + Color(#[allow(dead_code)] [i16; 4]), +} + +impl<'a> Setting<'a> { + /// Parse a new `SettingData`. + fn parse(parser: &mut Parser<'a>) -> Result { + // Read the type. + let ty: SettingType = parser.i8()?.try_into()?; + + // Read another byte of padding. + parser.advance(1)?; + + // Read the name of the setting. + let name_len = parser.i16()?; + let name = parser.advance(name_len as usize)?; + parser.pad(name.len(), 4)?; + + // Ignore the serial number. + parser.advance(4)?; + + let data = match ty { + SettingType::Integer => { + // Read a 32-bit integer. + SettingData::Integer(parser.i32()?) + }, + + SettingType::String => { + // Read the data. + let data_len = parser.i32()?; + let data = parser.advance(data_len as usize)?; + parser.pad(data.len(), 4)?; + + SettingData::String(data) + }, + + SettingType::Color => { + // Read i16's of color. + let (red, blue, green, alpha) = + (parser.i16()?, parser.i16()?, parser.i16()?, parser.i16()?); + + SettingData::Color([red, blue, green, alpha]) + }, + }; + + Ok(Setting { name, data }) + } +} + +#[derive(Debug)] +pub enum SettingType { + Integer = 0, + String = 1, + Color = 2, +} + +impl TryFrom for SettingType { + type Error = ParserError; + + fn try_from(value: i8) -> Result { + Ok(match value { + 0 => Self::Integer, + 1 => Self::String, + 2 => Self::Color, + x => return Err(ParserError::InvalidType(x)), + }) + } +} + +/// Parser for the incoming byte stream. +struct Parser<'a> { + bytes: &'a [u8], + endianness: Endianness, +} + +impl<'a> Parser<'a> { + /// Create a new parser. + fn new(bytes: &'a [u8]) -> Result { + let (endianness, bytes) = bytes.split_first().ok_or_else(|| ParserError::ran_out(1, 0))?; + let endianness = match *endianness { + BIG_ENDIAN => Endianness::Big, + LITTLE_ENDIAN => Endianness::Little, + _ => Endianness::native(), + }; + + Ok(Self { + // Ignore three bytes of padding and the four-byte serial. + bytes: bytes.get(7..).ok_or_else(|| ParserError::ran_out(7, bytes.len()))?, + endianness, + }) + } + + /// Get a slice of bytes. + fn advance(&mut self, n: usize) -> Result<&'a [u8]> { + if n == 0 { + return Ok(&[]); + } + + if n > self.bytes.len() { + Err(ParserError::ran_out(n, self.bytes.len())) + } else { + let (part, rem) = self.bytes.split_at(n); + self.bytes = rem; + Ok(part) + } + } + + /// Skip some padding. + fn pad(&mut self, size: usize, pad: usize) -> Result<()> { + let advance = (pad - (size % pad)) % pad; + self.advance(advance)?; + Ok(()) + } + + /// Get a single byte. + fn i8(&mut self) -> Result { + self.advance(1).map(|s| s[0] as i8) + } + + /// Get two bytes. + fn i16(&mut self) -> Result { + self.advance(2).map(|s| { + let bytes: &[u8; 2] = s.try_into().unwrap(); + match self.endianness { + Endianness::Big => i16::from_be_bytes(*bytes), + Endianness::Little => i16::from_le_bytes(*bytes), + } + }) + } + + /// Get four bytes. + fn i32(&mut self) -> Result { + self.advance(4).map(|s| { + let bytes: &[u8; 4] = s.try_into().unwrap(); + match self.endianness { + Endianness::Big => i32::from_be_bytes(*bytes), + Endianness::Little => i32::from_le_bytes(*bytes), + } + }) + } +} + +/// Endianness of the incoming data. +enum Endianness { + Little, + Big, +} + +impl Endianness { + #[cfg(target_endian = "little")] + fn native() -> Self { + Endianness::Little + } + + #[cfg(target_endian = "big")] + fn native() -> Self { + Endianness::Big + } +} + +/// Parser errors. +#[allow(dead_code)] +#[derive(Debug)] +pub enum ParserError { + /// Ran out of bytes. + NoMoreBytes { expected: NonZeroUsize, found: usize }, + + /// Invalid type. + InvalidType(i8), + + /// Bad setting type. + BadType(SettingType), +} + +impl ParserError { + fn ran_out(expected: usize, found: usize) -> ParserError { + let expected = NonZeroUsize::new(expected).unwrap(); + Self::NoMoreBytes { expected, found } + } +} + +#[cfg(test)] +/// Tests for the XSETTINGS parser. +mod tests { + use super::*; + + const XSETTINGS: &str = include_str!("tests/xsettings.dat"); + + #[test] + fn empty() { + let err = match read_settings(&[]) { + Ok(_) => panic!(), + Err(err) => err, + }; + match err { + ParserError::NoMoreBytes { expected, found } => { + assert_eq!(expected.get(), 1); + assert_eq!(found, 0); + }, + + _ => panic!(), + } + } + + #[test] + fn parse_xsettings() { + let data = XSETTINGS + .trim() + .split(',') + .map(|tok| { + let val = tok.strip_prefix("0x").unwrap(); + u8::from_str_radix(val, 16).unwrap() + }) + .collect::>(); + + let settings = read_settings(&data).unwrap().collect::>>().unwrap(); + + let dpi = settings.iter().find(|s| s.name == b"Xft/DPI").unwrap(); + assert_int(&dpi.data, 96 * 1024); + let hinting = settings.iter().find(|s| s.name == b"Xft/Hinting").unwrap(); + assert_int(&hinting.data, 1); + + let rgba = settings.iter().find(|s| s.name == b"Xft/RGBA").unwrap(); + assert_string(&rgba.data, "rgb"); + let lcd = settings.iter().find(|s| s.name == b"Xft/Lcdfilter").unwrap(); + assert_string(&lcd.data, "lcddefault"); + } + + fn assert_string(dat: &SettingData<'_>, s: &str) { + match dat { + SettingData::String(left) => assert_eq!(*left, s.as_bytes()), + _ => panic!("invalid data type"), + } + } + + fn assert_int(dat: &SettingData<'_>, i: i32) { + match dat { + SettingData::Integer(left) => assert_eq!(*left, i), + _ => panic!("invalid data type"), + } + } +} diff --git a/vendor/winit/src/platform_impl/macos/app.rs b/vendor/winit/src/platform_impl/macos/app.rs new file mode 100644 index 00000000..4fb95dbd --- /dev/null +++ b/vendor/winit/src/platform_impl/macos/app.rs @@ -0,0 +1,195 @@ +#![allow(clippy::unnecessary_cast)] +#![allow(unknown_lints)] // New lint below +#![allow(static_mut_refs)] // Uses `MainThreadBound` in new version. + +use std::cell::Cell; +use std::mem; + +use objc2::runtime::{Imp, Sel}; +use objc2::sel; +use objc2_app_kit::{NSApplication, NSEvent, NSEventModifierFlags, NSEventType}; +use objc2_foundation::MainThreadMarker; + +use super::app_state::ApplicationDelegate; +use crate::event::{DeviceEvent, ElementState}; + +type SendEvent = extern "C" fn(&NSApplication, Sel, &NSEvent); + +// NOTE: Only used on the main thread. Ideally, we'd use `MainThreadBound`, but that isn't +// constructible from `const` with this `objc2` version. +static mut ORIGINAL: Cell> = Cell::new(None); + +extern "C" fn send_event(app: &NSApplication, sel: Sel, event: &NSEvent) { + let mtm = MainThreadMarker::from(app); + + // Normally, holding Cmd + any key never sends us a `keyUp` event for that key. + // Overriding `sendEvent:` fixes that. (https://stackoverflow.com/a/15294196) + // Fun fact: Firefox still has this bug! (https://bugzilla.mozilla.org/show_bug.cgi?id=1299553) + // + // For posterity, there are some undocumented event types + // (https://github.com/servo/cocoa-rs/issues/155) + // but that doesn't really matter here. + let event_type = unsafe { event.r#type() }; + let modifier_flags = unsafe { event.modifierFlags() }; + if event_type == NSEventType::KeyUp + && modifier_flags.contains(NSEventModifierFlags::NSEventModifierFlagCommand) + { + if let Some(key_window) = app.keyWindow() { + key_window.sendEvent(event); + } + return; + } + + // Events are generally scoped to the window level, so the best way + // to get device events is to listen for them on NSApplication. + let delegate = ApplicationDelegate::get(mtm); + maybe_dispatch_device_event(&delegate, event); + + let _ = mtm; + let original = unsafe { ORIGINAL.get().expect("no existing sendEvent: handler set") }; + original(app, sel, event) +} + +/// Override the [`sendEvent:`][NSApplication::sendEvent] method on the given application class. +/// +/// The previous implementation created a subclass of [`NSApplication`], however we would like to +/// give the user full control over their `NSApplication`, so we override the method here using +/// method swizzling instead. +/// +/// This _should_ also allow two versions of Winit to exist in the same application. +/// +/// See the following links for more info on method swizzling: +/// - +/// - +/// - +/// +/// NOTE: This function assumes that the passed in application object is the one returned from +/// [`NSApplication::sharedApplication`], i.e. the one and only global shared application object. +/// For testing though, we allow it to be a different object. +pub(crate) fn override_send_event(global_app: &NSApplication) { + let mtm = MainThreadMarker::from(global_app); + let class = global_app.class(); + + let method = + class.instance_method(sel!(sendEvent:)).expect("NSApplication must have sendEvent: method"); + + // SAFETY: Converting our `sendEvent:` implementation to an IMP. + let overridden = unsafe { mem::transmute::(send_event) }; + + // If we've already overridden the method, don't do anything. + // FIXME(madsmtm): Use `std::ptr::fn_addr_eq` (Rust 1.85) once available in MSRV. + #[allow(unknown_lints, unpredictable_function_pointer_comparisons)] + if overridden == method.implementation() { + return; + } + + // SAFETY: Our implementation has: + // 1. The same signature as `sendEvent:`. + // 2. Does not impose extra safety requirements on callers. + let original = unsafe { method.set_implementation(overridden) }; + + // SAFETY: This is the actual signature of `sendEvent:`. + let original = unsafe { mem::transmute::(original) }; + + // NOTE: If NSApplication was safe to use from multiple threads, then this would potentially be + // a (checked) race-condition, since one could call `sendEvent:` before the original had been + // stored here. + // + // It is only usable from the main thread, however, so we're good! + let _ = mtm; + unsafe { ORIGINAL.set(Some(original)) }; +} + +fn maybe_dispatch_device_event(delegate: &ApplicationDelegate, event: &NSEvent) { + let event_type = unsafe { event.r#type() }; + #[allow(non_upper_case_globals)] + match event_type { + NSEventType::MouseMoved + | NSEventType::LeftMouseDragged + | NSEventType::OtherMouseDragged + | NSEventType::RightMouseDragged => { + let delta_x = unsafe { event.deltaX() } as f64; + let delta_y = unsafe { event.deltaY() } as f64; + + if delta_x != 0.0 { + delegate.maybe_queue_device_event(DeviceEvent::Motion { axis: 0, value: delta_x }); + } + + if delta_y != 0.0 { + delegate.maybe_queue_device_event(DeviceEvent::Motion { axis: 1, value: delta_y }) + } + + if delta_x != 0.0 || delta_y != 0.0 { + delegate.maybe_queue_device_event(DeviceEvent::MouseMotion { + delta: (delta_x, delta_y), + }); + } + }, + NSEventType::LeftMouseDown | NSEventType::RightMouseDown | NSEventType::OtherMouseDown => { + delegate.maybe_queue_device_event(DeviceEvent::Button { + button: unsafe { event.buttonNumber() } as u32, + state: ElementState::Pressed, + }); + }, + NSEventType::LeftMouseUp | NSEventType::RightMouseUp | NSEventType::OtherMouseUp => { + delegate.maybe_queue_device_event(DeviceEvent::Button { + button: unsafe { event.buttonNumber() } as u32, + state: ElementState::Released, + }); + }, + _ => (), + } +} + +#[cfg(test)] +mod tests { + use objc2::rc::Retained; + use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass}; + + use super::*; + + #[test] + fn test_override() { + // FIXME(madsmtm): Ensure this always runs (maybe use cargo-nextest or `--test-threads=1`?) + let Some(mtm) = MainThreadMarker::new() else { return }; + + // Create a new application, without making it the shared application. + let app = unsafe { NSApplication::new(mtm) }; + override_send_event(&app); + // Test calling twice works. + override_send_event(&app); + + // FIXME(madsmtm): Can't test this yet, need some way to mock AppState. + // unsafe { + // let event = super::super::event::dummy_event().unwrap(); + // app.sendEvent(&event) + // } + } + + #[test] + fn test_custom_class() { + let Some(_mtm) = MainThreadMarker::new() else { return }; + + declare_class!( + struct TestApplication; + + unsafe impl ClassType for TestApplication { + type Super = NSApplication; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "TestApplication"; + } + + impl DeclaredClass for TestApplication {} + + unsafe impl TestApplication { + #[method(sendEvent:)] + fn send_event(&self, _event: &NSEvent) { + todo!() + } + } + ); + + let app: Retained = unsafe { msg_send_id![TestApplication::class(), new] }; + override_send_event(&app); + } +} diff --git a/vendor/winit/src/platform_impl/macos/app_state.rs b/vendor/winit/src/platform_impl/macos/app_state.rs new file mode 100644 index 00000000..dd2ccd11 --- /dev/null +++ b/vendor/winit/src/platform_impl/macos/app_state.rs @@ -0,0 +1,446 @@ +use std::cell::{Cell, RefCell}; +use std::mem; +use std::rc::Weak; +use std::time::Instant; + +use objc2::rc::Retained; +use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass}; +use objc2_app_kit::{ + NSApplication, NSApplicationActivationPolicy, NSApplicationDelegate, NSRunningApplication, +}; +use objc2_foundation::{MainThreadMarker, NSNotification, NSObject, NSObjectProtocol}; + +use super::event_handler::EventHandler; +use super::event_loop::{notify_windows_of_exit, stop_app_immediately, ActiveEventLoop, PanicInfo}; +use super::observer::{EventLoopWaker, RunLoop}; +use super::{menu, WindowId, DEVICE_ID}; +use crate::event::{DeviceEvent, Event, StartCause, WindowEvent}; +use crate::event_loop::{ActiveEventLoop as RootActiveEventLoop, ControlFlow}; +use crate::window::WindowId as RootWindowId; + +#[derive(Debug)] +pub(super) struct AppState { + activation_policy: Option, + default_menu: bool, + activate_ignoring_other_apps: bool, + run_loop: RunLoop, + event_handler: EventHandler, + stop_on_launch: Cell, + stop_before_wait: Cell, + stop_after_wait: Cell, + stop_on_redraw: Cell, + /// Whether `applicationDidFinishLaunching:` has been run or not. + is_launched: Cell, + /// Whether an `EventLoop` is currently running. + is_running: Cell, + /// Whether the user has requested the event loop to exit. + exit: Cell, + control_flow: Cell, + waker: RefCell, + start_time: Cell>, + wait_timeout: Cell>, + pending_redraw: RefCell>, + // NOTE: This is strongly referenced by our `NSWindowDelegate` and our `NSView` subclass, and + // as such should be careful to not add fields that, in turn, strongly reference those. +} + +declare_class!( + #[derive(Debug)] + pub(super) struct ApplicationDelegate; + + unsafe impl ClassType for ApplicationDelegate { + type Super = NSObject; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitApplicationDelegate"; + } + + impl DeclaredClass for ApplicationDelegate { + type Ivars = AppState; + } + + unsafe impl NSObjectProtocol for ApplicationDelegate {} + + unsafe impl NSApplicationDelegate for ApplicationDelegate { + #[method(applicationDidFinishLaunching:)] + fn app_did_finish_launching(&self, notification: &NSNotification) { + self.did_finish_launching(notification) + } + + #[method(applicationWillTerminate:)] + fn app_will_terminate(&self, notification: &NSNotification) { + self.will_terminate(notification) + } + } +); + +impl ApplicationDelegate { + pub(super) fn new( + mtm: MainThreadMarker, + activation_policy: Option, + default_menu: bool, + activate_ignoring_other_apps: bool, + ) -> Retained { + let this = mtm.alloc().set_ivars(AppState { + activation_policy, + default_menu, + activate_ignoring_other_apps, + run_loop: RunLoop::main(mtm), + event_handler: EventHandler::new(), + stop_on_launch: Cell::new(false), + stop_before_wait: Cell::new(false), + stop_after_wait: Cell::new(false), + stop_on_redraw: Cell::new(false), + is_launched: Cell::new(false), + is_running: Cell::new(false), + exit: Cell::new(false), + control_flow: Cell::new(ControlFlow::default()), + waker: RefCell::new(EventLoopWaker::new()), + start_time: Cell::new(None), + wait_timeout: Cell::new(None), + pending_redraw: RefCell::new(vec![]), + }); + unsafe { msg_send_id![super(this), init] } + } + + // NOTE: This will, globally, only be run once, no matter how many + // `EventLoop`s the user creates. + fn did_finish_launching(&self, _notification: &NSNotification) { + trace_scope!("applicationDidFinishLaunching:"); + self.ivars().is_launched.set(true); + + let mtm = MainThreadMarker::from(self); + let app = NSApplication::sharedApplication(mtm); + // We need to delay setting the activation policy and activating the app + // until `applicationDidFinishLaunching` has been called. Otherwise the + // menu bar is initially unresponsive on macOS 10.15. + // If no activation policy is explicitly provided, do not set it at all + // to allow the package manifest to define behavior via LSUIElement. + if let Some(activation_policy) = self.ivars().activation_policy { + app.setActivationPolicy(activation_policy); + } else { + // If no activation policy is explicitly provided, and the application + // is bundled, do not set the activation policy at all, to allow the + // package manifest to define the behavior via LSUIElement. + // + // See: + // - https://github.com/rust-windowing/winit/issues/261 + // - https://github.com/rust-windowing/winit/issues/3958 + let is_bundled = + unsafe { NSRunningApplication::currentApplication().bundleIdentifier().is_some() }; + if !is_bundled { + app.setActivationPolicy(NSApplicationActivationPolicy::Regular); + } + } + + window_activation_hack(&app); + #[allow(deprecated)] + app.activateIgnoringOtherApps(self.ivars().activate_ignoring_other_apps); + + if self.ivars().default_menu { + // The menubar initialization should be before the `NewEvents` event, to allow + // overriding of the default menu even if it's created + menu::initialize(&app); + } + + self.ivars().waker.borrow_mut().start(); + + self.set_is_running(true); + self.dispatch_init_events(); + + // If the application is being launched via `EventLoop::pump_app_events()` then we'll + // want to stop the app once it is launched (and return to the external loop) + // + // In this case we still want to consider Winit's `EventLoop` to be "running", + // so we call `start_running()` above. + if self.ivars().stop_on_launch.get() { + // NOTE: the original idea had been to only stop the underlying `RunLoop` + // for the app but that didn't work as expected (`-[NSApplication run]` + // effectively ignored the attempt to stop the RunLoop and re-started it). + // + // So we return from `pump_events` by stopping the application. + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + } + } + + fn will_terminate(&self, _notification: &NSNotification) { + trace_scope!("applicationWillTerminate:"); + let mtm = MainThreadMarker::from(self); + let app = NSApplication::sharedApplication(mtm); + notify_windows_of_exit(&app); + self.internal_exit(); + } + + pub fn get(mtm: MainThreadMarker) -> Retained { + let app = NSApplication::sharedApplication(mtm); + let delegate = + unsafe { app.delegate() }.expect("a delegate was not configured on the application"); + if delegate.is_kind_of::() { + // SAFETY: Just checked that the delegate is an instance of `ApplicationDelegate` + unsafe { Retained::cast(delegate) } + } else { + panic!("tried to get a delegate that was not the one Winit has registered") + } + } + + /// Place the event handler in the application delegate for the duration + /// of the given closure. + pub fn set_event_handler( + &self, + handler: impl FnMut(Event, &RootActiveEventLoop), + closure: impl FnOnce() -> R, + ) -> R { + self.ivars().event_handler.set(handler, closure) + } + + /// If `pump_events` is called to progress the event loop then we + /// bootstrap the event loop via `-[NSApplication run]` but will use + /// `CFRunLoopRunInMode` for subsequent calls to `pump_events`. + pub fn set_stop_on_launch(&self) { + self.ivars().stop_on_launch.set(true); + } + + pub fn set_stop_before_wait(&self, value: bool) { + self.ivars().stop_before_wait.set(value) + } + + pub fn set_stop_after_wait(&self, value: bool) { + self.ivars().stop_after_wait.set(value) + } + + pub fn set_stop_on_redraw(&self, value: bool) { + self.ivars().stop_on_redraw.set(value) + } + + pub fn set_wait_timeout(&self, value: Option) { + self.ivars().wait_timeout.set(value) + } + + /// Clears the `running` state and resets the `control_flow` state when an `EventLoop` exits. + /// + /// NOTE: that if the `NSApplication` has been launched then that state is preserved, + /// and we won't need to re-launch the app if subsequent EventLoops are run. + pub fn internal_exit(&self) { + self.handle_event(Event::LoopExiting); + + self.set_is_running(false); + self.set_stop_on_redraw(false); + self.set_stop_before_wait(false); + self.set_stop_after_wait(false); + self.set_wait_timeout(None); + } + + pub fn is_launched(&self) -> bool { + self.ivars().is_launched.get() + } + + pub fn set_is_running(&self, value: bool) { + self.ivars().is_running.set(value) + } + + pub fn is_running(&self) -> bool { + self.ivars().is_running.get() + } + + pub fn exit(&self) { + self.ivars().exit.set(true) + } + + pub fn clear_exit(&self) { + self.ivars().exit.set(false) + } + + pub fn exiting(&self) -> bool { + self.ivars().exit.get() + } + + pub fn set_control_flow(&self, value: ControlFlow) { + self.ivars().control_flow.set(value) + } + + pub fn control_flow(&self) -> ControlFlow { + self.ivars().control_flow.get() + } + + pub fn maybe_queue_window_event(&self, window_id: WindowId, event: WindowEvent) { + self.maybe_queue_event(Event::WindowEvent { window_id: RootWindowId(window_id), event }); + } + + pub fn handle_window_event(&self, window_id: WindowId, event: WindowEvent) { + self.handle_event(Event::WindowEvent { window_id: RootWindowId(window_id), event }); + } + + pub fn maybe_queue_device_event(&self, event: DeviceEvent) { + self.maybe_queue_event(Event::DeviceEvent { device_id: DEVICE_ID, event }); + } + + pub fn handle_redraw(&self, window_id: WindowId) { + let mtm = MainThreadMarker::from(self); + // Redraw request might come out of order from the OS. + // -> Don't go back into the event handler when our callstack originates from there + if !self.ivars().event_handler.in_use() { + self.handle_event(Event::WindowEvent { + window_id: RootWindowId(window_id), + event: WindowEvent::RedrawRequested, + }); + + // `pump_events` will request to stop immediately _after_ dispatching RedrawRequested + // events as a way to ensure that `pump_events` can't block an external loop + // indefinitely + if self.ivars().stop_on_redraw.get() { + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + } + } + } + + pub fn queue_redraw(&self, window_id: WindowId) { + let mut pending_redraw = self.ivars().pending_redraw.borrow_mut(); + if !pending_redraw.contains(&window_id) { + pending_redraw.push(window_id); + } + self.ivars().run_loop.wakeup(); + } + + #[track_caller] + fn maybe_queue_event(&self, event: Event) { + // Most programmer actions in AppKit (e.g. change window fullscreen, set focused, etc.) + // result in an event being queued, and applied at a later point. + // + // However, it is not documented which actions do this, and which ones are done immediately, + // so to make sure that we don't encounter re-entrancy issues, we first check if we're + // currently handling another event, and if we are, we queue the event instead. + if !self.ivars().event_handler.in_use() { + self.handle_event(event); + } else { + tracing::debug!(?event, "had to queue event since another is currently being handled"); + let this = self.retain(); + self.ivars().run_loop.queue_closure(move || this.handle_event(event)); + } + } + + #[track_caller] + fn handle_event(&self, event: Event) { + self.ivars().event_handler.handle_event(event, &ActiveEventLoop::new_root(self.retain())) + } + + /// dispatch `NewEvents(Init)` + `Resumed` + pub fn dispatch_init_events(&self) { + self.handle_event(Event::NewEvents(StartCause::Init)); + // NB: For consistency all platforms must emit a 'resumed' event even though macOS + // applications don't themselves have a formal suspend/resume lifecycle. + self.handle_event(Event::Resumed); + } + + // Called by RunLoopObserver after finishing waiting for new events + pub fn wakeup(&self, panic_info: Weak) { + let mtm = MainThreadMarker::from(self); + let panic_info = panic_info + .upgrade() + .expect("The panic info must exist here. This failure indicates a developer error."); + + // Return when in event handler due to https://github.com/rust-windowing/winit/issues/1779 + if panic_info.is_panicking() || !self.ivars().event_handler.ready() || !self.is_running() { + return; + } + + if self.ivars().stop_after_wait.get() { + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + } + + let start = self.ivars().start_time.get().unwrap(); + let cause = match self.control_flow() { + ControlFlow::Poll => StartCause::Poll, + ControlFlow::Wait => StartCause::WaitCancelled { start, requested_resume: None }, + ControlFlow::WaitUntil(requested_resume) => { + if Instant::now() >= requested_resume { + StartCause::ResumeTimeReached { start, requested_resume } + } else { + StartCause::WaitCancelled { start, requested_resume: Some(requested_resume) } + } + }, + }; + + self.handle_event(Event::NewEvents(cause)); + } + + // Called by RunLoopObserver before waiting for new events + pub fn cleared(&self, panic_info: Weak) { + let mtm = MainThreadMarker::from(self); + let panic_info = panic_info + .upgrade() + .expect("The panic info must exist here. This failure indicates a developer error."); + + // Return when in event handler due to https://github.com/rust-windowing/winit/issues/1779 + // XXX: how does it make sense that `event_handler.ready()` can ever return `false` here if + // we're about to return to the `CFRunLoop` to poll for new events? + if panic_info.is_panicking() || !self.ivars().event_handler.ready() || !self.is_running() { + return; + } + + self.handle_event(Event::UserEvent(HandlePendingUserEvents)); + + let redraw = mem::take(&mut *self.ivars().pending_redraw.borrow_mut()); + for window_id in redraw { + self.handle_event(Event::WindowEvent { + window_id: RootWindowId(window_id), + event: WindowEvent::RedrawRequested, + }); + } + + self.handle_event(Event::AboutToWait); + + if self.exiting() { + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + notify_windows_of_exit(&app); + } + + if self.ivars().stop_before_wait.get() { + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + } + self.ivars().start_time.set(Some(Instant::now())); + let wait_timeout = self.ivars().wait_timeout.get(); // configured by pump_events + let app_timeout = match self.control_flow() { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Instant::now()), + ControlFlow::WaitUntil(instant) => Some(instant), + }; + self.ivars().waker.borrow_mut().start_at(min_timeout(wait_timeout, app_timeout)); + } +} + +#[derive(Debug)] +pub(crate) struct HandlePendingUserEvents; + +/// Returns the minimum `Option`, taking into account that `None` +/// equates to an infinite timeout, not a zero timeout (so can't just use +/// `Option::min`) +fn min_timeout(a: Option, b: Option) -> Option { + a.map_or(b, |a_timeout| b.map_or(Some(a_timeout), |b_timeout| Some(a_timeout.min(b_timeout)))) +} + +/// A hack to make activation of multiple windows work when creating them before +/// `applicationDidFinishLaunching:` / `Event::Event::NewEvents(StartCause::Init)`. +/// +/// Alternative to this would be the user calling `window.set_visible(true)` in +/// `StartCause::Init`. +/// +/// If this becomes too bothersome to maintain, it can probably be removed +/// without too much damage. +fn window_activation_hack(app: &NSApplication) { + // TODO: Proper ordering of the windows + app.windows().into_iter().for_each(|window| { + // Call `makeKeyAndOrderFront` if it was called on the window in `WinitWindow::new` + // This way we preserve the user's desired initial visibility status + // TODO: Also filter on the type/"level" of the window, and maybe other things? + if window.isVisible() { + tracing::trace!("Activating visible window"); + window.makeKeyAndOrderFront(None); + } else { + tracing::trace!("Skipping activating invisible window"); + } + }) +} diff --git a/vendor/winit/src/platform_impl/macos/cursor.rs b/vendor/winit/src/platform_impl/macos/cursor.rs new file mode 100644 index 00000000..9e14e8be --- /dev/null +++ b/vendor/winit/src/platform_impl/macos/cursor.rs @@ -0,0 +1,225 @@ +use std::ffi::c_uchar; +use std::slice; +use std::sync::OnceLock; + +use objc2::rc::Retained; +use objc2::runtime::Sel; +use objc2::{msg_send, msg_send_id, sel, ClassType}; +use objc2_app_kit::{NSBitmapImageRep, NSCursor, NSDeviceRGBColorSpace, NSImage}; +use objc2_foundation::{ + ns_string, NSData, NSDictionary, NSNumber, NSObject, NSObjectProtocol, NSPoint, NSSize, + NSString, +}; + +use crate::cursor::{CursorImage, OnlyCursorImageSource}; +use crate::window::CursorIcon; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct CustomCursor(pub(crate) Retained); + +// SAFETY: NSCursor is immutable and thread-safe +// TODO(madsmtm): Put this logic in objc2-app-kit itself +unsafe impl Send for CustomCursor {} +unsafe impl Sync for CustomCursor {} + +impl CustomCursor { + pub(crate) fn new(cursor: OnlyCursorImageSource) -> CustomCursor { + Self(cursor_from_image(&cursor.0)) + } +} + +pub(crate) fn cursor_from_image(cursor: &CursorImage) -> Retained { + let width = cursor.width; + let height = cursor.height; + + let bitmap = unsafe { + NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel( + NSBitmapImageRep::alloc(), + std::ptr::null_mut::<*mut c_uchar>(), + width as isize, + height as isize, + 8, + 4, + true, + false, + NSDeviceRGBColorSpace, + width as isize * 4, + 32, + ).unwrap() + }; + let bitmap_data = unsafe { slice::from_raw_parts_mut(bitmap.bitmapData(), cursor.rgba.len()) }; + bitmap_data.copy_from_slice(&cursor.rgba); + + let image = unsafe { + NSImage::initWithSize(NSImage::alloc(), NSSize::new(width.into(), height.into())) + }; + unsafe { image.addRepresentation(&bitmap) }; + + let hotspot = NSPoint::new(cursor.hotspot_x as f64, cursor.hotspot_y as f64); + + NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hotspot) +} + +pub(crate) fn default_cursor() -> Retained { + NSCursor::arrowCursor() +} + +unsafe fn try_cursor_from_selector(sel: Sel) -> Option> { + let cls = NSCursor::class(); + if msg_send![cls, respondsToSelector: sel] { + let cursor: Retained = unsafe { msg_send_id![cls, performSelector: sel] }; + Some(cursor) + } else { + tracing::warn!("cursor `{sel}` appears to be invalid"); + None + } +} + +macro_rules! def_undocumented_cursor { + {$( + $(#[$($m:meta)*])* + fn $name:ident(); + )*} => {$( + $(#[$($m)*])* + #[allow(non_snake_case)] + fn $name() -> Retained { + unsafe { try_cursor_from_selector(sel!($name)).unwrap_or_else(|| default_cursor()) } + } + )*}; +} + +def_undocumented_cursor!( + // Undocumented cursors: https://stackoverflow.com/a/46635398/5435443 + fn _helpCursor(); + fn _zoomInCursor(); + fn _zoomOutCursor(); + fn _windowResizeNorthEastCursor(); + fn _windowResizeNorthWestCursor(); + fn _windowResizeSouthEastCursor(); + fn _windowResizeSouthWestCursor(); + fn _windowResizeNorthEastSouthWestCursor(); + fn _windowResizeNorthWestSouthEastCursor(); + + // While these two are available, the former just loads a white arrow, + // and the latter loads an ugly deflated beachball! + // pub fn _moveCursor(); + // pub fn _waitCursor(); + + // An even more undocumented cursor... + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=522349 + fn busyButClickableCursor(); +); + +// Note that loading `busybutclickable` with this code won't animate +// the frames; instead you'll just get them all in a column. +unsafe fn load_webkit_cursor(name: &NSString) -> Retained { + // Snatch a cursor from WebKit; They fit the style of the native + // cursors, and will seem completely standard to macOS users. + // + // https://stackoverflow.com/a/21786835/5435443 + let root = ns_string!( + "/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/\ + HIServices.framework/Versions/A/Resources/cursors" + ); + let cursor_path = root.stringByAppendingPathComponent(name); + + let pdf_path = cursor_path.stringByAppendingPathComponent(ns_string!("cursor.pdf")); + let image = NSImage::initByReferencingFile(NSImage::alloc(), &pdf_path).unwrap(); + + // TODO: Handle PLists better + let info_path = cursor_path.stringByAppendingPathComponent(ns_string!("info.plist")); + let info: Retained> = unsafe { + msg_send_id![ + >::class(), + dictionaryWithContentsOfFile: &*info_path, + ] + }; + let mut x = 0.0; + if let Some(n) = info.get(&*ns_string!("hotx")) { + if n.is_kind_of::() { + let ptr: *const NSObject = n; + let ptr: *const NSNumber = ptr.cast(); + x = unsafe { &*ptr }.as_cgfloat() + } + } + let mut y = 0.0; + if let Some(n) = info.get(&*ns_string!("hotx")) { + if n.is_kind_of::() { + let ptr: *const NSObject = n; + let ptr: *const NSNumber = ptr.cast(); + y = unsafe { &*ptr }.as_cgfloat() + } + } + + let hotspot = NSPoint::new(x, y); + NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hotspot) +} + +fn webkit_move() -> Retained { + unsafe { load_webkit_cursor(ns_string!("move")) } +} + +fn webkit_cell() -> Retained { + unsafe { load_webkit_cursor(ns_string!("cell")) } +} + +pub(crate) fn invisible_cursor() -> Retained { + // 16x16 GIF data for invisible cursor + // You can reproduce this via ImageMagick. + // $ convert -size 16x16 xc:none cursor.gif + static CURSOR_BYTES: &[u8] = &[ + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x10, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, + 0x00, 0x00, 0x10, 0x00, 0x10, 0x00, 0x00, 0x02, 0x0e, 0x84, 0x8f, 0xa9, 0xcb, 0xed, 0x0f, + 0xa3, 0x9c, 0xb4, 0xda, 0x8b, 0xb3, 0x3e, 0x05, 0x00, 0x3b, + ]; + + fn new_invisible() -> Retained { + // TODO: Consider using `dataWithBytesNoCopy:` + let data = NSData::with_bytes(CURSOR_BYTES); + let image = NSImage::initWithData(NSImage::alloc(), &data).unwrap(); + let hotspot = NSPoint::new(0.0, 0.0); + NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hotspot) + } + + // Cache this for efficiency + static CURSOR: OnceLock = OnceLock::new(); + CURSOR.get_or_init(|| CustomCursor(new_invisible())).0.clone() +} + +pub(crate) fn cursor_from_icon(icon: CursorIcon) -> Retained { + match icon { + CursorIcon::Default => default_cursor(), + CursorIcon::Pointer => NSCursor::pointingHandCursor(), + CursorIcon::Grab => NSCursor::openHandCursor(), + CursorIcon::Grabbing => NSCursor::closedHandCursor(), + CursorIcon::Text => NSCursor::IBeamCursor(), + CursorIcon::VerticalText => NSCursor::IBeamCursorForVerticalLayout(), + CursorIcon::Copy => NSCursor::dragCopyCursor(), + CursorIcon::Alias => NSCursor::dragLinkCursor(), + CursorIcon::NotAllowed | CursorIcon::NoDrop => NSCursor::operationNotAllowedCursor(), + CursorIcon::ContextMenu => NSCursor::contextualMenuCursor(), + CursorIcon::Crosshair => NSCursor::crosshairCursor(), + CursorIcon::EResize => NSCursor::resizeRightCursor(), + CursorIcon::NResize => NSCursor::resizeUpCursor(), + CursorIcon::WResize => NSCursor::resizeLeftCursor(), + CursorIcon::SResize => NSCursor::resizeDownCursor(), + CursorIcon::EwResize | CursorIcon::ColResize => NSCursor::resizeLeftRightCursor(), + CursorIcon::NsResize | CursorIcon::RowResize => NSCursor::resizeUpDownCursor(), + CursorIcon::Help => _helpCursor(), + CursorIcon::ZoomIn => _zoomInCursor(), + CursorIcon::ZoomOut => _zoomOutCursor(), + CursorIcon::NeResize => _windowResizeNorthEastCursor(), + CursorIcon::NwResize => _windowResizeNorthWestCursor(), + CursorIcon::SeResize => _windowResizeSouthEastCursor(), + CursorIcon::SwResize => _windowResizeSouthWestCursor(), + CursorIcon::NeswResize => _windowResizeNorthEastSouthWestCursor(), + CursorIcon::NwseResize => _windowResizeNorthWestSouthEastCursor(), + // This is the wrong semantics for `Wait`, but it's the same as + // what's used in Safari and Chrome. + CursorIcon::Wait | CursorIcon::Progress => busyButClickableCursor(), + CursorIcon::Move | CursorIcon::AllScroll => webkit_move(), + CursorIcon::Cell => webkit_cell(), + _ => default_cursor(), + } +} diff --git a/vendor/winit/src/platform_impl/macos/event.rs b/vendor/winit/src/platform_impl/macos/event.rs new file mode 100644 index 00000000..9b7f35b0 --- /dev/null +++ b/vendor/winit/src/platform_impl/macos/event.rs @@ -0,0 +1,616 @@ +use std::ffi::c_void; + +use core_foundation::base::CFRelease; +use core_foundation::data::{CFDataGetBytePtr, CFDataRef}; +use objc2::rc::Retained; +use objc2_app_kit::{NSEvent, NSEventModifierFlags, NSEventSubtype, NSEventType}; +use objc2_foundation::{run_on_main, NSPoint}; +use smol_str::SmolStr; + +use crate::event::{ElementState, KeyEvent, Modifiers}; +use crate::keyboard::{ + Key, KeyCode, KeyLocation, ModifiersKeys, ModifiersState, NamedKey, NativeKey, NativeKeyCode, + PhysicalKey, +}; +use crate::platform_impl::platform::ffi; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct KeyEventExtra { + pub text_with_all_modifiers: Option, + pub key_without_modifiers: Key, +} + +/// Ignores ALL modifiers. +pub fn get_modifierless_char(scancode: u16) -> Key { + let mut string = [0; 16]; + let input_source; + let layout; + unsafe { + input_source = ffi::TISCopyCurrentKeyboardLayoutInputSource(); + if input_source.is_null() { + tracing::error!("`TISCopyCurrentKeyboardLayoutInputSource` returned null ptr"); + return Key::Unidentified(NativeKey::MacOS(scancode)); + } + let layout_data = + ffi::TISGetInputSourceProperty(input_source, ffi::kTISPropertyUnicodeKeyLayoutData); + if layout_data.is_null() { + CFRelease(input_source as *mut c_void); + tracing::error!("`TISGetInputSourceProperty` returned null ptr"); + return Key::Unidentified(NativeKey::MacOS(scancode)); + } + layout = CFDataGetBytePtr(layout_data as CFDataRef) as *const ffi::UCKeyboardLayout; + } + let keyboard_type = run_on_main(|_mtm| unsafe { ffi::LMGetKbdType() }); + + let mut result_len = 0; + let mut dead_keys = 0; + let modifiers = 0; + let translate_result = unsafe { + ffi::UCKeyTranslate( + layout, + scancode, + ffi::kUCKeyActionDisplay, + modifiers, + keyboard_type as u32, + ffi::kUCKeyTranslateNoDeadKeysMask, + &mut dead_keys, + string.len() as ffi::UniCharCount, + &mut result_len, + string.as_mut_ptr(), + ) + }; + unsafe { + CFRelease(input_source as *mut c_void); + } + if translate_result != 0 { + tracing::error!("`UCKeyTranslate` returned with the non-zero value: {}", translate_result); + return Key::Unidentified(NativeKey::MacOS(scancode)); + } + if result_len == 0 { + // This is fine - not all keys have text representation. + // For instance, users that have mapped the `Fn` key to toggle + // keyboard layouts will hit this code path. + return Key::Unidentified(NativeKey::MacOS(scancode)); + } + let chars = String::from_utf16_lossy(&string[0..result_len as usize]); + Key::Character(SmolStr::new(chars)) +} + +// Ignores all modifiers except for SHIFT (yes, even ALT is ignored). +fn get_logical_key_char(ns_event: &NSEvent, modifierless_chars: &str) -> Key { + let string = unsafe { ns_event.charactersIgnoringModifiers() } + .map(|s| s.to_string()) + .unwrap_or_default(); + if string.is_empty() { + // Probably a dead key + let first_char = modifierless_chars.chars().next(); + return Key::Dead(first_char); + } + Key::Character(SmolStr::new(string)) +} + +/// Create `KeyEvent` for the given `NSEvent`. +/// +/// This function shouldn't be called when the IME input is in process. +pub(crate) fn create_key_event(ns_event: &NSEvent, is_press: bool, is_repeat: bool) -> KeyEvent { + use ElementState::{Pressed, Released}; + let state = if is_press { Pressed } else { Released }; + + let scancode = unsafe { ns_event.keyCode() }; + let mut physical_key = scancode_to_physicalkey(scancode as u32); + + // NOTE: The logical key should heed both SHIFT and ALT if possible. + // For instance: + // * Pressing the A key: logical key should be "a" + // * Pressing SHIFT A: logical key should be "A" + // * Pressing CTRL SHIFT A: logical key should also be "A" + // This is not easy to tease out of `NSEvent`, but we do our best. + + let characters = unsafe { ns_event.characters() }.map(|s| s.to_string()).unwrap_or_default(); + let text_with_all_modifiers = if characters.is_empty() { + None + } else { + if matches!(physical_key, PhysicalKey::Unidentified(_)) { + // The key may be one of the funky function keys + physical_key = extra_function_key_to_code(scancode, &characters); + } + Some(SmolStr::new(characters)) + }; + + let key_from_code = code_to_key(physical_key, scancode); + let (logical_key, key_without_modifiers) = if matches!(key_from_code, Key::Unidentified(_)) { + // `get_modifierless_char/key_without_modifiers` ignores ALL modifiers. + let key_without_modifiers = get_modifierless_char(scancode); + + let modifiers = unsafe { ns_event.modifierFlags() }; + let has_ctrl = modifiers.contains(NSEventModifierFlags::NSEventModifierFlagControl); + let has_cmd = modifiers.contains(NSEventModifierFlags::NSEventModifierFlagCommand); + + let logical_key = match text_with_all_modifiers.as_ref() { + // Only checking for ctrl and cmd here, not checking for alt because we DO want to + // include its effect in the key. For example if -on the German layout- one + // presses alt+8, the logical key should be "{" + // Also not checking if this is a release event because then this issue would + // still affect the key release. + Some(text) if !has_ctrl && !has_cmd => { + // Character heeding both SHIFT and ALT. + Key::Character(text.clone()) + }, + + _ => match key_without_modifiers.as_ref() { + // Character heeding just SHIFT, ignoring ALT. + Key::Character(ch) => get_logical_key_char(ns_event, ch), + + // Character ignoring ALL modifiers. + _ => key_without_modifiers.clone(), + }, + }; + + (logical_key, key_without_modifiers) + } else { + (key_from_code.clone(), key_from_code) + }; + + let text = if is_press { logical_key.to_text().map(SmolStr::new) } else { None }; + + let location = code_to_location(physical_key); + + KeyEvent { + location, + logical_key, + physical_key, + repeat: is_repeat, + state, + text, + platform_specific: KeyEventExtra { text_with_all_modifiers, key_without_modifiers }, + } +} + +pub fn code_to_key(key: PhysicalKey, scancode: u16) -> Key { + let code = match key { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(code) => return Key::Unidentified(code.into()), + }; + + Key::Named(match code { + KeyCode::Enter => NamedKey::Enter, + KeyCode::Tab => NamedKey::Tab, + KeyCode::Space => NamedKey::Space, + KeyCode::Backspace => NamedKey::Backspace, + KeyCode::Escape => NamedKey::Escape, + KeyCode::SuperRight => NamedKey::Super, + KeyCode::SuperLeft => NamedKey::Super, + KeyCode::ShiftLeft => NamedKey::Shift, + KeyCode::AltLeft => NamedKey::Alt, + KeyCode::ControlLeft => NamedKey::Control, + KeyCode::ShiftRight => NamedKey::Shift, + KeyCode::AltRight => NamedKey::Alt, + KeyCode::ControlRight => NamedKey::Control, + + KeyCode::NumLock => NamedKey::NumLock, + KeyCode::AudioVolumeUp => NamedKey::AudioVolumeUp, + KeyCode::AudioVolumeDown => NamedKey::AudioVolumeDown, + + // Other numpad keys all generate text on macOS (if I understand correctly) + KeyCode::NumpadEnter => NamedKey::Enter, + + KeyCode::F1 => NamedKey::F1, + KeyCode::F2 => NamedKey::F2, + KeyCode::F3 => NamedKey::F3, + KeyCode::F4 => NamedKey::F4, + KeyCode::F5 => NamedKey::F5, + KeyCode::F6 => NamedKey::F6, + KeyCode::F7 => NamedKey::F7, + KeyCode::F8 => NamedKey::F8, + KeyCode::F9 => NamedKey::F9, + KeyCode::F10 => NamedKey::F10, + KeyCode::F11 => NamedKey::F11, + KeyCode::F12 => NamedKey::F12, + KeyCode::F13 => NamedKey::F13, + KeyCode::F14 => NamedKey::F14, + KeyCode::F15 => NamedKey::F15, + KeyCode::F16 => NamedKey::F16, + KeyCode::F17 => NamedKey::F17, + KeyCode::F18 => NamedKey::F18, + KeyCode::F19 => NamedKey::F19, + KeyCode::F20 => NamedKey::F20, + + KeyCode::Insert => NamedKey::Insert, + KeyCode::Home => NamedKey::Home, + KeyCode::PageUp => NamedKey::PageUp, + KeyCode::Delete => NamedKey::Delete, + KeyCode::End => NamedKey::End, + KeyCode::PageDown => NamedKey::PageDown, + KeyCode::ArrowLeft => NamedKey::ArrowLeft, + KeyCode::ArrowRight => NamedKey::ArrowRight, + KeyCode::ArrowDown => NamedKey::ArrowDown, + KeyCode::ArrowUp => NamedKey::ArrowUp, + _ => return Key::Unidentified(NativeKey::MacOS(scancode)), + }) +} + +pub fn code_to_location(key: PhysicalKey) -> KeyLocation { + let code = match key { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(_) => return KeyLocation::Standard, + }; + + match code { + KeyCode::SuperRight => KeyLocation::Right, + KeyCode::SuperLeft => KeyLocation::Left, + KeyCode::ShiftLeft => KeyLocation::Left, + KeyCode::AltLeft => KeyLocation::Left, + KeyCode::ControlLeft => KeyLocation::Left, + KeyCode::ShiftRight => KeyLocation::Right, + KeyCode::AltRight => KeyLocation::Right, + KeyCode::ControlRight => KeyLocation::Right, + + KeyCode::NumLock => KeyLocation::Numpad, + KeyCode::NumpadDecimal => KeyLocation::Numpad, + KeyCode::NumpadMultiply => KeyLocation::Numpad, + KeyCode::NumpadAdd => KeyLocation::Numpad, + KeyCode::NumpadDivide => KeyLocation::Numpad, + KeyCode::NumpadEnter => KeyLocation::Numpad, + KeyCode::NumpadSubtract => KeyLocation::Numpad, + KeyCode::NumpadEqual => KeyLocation::Numpad, + KeyCode::Numpad0 => KeyLocation::Numpad, + KeyCode::Numpad1 => KeyLocation::Numpad, + KeyCode::Numpad2 => KeyLocation::Numpad, + KeyCode::Numpad3 => KeyLocation::Numpad, + KeyCode::Numpad4 => KeyLocation::Numpad, + KeyCode::Numpad5 => KeyLocation::Numpad, + KeyCode::Numpad6 => KeyLocation::Numpad, + KeyCode::Numpad7 => KeyLocation::Numpad, + KeyCode::Numpad8 => KeyLocation::Numpad, + KeyCode::Numpad9 => KeyLocation::Numpad, + + _ => KeyLocation::Standard, + } +} + +// While F1-F20 have scancodes we can match on, we have to check against UTF-16 +// constants for the rest. +// https://developer.apple.com/documentation/appkit/1535851-function-key_unicodes?preferredLanguage=occ +pub fn extra_function_key_to_code(scancode: u16, string: &str) -> PhysicalKey { + if let Some(ch) = string.encode_utf16().next() { + match ch { + 0xf718 => PhysicalKey::Code(KeyCode::F21), + 0xf719 => PhysicalKey::Code(KeyCode::F22), + 0xf71a => PhysicalKey::Code(KeyCode::F23), + 0xf71b => PhysicalKey::Code(KeyCode::F24), + _ => PhysicalKey::Unidentified(NativeKeyCode::MacOS(scancode)), + } + } else { + PhysicalKey::Unidentified(NativeKeyCode::MacOS(scancode)) + } +} + +// The values are from the https://github.com/apple-oss-distributions/IOHIDFamily/blob/19666c840a6d896468416ff0007040a10b7b46b8/IOHIDSystem/IOKit/hidsystem/IOLLEvent.h#L258-L259 +const NX_DEVICELCTLKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000001); +const NX_DEVICELSHIFTKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000002); +const NX_DEVICERSHIFTKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000004); +const NX_DEVICELCMDKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000008); +const NX_DEVICERCMDKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000010); +const NX_DEVICELALTKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000020); +const NX_DEVICERALTKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000040); +const NX_DEVICERCTLKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00002000); + +pub(super) fn lalt_pressed(event: &NSEvent) -> bool { + unsafe { event.modifierFlags() }.contains(NX_DEVICELALTKEYMASK) +} + +pub(super) fn ralt_pressed(event: &NSEvent) -> bool { + unsafe { event.modifierFlags() }.contains(NX_DEVICERALTKEYMASK) +} + +pub(super) fn event_mods(event: &NSEvent) -> Modifiers { + let flags = unsafe { event.modifierFlags() }; + let mut state = ModifiersState::empty(); + let mut pressed_mods = ModifiersKeys::empty(); + + state + .set(ModifiersState::SHIFT, flags.contains(NSEventModifierFlags::NSEventModifierFlagShift)); + pressed_mods.set(ModifiersKeys::LSHIFT, flags.contains(NX_DEVICELSHIFTKEYMASK)); + pressed_mods.set(ModifiersKeys::RSHIFT, flags.contains(NX_DEVICERSHIFTKEYMASK)); + + state.set( + ModifiersState::CONTROL, + flags.contains(NSEventModifierFlags::NSEventModifierFlagControl), + ); + pressed_mods.set(ModifiersKeys::LCONTROL, flags.contains(NX_DEVICELCTLKEYMASK)); + pressed_mods.set(ModifiersKeys::RCONTROL, flags.contains(NX_DEVICERCTLKEYMASK)); + + state.set(ModifiersState::ALT, flags.contains(NSEventModifierFlags::NSEventModifierFlagOption)); + pressed_mods.set(ModifiersKeys::LALT, flags.contains(NX_DEVICELALTKEYMASK)); + pressed_mods.set(ModifiersKeys::RALT, flags.contains(NX_DEVICERALTKEYMASK)); + + state.set( + ModifiersState::SUPER, + flags.contains(NSEventModifierFlags::NSEventModifierFlagCommand), + ); + pressed_mods.set(ModifiersKeys::LSUPER, flags.contains(NX_DEVICELCMDKEYMASK)); + pressed_mods.set(ModifiersKeys::RSUPER, flags.contains(NX_DEVICERCMDKEYMASK)); + + Modifiers { state, pressed_mods } +} + +pub(super) fn dummy_event() -> Option> { + unsafe { + NSEvent::otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2( + NSEventType::ApplicationDefined, + NSPoint::new(0.0, 0.0), + NSEventModifierFlags(0), + 0.0, + 0, + None, + NSEventSubtype::WindowExposed.0, + 0, + 0, + ) + } +} + +pub(crate) fn physicalkey_to_scancode(physical_key: PhysicalKey) -> Option { + let code = match physical_key { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(_) => return None, + }; + + match code { + KeyCode::KeyA => Some(0x00), + KeyCode::KeyS => Some(0x01), + KeyCode::KeyD => Some(0x02), + KeyCode::KeyF => Some(0x03), + KeyCode::KeyH => Some(0x04), + KeyCode::KeyG => Some(0x05), + KeyCode::KeyZ => Some(0x06), + KeyCode::KeyX => Some(0x07), + KeyCode::KeyC => Some(0x08), + KeyCode::KeyV => Some(0x09), + KeyCode::KeyB => Some(0x0b), + KeyCode::KeyQ => Some(0x0c), + KeyCode::KeyW => Some(0x0d), + KeyCode::KeyE => Some(0x0e), + KeyCode::KeyR => Some(0x0f), + KeyCode::KeyY => Some(0x10), + KeyCode::KeyT => Some(0x11), + KeyCode::Digit1 => Some(0x12), + KeyCode::Digit2 => Some(0x13), + KeyCode::Digit3 => Some(0x14), + KeyCode::Digit4 => Some(0x15), + KeyCode::Digit6 => Some(0x16), + KeyCode::Digit5 => Some(0x17), + KeyCode::Equal => Some(0x18), + KeyCode::Digit9 => Some(0x19), + KeyCode::Digit7 => Some(0x1a), + KeyCode::Minus => Some(0x1b), + KeyCode::Digit8 => Some(0x1c), + KeyCode::Digit0 => Some(0x1d), + KeyCode::BracketRight => Some(0x1e), + KeyCode::KeyO => Some(0x1f), + KeyCode::KeyU => Some(0x20), + KeyCode::BracketLeft => Some(0x21), + KeyCode::KeyI => Some(0x22), + KeyCode::KeyP => Some(0x23), + KeyCode::Enter => Some(0x24), + KeyCode::KeyL => Some(0x25), + KeyCode::KeyJ => Some(0x26), + KeyCode::Quote => Some(0x27), + KeyCode::KeyK => Some(0x28), + KeyCode::Semicolon => Some(0x29), + KeyCode::Backslash => Some(0x2a), + KeyCode::Comma => Some(0x2b), + KeyCode::Slash => Some(0x2c), + KeyCode::KeyN => Some(0x2d), + KeyCode::KeyM => Some(0x2e), + KeyCode::Period => Some(0x2f), + KeyCode::Tab => Some(0x30), + KeyCode::Space => Some(0x31), + KeyCode::Backquote => Some(0x32), + KeyCode::Backspace => Some(0x33), + KeyCode::Escape => Some(0x35), + KeyCode::SuperRight => Some(0x36), + KeyCode::SuperLeft => Some(0x37), + KeyCode::ShiftLeft => Some(0x38), + KeyCode::AltLeft => Some(0x3a), + KeyCode::ControlLeft => Some(0x3b), + KeyCode::ShiftRight => Some(0x3c), + KeyCode::AltRight => Some(0x3d), + KeyCode::ControlRight => Some(0x3e), + KeyCode::F17 => Some(0x40), + KeyCode::NumpadDecimal => Some(0x41), + KeyCode::NumpadMultiply => Some(0x43), + KeyCode::NumpadAdd => Some(0x45), + KeyCode::NumLock => Some(0x47), + KeyCode::AudioVolumeUp => Some(0x49), + KeyCode::AudioVolumeDown => Some(0x4a), + KeyCode::NumpadDivide => Some(0x4b), + KeyCode::NumpadEnter => Some(0x4c), + KeyCode::NumpadSubtract => Some(0x4e), + KeyCode::F18 => Some(0x4f), + KeyCode::F19 => Some(0x50), + KeyCode::NumpadEqual => Some(0x51), + KeyCode::Numpad0 => Some(0x52), + KeyCode::Numpad1 => Some(0x53), + KeyCode::Numpad2 => Some(0x54), + KeyCode::Numpad3 => Some(0x55), + KeyCode::Numpad4 => Some(0x56), + KeyCode::Numpad5 => Some(0x57), + KeyCode::Numpad6 => Some(0x58), + KeyCode::Numpad7 => Some(0x59), + KeyCode::F20 => Some(0x5a), + KeyCode::Numpad8 => Some(0x5b), + KeyCode::Numpad9 => Some(0x5c), + KeyCode::IntlYen => Some(0x5d), + KeyCode::F5 => Some(0x60), + KeyCode::F6 => Some(0x61), + KeyCode::F7 => Some(0x62), + KeyCode::F3 => Some(0x63), + KeyCode::F8 => Some(0x64), + KeyCode::F9 => Some(0x65), + KeyCode::F11 => Some(0x67), + KeyCode::F13 => Some(0x69), + KeyCode::F16 => Some(0x6a), + KeyCode::F14 => Some(0x6b), + KeyCode::F10 => Some(0x6d), + KeyCode::F12 => Some(0x6f), + KeyCode::F15 => Some(0x71), + KeyCode::Insert => Some(0x72), + KeyCode::Home => Some(0x73), + KeyCode::PageUp => Some(0x74), + KeyCode::Delete => Some(0x75), + KeyCode::F4 => Some(0x76), + KeyCode::End => Some(0x77), + KeyCode::F2 => Some(0x78), + KeyCode::PageDown => Some(0x79), + KeyCode::F1 => Some(0x7a), + KeyCode::ArrowLeft => Some(0x7b), + KeyCode::ArrowRight => Some(0x7c), + KeyCode::ArrowDown => Some(0x7d), + KeyCode::ArrowUp => Some(0x7e), + _ => None, + } +} + +pub(crate) fn scancode_to_physicalkey(scancode: u32) -> PhysicalKey { + PhysicalKey::Code(match scancode { + 0x00 => KeyCode::KeyA, + 0x01 => KeyCode::KeyS, + 0x02 => KeyCode::KeyD, + 0x03 => KeyCode::KeyF, + 0x04 => KeyCode::KeyH, + 0x05 => KeyCode::KeyG, + 0x06 => KeyCode::KeyZ, + 0x07 => KeyCode::KeyX, + 0x08 => KeyCode::KeyC, + 0x09 => KeyCode::KeyV, + // 0x0a => World 1, + 0x0b => KeyCode::KeyB, + 0x0c => KeyCode::KeyQ, + 0x0d => KeyCode::KeyW, + 0x0e => KeyCode::KeyE, + 0x0f => KeyCode::KeyR, + 0x10 => KeyCode::KeyY, + 0x11 => KeyCode::KeyT, + 0x12 => KeyCode::Digit1, + 0x13 => KeyCode::Digit2, + 0x14 => KeyCode::Digit3, + 0x15 => KeyCode::Digit4, + 0x16 => KeyCode::Digit6, + 0x17 => KeyCode::Digit5, + 0x18 => KeyCode::Equal, + 0x19 => KeyCode::Digit9, + 0x1a => KeyCode::Digit7, + 0x1b => KeyCode::Minus, + 0x1c => KeyCode::Digit8, + 0x1d => KeyCode::Digit0, + 0x1e => KeyCode::BracketRight, + 0x1f => KeyCode::KeyO, + 0x20 => KeyCode::KeyU, + 0x21 => KeyCode::BracketLeft, + 0x22 => KeyCode::KeyI, + 0x23 => KeyCode::KeyP, + 0x24 => KeyCode::Enter, + 0x25 => KeyCode::KeyL, + 0x26 => KeyCode::KeyJ, + 0x27 => KeyCode::Quote, + 0x28 => KeyCode::KeyK, + 0x29 => KeyCode::Semicolon, + 0x2a => KeyCode::Backslash, + 0x2b => KeyCode::Comma, + 0x2c => KeyCode::Slash, + 0x2d => KeyCode::KeyN, + 0x2e => KeyCode::KeyM, + 0x2f => KeyCode::Period, + 0x30 => KeyCode::Tab, + 0x31 => KeyCode::Space, + 0x32 => KeyCode::Backquote, + 0x33 => KeyCode::Backspace, + // 0x34 => unknown, + 0x35 => KeyCode::Escape, + 0x36 => KeyCode::SuperRight, + 0x37 => KeyCode::SuperLeft, + 0x38 => KeyCode::ShiftLeft, + 0x39 => KeyCode::CapsLock, + 0x3a => KeyCode::AltLeft, + 0x3b => KeyCode::ControlLeft, + 0x3c => KeyCode::ShiftRight, + 0x3d => KeyCode::AltRight, + 0x3e => KeyCode::ControlRight, + 0x3f => KeyCode::Fn, + 0x40 => KeyCode::F17, + 0x41 => KeyCode::NumpadDecimal, + // 0x42 -> unknown, + 0x43 => KeyCode::NumpadMultiply, + // 0x44 => unknown, + 0x45 => KeyCode::NumpadAdd, + // 0x46 => unknown, + 0x47 => KeyCode::NumLock, + // 0x48 => KeyCode::NumpadClear, + + // TODO: (Artur) for me, kVK_VolumeUp is 0x48 + // macOS 10.11 + // /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/ + // Versions/A/Headers/Events.h + 0x49 => KeyCode::AudioVolumeUp, + 0x4a => KeyCode::AudioVolumeDown, + 0x4b => KeyCode::NumpadDivide, + 0x4c => KeyCode::NumpadEnter, + // 0x4d => unknown, + 0x4e => KeyCode::NumpadSubtract, + 0x4f => KeyCode::F18, + 0x50 => KeyCode::F19, + 0x51 => KeyCode::NumpadEqual, + 0x52 => KeyCode::Numpad0, + 0x53 => KeyCode::Numpad1, + 0x54 => KeyCode::Numpad2, + 0x55 => KeyCode::Numpad3, + 0x56 => KeyCode::Numpad4, + 0x57 => KeyCode::Numpad5, + 0x58 => KeyCode::Numpad6, + 0x59 => KeyCode::Numpad7, + 0x5a => KeyCode::F20, + 0x5b => KeyCode::Numpad8, + 0x5c => KeyCode::Numpad9, + 0x5d => KeyCode::IntlYen, + // 0x5e => JIS Ro, + // 0x5f => unknown, + 0x60 => KeyCode::F5, + 0x61 => KeyCode::F6, + 0x62 => KeyCode::F7, + 0x63 => KeyCode::F3, + 0x64 => KeyCode::F8, + 0x65 => KeyCode::F9, + // 0x66 => JIS Eisuu (macOS), + 0x67 => KeyCode::F11, + // 0x68 => JIS Kanna (macOS), + 0x69 => KeyCode::F13, + 0x6a => KeyCode::F16, + 0x6b => KeyCode::F14, + // 0x6c => unknown, + 0x6d => KeyCode::F10, + // 0x6e => unknown, + 0x6f => KeyCode::F12, + // 0x70 => unknown, + 0x71 => KeyCode::F15, + 0x72 => KeyCode::Insert, + 0x73 => KeyCode::Home, + 0x74 => KeyCode::PageUp, + 0x75 => KeyCode::Delete, + 0x76 => KeyCode::F4, + 0x77 => KeyCode::End, + 0x78 => KeyCode::F2, + 0x79 => KeyCode::PageDown, + 0x7a => KeyCode::F1, + 0x7b => KeyCode::ArrowLeft, + 0x7c => KeyCode::ArrowRight, + 0x7d => KeyCode::ArrowDown, + 0x7e => KeyCode::ArrowUp, + // 0x7f => unknown, + + // 0xA is the caret (^) an macOS's German QERTZ layout. This key is at the same location as + // backquote (`) on Windows' US layout. + 0xa => KeyCode::Backquote, + _ => return PhysicalKey::Unidentified(NativeKeyCode::MacOS(scancode as u16)), + }) +} diff --git a/vendor/winit/src/platform_impl/macos/event_handler.rs b/vendor/winit/src/platform_impl/macos/event_handler.rs new file mode 100644 index 00000000..5c353c14 --- /dev/null +++ b/vendor/winit/src/platform_impl/macos/event_handler.rs @@ -0,0 +1,139 @@ +use std::cell::RefCell; +use std::{fmt, mem}; + +use super::app_state::HandlePendingUserEvents; +use crate::event::Event; +use crate::event_loop::ActiveEventLoop as RootActiveEventLoop; + +struct EventHandlerData { + #[allow(clippy::type_complexity)] + handler: Box, &RootActiveEventLoop) + 'static>, +} + +impl fmt::Debug for EventHandlerData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("EventHandlerData").finish_non_exhaustive() + } +} + +#[derive(Debug)] +pub(crate) struct EventHandler { + /// This can be in the following states: + /// - Not registered by the event loop (None). + /// - Present (Some(handler)). + /// - Currently executing the handler / in use (RefCell borrowed). + inner: RefCell>, +} + +impl EventHandler { + pub(crate) const fn new() -> Self { + Self { inner: RefCell::new(None) } + } + + /// Set the event loop handler for the duration of the given closure. + /// + /// This is similar to using the `scoped-tls` or `scoped-tls-hkt` crates + /// to store the handler in a thread local, such that it can be accessed + /// from within the closure. + pub(crate) fn set<'handler, R>( + &self, + handler: impl FnMut(Event, &RootActiveEventLoop) + 'handler, + closure: impl FnOnce() -> R, + ) -> R { + // SAFETY: We extend the lifetime of the handler here so that we can + // store it in `EventHandler`'s `RefCell`. + // + // This is sound, since we make sure to unset the handler again at the + // end of this function, and as such the lifetime isn't actually + // extended beyond `'handler`. + let handler = unsafe { + mem::transmute::< + Box, &RootActiveEventLoop) + 'handler>, + Box, &RootActiveEventLoop) + 'static>, + >(Box::new(handler)) + }; + + match self.inner.try_borrow_mut().as_deref_mut() { + Ok(Some(_)) => { + unreachable!("tried to set handler while another was already set"); + }, + Ok(data @ None) => { + *data = Some(EventHandlerData { handler }); + }, + Err(_) => { + unreachable!("tried to set handler that is currently in use"); + }, + } + + struct ClearOnDrop<'a>(&'a EventHandler); + + impl Drop for ClearOnDrop<'_> { + fn drop(&mut self) { + match self.0.inner.try_borrow_mut().as_deref_mut() { + Ok(data @ Some(_)) => { + *data = None; + }, + Ok(None) => { + tracing::error!("tried to clear handler, but no handler was set"); + }, + Err(_) => { + // Note: This is not expected to ever happen, this + // module generally controls the `RefCell`, and + // prevents it from ever being borrowed outside of it. + // + // But if it _does_ happen, it is a serious error, and + // we must abort the process, it'd be unsound if we + // weren't able to unset the handler. + eprintln!("tried to clear handler that is currently in use"); + std::process::abort(); + }, + } + } + } + + let _clear_on_drop = ClearOnDrop(self); + + // Note: The RefCell should not be borrowed while executing the + // closure, that'd defeat the whole point. + closure() + + // `_clear_on_drop` will be dropped here, or when unwinding, ensuring + // soundness. + } + + pub(crate) fn in_use(&self) -> bool { + self.inner.try_borrow().is_err() + } + + pub(crate) fn ready(&self) -> bool { + matches!(self.inner.try_borrow().as_deref(), Ok(Some(_))) + } + + pub(crate) fn handle_event( + &self, + event: Event, + event_loop: &RootActiveEventLoop, + ) { + match self.inner.try_borrow_mut().as_deref_mut() { + Ok(Some(EventHandlerData { handler })) => { + // It is important that we keep the reference borrowed here, + // so that `in_use` can properly detect that the handler is + // still in use. + // + // If the handler unwinds, the `RefMut` will ensure that the + // handler is no longer borrowed. + (handler)(event, event_loop); + }, + Ok(None) => { + // `NSApplication`, our app delegate and this handler are all + // global state and so it's not impossible that we could get + // an event after the application has exited the `EventLoop`. + tracing::error!("tried to run event handler, but no handler was set"); + }, + Err(_) => { + // Prevent re-entrancy. + panic!("tried to handle event while another event is currently being handled"); + }, + } + } +} diff --git a/vendor/winit/src/platform_impl/macos/event_loop.rs b/vendor/winit/src/platform_impl/macos/event_loop.rs new file mode 100644 index 00000000..5c093d5a --- /dev/null +++ b/vendor/winit/src/platform_impl/macos/event_loop.rs @@ -0,0 +1,523 @@ +use std::any::Any; +use std::cell::Cell; +use std::collections::VecDeque; +use std::marker::PhantomData; +use std::os::raw::c_void; +use std::panic::{catch_unwind, resume_unwind, RefUnwindSafe, UnwindSafe}; +use std::ptr; +use std::rc::{Rc, Weak}; +use std::sync::mpsc; +use std::time::{Duration, Instant}; + +use core_foundation::base::{CFIndex, CFRelease}; +use core_foundation::runloop::{ + kCFRunLoopCommonModes, CFRunLoopAddSource, CFRunLoopGetMain, CFRunLoopSourceContext, + CFRunLoopSourceCreate, CFRunLoopSourceRef, CFRunLoopSourceSignal, CFRunLoopWakeUp, +}; +use objc2::rc::{autoreleasepool, Retained}; +use objc2::runtime::ProtocolObject; +use objc2::sel; +use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSWindow}; +use objc2_foundation::{MainThreadMarker, NSObjectProtocol}; + +use super::app::override_send_event; +use super::app_state::{ApplicationDelegate, HandlePendingUserEvents}; +use super::event::dummy_event; +use super::monitor::{self, MonitorHandle}; +use super::observer::setup_control_flow_observers; +use crate::error::EventLoopError; +use crate::event::Event; +use crate::event_loop::{ + ActiveEventLoop as RootWindowTarget, ControlFlow, DeviceEvents, EventLoopClosed, +}; +use crate::platform::macos::ActivationPolicy; +use crate::platform::pump_events::PumpStatus; +use crate::platform_impl::platform::cursor::CustomCursor; +use crate::window::{CustomCursor as RootCustomCursor, CustomCursorSource, Theme}; + +#[derive(Default)] +pub struct PanicInfo { + inner: Cell>>, +} + +// WARNING: +// As long as this struct is used through its `impl`, it is UnwindSafe. +// (If `get_mut` is called on `inner`, unwind safety may get broken.) +impl UnwindSafe for PanicInfo {} +impl RefUnwindSafe for PanicInfo {} +impl PanicInfo { + pub fn is_panicking(&self) -> bool { + let inner = self.inner.take(); + let result = inner.is_some(); + self.inner.set(inner); + result + } + + /// Overwrites the current state if the current state is not panicking + pub fn set_panic(&self, p: Box) { + if !self.is_panicking() { + self.inner.set(Some(p)); + } + } + + pub fn take(&self) -> Option> { + self.inner.take() + } +} + +#[derive(Debug)] +pub struct ActiveEventLoop { + delegate: Retained, + pub(super) mtm: MainThreadMarker, +} + +impl ActiveEventLoop { + pub(super) fn new_root(delegate: Retained) -> RootWindowTarget { + let mtm = MainThreadMarker::from(&*delegate); + let p = Self { delegate, mtm }; + RootWindowTarget { p, _marker: PhantomData } + } + + pub(super) fn app_delegate(&self) -> &ApplicationDelegate { + &self.delegate + } + + pub fn create_custom_cursor(&self, source: CustomCursorSource) -> RootCustomCursor { + RootCustomCursor { inner: CustomCursor::new(source.inner) } + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + monitor::available_monitors() + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + let monitor = monitor::primary_monitor(); + Some(monitor) + } + + #[inline] + pub fn listen_device_events(&self, _allowed: DeviceEvents) {} + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::AppKit(rwh_05::AppKitDisplayHandle::empty()) + } + + #[inline] + pub fn system_theme(&self) -> Option { + let app = NSApplication::sharedApplication(self.mtm); + + if app.respondsToSelector(sel!(effectiveAppearance)) { + Some(super::window_delegate::appearance_to_theme(&app.effectiveAppearance())) + } else { + Some(Theme::Light) + } + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::AppKit(rwh_06::AppKitDisplayHandle::new())) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.delegate.set_control_flow(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.delegate.control_flow() + } + + pub(crate) fn exit(&self) { + self.delegate.exit() + } + + pub(crate) fn clear_exit(&self) { + self.delegate.clear_exit() + } + + pub(crate) fn exiting(&self) -> bool { + self.delegate.exiting() + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle + } + + pub(crate) fn hide_application(&self) { + NSApplication::sharedApplication(self.mtm).hide(None) + } + + pub(crate) fn hide_other_applications(&self) { + NSApplication::sharedApplication(self.mtm).hideOtherApplications(None) + } + + pub(crate) fn set_allows_automatic_window_tabbing(&self, enabled: bool) { + NSWindow::setAllowsAutomaticWindowTabbing(enabled, self.mtm) + } + + pub(crate) fn allows_automatic_window_tabbing(&self) -> bool { + NSWindow::allowsAutomaticWindowTabbing(self.mtm) + } +} + +fn map_user_event( + mut handler: impl FnMut(Event, &RootWindowTarget), + receiver: Rc>, +) -> impl FnMut(Event, &RootWindowTarget) { + move |event, window_target| match event.map_nonuser_event() { + Ok(event) => (handler)(event, window_target), + Err(_) => { + for event in receiver.try_iter() { + (handler)(Event::UserEvent(event), window_target); + } + }, + } +} + +pub struct EventLoop { + /// Store a reference to the application for convenience. + /// + /// We intentionally don't store `WinitApplication` since we want to have + /// the possibility of swapping that out at some point. + app: Retained, + /// The application delegate that we've registered. + /// + /// The delegate is only weakly referenced by NSApplication, so we must + /// keep it around here as well. + delegate: Retained, + + // Event sender and receiver, used for EventLoopProxy. + sender: mpsc::Sender, + receiver: Rc>, + + window_target: RootWindowTarget, + panic_info: Rc, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PlatformSpecificEventLoopAttributes { + pub(crate) activation_policy: Option, + pub(crate) default_menu: bool, + pub(crate) activate_ignoring_other_apps: bool, +} + +impl Default for PlatformSpecificEventLoopAttributes { + fn default() -> Self { + Self { activation_policy: None, default_menu: true, activate_ignoring_other_apps: true } + } +} + +impl EventLoop { + pub(crate) fn new( + attributes: &PlatformSpecificEventLoopAttributes, + ) -> Result { + let mtm = MainThreadMarker::new() + .expect("on macOS, `EventLoop` must be created on the main thread!"); + + // Initialize the application (if it has not already been). + let app = NSApplication::sharedApplication(mtm); + + let activation_policy = match attributes.activation_policy { + None => None, + Some(ActivationPolicy::Regular) => Some(NSApplicationActivationPolicy::Regular), + Some(ActivationPolicy::Accessory) => Some(NSApplicationActivationPolicy::Accessory), + Some(ActivationPolicy::Prohibited) => Some(NSApplicationActivationPolicy::Prohibited), + }; + let delegate = ApplicationDelegate::new( + mtm, + activation_policy, + attributes.default_menu, + attributes.activate_ignoring_other_apps, + ); + + autoreleasepool(|_| { + app.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); + }); + + // Override `sendEvent:` on the application to forward to our application state. + override_send_event(&app); + + let panic_info: Rc = Default::default(); + setup_control_flow_observers(mtm, Rc::downgrade(&panic_info)); + + let (sender, receiver) = mpsc::channel(); + Ok(EventLoop { + app, + delegate: delegate.clone(), + sender, + receiver: Rc::new(receiver), + window_target: RootWindowTarget { + p: ActiveEventLoop { delegate, mtm }, + _marker: PhantomData, + }, + panic_info, + }) + } + + pub fn window_target(&self) -> &RootWindowTarget { + &self.window_target + } + + pub fn run(mut self, handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &RootWindowTarget), + { + self.run_on_demand(handler) + } + + // NB: we don't base this on `pump_events` because for `MacOs` we can't support + // `pump_events` elegantly (we just ask to run the loop for a "short" amount of + // time and so a layered implementation would end up using a lot of CPU due to + // redundant wake ups. + pub fn run_on_demand(&mut self, handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &RootWindowTarget), + { + let handler = map_user_event(handler, self.receiver.clone()); + + self.delegate.set_event_handler(handler, || { + autoreleasepool(|_| { + // clear / normalize pump_events state + self.delegate.set_wait_timeout(None); + self.delegate.set_stop_before_wait(false); + self.delegate.set_stop_after_wait(false); + self.delegate.set_stop_on_redraw(false); + + if self.delegate.is_launched() { + debug_assert!(!self.delegate.is_running()); + self.delegate.set_is_running(true); + self.delegate.dispatch_init_events(); + } + + // SAFETY: We do not run the application re-entrantly + unsafe { self.app.run() }; + + // While the app is running it's possible that we catch a panic + // to avoid unwinding across an objective-c ffi boundary, which + // will lead to us stopping the `NSApplication` and saving the + // `PanicInfo` so that we can resume the unwind at a controlled, + // safe point in time. + if let Some(panic) = self.panic_info.take() { + resume_unwind(panic); + } + + self.delegate.internal_exit() + }) + }); + + Ok(()) + } + + pub fn pump_events(&mut self, timeout: Option, handler: F) -> PumpStatus + where + F: FnMut(Event, &RootWindowTarget), + { + let handler = map_user_event(handler, self.receiver.clone()); + + self.delegate.set_event_handler(handler, || { + autoreleasepool(|_| { + // As a special case, if the application hasn't been launched yet then we at least + // run the loop until it has fully launched. + if !self.delegate.is_launched() { + debug_assert!(!self.delegate.is_running()); + + self.delegate.set_stop_on_launch(); + // SAFETY: We do not run the application re-entrantly + unsafe { self.app.run() }; + + // Note: we dispatch `NewEvents(Init)` + `Resumed` events after the application + // has launched + } else if !self.delegate.is_running() { + // Even though the application may have been launched, it's possible we aren't + // running if the `EventLoop` was run before and has since + // exited. This indicates that we just starting to re-run + // the same `EventLoop` again. + self.delegate.set_is_running(true); + self.delegate.dispatch_init_events(); + } else { + // Only run for as long as the given `Duration` allows so we don't block the + // external loop. + match timeout { + Some(Duration::ZERO) => { + self.delegate.set_wait_timeout(None); + self.delegate.set_stop_before_wait(true); + }, + Some(duration) => { + self.delegate.set_stop_before_wait(false); + let timeout = Instant::now() + duration; + self.delegate.set_wait_timeout(Some(timeout)); + self.delegate.set_stop_after_wait(true); + }, + None => { + self.delegate.set_wait_timeout(None); + self.delegate.set_stop_before_wait(false); + self.delegate.set_stop_after_wait(true); + }, + } + self.delegate.set_stop_on_redraw(true); + // SAFETY: We do not run the application re-entrantly + unsafe { self.app.run() }; + } + + // While the app is running it's possible that we catch a panic + // to avoid unwinding across an objective-c ffi boundary, which + // will lead to us stopping the application and saving the + // `PanicInfo` so that we can resume the unwind at a controlled, + // safe point in time. + if let Some(panic) = self.panic_info.take() { + resume_unwind(panic); + } + + if self.delegate.exiting() { + self.delegate.internal_exit(); + PumpStatus::Exit(0) + } else { + PumpStatus::Continue + } + }) + }) + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy::new(self.sender.clone()) + } +} + +#[derive(Clone)] +pub(crate) struct OwnedDisplayHandle; + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::AppKitDisplayHandle::empty().into() + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::AppKitDisplayHandle::new().into()) + } +} + +pub(super) fn stop_app_immediately(app: &NSApplication) { + autoreleasepool(|_| { + app.stop(None); + // To stop event loop immediately, we need to post some event here. + // See: https://stackoverflow.com/questions/48041279/stopping-the-nsapplication-main-event-loop/48064752#48064752 + app.postEvent_atStart(&dummy_event().unwrap(), true); + }); +} + +/// Tell all windows to close. +/// +/// This will synchronously trigger `WindowEvent::Destroyed` within +/// `windowWillClose:`, giving the application one last chance to handle +/// those events. It doesn't matter if the user also ends up closing the +/// windows in `Window`'s `Drop` impl, once a window has been closed once, it +/// stays closed. +/// +/// This ensures that no windows linger on after the event loop has exited, +/// see . +pub(super) fn notify_windows_of_exit(app: &NSApplication) { + for window in app.windows() { + window.close(); + } +} + +/// Catches panics that happen inside `f` and when a panic +/// happens, stops the `sharedApplication` +#[inline] +pub fn stop_app_on_panic R + UnwindSafe, R>( + mtm: MainThreadMarker, + panic_info: Weak, + f: F, +) -> Option { + match catch_unwind(f) { + Ok(r) => Some(r), + Err(e) => { + // It's important that we set the panic before requesting a `stop` + // because some callback are still called during the `stop` message + // and we need to know in those callbacks if the application is currently + // panicking + { + let panic_info = panic_info.upgrade().unwrap(); + panic_info.set_panic(e); + } + let app = NSApplication::sharedApplication(mtm); + stop_app_immediately(&app); + None + }, + } +} + +pub struct EventLoopProxy { + sender: mpsc::Sender, + source: CFRunLoopSourceRef, +} + +unsafe impl Send for EventLoopProxy {} +unsafe impl Sync for EventLoopProxy {} + +impl Drop for EventLoopProxy { + fn drop(&mut self) { + unsafe { + CFRelease(self.source as _); + } + } +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + EventLoopProxy::new(self.sender.clone()) + } +} + +impl EventLoopProxy { + fn new(sender: mpsc::Sender) -> Self { + unsafe { + // just wake up the eventloop + extern "C" fn event_loop_proxy_handler(_: *const c_void) {} + + // adding a Source to the main CFRunLoop lets us wake it up and + // process user events through the normal OS EventLoop mechanisms. + let rl = CFRunLoopGetMain(); + let mut context = CFRunLoopSourceContext { + version: 0, + info: ptr::null_mut(), + retain: None, + release: None, + copyDescription: None, + equal: None, + hash: None, + schedule: None, + cancel: None, + perform: event_loop_proxy_handler, + }; + let source = CFRunLoopSourceCreate(ptr::null_mut(), CFIndex::MAX - 1, &mut context); + CFRunLoopAddSource(rl, source, kCFRunLoopCommonModes); + CFRunLoopWakeUp(rl); + + EventLoopProxy { sender, source } + } + } + + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.sender.send(event).map_err(|mpsc::SendError(x)| EventLoopClosed(x))?; + unsafe { + // let the main thread know there's a new event + CFRunLoopSourceSignal(self.source); + let rl = CFRunLoopGetMain(); + CFRunLoopWakeUp(rl); + } + Ok(()) + } +} diff --git a/vendor/winit/src/platform_impl/macos/ffi.rs b/vendor/winit/src/platform_impl/macos/ffi.rs new file mode 100644 index 00000000..1daee071 --- /dev/null +++ b/vendor/winit/src/platform_impl/macos/ffi.rs @@ -0,0 +1,256 @@ +// TODO: Upstream these + +#![allow(dead_code, non_snake_case, non_upper_case_globals)] + +use std::ffi::c_void; + +use core_foundation::array::CFArrayRef; +use core_foundation::dictionary::CFDictionaryRef; +use core_foundation::string::CFStringRef; +use core_foundation::uuid::CFUUIDRef; +use core_graphics::base::CGError; +use core_graphics::display::{CGDirectDisplayID, CGDisplayConfigRef}; +use objc2::ffi::NSInteger; +use objc2::runtime::AnyObject; + +pub type CGDisplayFadeInterval = f32; +pub type CGDisplayReservationInterval = f32; +pub type CGDisplayBlendFraction = f32; + +pub const kCGDisplayBlendNormal: f32 = 0.0; +pub const kCGDisplayBlendSolidColor: f32 = 1.0; + +pub type CGDisplayFadeReservationToken = u32; +pub const kCGDisplayFadeReservationInvalidToken: CGDisplayFadeReservationToken = 0; + +pub type Boolean = u8; +pub const FALSE: Boolean = 0; +pub const TRUE: Boolean = 1; + +pub const kCGErrorSuccess: i32 = 0; +pub const kCGErrorFailure: i32 = 1000; +pub const kCGErrorIllegalArgument: i32 = 1001; +pub const kCGErrorInvalidConnection: i32 = 1002; +pub const kCGErrorInvalidContext: i32 = 1003; +pub const kCGErrorCannotComplete: i32 = 1004; +pub const kCGErrorNotImplemented: i32 = 1006; +pub const kCGErrorRangeCheck: i32 = 1007; +pub const kCGErrorTypeCheck: i32 = 1008; +pub const kCGErrorInvalidOperation: i32 = 1010; +pub const kCGErrorNoneAvailable: i32 = 1011; + +pub const IO1BitIndexedPixels: &str = "P"; +pub const IO2BitIndexedPixels: &str = "PP"; +pub const IO4BitIndexedPixels: &str = "PPPP"; +pub const IO8BitIndexedPixels: &str = "PPPPPPPP"; +pub const IO16BitDirectPixels: &str = "-RRRRRGGGGGBBBBB"; +pub const IO32BitDirectPixels: &str = "--------RRRRRRRRGGGGGGGGBBBBBBBB"; + +pub const kIO30BitDirectPixels: &str = "--RRRRRRRRRRGGGGGGGGGGBBBBBBBBBB"; +pub const kIO64BitDirectPixels: &str = "-16R16G16B16"; + +pub const kIO16BitFloatPixels: &str = "-16FR16FG16FB16"; +pub const kIO32BitFloatPixels: &str = "-32FR32FG32FB32"; + +pub const IOYUV422Pixels: &str = "Y4U2V2"; +pub const IO8BitOverlayPixels: &str = "O8"; + +pub type CGWindowLevel = i32; +pub type CGDisplayModeRef = *mut c_void; + +// `CGDisplayCreateUUIDFromDisplayID` comes from the `ColorSync` framework. +// However, that framework was only introduced "publicly" in macOS 10.13. +// +// Since we want to support older versions, we can't link to `ColorSync` +// directly. Fortunately, it has always been available as a subframework of +// `ApplicationServices`, see: +// https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/OSX_Technology_Overview/SystemFrameworks/SystemFrameworks.html#//apple_ref/doc/uid/TP40001067-CH210-BBCFFIEG +#[link(name = "ApplicationServices", kind = "framework")] +extern "C" { + pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; + + pub fn CGDisplayGetDisplayIDFromUUID(uuid: CFUUIDRef) -> CGDirectDisplayID; +} + +#[link(name = "CoreGraphics", kind = "framework")] +extern "C" { + pub fn CGRestorePermanentDisplayConfiguration(); + pub fn CGDisplayCapture(display: CGDirectDisplayID) -> CGError; + pub fn CGDisplayRelease(display: CGDirectDisplayID) -> CGError; + pub fn CGConfigureDisplayFadeEffect( + config: CGDisplayConfigRef, + fadeOutSeconds: CGDisplayFadeInterval, + fadeInSeconds: CGDisplayFadeInterval, + fadeRed: f32, + fadeGreen: f32, + fadeBlue: f32, + ) -> CGError; + pub fn CGAcquireDisplayFadeReservation( + seconds: CGDisplayReservationInterval, + token: *mut CGDisplayFadeReservationToken, + ) -> CGError; + pub fn CGDisplayFade( + token: CGDisplayFadeReservationToken, + duration: CGDisplayFadeInterval, + startBlend: CGDisplayBlendFraction, + endBlend: CGDisplayBlendFraction, + redBlend: f32, + greenBlend: f32, + blueBlend: f32, + synchronous: Boolean, + ) -> CGError; + pub fn CGReleaseDisplayFadeReservation(token: CGDisplayFadeReservationToken) -> CGError; + pub fn CGShieldingWindowLevel() -> CGWindowLevel; + pub fn CGDisplaySetDisplayMode( + display: CGDirectDisplayID, + mode: CGDisplayModeRef, + options: CFDictionaryRef, + ) -> CGError; + pub fn CGDisplayCopyAllDisplayModes( + display: CGDirectDisplayID, + options: CFDictionaryRef, + ) -> CFArrayRef; + pub fn CGDisplayModeGetPixelWidth(mode: CGDisplayModeRef) -> usize; + pub fn CGDisplayModeGetPixelHeight(mode: CGDisplayModeRef) -> usize; + pub fn CGDisplayModeGetRefreshRate(mode: CGDisplayModeRef) -> f64; + pub fn CGDisplayModeCopyPixelEncoding(mode: CGDisplayModeRef) -> CFStringRef; + pub fn CGDisplayModeRetain(mode: CGDisplayModeRef); + pub fn CGDisplayModeRelease(mode: CGDisplayModeRef); + + // Wildly used private APIs; Apple uses them for their Terminal.app. + pub fn CGSMainConnectionID() -> *mut AnyObject; + pub fn CGSSetWindowBackgroundBlurRadius( + connection_id: *mut AnyObject, + window_id: NSInteger, + radius: i64, + ) -> i32; +} + +mod core_video { + use super::*; + + #[link(name = "CoreVideo", kind = "framework")] + extern "C" {} + + // CVBase.h + + pub type CVTimeFlags = i32; // int32_t + pub const kCVTimeIsIndefinite: CVTimeFlags = 1 << 0; + + #[repr(C)] + #[derive(Debug, Clone)] + pub struct CVTime { + pub time_value: i64, // int64_t + pub time_scale: i32, // int32_t + pub flags: i32, // int32_t + } + + // CVReturn.h + + pub type CVReturn = i32; // int32_t + pub const kCVReturnSuccess: CVReturn = 0; + + // CVDisplayLink.h + + pub type CVDisplayLinkRef = *mut c_void; + + extern "C" { + pub fn CVDisplayLinkCreateWithCGDisplay( + displayID: CGDirectDisplayID, + displayLinkOut: *mut CVDisplayLinkRef, + ) -> CVReturn; + pub fn CVDisplayLinkGetNominalOutputVideoRefreshPeriod( + displayLink: CVDisplayLinkRef, + ) -> CVTime; + pub fn CVDisplayLinkRelease(displayLink: CVDisplayLinkRef); + } +} + +pub use core_video::*; +#[repr(transparent)] +pub struct TISInputSource(std::ffi::c_void); +pub type TISInputSourceRef = *mut TISInputSource; + +#[repr(transparent)] +pub struct UCKeyboardLayout(std::ffi::c_void); + +pub type OptionBits = u32; +pub type UniCharCount = std::os::raw::c_ulong; +pub type UniChar = std::os::raw::c_ushort; +pub type OSStatus = i32; + +#[allow(non_upper_case_globals)] +pub const kUCKeyActionDisplay: u16 = 3; +#[allow(non_upper_case_globals)] +pub const kUCKeyTranslateNoDeadKeysMask: OptionBits = 1; + +#[link(name = "Carbon", kind = "framework")] +extern "C" { + pub static kTISPropertyUnicodeKeyLayoutData: CFStringRef; + + #[allow(non_snake_case)] + pub fn TISGetInputSourceProperty( + inputSource: TISInputSourceRef, + propertyKey: CFStringRef, + ) -> *mut c_void; + + pub fn TISCopyCurrentKeyboardLayoutInputSource() -> TISInputSourceRef; + + pub fn LMGetKbdType() -> u8; + + #[allow(non_snake_case)] + pub fn UCKeyTranslate( + keyLayoutPtr: *const UCKeyboardLayout, + virtualKeyCode: u16, + keyAction: u16, + modifierKeyState: u32, + keyboardType: u32, + keyTranslateOptions: OptionBits, + deadKeyState: *mut u32, + maxStringLength: UniCharCount, + actualStringLength: *mut UniCharCount, + unicodeString: *mut UniChar, + ) -> OSStatus; +} + +// CGWindowLevel.h +// +// Note: There are two different things at play in this header: +// `CGWindowLevel` and `CGWindowLevelKey`. +// +// It seems like there was a push towards using "key" values instead of the +// raw window level values, and then you were supposed to use +// `CGWindowLevelForKey` to get the actual level. +// +// But the values that `NSWindowLevel` has are compiled in, and as such has +// to remain ABI compatible, so they're safe for us to hardcode as well. +#[allow(dead_code, non_upper_case_globals)] +mod window_level { + const kCGNumReservedWindowLevels: i32 = 16; + const kCGNumReservedBaseWindowLevels: i32 = 5; + + pub const kCGBaseWindowLevel: i32 = i32::MIN; + pub const kCGMinimumWindowLevel: i32 = kCGBaseWindowLevel + kCGNumReservedBaseWindowLevels; + pub const kCGMaximumWindowLevel: i32 = i32::MAX - kCGNumReservedWindowLevels; + + pub const kCGDesktopWindowLevel: i32 = kCGMinimumWindowLevel + 20; + pub const kCGDesktopIconWindowLevel: i32 = kCGDesktopWindowLevel + 20; + pub const kCGBackstopMenuLevel: i32 = -20; + pub const kCGNormalWindowLevel: i32 = 0; + pub const kCGFloatingWindowLevel: i32 = 3; + pub const kCGTornOffMenuWindowLevel: i32 = 3; + pub const kCGModalPanelWindowLevel: i32 = 8; + pub const kCGUtilityWindowLevel: i32 = 19; + pub const kCGDockWindowLevel: i32 = 20; + pub const kCGMainMenuWindowLevel: i32 = 24; + pub const kCGStatusWindowLevel: i32 = 25; + pub const kCGPopUpMenuWindowLevel: i32 = 101; + pub const kCGOverlayWindowLevel: i32 = 102; + pub const kCGHelpWindowLevel: i32 = 200; + pub const kCGDraggingWindowLevel: i32 = 500; + pub const kCGScreenSaverWindowLevel: i32 = 1000; + pub const kCGAssistiveTechHighWindowLevel: i32 = 1500; + pub const kCGCursorWindowLevel: i32 = kCGMaximumWindowLevel - 1; +} + +pub use window_level::*; diff --git a/vendor/winit/src/platform_impl/macos/menu.rs b/vendor/winit/src/platform_impl/macos/menu.rs new file mode 100644 index 00000000..cdebea0b --- /dev/null +++ b/vendor/winit/src/platform_impl/macos/menu.rs @@ -0,0 +1,107 @@ +use objc2::rc::Retained; +use objc2::runtime::Sel; +use objc2::sel; +use objc2_app_kit::{NSApplication, NSEventModifierFlags, NSMenu, NSMenuItem}; +use objc2_foundation::{ns_string, MainThreadMarker, NSProcessInfo, NSString}; + +struct KeyEquivalent<'a> { + key: &'a NSString, + masks: Option, +} + +pub fn initialize(app: &NSApplication) { + let mtm = MainThreadMarker::from(app); + let menubar = NSMenu::new(mtm); + let app_menu_item = NSMenuItem::new(mtm); + menubar.addItem(&app_menu_item); + + let app_menu = NSMenu::new(mtm); + let process_name = NSProcessInfo::processInfo().processName(); + + // About menu item + let about_item_title = ns_string!("About ").stringByAppendingString(&process_name); + let about_item = + menu_item(mtm, &about_item_title, Some(sel!(orderFrontStandardAboutPanel:)), None); + + // Services menu item + let services_menu = NSMenu::new(mtm); + let services_item = menu_item(mtm, ns_string!("Services"), None, None); + services_item.setSubmenu(Some(&services_menu)); + + // Separator menu item + let sep_first = NSMenuItem::separatorItem(mtm); + + // Hide application menu item + let hide_item_title = ns_string!("Hide ").stringByAppendingString(&process_name); + let hide_item = menu_item( + mtm, + &hide_item_title, + Some(sel!(hide:)), + Some(KeyEquivalent { key: ns_string!("h"), masks: None }), + ); + + // Hide other applications menu item + let hide_others_item_title = ns_string!("Hide Others"); + let hide_others_item = menu_item( + mtm, + hide_others_item_title, + Some(sel!(hideOtherApplications:)), + Some(KeyEquivalent { + key: ns_string!("h"), + masks: Some( + NSEventModifierFlags::NSEventModifierFlagOption + | NSEventModifierFlags::NSEventModifierFlagCommand, + ), + }), + ); + + // Show applications menu item + let show_all_item_title = ns_string!("Show All"); + let show_all_item = + menu_item(mtm, show_all_item_title, Some(sel!(unhideAllApplications:)), None); + + // Separator menu item + let sep = NSMenuItem::separatorItem(mtm); + + // Quit application menu item + let quit_item_title = ns_string!("Quit ").stringByAppendingString(&process_name); + let quit_item = menu_item( + mtm, + &quit_item_title, + Some(sel!(terminate:)), + Some(KeyEquivalent { key: ns_string!("q"), masks: None }), + ); + + app_menu.addItem(&about_item); + app_menu.addItem(&sep_first); + app_menu.addItem(&services_item); + app_menu.addItem(&hide_item); + app_menu.addItem(&hide_others_item); + app_menu.addItem(&show_all_item); + app_menu.addItem(&sep); + app_menu.addItem(&quit_item); + app_menu_item.setSubmenu(Some(&app_menu)); + + unsafe { app.setServicesMenu(Some(&services_menu)) }; + app.setMainMenu(Some(&menubar)); +} + +fn menu_item( + mtm: MainThreadMarker, + title: &NSString, + selector: Option, + key_equivalent: Option>, +) -> Retained { + let (key, masks) = match key_equivalent { + Some(ke) => (ke.key, ke.masks), + None => (ns_string!(""), None), + }; + let item = unsafe { + NSMenuItem::initWithTitle_action_keyEquivalent(mtm.alloc(), title, selector, key) + }; + if let Some(masks) = masks { + item.setKeyEquivalentModifierMask(masks) + } + + item +} diff --git a/vendor/winit/src/platform_impl/macos/mod.rs b/vendor/winit/src/platform_impl/macos/mod.rs new file mode 100644 index 00000000..1b427a87 --- /dev/null +++ b/vendor/winit/src/platform_impl/macos/mod.rs @@ -0,0 +1,61 @@ +#[macro_use] +mod util; + +mod app; +mod app_state; +mod cursor; +mod event; +mod event_handler; +mod event_loop; +mod ffi; +mod menu; +mod monitor; +mod observer; +mod view; +mod window; +mod window_delegate; + +use std::fmt; + +pub(crate) use self::event::{physicalkey_to_scancode, scancode_to_physicalkey, KeyEventExtra}; +pub(crate) use self::event_loop::{ + ActiveEventLoop, EventLoop, EventLoopProxy, OwnedDisplayHandle, + PlatformSpecificEventLoopAttributes, +}; +pub(crate) use self::monitor::{MonitorHandle, VideoModeHandle}; +pub(crate) use self::window::WindowId; +pub(crate) use self::window_delegate::PlatformSpecificWindowAttributes; +use crate::event::DeviceId as RootDeviceId; + +pub(crate) use self::cursor::CustomCursor as PlatformCustomCursor; +pub(crate) use self::window::Window; +pub(crate) use crate::cursor::OnlyCursorImageSource as PlatformCustomCursorSource; +pub(crate) use crate::icon::NoIcon as PlatformIcon; +pub(crate) use crate::platform_impl::Fullscreen; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId; + +impl DeviceId { + pub const fn dummy() -> Self { + DeviceId + } +} + +// Constant device ID; to be removed when if backend is updated to report real device IDs. +pub(crate) const DEVICE_ID: RootDeviceId = RootDeviceId(DeviceId); + +#[derive(Debug)] +pub enum OsError { + CGError(core_graphics::base::CGError), + CreationError(&'static str), +} + +impl fmt::Display for OsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + OsError::CGError(e) => f.pad(&format!("CGError {e}")), + OsError::CreationError(e) => f.pad(e), + } + } +} diff --git a/vendor/winit/src/platform_impl/macos/monitor.rs b/vendor/winit/src/platform_impl/macos/monitor.rs new file mode 100644 index 00000000..44e6316b --- /dev/null +++ b/vendor/winit/src/platform_impl/macos/monitor.rs @@ -0,0 +1,409 @@ +#![allow(clippy::unnecessary_cast)] + +use std::collections::VecDeque; +use std::fmt; + +use core_foundation::array::{CFArrayGetCount, CFArrayGetValueAtIndex}; +use core_foundation::base::{CFRelease, TCFType}; +use core_foundation::string::CFString; +use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUID}; +use core_graphics::display::{ + CGDirectDisplayID, CGDisplay, CGDisplayBounds, CGDisplayCopyDisplayMode, +}; +use objc2::rc::Retained; +use objc2::runtime::AnyObject; +use objc2_app_kit::NSScreen; +use objc2_foundation::{ns_string, run_on_main, MainThreadMarker, NSNumber, NSPoint, NSRect}; +use tracing::warn; + +use super::ffi; +use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; + +#[derive(Clone)] +pub struct VideoModeHandle { + size: PhysicalSize, + bit_depth: u16, + refresh_rate_millihertz: u32, + pub(crate) monitor: MonitorHandle, + pub(crate) native_mode: NativeDisplayMode, +} + +impl PartialEq for VideoModeHandle { + fn eq(&self, other: &Self) -> bool { + self.size == other.size + && self.bit_depth == other.bit_depth + && self.refresh_rate_millihertz == other.refresh_rate_millihertz + && self.monitor == other.monitor + } +} + +impl Eq for VideoModeHandle {} + +impl std::hash::Hash for VideoModeHandle { + fn hash(&self, state: &mut H) { + self.size.hash(state); + self.bit_depth.hash(state); + self.refresh_rate_millihertz.hash(state); + self.monitor.hash(state); + } +} + +impl std::fmt::Debug for VideoModeHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VideoModeHandle") + .field("size", &self.size) + .field("bit_depth", &self.bit_depth) + .field("refresh_rate_millihertz", &self.refresh_rate_millihertz) + .field("monitor", &self.monitor) + .finish() + } +} + +pub struct NativeDisplayMode(pub ffi::CGDisplayModeRef); + +unsafe impl Send for NativeDisplayMode {} +unsafe impl Sync for NativeDisplayMode {} + +impl Drop for NativeDisplayMode { + fn drop(&mut self) { + unsafe { + ffi::CGDisplayModeRelease(self.0); + } + } +} + +impl Clone for NativeDisplayMode { + fn clone(&self) -> Self { + unsafe { + ffi::CGDisplayModeRetain(self.0); + } + NativeDisplayMode(self.0) + } +} + +impl VideoModeHandle { + pub fn size(&self) -> PhysicalSize { + self.size + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone() + } +} + +/// `CGDirectDisplayID` is documented as: +/// > a framebuffer, a color correction (gamma) table, and possibly an attached monitor. +/// +/// That is, it doesn't actually represent the monitor itself. Instead, we use the UUID of the +/// monitor, as retrieved from `CGDisplayCreateUUIDFromDisplayID` (this makes the monitor ID stable, +/// even across reboots and video mode changes). +/// +/// NOTE: I'd be perfectly valid to store `[u8; 16]` in here instead, we only store `CFUUID` to +/// avoid having to re-create it when we want to fetch the display ID. +#[derive(Clone)] +pub struct MonitorHandle(CFUUID); + +// SAFETY: CFUUID is immutable. +// FIXME(madsmtm): Upstream this into `objc2-core-foundation`. +unsafe impl Send for MonitorHandle {} +unsafe impl Sync for MonitorHandle {} + +type MonitorUuid = [u8; 16]; + +impl MonitorHandle { + /// Internal comparisons of [`MonitorHandle`]s are done first requesting a UUID for the handle. + fn uuid(&self) -> MonitorUuid { + let uuid = unsafe { CFUUIDGetUUIDBytes(self.0.as_concrete_TypeRef()) }; + MonitorUuid::from([ + uuid.byte0, + uuid.byte1, + uuid.byte2, + uuid.byte3, + uuid.byte4, + uuid.byte5, + uuid.byte6, + uuid.byte7, + uuid.byte8, + uuid.byte9, + uuid.byte10, + uuid.byte11, + uuid.byte12, + uuid.byte13, + uuid.byte14, + uuid.byte15, + ]) + } + + fn display_id(&self) -> CGDirectDisplayID { + unsafe { ffi::CGDisplayGetDisplayIDFromUUID(self.0.as_concrete_TypeRef()) } + } + + #[track_caller] + pub(crate) fn new(display_id: CGDirectDisplayID) -> Option { + // kCGNullDirectDisplay + if display_id == 0 { + // `CGDisplayCreateUUIDFromDisplayID` checks kCGNullDirectDisplay internally. + warn!("constructing monitor from invalid display ID 0; falling back to main monitor"); + } + let ptr = unsafe { ffi::CGDisplayCreateUUIDFromDisplayID(display_id) }; + if ptr.is_null() { + return None; + } + Some(Self(unsafe { CFUUID::wrap_under_create_rule(ptr) })) + } +} + +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + self.uuid() == other.uuid() + } +} + +impl Eq for MonitorHandle {} + +impl PartialOrd for MonitorHandle { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for MonitorHandle { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.uuid().cmp(&other.uuid()) + } +} + +impl std::hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + self.uuid().hash(state); + } +} + +pub fn available_monitors() -> VecDeque { + if let Ok(displays) = CGDisplay::active_displays() { + let mut monitors = VecDeque::with_capacity(displays.len()); + for display in displays { + // Display ID just fetched from `CGGetActiveDisplayList`, should be fine to unwrap. + monitors.push_back(MonitorHandle::new(display).expect("invalid display ID")); + } + monitors + } else { + VecDeque::with_capacity(0) + } +} + +pub fn primary_monitor() -> MonitorHandle { + // Display ID just fetched from `CGMainDisplayID`, should be fine to unwrap. + MonitorHandle::new(CGDisplay::main().id).expect("invalid display ID") +} + +impl fmt::Debug for MonitorHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MonitorHandle") + .field("name", &self.name()) + .field("native_identifier", &self.native_identifier()) + .field("size", &self.size()) + .field("position", &self.position()) + .field("scale_factor", &self.scale_factor()) + .field("refresh_rate_millihertz", &self.refresh_rate_millihertz()) + .finish_non_exhaustive() + } +} + +impl MonitorHandle { + // TODO: Be smarter about this: + // + pub fn name(&self) -> Option { + let screen_num = CGDisplay::new(self.display_id()).model_number(); + Some(format!("Monitor #{screen_num}")) + } + + #[inline] + pub fn native_identifier(&self) -> u32 { + self.display_id() + } + + pub fn size(&self) -> PhysicalSize { + let display = CGDisplay::new(self.display_id()); + let height = display.pixels_high(); + let width = display.pixels_wide(); + PhysicalSize::from_logical::<_, f64>((width as f64, height as f64), self.scale_factor()) + } + + #[inline] + pub fn position(&self) -> PhysicalPosition { + // This is already in screen coordinates. If we were using `NSScreen`, + // then a conversion would've been needed: + // flip_window_screen_coordinates(self.ns_screen(mtm)?.frame()) + let bounds = unsafe { CGDisplayBounds(self.native_identifier()) }; + let position = LogicalPosition::new(bounds.origin.x, bounds.origin.y); + position.to_physical(self.scale_factor()) + } + + pub fn scale_factor(&self) -> f64 { + run_on_main(|mtm| { + match self.ns_screen(mtm) { + Some(screen) => screen.backingScaleFactor() as f64, + None => 1.0, // default to 1.0 when we can't find the screen + } + }) + } + + pub fn refresh_rate_millihertz(&self) -> Option { + unsafe { + let current_display_mode = + NativeDisplayMode(CGDisplayCopyDisplayMode(self.display_id()) as _); + let refresh_rate = ffi::CGDisplayModeGetRefreshRate(current_display_mode.0); + if refresh_rate > 0.0 { + return Some((refresh_rate * 1000.0).round() as u32); + } + + let mut display_link = std::ptr::null_mut(); + if ffi::CVDisplayLinkCreateWithCGDisplay(self.display_id(), &mut display_link) + != ffi::kCVReturnSuccess + { + return None; + } + let time = ffi::CVDisplayLinkGetNominalOutputVideoRefreshPeriod(display_link); + ffi::CVDisplayLinkRelease(display_link); + + // This value is indefinite if an invalid display link was specified + if time.flags & ffi::kCVTimeIsIndefinite != 0 { + return None; + } + + (time.time_scale as i64).checked_div(time.time_value).map(|v| (v * 1000) as u32) + } + } + + pub fn video_modes(&self) -> impl Iterator { + let refresh_rate_millihertz = self.refresh_rate_millihertz().unwrap_or(0); + let monitor = self.clone(); + + unsafe { + let modes = { + let array = ffi::CGDisplayCopyAllDisplayModes(self.display_id(), std::ptr::null()); + if array.is_null() { + // Occasionally, certain CalDigit Thunderbolt Hubs report a spurious monitor + // during sleep/wake/cycling monitors. It tends to have null + // or 1 video mode only. See . + warn!(monitor = ?self, "failed to get a list of display modes"); + Vec::new() + } else { + let array_count = CFArrayGetCount(array); + let modes: Vec<_> = (0..array_count) + .map(move |i| { + let mode = CFArrayGetValueAtIndex(array, i) as *mut _; + ffi::CGDisplayModeRetain(mode); + mode + }) + .collect(); + CFRelease(array as *const _); + modes + } + }; + + modes.into_iter().map(move |mode| { + let cg_refresh_rate_hertz = ffi::CGDisplayModeGetRefreshRate(mode); + + // CGDisplayModeGetRefreshRate returns 0.0 for any display that + // isn't a CRT + let refresh_rate_millihertz = if cg_refresh_rate_hertz > 0.0 { + (cg_refresh_rate_hertz * 1000.0).round() as u32 + } else { + refresh_rate_millihertz + }; + + let pixel_encoding = + CFString::wrap_under_create_rule(ffi::CGDisplayModeCopyPixelEncoding(mode)) + .to_string(); + let bit_depth = if pixel_encoding.eq_ignore_ascii_case(ffi::IO32BitDirectPixels) { + 32 + } else if pixel_encoding.eq_ignore_ascii_case(ffi::IO16BitDirectPixels) { + 16 + } else if pixel_encoding.eq_ignore_ascii_case(ffi::kIO30BitDirectPixels) { + 30 + } else { + unimplemented!() + }; + + VideoModeHandle { + size: PhysicalSize::new( + ffi::CGDisplayModeGetPixelWidth(mode) as u32, + ffi::CGDisplayModeGetPixelHeight(mode) as u32, + ), + refresh_rate_millihertz, + bit_depth, + monitor: monitor.clone(), + native_mode: NativeDisplayMode(mode), + } + }) + } + } + + pub(crate) fn ns_screen(&self, mtm: MainThreadMarker) -> Option> { + let uuid = self.uuid(); + NSScreen::screens(mtm).into_iter().find(|screen| { + let other_native_id = get_display_id(screen); + if let Some(other) = MonitorHandle::new(other_native_id) { + uuid == other.uuid() + } else { + // Display ID was just fetched from live NSScreen, but can still result in `None` + // with certain Thunderbolt docked monitors. + warn!(other_native_id, "comparing against screen with invalid display ID"); + false + } + }) + } +} + +pub(crate) fn get_display_id(screen: &NSScreen) -> u32 { + let key = ns_string!("NSScreenNumber"); + + objc2::rc::autoreleasepool(|_| { + let device_description = screen.deviceDescription(); + + // Retrieve the CGDirectDisplayID associated with this screen + // + // SAFETY: The value from @"NSScreenNumber" in deviceDescription is guaranteed + // to be an NSNumber. See documentation for `deviceDescription` for details: + // + let obj = device_description + .get(key) + .expect("failed getting screen display id from device description"); + let obj: *const AnyObject = obj; + let obj: *const NSNumber = obj.cast(); + let obj: &NSNumber = unsafe { &*obj }; + + obj.as_u32() + }) +} + +/// Core graphics screen coordinates are relative to the top-left corner of +/// the so-called "main" display, with y increasing downwards - which is +/// exactly what we want in Winit. +/// +/// However, `NSWindow` and `NSScreen` changes these coordinates to: +/// 1. Be relative to the bottom-left corner of the "main" screen. +/// 2. Be relative to the bottom-left corner of the window/screen itself. +/// 3. Have y increasing upwards. +/// +/// This conversion happens to be symmetric, so we only need this one function +/// to convert between the two coordinate systems. +pub(crate) fn flip_window_screen_coordinates(frame: NSRect) -> NSPoint { + // It is intentional that we use `CGMainDisplayID` (as opposed to + // `NSScreen::mainScreen`), because that's what the screen coordinates + // are relative to, no matter which display the window is currently on. + let main_screen_height = CGDisplay::main().bounds().size.height; + + let y = main_screen_height - frame.size.height - frame.origin.y; + NSPoint::new(frame.origin.x, y) +} diff --git a/vendor/winit/src/platform_impl/macos/observer.rs b/vendor/winit/src/platform_impl/macos/observer.rs new file mode 100644 index 00000000..83398030 --- /dev/null +++ b/vendor/winit/src/platform_impl/macos/observer.rs @@ -0,0 +1,312 @@ +//! Utilities for working with `CFRunLoop`. +//! +//! See Apple's documentation on Run Loops for details: +//! +use std::cell::Cell; +use std::ffi::c_void; +use std::panic::{AssertUnwindSafe, UnwindSafe}; +use std::ptr; +use std::rc::Weak; +use std::time::Instant; + +use block2::Block; +use core_foundation::base::{CFIndex, CFOptionFlags, CFRelease, CFTypeRef}; +use core_foundation::date::CFAbsoluteTimeGetCurrent; +use core_foundation::runloop::{ + kCFRunLoopAfterWaiting, kCFRunLoopBeforeWaiting, kCFRunLoopCommonModes, kCFRunLoopDefaultMode, + kCFRunLoopExit, CFRunLoopActivity, CFRunLoopAddObserver, CFRunLoopAddTimer, CFRunLoopGetMain, + CFRunLoopObserverCallBack, CFRunLoopObserverContext, CFRunLoopObserverCreate, + CFRunLoopObserverRef, CFRunLoopRef, CFRunLoopTimerCreate, CFRunLoopTimerInvalidate, + CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate, CFRunLoopWakeUp, +}; +use objc2_foundation::MainThreadMarker; +use tracing::error; + +use super::app_state::ApplicationDelegate; +use super::event_loop::{stop_app_on_panic, PanicInfo}; +use super::ffi; + +unsafe fn control_flow_handler(panic_info: *mut c_void, f: F) +where + F: FnOnce(Weak) + UnwindSafe, +{ + let info_from_raw = unsafe { Weak::from_raw(panic_info as *mut PanicInfo) }; + // Asserting unwind safety on this type should be fine because `PanicInfo` is + // `RefUnwindSafe` and `Rc` is `UnwindSafe` if `T` is `RefUnwindSafe`. + let panic_info = AssertUnwindSafe(Weak::clone(&info_from_raw)); + // `from_raw` takes ownership of the data behind the pointer. + // But if this scope takes ownership of the weak pointer, then + // the weak pointer will get free'd at the end of the scope. + // However we want to keep that weak reference around after the function. + std::mem::forget(info_from_raw); + + let mtm = MainThreadMarker::new().unwrap(); + stop_app_on_panic(mtm, Weak::clone(&panic_info), move || { + let _ = &panic_info; + f(panic_info.0) + }); +} + +// begin is queued with the highest priority to ensure it is processed before other observers +extern "C" fn control_flow_begin_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + panic_info: *mut c_void, +) { + unsafe { + control_flow_handler(panic_info, |panic_info| { + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopAfterWaiting => { + // trace!("Triggered `CFRunLoopAfterWaiting`"); + ApplicationDelegate::get(MainThreadMarker::new().unwrap()).wakeup(panic_info); + // trace!("Completed `CFRunLoopAfterWaiting`"); + }, + _ => unreachable!(), + } + }); + } +} + +// end is queued with the lowest priority to ensure it is processed after other observers +// without that, LoopExiting would get sent after AboutToWait +extern "C" fn control_flow_end_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + panic_info: *mut c_void, +) { + unsafe { + control_flow_handler(panic_info, |panic_info| { + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopBeforeWaiting => { + // trace!("Triggered `CFRunLoopBeforeWaiting`"); + ApplicationDelegate::get(MainThreadMarker::new().unwrap()).cleared(panic_info); + // trace!("Completed `CFRunLoopBeforeWaiting`"); + }, + kCFRunLoopExit => (), // unimplemented!(), // not expected to ever happen + _ => unreachable!(), + } + }); + } +} + +#[derive(Debug)] +pub struct RunLoop(CFRunLoopRef); + +impl Default for RunLoop { + fn default() -> Self { + Self(ptr::null_mut()) + } +} + +impl RunLoop { + pub fn main(mtm: MainThreadMarker) -> Self { + // SAFETY: We have a MainThreadMarker here, which means we know we're on the main thread, so + // scheduling (and scheduling a non-`Send` block) to that thread is allowed. + let _ = mtm; + RunLoop(unsafe { CFRunLoopGetMain() }) + } + + pub fn wakeup(&self) { + unsafe { CFRunLoopWakeUp(self.0) } + } + + unsafe fn add_observer( + &self, + flags: CFOptionFlags, + priority: CFIndex, + handler: CFRunLoopObserverCallBack, + context: *mut CFRunLoopObserverContext, + ) { + let observer = unsafe { + CFRunLoopObserverCreate( + ptr::null_mut(), + flags, + ffi::TRUE, // Indicates we want this to run repeatedly + priority, // The lower the value, the sooner this will run + handler, + context, + ) + }; + unsafe { CFRunLoopAddObserver(self.0, observer, kCFRunLoopCommonModes) }; + } + + /// Submit a closure to run on the main thread as the next step in the run loop, before other + /// event sources are processed. + /// + /// This is used for running event handlers, as those are not allowed to run re-entrantly. + /// + /// # Implementation + /// + /// This queuing could be implemented in the following several ways with subtle differences in + /// timing. This list is sorted in rough order in which they are run: + /// + /// 1. Using `CFRunLoopPerformBlock` or `-[NSRunLoop performBlock:]`. + /// + /// 2. Using `-[NSObject performSelectorOnMainThread:withObject:waitUntilDone:]` or wrapping the + /// event in `NSEvent` and posting that to `-[NSApplication postEvent:atStart:]` (both + /// creates a custom `CFRunLoopSource`, and signals that to wake up the main event loop). + /// + /// a. `atStart = true`. + /// + /// b. `atStart = false`. + /// + /// 3. `dispatch_async` or `dispatch_async_f`. Note that this may appear before 2b, it does not + /// respect the ordering that runloop events have. + /// + /// We choose the first one, both for ease-of-implementation, but mostly for consistency, as we + /// want the event to be queued in a way that preserves the order the events originally arrived + /// in. + /// + /// As an example, let's assume that we receive two events from the user, a mouse click which we + /// handled by queuing it, and a window resize which we handled immediately. If we allowed + /// AppKit to choose the ordering when queuing the mouse event, it might get put in the back of + /// the queue, and the events would appear out of order to the user of Winit. So we must instead + /// put the event at the very front of the queue, to be handled as soon as possible after + /// handling whatever event it's currently handling. + pub fn queue_closure(&self, closure: impl FnOnce() + 'static) { + extern "C" { + fn CFRunLoopPerformBlock(rl: CFRunLoopRef, mode: CFTypeRef, block: &Block); + } + + // Convert `FnOnce()` to `Block`. + let closure = Cell::new(Some(closure)); + let block = block2::RcBlock::new(move || { + if let Some(closure) = closure.take() { + closure() + } else { + error!("tried to execute queued closure on main thread twice"); + } + }); + + // There are a few common modes (`kCFRunLoopCommonModes`) defined by Cocoa: + // - `NSDefaultRunLoopMode`, alias of `kCFRunLoopDefaultMode`. + // - `NSEventTrackingRunLoopMode`, used when mouse-dragging and live-resizing a window. + // - `NSModalPanelRunLoopMode`, used when running a modal inside the Winit event loop. + // - `NSConnectionReplyMode`: TODO. + // + // We only want to run event handlers in the default mode, as we support running a blocking + // modal inside a Winit event handler (see [#1779]) which outrules the modal panel mode, and + // resizing such panel window enters the event tracking run loop mode, so we can't directly + // trigger events inside that mode either. + // + // Any events that are queued while running a modal or when live-resizing will instead wait, + // and be delivered to the application afterwards. + // + // [#1779]: https://github.com/rust-windowing/winit/issues/1779 + let mode = unsafe { kCFRunLoopDefaultMode as CFTypeRef }; + + // SAFETY: The runloop is valid, the mode is a `CFStringRef`, and the block is `'static`. + unsafe { CFRunLoopPerformBlock(self.0, mode, &block) } + } +} + +pub fn setup_control_flow_observers(mtm: MainThreadMarker, panic_info: Weak) { + let run_loop = RunLoop::main(mtm); + unsafe { + let mut context = CFRunLoopObserverContext { + info: Weak::into_raw(panic_info) as *mut _, + version: 0, + retain: None, + release: None, + copyDescription: None, + }; + run_loop.add_observer( + kCFRunLoopAfterWaiting, + CFIndex::MIN, + control_flow_begin_handler, + &mut context as *mut _, + ); + run_loop.add_observer( + kCFRunLoopExit | kCFRunLoopBeforeWaiting, + CFIndex::MAX, + control_flow_end_handler, + &mut context as *mut _, + ); + } +} + +#[derive(Debug)] +pub struct EventLoopWaker { + timer: CFRunLoopTimerRef, + + /// An arbitrary instant in the past, that will trigger an immediate wake + /// We save this as the `next_fire_date` for consistency so we can + /// easily check if the next_fire_date needs updating. + start_instant: Instant, + + /// This is what the `NextFireDate` has been set to. + /// `None` corresponds to `waker.stop()` and `start_instant` is used + /// for `waker.start()` + next_fire_date: Option, +} + +impl Drop for EventLoopWaker { + fn drop(&mut self) { + unsafe { + CFRunLoopTimerInvalidate(self.timer); + CFRelease(self.timer as _); + } + } +} + +impl EventLoopWaker { + pub(crate) fn new() -> Self { + extern "C" fn wakeup_main_loop(_timer: CFRunLoopTimerRef, _info: *mut c_void) {} + unsafe { + // Create a timer with a 0.1µs interval (1ns does not work) to mimic polling. + // It is initially setup with a first fire time really far into the + // future, but that gets changed to fire immediately in did_finish_launching + let timer = CFRunLoopTimerCreate( + ptr::null_mut(), + f64::MAX, + 0.000_000_1, + 0, + 0, + wakeup_main_loop, + ptr::null_mut(), + ); + CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes); + Self { timer, start_instant: Instant::now(), next_fire_date: None } + } + } + + pub fn stop(&mut self) { + if self.next_fire_date.is_some() { + self.next_fire_date = None; + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MAX) } + } + } + + pub fn start(&mut self) { + if self.next_fire_date != Some(self.start_instant) { + self.next_fire_date = Some(self.start_instant); + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MIN) } + } + } + + pub fn start_at(&mut self, instant: Option) { + let now = Instant::now(); + match instant { + Some(instant) if now >= instant => { + self.start(); + }, + Some(instant) => { + if self.next_fire_date != Some(instant) { + self.next_fire_date = Some(instant); + unsafe { + let current = CFAbsoluteTimeGetCurrent(); + let duration = instant - now; + let fsecs = duration.subsec_nanos() as f64 / 1_000_000_000.0 + + duration.as_secs() as f64; + CFRunLoopTimerSetNextFireDate(self.timer, current + fsecs) + } + } + }, + None => { + self.stop(); + }, + } + } +} diff --git a/vendor/winit/src/platform_impl/macos/util.rs b/vendor/winit/src/platform_impl/macos/util.rs new file mode 100644 index 00000000..8110573a --- /dev/null +++ b/vendor/winit/src/platform_impl/macos/util.rs @@ -0,0 +1,27 @@ +use tracing::trace; + +macro_rules! trace_scope { + ($s:literal) => { + let _crate = $crate::platform_impl::platform::util::TraceGuard::new(module_path!(), $s); + }; +} + +pub(crate) struct TraceGuard { + module_path: &'static str, + called_from_fn: &'static str, +} + +impl TraceGuard { + #[inline] + pub(crate) fn new(module_path: &'static str, called_from_fn: &'static str) -> Self { + trace!(target = module_path, "Triggered `{}`", called_from_fn); + Self { module_path, called_from_fn } + } +} + +impl Drop for TraceGuard { + #[inline] + fn drop(&mut self) { + trace!(target = self.module_path, "Completed `{}`", self.called_from_fn); + } +} diff --git a/vendor/winit/src/platform_impl/macos/view.rs b/vendor/winit/src/platform_impl/macos/view.rs new file mode 100644 index 00000000..c7ca5bc6 --- /dev/null +++ b/vendor/winit/src/platform_impl/macos/view.rs @@ -0,0 +1,1142 @@ +#![allow(clippy::unnecessary_cast)] +use std::cell::{Cell, RefCell}; +use std::collections::{HashMap, VecDeque}; +use std::ptr; + +use objc2::rc::{Retained, WeakId}; +use objc2::runtime::{AnyObject, Sel}; +use objc2::{declare_class, msg_send_id, mutability, sel, ClassType, DeclaredClass}; +use objc2_app_kit::{ + NSApplication, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient, + NSTrackingRectTag, NSView, NSViewFrameDidChangeNotification, +}; +use objc2_foundation::{ + MainThreadMarker, NSArray, NSAttributedString, NSAttributedStringKey, NSCopying, + NSMutableAttributedString, NSNotFound, NSNotificationCenter, NSObject, NSObjectProtocol, + NSPoint, NSRange, NSRect, NSSize, NSString, NSUInteger, +}; + +use super::app_state::ApplicationDelegate; +use super::cursor::{default_cursor, invisible_cursor}; +use super::event::{ + code_to_key, code_to_location, create_key_event, event_mods, lalt_pressed, ralt_pressed, + scancode_to_physicalkey, KeyEventExtra, +}; +use super::window::WinitWindow; +use super::DEVICE_ID; +use crate::dpi::{LogicalPosition, LogicalSize}; +use crate::event::{ + DeviceEvent, ElementState, Ime, KeyEvent, Modifiers, MouseButton, MouseScrollDelta, TouchPhase, + WindowEvent, +}; +use crate::keyboard::{Key, KeyCode, KeyLocation, ModifiersState, NamedKey}; +use crate::platform::macos::OptionAsAlt; + +#[derive(Debug)] +struct CursorState { + visible: bool, + cursor: Retained, +} + +impl Default for CursorState { + fn default() -> Self { + Self { visible: true, cursor: default_cursor() } + } +} + +#[derive(Debug, Eq, PartialEq, Clone, Copy, Default)] +enum ImeState { + #[default] + /// The IME events are disabled, so only `ReceivedCharacter` is being sent to the user. + Disabled, + + /// The ground state of enabled IME input. It means that both Preedit and regular keyboard + /// input could be start from it. + Ground, + + /// The IME is in preedit. + Preedit, + + /// The text was just committed, so the next input from the keyboard must be ignored. + Committed, +} + +bitflags::bitflags! { + #[derive(Debug, Clone, Copy, PartialEq)] + struct ModLocationMask: u8 { + const LEFT = 0b0001; + const RIGHT = 0b0010; + } +} +impl ModLocationMask { + fn from_location(loc: KeyLocation) -> ModLocationMask { + match loc { + KeyLocation::Left => ModLocationMask::LEFT, + KeyLocation::Right => ModLocationMask::RIGHT, + _ => unreachable!(), + } + } +} + +fn key_to_modifier(key: &Key) -> Option { + match key { + Key::Named(NamedKey::Alt) => Some(ModifiersState::ALT), + Key::Named(NamedKey::Control) => Some(ModifiersState::CONTROL), + Key::Named(NamedKey::Super) => Some(ModifiersState::SUPER), + Key::Named(NamedKey::Shift) => Some(ModifiersState::SHIFT), + _ => None, + } +} + +fn get_right_modifier_code(key: &Key) -> KeyCode { + match key { + Key::Named(NamedKey::Alt) => KeyCode::AltRight, + Key::Named(NamedKey::Control) => KeyCode::ControlRight, + Key::Named(NamedKey::Shift) => KeyCode::ShiftRight, + Key::Named(NamedKey::Super) => KeyCode::SuperRight, + _ => unreachable!(), + } +} + +fn get_left_modifier_code(key: &Key) -> KeyCode { + match key { + Key::Named(NamedKey::Alt) => KeyCode::AltLeft, + Key::Named(NamedKey::Control) => KeyCode::ControlLeft, + Key::Named(NamedKey::Shift) => KeyCode::ShiftLeft, + Key::Named(NamedKey::Super) => KeyCode::SuperLeft, + _ => unreachable!(), + } +} + +#[derive(Debug)] +pub struct ViewState { + /// Strong reference to the global application state. + app_delegate: Retained, + + cursor_state: RefCell, + ime_position: Cell, + ime_size: Cell, + modifiers: Cell, + phys_modifiers: RefCell>, + tracking_rect: Cell>, + ime_state: Cell, + input_source: RefCell, + + /// True iff the application wants IME events. + /// + /// Can be set using `set_ime_allowed` + ime_allowed: Cell, + + /// True if the current key event should be forwarded + /// to the application, even during IME + forward_key_to_app: Cell, + + marked_text: RefCell>, + accepts_first_mouse: bool, + + // Weak reference because the window keeps a strong reference to the view + _ns_window: WeakId, + + /// The state of the `Option` as `Alt`. + option_as_alt: Cell, +} + +declare_class!( + pub(super) struct WinitView; + + unsafe impl ClassType for WinitView { + #[inherits(NSResponder, NSObject)] + type Super = NSView; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitView"; + } + + impl DeclaredClass for WinitView { + type Ivars = ViewState; + } + + unsafe impl WinitView { + #[method(isFlipped)] + fn is_flipped(&self) -> bool { + // `winit` uses the upper-left corner as the origin. + true + } + + #[method(viewDidMoveToWindow)] + fn view_did_move_to_window(&self) { + trace_scope!("viewDidMoveToWindow"); + if let Some(tracking_rect) = self.ivars().tracking_rect.take() { + self.removeTrackingRect(tracking_rect); + } + + let rect = self.frame(); + let tracking_rect = unsafe { + self.addTrackingRect_owner_userData_assumeInside(rect, self, ptr::null_mut(), false) + }; + assert_ne!(tracking_rect, 0, "failed adding tracking rect"); + self.ivars().tracking_rect.set(Some(tracking_rect)); + } + + #[method(frameDidChange:)] + fn frame_did_change(&self, _event: &NSEvent) { + trace_scope!("frameDidChange:"); + if let Some(tracking_rect) = self.ivars().tracking_rect.take() { + self.removeTrackingRect(tracking_rect); + } + + let rect = self.frame(); + let tracking_rect = unsafe { + self.addTrackingRect_owner_userData_assumeInside(rect, self, ptr::null_mut(), false) + }; + assert_ne!(tracking_rect, 0, "failed adding tracking rect"); + self.ivars().tracking_rect.set(Some(tracking_rect)); + + // Emit resize event here rather than from windowDidResize because: + // 1. When a new window is created as a tab, the frame size may change without a window resize occurring. + // 2. Even when a window resize does occur on a new tabbed window, it contains the wrong size (includes tab height). + let logical_size = LogicalSize::new(rect.size.width as f64, rect.size.height as f64); + let size = logical_size.to_physical::(self.scale_factor()); + self.queue_event(WindowEvent::Resized(size)); + } + + #[method(drawRect:)] + fn draw_rect(&self, _rect: NSRect) { + trace_scope!("drawRect:"); + + // It's a workaround for https://github.com/rust-windowing/winit/issues/2640, don't replace with `self.window_id()`. + if let Some(window) = self.ivars()._ns_window.load() { + self.ivars().app_delegate.handle_redraw(window.id()); + } + + // This is a direct subclass of NSView, no need to call superclass' drawRect: + } + + #[method(acceptsFirstResponder)] + fn accepts_first_responder(&self) -> bool { + trace_scope!("acceptsFirstResponder"); + true + } + + // This is necessary to prevent a beefy terminal error on MacBook Pros: + // IMKInputSession [0x7fc573576ff0 presentFunctionRowItemTextInputViewWithEndpoint:completionHandler:] : [self textInputContext]=0x7fc573558e10 *NO* NSRemoteViewController to client, NSError=Error Domain=NSCocoaErrorDomain Code=4099 "The connection from pid 0 was invalidated from this process." UserInfo={NSDebugDescription=The connection from pid 0 was invalidated from this process.}, com.apple.inputmethod.EmojiFunctionRowItem + // TODO: Add an API extension for using `NSTouchBar` + #[method_id(touchBar)] + fn touch_bar(&self) -> Option> { + trace_scope!("touchBar"); + None + } + + #[method(resetCursorRects)] + fn reset_cursor_rects(&self) { + trace_scope!("resetCursorRects"); + let bounds = self.bounds(); + let cursor_state = self.ivars().cursor_state.borrow(); + // We correctly invoke `addCursorRect` only from inside `resetCursorRects` + if cursor_state.visible { + self.addCursorRect_cursor(bounds, &cursor_state.cursor); + } else { + self.addCursorRect_cursor(bounds, &invisible_cursor()); + } + } + } + + unsafe impl NSTextInputClient for WinitView { + #[method(hasMarkedText)] + fn has_marked_text(&self) -> bool { + trace_scope!("hasMarkedText"); + self.ivars().marked_text.borrow().length() > 0 + } + + #[method(markedRange)] + fn marked_range(&self) -> NSRange { + trace_scope!("markedRange"); + let length = self.ivars().marked_text.borrow().length(); + if length > 0 { + NSRange::new(0, length) + } else { + // Documented to return `{NSNotFound, 0}` if there is no marked range. + NSRange::new(NSNotFound as NSUInteger, 0) + } + } + + #[method(selectedRange)] + fn selected_range(&self) -> NSRange { + trace_scope!("selectedRange"); + // Documented to return `{NSNotFound, 0}` if there is no selection. + NSRange::new(NSNotFound as NSUInteger, 0) + } + + #[method(setMarkedText:selectedRange:replacementRange:)] + fn set_marked_text( + &self, + string: &NSObject, + selected_range: NSRange, + _replacement_range: NSRange, + ) { + // TODO: Use _replacement_range, requires changing the event to report surrounding text. + trace_scope!("setMarkedText:selectedRange:replacementRange:"); + + // SAFETY: This method is guaranteed to get either a `NSString` or a `NSAttributedString`. + let (marked_text, string) = if string.is_kind_of::() { + let string: *const NSObject = string; + let string: *const NSAttributedString = string.cast(); + let string = unsafe { &*string }; + ( + NSMutableAttributedString::from_attributed_nsstring(string), + string.string(), + ) + } else { + let string: *const NSObject = string; + let string: *const NSString = string.cast(); + let string = unsafe { &*string }; + ( + NSMutableAttributedString::from_nsstring(string), + string.copy(), + ) + }; + + // Update marked text. + *self.ivars().marked_text.borrow_mut() = marked_text; + + // Notify IME is active if application still doesn't know it. + if self.ivars().ime_state.get() == ImeState::Disabled { + *self.ivars().input_source.borrow_mut() = self.current_input_source(); + self.queue_event(WindowEvent::Ime(Ime::Enabled)); + } + + if unsafe { self.hasMarkedText() } { + self.ivars().ime_state.set(ImeState::Preedit); + } else { + // In case the preedit was cleared, set IME into the Ground state. + self.ivars().ime_state.set(ImeState::Ground); + } + + let cursor_range = if string.is_empty() { + // An empty string basically means that there's no preedit, so indicate that by + // sending a `None` cursor range. + None + } else { + // Clamp to string length to avoid NSRangeException from out-of-bounds + // indices sent by macOS IME (e.g. native Pinyin, see + // https://github.com/alacritty/alacritty/issues/8791). + let len = string.length(); + let location = selected_range.location.min(len); + let end = selected_range.end().min(len); + // Convert the selected range from UTF-16 indices to UTF-8 indices. + let sub_string_a = unsafe { string.substringToIndex(location) }; + let sub_string_b = unsafe { string.substringToIndex(end) }; + let lowerbound_utf8 = sub_string_a.len(); + let upperbound_utf8 = sub_string_b.len(); + Some((lowerbound_utf8, upperbound_utf8)) + }; + + // Send WindowEvent for updating marked text + self.queue_event(WindowEvent::Ime(Ime::Preedit(string.to_string(), cursor_range))); + } + + #[method(unmarkText)] + fn unmark_text(&self) { + trace_scope!("unmarkText"); + *self.ivars().marked_text.borrow_mut() = NSMutableAttributedString::new(); + + let input_context = self.inputContext().expect("input context"); + input_context.discardMarkedText(); + + self.queue_event(WindowEvent::Ime(Ime::Preedit(String::new(), None))); + if self.is_ime_enabled() { + // Leave the Preedit self.ivars() + self.ivars().ime_state.set(ImeState::Ground); + } else { + tracing::warn!("Expected to have IME enabled when receiving unmarkText"); + } + } + + #[method_id(validAttributesForMarkedText)] + fn valid_attributes_for_marked_text(&self) -> Retained> { + trace_scope!("validAttributesForMarkedText"); + NSArray::new() + } + + #[method_id(attributedSubstringForProposedRange:actualRange:)] + fn attributed_substring_for_proposed_range( + &self, + _range: NSRange, + _actual_range: *mut NSRange, + ) -> Option> { + trace_scope!("attributedSubstringForProposedRange:actualRange:"); + None + } + + #[method(characterIndexForPoint:)] + fn character_index_for_point(&self, _point: NSPoint) -> NSUInteger { + trace_scope!("characterIndexForPoint:"); + 0 + } + + #[method(firstRectForCharacterRange:actualRange:)] + fn first_rect_for_character_range( + &self, + _range: NSRange, + _actual_range: *mut NSRange, + ) -> NSRect { + trace_scope!("firstRectForCharacterRange:actualRange:"); + let rect = NSRect::new( + self.ivars().ime_position.get(), + self.ivars().ime_size.get() + ); + // Return value is expected to be in screen coordinates, so we need a conversion here + self.window() + .convertRectToScreen(self.convertRect_toView(rect, None)) + } + + #[method(insertText:replacementRange:)] + fn insert_text(&self, string: &NSObject, _replacement_range: NSRange) { + // TODO: Use _replacement_range, requires changing the event to report surrounding text. + trace_scope!("insertText:replacementRange:"); + + // SAFETY: This method is guaranteed to get either a `NSString` or a `NSAttributedString`. + let string = if string.is_kind_of::() { + let string: *const NSObject = string; + let string: *const NSAttributedString = string.cast(); + unsafe { &*string }.string().to_string() + } else { + let string: *const NSObject = string; + let string: *const NSString = string.cast(); + unsafe { &*string }.to_string() + }; + + let is_control = string.chars().next().is_some_and(|c| c.is_control()); + + // Commit only if we have marked text. + if unsafe { self.hasMarkedText() } && self.is_ime_enabled() && !is_control { + self.queue_event(WindowEvent::Ime(Ime::Preedit(String::new(), None))); + self.queue_event(WindowEvent::Ime(Ime::Commit(string))); + self.ivars().ime_state.set(ImeState::Committed); + } + } + + // Basically, we're sent this message whenever a keyboard event that doesn't generate a "human + // readable" character happens, i.e. newlines, tabs, and Ctrl+C. + #[method(doCommandBySelector:)] + fn do_command_by_selector(&self, _command: Sel) { + trace_scope!("doCommandBySelector:"); + // We shouldn't forward any character from just committed text, since we'll end up sending + // it twice with some IMEs like Korean one. We'll also always send `Enter` in that case, + // which is not desired given it was used to confirm IME input. + if self.ivars().ime_state.get() == ImeState::Committed { + return; + } + + self.ivars().forward_key_to_app.set(true); + + if unsafe { self.hasMarkedText() } && self.ivars().ime_state.get() == ImeState::Preedit + { + // Leave preedit so that we also report the key-up for this key. + self.ivars().ime_state.set(ImeState::Ground); + } + } + } + + unsafe impl WinitView { + #[method(keyDown:)] + fn key_down(&self, event: &NSEvent) { + trace_scope!("keyDown:"); + { + let mut prev_input_source = self.ivars().input_source.borrow_mut(); + let current_input_source = self.current_input_source(); + if *prev_input_source != current_input_source && self.is_ime_enabled() { + *prev_input_source = current_input_source; + drop(prev_input_source); + self.ivars().ime_state.set(ImeState::Disabled); + self.queue_event(WindowEvent::Ime(Ime::Disabled)); + } + } + + // Get the characters from the event. + let old_ime_state = self.ivars().ime_state.get(); + self.ivars().forward_key_to_app.set(false); + let event = replace_event(event, self.option_as_alt()); + + // The `interpretKeyEvents` function might call + // `setMarkedText`, `insertText`, and `doCommandBySelector`. + // It's important that we call this before queuing the KeyboardInput, because + // we must send the `KeyboardInput` event during IME if it triggered + // `doCommandBySelector`. (doCommandBySelector means that the keyboard input + // is not handled by IME and should be handled by the application) + if self.ivars().ime_allowed.get() { + let events_for_nsview = NSArray::from_slice(&[&*event]); + unsafe { self.interpretKeyEvents(&events_for_nsview) }; + + // If the text was committed we must treat the next keyboard event as IME related. + if self.ivars().ime_state.get() == ImeState::Committed { + // Remove any marked text, so normal input can continue. + *self.ivars().marked_text.borrow_mut() = NSMutableAttributedString::new(); + } + } + + self.update_modifiers(&event, false); + + let had_ime_input = match self.ivars().ime_state.get() { + ImeState::Committed => { + // Allow normal input after the commit. + self.ivars().ime_state.set(ImeState::Ground); + true + } + ImeState::Preedit => true, + // `key_down` could result in preedit clear, so compare old and current state. + _ => old_ime_state != self.ivars().ime_state.get(), + }; + + if !had_ime_input || self.ivars().forward_key_to_app.get() { + let key_event = create_key_event(&event, true, unsafe { event.isARepeat() }); + self.queue_event(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event: key_event, + is_synthetic: false, + }); + } + } + + #[method(keyUp:)] + fn key_up(&self, event: &NSEvent) { + trace_scope!("keyUp:"); + + let event = replace_event(event, self.option_as_alt()); + self.update_modifiers(&event, false); + + // We want to send keyboard input when we are currently in the ground state. + if matches!( + self.ivars().ime_state.get(), + ImeState::Ground | ImeState::Disabled + ) { + self.queue_event(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event: create_key_event(&event, false, false), + is_synthetic: false, + }); + } + } + + #[method(flagsChanged:)] + fn flags_changed(&self, event: &NSEvent) { + trace_scope!("flagsChanged:"); + + self.update_modifiers(event, true); + } + + #[method(insertTab:)] + fn insert_tab(&self, _sender: Option<&AnyObject>) { + trace_scope!("insertTab:"); + let window = self.window(); + if let Some(first_responder) = window.firstResponder() { + if *first_responder == ***self { + window.selectNextKeyView(Some(self)) + } + } + } + + #[method(insertBackTab:)] + fn insert_back_tab(&self, _sender: Option<&AnyObject>) { + trace_scope!("insertBackTab:"); + let window = self.window(); + if let Some(first_responder) = window.firstResponder() { + if *first_responder == ***self { + window.selectPreviousKeyView(Some(self)) + } + } + } + + // Allows us to receive Cmd-. (the shortcut for closing a dialog) + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=300620#c6 + #[method(cancelOperation:)] + fn cancel_operation(&self, _sender: Option<&AnyObject>) { + let mtm = MainThreadMarker::from(self); + trace_scope!("cancelOperation:"); + + let event = NSApplication::sharedApplication(mtm) + .currentEvent() + .expect("could not find current event"); + + self.update_modifiers(&event, false); + let event = create_key_event(&event, true, unsafe { event.isARepeat() }); + + self.queue_event(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event, + is_synthetic: false, + }); + } + + // In the past (?), `mouseMoved:` events were not generated when the + // user hovered over a window from a separate window, and as such the + // application might not know the location of the mouse in the event. + // + // To fix this, we emit `mouse_motion` inside of mouse click, mouse + // scroll, magnify and other gesture event handlers, to ensure that + // the application's state of where the mouse click was located is up + // to date. + // + // See https://github.com/rust-windowing/winit/pull/1490 for history. + + #[method(mouseDown:)] + fn mouse_down(&self, event: &NSEvent) { + trace_scope!("mouseDown:"); + self.mouse_motion(event); + self.mouse_click(event, ElementState::Pressed); + } + + #[method(mouseUp:)] + fn mouse_up(&self, event: &NSEvent) { + trace_scope!("mouseUp:"); + self.mouse_motion(event); + self.mouse_click(event, ElementState::Released); + } + + #[method(rightMouseDown:)] + fn right_mouse_down(&self, event: &NSEvent) { + trace_scope!("rightMouseDown:"); + self.mouse_motion(event); + self.mouse_click(event, ElementState::Pressed); + } + + #[method(rightMouseUp:)] + fn right_mouse_up(&self, event: &NSEvent) { + trace_scope!("rightMouseUp:"); + self.mouse_motion(event); + self.mouse_click(event, ElementState::Released); + } + + #[method(otherMouseDown:)] + fn other_mouse_down(&self, event: &NSEvent) { + trace_scope!("otherMouseDown:"); + self.mouse_motion(event); + self.mouse_click(event, ElementState::Pressed); + } + + #[method(otherMouseUp:)] + fn other_mouse_up(&self, event: &NSEvent) { + trace_scope!("otherMouseUp:"); + self.mouse_motion(event); + self.mouse_click(event, ElementState::Released); + } + + // No tracing on these because that would be overly verbose + + #[method(mouseMoved:)] + fn mouse_moved(&self, event: &NSEvent) { + self.mouse_motion(event); + } + + #[method(mouseDragged:)] + fn mouse_dragged(&self, event: &NSEvent) { + self.mouse_motion(event); + } + + #[method(rightMouseDragged:)] + fn right_mouse_dragged(&self, event: &NSEvent) { + self.mouse_motion(event); + } + + #[method(otherMouseDragged:)] + fn other_mouse_dragged(&self, event: &NSEvent) { + self.mouse_motion(event); + } + + #[method(mouseEntered:)] + fn mouse_entered(&self, _event: &NSEvent) { + trace_scope!("mouseEntered:"); + self.queue_event(WindowEvent::CursorEntered { + device_id: DEVICE_ID, + }); + } + + #[method(mouseExited:)] + fn mouse_exited(&self, _event: &NSEvent) { + trace_scope!("mouseExited:"); + + self.queue_event(WindowEvent::CursorLeft { + device_id: DEVICE_ID, + }); + } + + #[method(scrollWheel:)] + fn scroll_wheel(&self, event: &NSEvent) { + trace_scope!("scrollWheel:"); + + self.mouse_motion(event); + + let delta = { + let (x, y) = unsafe { (event.scrollingDeltaX(), event.scrollingDeltaY()) }; + if unsafe { event.hasPreciseScrollingDeltas() } { + let delta = LogicalPosition::new(x, y).to_physical(self.scale_factor()); + MouseScrollDelta::PixelDelta(delta) + } else { + MouseScrollDelta::LineDelta(x as f32, y as f32) + } + }; + + // The "momentum phase," if any, has higher priority than touch phase (the two should + // be mutually exclusive anyhow, which is why the API is rather incoherent). If no momentum + // phase is recorded (or rather, the started/ended cases of the momentum phase) then we + // report the touch phase. + #[allow(non_upper_case_globals)] + let phase = match unsafe { event.momentumPhase() } { + NSEventPhase::MayBegin | NSEventPhase::Began => TouchPhase::Started, + NSEventPhase::Ended | NSEventPhase::Cancelled => TouchPhase::Ended, + _ => match unsafe { event.phase() } { + NSEventPhase::MayBegin | NSEventPhase::Began => TouchPhase::Started, + NSEventPhase::Ended | NSEventPhase::Cancelled => TouchPhase::Ended, + _ => TouchPhase::Moved, + }, + }; + + self.update_modifiers(event, false); + + self.ivars().app_delegate.maybe_queue_device_event(DeviceEvent::MouseWheel { delta }); + self.queue_event(WindowEvent::MouseWheel { + device_id: DEVICE_ID, + delta, + phase, + }); + } + + #[method(magnifyWithEvent:)] + fn magnify_with_event(&self, event: &NSEvent) { + trace_scope!("magnifyWithEvent:"); + + self.mouse_motion(event); + + #[allow(non_upper_case_globals)] + let phase = match unsafe { event.phase() } { + NSEventPhase::Began => TouchPhase::Started, + NSEventPhase::Changed => TouchPhase::Moved, + NSEventPhase::Cancelled => TouchPhase::Cancelled, + NSEventPhase::Ended => TouchPhase::Ended, + _ => return, + }; + + self.queue_event(WindowEvent::PinchGesture { + device_id: DEVICE_ID, + delta: unsafe { event.magnification() }, + phase, + }); + } + + #[method(smartMagnifyWithEvent:)] + fn smart_magnify_with_event(&self, event: &NSEvent) { + trace_scope!("smartMagnifyWithEvent:"); + + self.mouse_motion(event); + + self.queue_event(WindowEvent::DoubleTapGesture { + device_id: DEVICE_ID, + }); + } + + #[method(rotateWithEvent:)] + fn rotate_with_event(&self, event: &NSEvent) { + trace_scope!("rotateWithEvent:"); + + self.mouse_motion(event); + + #[allow(non_upper_case_globals)] + let phase = match unsafe { event.phase() } { + NSEventPhase::Began => TouchPhase::Started, + NSEventPhase::Changed => TouchPhase::Moved, + NSEventPhase::Cancelled => TouchPhase::Cancelled, + NSEventPhase::Ended => TouchPhase::Ended, + _ => return, + }; + + self.queue_event(WindowEvent::RotationGesture { + device_id: DEVICE_ID, + delta: unsafe { event.rotation() }, + phase, + }); + } + + #[method(pressureChangeWithEvent:)] + fn pressure_change_with_event(&self, event: &NSEvent) { + trace_scope!("pressureChangeWithEvent:"); + + self.queue_event(WindowEvent::TouchpadPressure { + device_id: DEVICE_ID, + pressure: unsafe { event.pressure() }, + stage: unsafe { event.stage() } as i64, + }); + } + + // Allows us to receive Ctrl-Tab and Ctrl-Esc. + // Note that this *doesn't* help with any missing Cmd inputs. + // https://github.com/chromium/chromium/blob/a86a8a6bcfa438fa3ac2eba6f02b3ad1f8e0756f/ui/views/cocoa/bridged_content_view.mm#L816 + #[method(_wantsKeyDownForEvent:)] + fn wants_key_down_for_event(&self, _event: &NSEvent) -> bool { + trace_scope!("_wantsKeyDownForEvent:"); + true + } + + #[method(acceptsFirstMouse:)] + fn accepts_first_mouse(&self, _event: &NSEvent) -> bool { + trace_scope!("acceptsFirstMouse:"); + self.ivars().accepts_first_mouse + } + } +); + +impl WinitView { + pub(super) fn new( + app_delegate: &ApplicationDelegate, + window: &WinitWindow, + accepts_first_mouse: bool, + option_as_alt: OptionAsAlt, + ) -> Retained { + let mtm = MainThreadMarker::from(window); + let this = mtm.alloc().set_ivars(ViewState { + app_delegate: app_delegate.retain(), + cursor_state: Default::default(), + ime_position: Default::default(), + ime_size: Default::default(), + modifiers: Default::default(), + phys_modifiers: Default::default(), + tracking_rect: Default::default(), + ime_state: Default::default(), + input_source: Default::default(), + ime_allowed: Default::default(), + forward_key_to_app: Default::default(), + marked_text: Default::default(), + accepts_first_mouse, + _ns_window: WeakId::new(&window.retain()), + option_as_alt: Cell::new(option_as_alt), + }); + let this: Retained = unsafe { msg_send_id![super(this), init] }; + + this.setPostsFrameChangedNotifications(true); + let notification_center = unsafe { NSNotificationCenter::defaultCenter() }; + unsafe { + notification_center.addObserver_selector_name_object( + &this, + sel!(frameDidChange:), + Some(NSViewFrameDidChangeNotification), + Some(&this), + ) + } + + *this.ivars().input_source.borrow_mut() = this.current_input_source(); + + this + } + + fn window(&self) -> Retained { + // TODO: Simply use `window` property on `NSView`. + // That only returns a window _after_ the view has been attached though! + // (which is incompatible with `frameDidChange:`) + // + // unsafe { msg_send_id![self, window] } + self.ivars()._ns_window.load().expect("view to have a window") + } + + fn queue_event(&self, event: WindowEvent) { + self.ivars().app_delegate.maybe_queue_window_event(self.window().id(), event); + } + + fn scale_factor(&self) -> f64 { + self.window().backingScaleFactor() as f64 + } + + fn is_ime_enabled(&self) -> bool { + !matches!(self.ivars().ime_state.get(), ImeState::Disabled) + } + + fn current_input_source(&self) -> String { + self.inputContext() + .expect("input context") + .selectedKeyboardInputSource() + .map(|input_source| input_source.to_string()) + .unwrap_or_default() + } + + pub(super) fn cursor_icon(&self) -> Retained { + self.ivars().cursor_state.borrow().cursor.clone() + } + + pub(super) fn set_cursor_icon(&self, icon: Retained) { + let mut cursor_state = self.ivars().cursor_state.borrow_mut(); + cursor_state.cursor = icon; + } + + /// Set whether the cursor should be visible or not. + /// + /// Returns whether the state changed. + pub(super) fn set_cursor_visible(&self, visible: bool) -> bool { + let mut cursor_state = self.ivars().cursor_state.borrow_mut(); + if visible != cursor_state.visible { + cursor_state.visible = visible; + true + } else { + false + } + } + + pub(super) fn set_ime_allowed(&self, ime_allowed: bool) { + if self.ivars().ime_allowed.get() == ime_allowed { + return; + } + self.ivars().ime_allowed.set(ime_allowed); + if self.ivars().ime_allowed.get() { + return; + } + + // Clear markedText + *self.ivars().marked_text.borrow_mut() = NSMutableAttributedString::new(); + + if self.ivars().ime_state.get() != ImeState::Disabled { + self.ivars().ime_state.set(ImeState::Disabled); + self.queue_event(WindowEvent::Ime(Ime::Disabled)); + } + } + + pub(super) fn set_ime_cursor_area(&self, position: NSPoint, size: NSSize) { + self.ivars().ime_position.set(position); + self.ivars().ime_size.set(size); + let input_context = self.inputContext().expect("input context"); + input_context.invalidateCharacterCoordinates(); + } + + /// Reset modifiers and emit a synthetic ModifiersChanged event if deemed necessary. + pub(super) fn reset_modifiers(&self) { + if !self.ivars().modifiers.get().state().is_empty() { + self.ivars().modifiers.set(Modifiers::default()); + self.queue_event(WindowEvent::ModifiersChanged(self.ivars().modifiers.get())); + } + } + + pub(super) fn set_option_as_alt(&self, value: OptionAsAlt) { + self.ivars().option_as_alt.set(value) + } + + pub(super) fn option_as_alt(&self) -> OptionAsAlt { + self.ivars().option_as_alt.get() + } + + /// Update modifiers if `event` has something different + fn update_modifiers(&self, ns_event: &NSEvent, is_flags_changed_event: bool) { + use ElementState::{Pressed, Released}; + + let current_modifiers = event_mods(ns_event); + let prev_modifiers = self.ivars().modifiers.get(); + self.ivars().modifiers.set(current_modifiers); + + // This function was called form the flagsChanged event, which is triggered + // when the user presses/releases a modifier even if the same kind of modifier + // has already been pressed. + // + // When flags changed event has key code of zero it means that event doesn't carry any key + // event, thus we can't generate regular presses based on that. The `ModifiersChanged` + // later will work though, since the flags are attached to the event and contain valid + // information. + 'send_event: { + if is_flags_changed_event && unsafe { ns_event.keyCode() } != 0 { + let scancode = unsafe { ns_event.keyCode() }; + let physical_key = scancode_to_physicalkey(scancode as u32); + + let logical_key = code_to_key(physical_key, scancode); + // Ignore processing of unknown modifiers because we can't determine whether + // it was pressed or release reliably. + // + // Furthermore, sometimes normal keys are reported inside flagsChanged:, such as + // when holding Caps Lock while pressing another key, see: + // https://github.com/alacritty/alacritty/issues/8268 + let Some(event_modifier) = key_to_modifier(&logical_key) else { + break 'send_event; + }; + + let mut event = KeyEvent { + location: code_to_location(physical_key), + logical_key: logical_key.clone(), + physical_key, + repeat: false, + // We'll correct this later. + state: Pressed, + text: None, + platform_specific: KeyEventExtra { + text_with_all_modifiers: None, + key_without_modifiers: logical_key.clone(), + }, + }; + + let location_mask = ModLocationMask::from_location(event.location); + + let mut phys_mod_state = self.ivars().phys_modifiers.borrow_mut(); + let phys_mod = + phys_mod_state.entry(logical_key).or_insert(ModLocationMask::empty()); + + let is_active = current_modifiers.state().contains(event_modifier); + let mut events = VecDeque::with_capacity(2); + + // There is no API for getting whether the button was pressed or released + // during this event. For this reason we have to do a bit of magic below + // to come up with a good guess whether this key was pressed or released. + // (This is not trivial because there are multiple buttons that may affect + // the same modifier) + if !is_active { + event.state = Released; + if phys_mod.contains(ModLocationMask::LEFT) { + let mut event = event.clone(); + event.location = KeyLocation::Left; + event.physical_key = get_left_modifier_code(&event.logical_key).into(); + events.push_back(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event, + is_synthetic: false, + }); + } + if phys_mod.contains(ModLocationMask::RIGHT) { + event.location = KeyLocation::Right; + event.physical_key = get_right_modifier_code(&event.logical_key).into(); + events.push_back(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event, + is_synthetic: false, + }); + } + *phys_mod = ModLocationMask::empty(); + } else { + if *phys_mod == location_mask { + // Here we hit a contradiction: + // The modifier state was "changed" to active, + // yet the only pressed modifier key was the one that we + // just got a change event for. + // This seemingly means that the only pressed modifier is now released, + // but at the same time the modifier became active. + // + // But this scenario is possible if we released modifiers + // while the application was not in focus. (Because we don't + // get informed of modifier key events while the application + // is not focused) + + // In this case we prioritize the information + // about the current modifier state which means + // that the button was pressed. + event.state = Pressed; + } else { + phys_mod.toggle(location_mask); + let is_pressed = phys_mod.contains(location_mask); + event.state = if is_pressed { Pressed } else { Released }; + } + + events.push_back(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event, + is_synthetic: false, + }); + } + + drop(phys_mod_state); + + for event in events { + self.queue_event(event); + } + } + } + + if prev_modifiers == current_modifiers { + return; + } + + self.queue_event(WindowEvent::ModifiersChanged(self.ivars().modifiers.get())); + } + + fn mouse_click(&self, event: &NSEvent, button_state: ElementState) { + let button = mouse_button(event); + + self.update_modifiers(event, false); + + self.queue_event(WindowEvent::MouseInput { + device_id: DEVICE_ID, + state: button_state, + button, + }); + } + + fn mouse_motion(&self, event: &NSEvent) { + let window_point = unsafe { event.locationInWindow() }; + let view_point = self.convertPoint_fromView(window_point, None); + let frame = self.frame(); + + if view_point.x.is_sign_negative() + || view_point.y.is_sign_negative() + || view_point.x > frame.size.width + || view_point.y > frame.size.height + { + let mouse_buttons_down = unsafe { NSEvent::pressedMouseButtons() }; + if mouse_buttons_down == 0 { + // Point is outside of the client area (view) and no buttons are pressed + return; + } + } + + let view_point = LogicalPosition::new(view_point.x, view_point.y); + + self.update_modifiers(event, false); + + self.queue_event(WindowEvent::CursorMoved { + device_id: DEVICE_ID, + position: view_point.to_physical(self.scale_factor()), + }); + } +} + +/// Get the mouse button from the NSEvent. +fn mouse_button(event: &NSEvent) -> MouseButton { + // The buttonNumber property only makes sense for the mouse events: + // NSLeftMouse.../NSRightMouse.../NSOtherMouse... + // For the other events, it's always set to 0. + // MacOS only defines the left, right and middle buttons, 3..=31 are left as generic buttons, + // but 3 and 4 are very commonly used as Back and Forward by hardware vendors and applications. + match unsafe { event.buttonNumber() } { + 0 => MouseButton::Left, + 1 => MouseButton::Right, + 2 => MouseButton::Middle, + 3 => MouseButton::Back, + 4 => MouseButton::Forward, + n => MouseButton::Other(n as u16), + } +} + +// NOTE: to get option as alt working we need to rewrite events +// we're getting from the operating system, which makes it +// impossible to provide such events as extra in `KeyEvent`. +fn replace_event(event: &NSEvent, option_as_alt: OptionAsAlt) -> Retained { + let ev_mods = event_mods(event).state; + let ignore_alt_characters = match option_as_alt { + OptionAsAlt::OnlyLeft if lalt_pressed(event) => true, + OptionAsAlt::OnlyRight if ralt_pressed(event) => true, + OptionAsAlt::Both if ev_mods.alt_key() => true, + _ => false, + } && !ev_mods.control_key() + && !ev_mods.super_key(); + + if ignore_alt_characters { + let ns_chars = unsafe { + event.charactersIgnoringModifiers().expect("expected characters to be non-null") + }; + + unsafe { + NSEvent::keyEventWithType_location_modifierFlags_timestamp_windowNumber_context_characters_charactersIgnoringModifiers_isARepeat_keyCode( + event.r#type(), + event.locationInWindow(), + event.modifierFlags(), + event.timestamp(), + event.windowNumber(), + None, + &ns_chars, + &ns_chars, + event.isARepeat(), + event.keyCode(), + ) + .unwrap() + } + } else { + event.copy() + } +} diff --git a/vendor/winit/src/platform_impl/macos/window.rs b/vendor/winit/src/platform_impl/macos/window.rs new file mode 100644 index 00000000..04da8b29 --- /dev/null +++ b/vendor/winit/src/platform_impl/macos/window.rs @@ -0,0 +1,126 @@ +#![allow(clippy::unnecessary_cast)] + +use objc2::rc::{autoreleasepool, Retained}; +use objc2::{declare_class, mutability, ClassType, DeclaredClass}; +use objc2_app_kit::{NSResponder, NSWindow}; +use objc2_foundation::{MainThreadBound, MainThreadMarker, NSObject}; + +use super::event_loop::ActiveEventLoop; +use super::window_delegate::WindowDelegate; +use crate::error::OsError as RootOsError; +use crate::window::WindowAttributes; + +pub(crate) struct Window { + window: MainThreadBound>, + /// The window only keeps a weak reference to this, so we must keep it around here. + delegate: MainThreadBound>, +} + +impl Drop for Window { + fn drop(&mut self) { + self.window.get_on_main(|window| autoreleasepool(|_| window.close())) + } +} + +impl Window { + pub(crate) fn new( + window_target: &ActiveEventLoop, + attributes: WindowAttributes, + ) -> Result { + let mtm = window_target.mtm; + let delegate = autoreleasepool(|_| { + WindowDelegate::new(window_target.app_delegate(), attributes, mtm) + })?; + Ok(Window { + window: MainThreadBound::new(delegate.window().retain(), mtm), + delegate: MainThreadBound::new(delegate, mtm), + }) + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&WindowDelegate) + Send + 'static) { + // For now, don't actually do queuing, since it may be less predictable + self.maybe_wait_on_main(f) + } + + pub(crate) fn maybe_wait_on_main( + &self, + f: impl FnOnce(&WindowDelegate) -> R + Send, + ) -> R { + self.delegate.get_on_main(|delegate| f(delegate)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub(crate) fn raw_window_handle_rwh_06( + &self, + ) -> Result { + if let Some(mtm) = MainThreadMarker::new() { + Ok(self.delegate.get(mtm).raw_window_handle_rwh_06()) + } else { + Err(rwh_06::HandleError::Unavailable) + } + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub(crate) fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::AppKit(rwh_06::AppKitDisplayHandle::new())) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId(pub usize); + +impl WindowId { + pub const fn dummy() -> Self { + Self(0) + } +} + +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0 as u64 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id as usize) + } +} + +declare_class!( + #[derive(Debug)] + pub struct WinitWindow; + + unsafe impl ClassType for WinitWindow { + #[inherits(NSResponder, NSObject)] + type Super = NSWindow; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitWindow"; + } + + impl DeclaredClass for WinitWindow {} + + unsafe impl WinitWindow { + #[method(canBecomeMainWindow)] + fn can_become_main_window(&self) -> bool { + trace_scope!("canBecomeMainWindow"); + true + } + + #[method(canBecomeKeyWindow)] + fn can_become_key_window(&self) -> bool { + trace_scope!("canBecomeKeyWindow"); + true + } + } +); + +impl WinitWindow { + pub(super) fn id(&self) -> WindowId { + WindowId(self as *const Self as usize) + } +} diff --git a/vendor/winit/src/platform_impl/macos/window_delegate.rs b/vendor/winit/src/platform_impl/macos/window_delegate.rs new file mode 100644 index 00000000..e293505e --- /dev/null +++ b/vendor/winit/src/platform_impl/macos/window_delegate.rs @@ -0,0 +1,1910 @@ +#![allow(clippy::unnecessary_cast)] +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::ffi::c_void; +use std::ptr; +use std::sync::{Arc, Mutex}; + +use core_graphics::display::{CGDisplay, CGPoint}; +use monitor::VideoModeHandle; +use objc2::rc::{autoreleasepool, Retained}; +use objc2::runtime::{AnyObject, ProtocolObject}; +use objc2::{declare_class, msg_send_id, mutability, sel, ClassType, DeclaredClass}; +use objc2_app_kit::{ + NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSAppearanceCustomization, + NSAppearanceNameAqua, NSApplication, NSApplicationPresentationOptions, NSBackingStoreType, + NSColor, NSDraggingDestination, NSFilenamesPboardType, NSPasteboard, + NSRequestUserAttentionType, NSScreen, NSView, NSWindowButton, NSWindowDelegate, + NSWindowFullScreenButton, NSWindowLevel, NSWindowOcclusionState, NSWindowOrderingMode, + NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode, NSWindowTitleVisibility, +}; +use objc2_foundation::{ + ns_string, CGFloat, MainThreadMarker, NSArray, NSCopying, NSDictionary, NSKeyValueChangeKey, + NSKeyValueChangeNewKey, NSKeyValueChangeOldKey, NSKeyValueObservingOptions, NSObject, + NSObjectNSDelayedPerforming, NSObjectNSKeyValueObserverRegistration, NSObjectProtocol, NSPoint, + NSRect, NSSize, NSString, +}; +use tracing::{trace, warn}; + +use super::app_state::ApplicationDelegate; +use super::cursor::cursor_from_icon; +use super::monitor::{self, flip_window_screen_coordinates, get_display_id}; +use super::observer::RunLoop; +use super::view::WinitView; +use super::window::WinitWindow; +use super::{ffi, Fullscreen, MonitorHandle, OsError, WindowId}; +use crate::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::event::{InnerSizeWriter, WindowEvent}; +use crate::platform::macos::{OptionAsAlt, WindowExtMacOS}; +use crate::window::{ + Cursor, CursorGrabMode, Icon, ImePurpose, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, WindowLevel, +}; + +#[derive(Clone, Debug)] +pub struct PlatformSpecificWindowAttributes { + pub movable_by_window_background: bool, + pub titlebar_transparent: bool, + pub title_hidden: bool, + pub titlebar_hidden: bool, + pub titlebar_buttons_hidden: bool, + pub fullsize_content_view: bool, + pub disallow_hidpi: bool, + pub has_shadow: bool, + pub accepts_first_mouse: bool, + pub tabbing_identifier: Option, + pub option_as_alt: OptionAsAlt, + pub borderless_game: bool, +} + +impl Default for PlatformSpecificWindowAttributes { + #[inline] + fn default() -> Self { + Self { + movable_by_window_background: false, + titlebar_transparent: false, + title_hidden: false, + titlebar_hidden: false, + titlebar_buttons_hidden: false, + fullsize_content_view: false, + disallow_hidpi: false, + has_shadow: true, + accepts_first_mouse: true, + tabbing_identifier: None, + option_as_alt: Default::default(), + borderless_game: false, + } + } +} + +#[derive(Debug)] +pub(crate) struct State { + /// Strong reference to the global application state. + app_delegate: Retained, + + window: Retained, + + // During `windowDidResize`, we use this to only send Moved if the position changed. + // + // This is expressed in desktop coordinates, and flipped to match Winit's coordinate system. + previous_position: Cell, + + // Used to prevent redundant events. + previous_scale_factor: Cell, + + /// The current resize increments for the window content. + resize_increments: Cell, + /// Whether the window is showing decorations. + decorations: Cell, + resizable: Cell, + maximized: Cell, + + /// Presentation options saved before entering `set_simple_fullscreen`, and + /// restored upon exiting it. Also used when transitioning from Borderless to + /// Exclusive fullscreen in `set_fullscreen` because we need to disable the menu + /// bar in exclusive fullscreen but want to restore the original options when + /// transitioning back to borderless fullscreen. + save_presentation_opts: Cell>, + // This is set when WindowAttributes::with_fullscreen was set, + // see comments of `window_did_fail_to_enter_fullscreen` + initial_fullscreen: Cell, + /// This field tracks the current fullscreen state of the window + /// (as seen by `WindowDelegate`). + fullscreen: RefCell>, + // If it is attempted to toggle fullscreen when in_fullscreen_transition is true, + // Set target_fullscreen and do after fullscreen transition is end. + target_fullscreen: RefCell>>, + // This is true between windowWillEnterFullScreen and windowDidEnterFullScreen + // or windowWillExitFullScreen and windowDidExitFullScreen. + // We must not toggle fullscreen when this is true. + in_fullscreen_transition: Cell, + standard_frame: Cell>, + is_simple_fullscreen: Cell, + saved_style: Cell>, + is_borderless_game: Cell, +} + +declare_class!( + pub(crate) struct WindowDelegate; + + unsafe impl ClassType for WindowDelegate { + type Super = NSObject; + type Mutability = mutability::MainThreadOnly; + const NAME: &'static str = "WinitWindowDelegate"; + } + + impl DeclaredClass for WindowDelegate { + type Ivars = State; + } + + unsafe impl NSObjectProtocol for WindowDelegate {} + + unsafe impl NSWindowDelegate for WindowDelegate { + #[method(windowShouldClose:)] + fn window_should_close(&self, _: Option<&AnyObject>) -> bool { + trace_scope!("windowShouldClose:"); + self.queue_event(WindowEvent::CloseRequested); + false + } + + #[method(windowWillClose:)] + fn window_will_close(&self, _: Option<&AnyObject>) { + trace_scope!("windowWillClose:"); + // `setDelegate:` retains the previous value and then autoreleases it + autoreleasepool(|_| { + // Since El Capitan, we need to be careful that delegate methods can't + // be called after the window closes. + self.window().setDelegate(None); + }); + self.queue_event(WindowEvent::Destroyed); + } + + #[method(windowDidResize:)] + fn window_did_resize(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidResize:"); + // NOTE: WindowEvent::Resized is reported in frameDidChange. + self.emit_move_event(); + } + + #[method(windowWillStartLiveResize:)] + fn window_will_start_live_resize(&self, _: Option<&AnyObject>) { + trace_scope!("windowWillStartLiveResize:"); + + let increments = self.ivars().resize_increments.get(); + self.set_resize_increments_inner(increments); + } + + #[method(windowDidEndLiveResize:)] + fn window_did_end_live_resize(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidEndLiveResize:"); + self.set_resize_increments_inner(NSSize::new(1., 1.)); + } + + // This won't be triggered if the move was part of a resize. + #[method(windowDidMove:)] + fn window_did_move(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidMove:"); + self.emit_move_event(); + } + + #[method(windowDidChangeBackingProperties:)] + fn window_did_change_backing_properties(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidChangeBackingProperties:"); + let scale_factor = self.scale_factor(); + if scale_factor == self.ivars().previous_scale_factor.get() { + return; + }; + self.ivars().previous_scale_factor.set(scale_factor); + + let mtm = MainThreadMarker::from(self); + let this = self.retain(); + RunLoop::main(mtm).queue_closure(move || { + this.handle_scale_factor_changed(scale_factor); + }); + } + + #[method(windowDidBecomeKey:)] + fn window_did_become_key(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidBecomeKey:"); + // TODO: center the cursor if the window had mouse grab when it + // lost focus + self.queue_event(WindowEvent::Focused(true)); + } + + #[method(windowDidResignKey:)] + fn window_did_resign_key(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidResignKey:"); + // It happens rather often, e.g. when the user is Cmd+Tabbing, that the + // NSWindowDelegate will receive a didResignKey event despite no event + // being received when the modifiers are released. This is because + // flagsChanged events are received by the NSView instead of the + // NSWindowDelegate, and as a result a tracked modifiers state can quite + // easily fall out of synchrony with reality. This requires us to emit + // a synthetic ModifiersChanged event when we lose focus. + self.view().reset_modifiers(); + + self.queue_event(WindowEvent::Focused(false)); + } + + /// Invoked when before enter fullscreen + #[method(windowWillEnterFullScreen:)] + fn window_will_enter_fullscreen(&self, _: Option<&AnyObject>) { + trace_scope!("windowWillEnterFullScreen:"); + + self.ivars().maximized.set(self.is_zoomed()); + let mut fullscreen = self.ivars().fullscreen.borrow_mut(); + match &*fullscreen { + // Exclusive mode sets the state in `set_fullscreen` as the user + // can't enter exclusive mode by other means (like the + // fullscreen button on the window decorations) + Some(Fullscreen::Exclusive(_)) => (), + // `window_will_enter_fullscreen` was triggered and we're already + // in fullscreen, so we must've reached here by `set_fullscreen` + // as it updates the state + Some(Fullscreen::Borderless(_)) => (), + // Otherwise, we must've reached fullscreen by the user clicking + // on the green fullscreen button. Update state! + None => { + let current_monitor = self.current_monitor_inner(); + *fullscreen = Some(Fullscreen::Borderless(current_monitor)); + }, + } + self.ivars().in_fullscreen_transition.set(true); + } + + /// Invoked when before exit fullscreen + #[method(windowWillExitFullScreen:)] + fn window_will_exit_fullscreen(&self, _: Option<&AnyObject>) { + trace_scope!("windowWillExitFullScreen:"); + + self.ivars().in_fullscreen_transition.set(true); + } + + #[method(window:willUseFullScreenPresentationOptions:)] + fn window_will_use_fullscreen_presentation_options( + &self, + _: Option<&AnyObject>, + proposed_options: NSApplicationPresentationOptions, + ) -> NSApplicationPresentationOptions { + trace_scope!("window:willUseFullScreenPresentationOptions:"); + // Generally, games will want to disable the menu bar and the dock. Ideally, + // this would be configurable by the user. Unfortunately because of our + // `CGShieldingWindowLevel() + 1` hack (see `set_fullscreen`), our window is + // placed on top of the menu bar in exclusive fullscreen mode. This looks + // broken so we always disable the menu bar in exclusive fullscreen. We may + // still want to make this configurable for borderless fullscreen. Right now + // we don't, for consistency. If we do, it should be documented that the + // user-provided options are ignored in exclusive fullscreen. + let mut options = proposed_options; + let fullscreen = self.ivars().fullscreen.borrow(); + if let Some(Fullscreen::Exclusive(_)) = &*fullscreen { + options = NSApplicationPresentationOptions::NSApplicationPresentationFullScreen + | NSApplicationPresentationOptions::NSApplicationPresentationHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar; + } + + options + } + + /// Invoked when entered fullscreen + #[method(windowDidEnterFullScreen:)] + fn window_did_enter_fullscreen(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidEnterFullScreen:"); + self.ivars().initial_fullscreen.set(false); + self.ivars().in_fullscreen_transition.set(false); + if let Some(target_fullscreen) = self.ivars().target_fullscreen.take() { + self.set_fullscreen(target_fullscreen); + } + } + + /// Invoked when exited fullscreen + #[method(windowDidExitFullScreen:)] + fn window_did_exit_fullscreen(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidExitFullScreen:"); + + self.restore_state_from_fullscreen(); + self.ivars().in_fullscreen_transition.set(false); + if let Some(target_fullscreen) = self.ivars().target_fullscreen.take() { + self.set_fullscreen(target_fullscreen); + } + } + + /// Invoked when fail to enter fullscreen + /// + /// When this window launch from a fullscreen app (e.g. launch from VS Code + /// terminal), it creates a new virtual desktop and a transition animation. + /// This animation takes one second and cannot be disable without + /// elevated privileges. In this animation time, all toggleFullscreen events + /// will be failed. In this implementation, we will try again by using + /// performSelector:withObject:afterDelay: until window_did_enter_fullscreen. + /// It should be fine as we only do this at initialization (i.e with_fullscreen + /// was set). + /// + /// From Apple doc: + /// In some cases, the transition to enter full-screen mode can fail, + /// due to being in the midst of handling some other animation or user gesture. + /// This method indicates that there was an error, and you should clean up any + /// work you may have done to prepare to enter full-screen mode. + #[method(windowDidFailToEnterFullScreen:)] + fn window_did_fail_to_enter_fullscreen(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidFailToEnterFullScreen:"); + self.ivars().in_fullscreen_transition.set(false); + self.ivars().target_fullscreen.replace(None); + if self.ivars().initial_fullscreen.get() { + unsafe { + self.window().performSelector_withObject_afterDelay( + sel!(toggleFullScreen:), + None, + 0.5, + ) + }; + } else { + self.restore_state_from_fullscreen(); + } + } + + // Invoked when the occlusion state of the window changes + #[method(windowDidChangeOcclusionState:)] + fn window_did_change_occlusion_state(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidChangeOcclusionState:"); + let visible = self.window().occlusionState().contains(NSWindowOcclusionState::Visible); + self.queue_event(WindowEvent::Occluded(!visible)); + } + + #[method(windowDidChangeScreen:)] + fn window_did_change_screen(&self, _: Option<&AnyObject>) { + trace_scope!("windowDidChangeScreen:"); + let is_simple_fullscreen = self.ivars().is_simple_fullscreen.get(); + if is_simple_fullscreen { + if let Some(screen) = self.window().screen() { + self.window().setFrame_display(screen.frame(), true); + } + } + } + } + + unsafe impl NSDraggingDestination for WindowDelegate { + /// Invoked when the dragged image enters destination bounds or frame + #[method(draggingEntered:)] + fn dragging_entered(&self, sender: &NSObject) -> bool { + trace_scope!("draggingEntered:"); + + use std::path::PathBuf; + + let pb: Retained = unsafe { msg_send_id![sender, draggingPasteboard] }; + let filenames = match pb.propertyListForType(unsafe { NSFilenamesPboardType }) { + Some(filenames) => filenames, + None => return false.into(), + }; + let filenames: Retained> = unsafe { Retained::cast(filenames) }; + + filenames.into_iter().for_each(|file| { + let path = PathBuf::from(file.to_string()); + self.queue_event(WindowEvent::HoveredFile(path)); + }); + + true + } + + /// Invoked when the image is released + #[method(prepareForDragOperation:)] + fn prepare_for_drag_operation(&self, _sender: &NSObject) -> bool { + trace_scope!("prepareForDragOperation:"); + true + } + + /// Invoked after the released image has been removed from the screen + #[method(performDragOperation:)] + fn perform_drag_operation(&self, sender: &NSObject) -> bool { + trace_scope!("performDragOperation:"); + + use std::path::PathBuf; + + let pb: Retained = unsafe { msg_send_id![sender, draggingPasteboard] }; + let filenames = match pb.propertyListForType(unsafe { NSFilenamesPboardType }) { + Some(filenames) => filenames, + None => return false.into(), + }; + let filenames: Retained> = unsafe { Retained::cast(filenames) }; + + filenames.into_iter().for_each(|file| { + let path = PathBuf::from(file.to_string()); + self.queue_event(WindowEvent::DroppedFile(path)); + }); + + true + } + + /// Invoked when the dragging operation is complete + #[method(concludeDragOperation:)] + fn conclude_drag_operation(&self, _sender: Option<&NSObject>) { + trace_scope!("concludeDragOperation:"); + } + + /// Invoked when the dragging operation is cancelled + #[method(draggingExited:)] + fn dragging_exited(&self, _sender: Option<&NSObject>) { + trace_scope!("draggingExited:"); + self.queue_event(WindowEvent::HoveredFileCancelled); + } + } + + // Key-Value Observing + unsafe impl WindowDelegate { + #[method(observeValueForKeyPath:ofObject:change:context:)] + fn observe_value( + &self, + key_path: Option<&NSString>, + _object: Option<&AnyObject>, + change: Option<&NSDictionary>, + _context: *mut c_void, + ) { + trace_scope!("observeValueForKeyPath:ofObject:change:context:"); + // NOTE: We don't _really_ need to check the key path, as there should only be one, but + // in the future we might want to observe other key paths. + if key_path == Some(ns_string!("effectiveAppearance")) { + let change = change.expect("requested a change dictionary in `addObserver`, but none was provided"); + let old = change.get(unsafe { NSKeyValueChangeOldKey }).expect("requested change dictionary did not contain `NSKeyValueChangeOldKey`"); + let new = change.get(unsafe { NSKeyValueChangeNewKey }).expect("requested change dictionary did not contain `NSKeyValueChangeNewKey`"); + + // SAFETY: The value of `effectiveAppearance` is `NSAppearance` + let old: *const AnyObject = old; + let old: *const NSAppearance = old.cast(); + let old: &NSAppearance = unsafe { &*old }; + let new: *const AnyObject = new; + let new: *const NSAppearance = new.cast(); + let new: &NSAppearance = unsafe { &*new }; + + trace!(old = %unsafe { old.name() }, new = %unsafe { new.name() }, "effectiveAppearance changed"); + + // Ignore the change if the window's theme is customized by the user (since in that + // case the `effectiveAppearance` is only emitted upon said customization, and then + // it's triggered directly by a user action, and we don't want to emit the event). + if unsafe { self.window().appearance() }.is_some() { + return; + } + + let old = appearance_to_theme(old); + let new = appearance_to_theme(new); + // Check that the theme changed in Winit's terms (the theme might have changed on + // other parameters, such as level of contrast, but the event should not be emitted + // in those cases). + if old == new { + return; + } + + self.queue_event(WindowEvent::ThemeChanged(new)); + } else { + panic!("unknown observed keypath {key_path:?}"); + } + } + } +); + +impl Drop for WindowDelegate { + fn drop(&mut self) { + unsafe { + self.window().removeObserver_forKeyPath(self, ns_string!("effectiveAppearance")); + } + } +} + +fn new_window( + app_delegate: &ApplicationDelegate, + attrs: &WindowAttributes, + mtm: MainThreadMarker, +) -> Option> { + autoreleasepool(|_| { + let screen = match attrs.fullscreen.clone().map(Into::into) { + Some(Fullscreen::Borderless(Some(monitor))) + | Some(Fullscreen::Exclusive(VideoModeHandle { monitor, .. })) => { + monitor.ns_screen(mtm).or_else(|| NSScreen::mainScreen(mtm)) + }, + Some(Fullscreen::Borderless(None)) => NSScreen::mainScreen(mtm), + None => None, + }; + let frame = match &screen { + Some(screen) => screen.frame(), + None => { + let scale_factor = NSScreen::mainScreen(mtm) + .map(|screen| screen.backingScaleFactor() as f64) + .unwrap_or(1.0); + let size = match attrs.inner_size { + Some(size) => { + let size = size.to_logical(scale_factor); + NSSize::new(size.width, size.height) + }, + None => NSSize::new(800.0, 600.0), + }; + let position = match attrs.position { + Some(position) => { + let position = position.to_logical(scale_factor); + flip_window_screen_coordinates(NSRect::new( + NSPoint::new(position.x, position.y), + size, + )) + }, + // This value is ignored by calling win.center() below + None => NSPoint::new(0.0, 0.0), + }; + NSRect::new(position, size) + }, + }; + + let mut masks = if (!attrs.decorations && screen.is_none()) + || attrs.platform_specific.titlebar_hidden + { + // Resizable without a titlebar or borders + // if decorations is set to false, ignore pl_attrs + // + // if the titlebar is hidden, ignore other pl_attrs + NSWindowStyleMask::Borderless + | NSWindowStyleMask::Resizable + | NSWindowStyleMask::Miniaturizable + } else { + // default case, resizable window with titlebar and titlebar buttons + NSWindowStyleMask::Closable + | NSWindowStyleMask::Miniaturizable + | NSWindowStyleMask::Resizable + | NSWindowStyleMask::Titled + }; + + if !attrs.resizable { + masks &= !NSWindowStyleMask::Resizable; + } + + if !attrs.enabled_buttons.contains(WindowButtons::MINIMIZE) { + masks &= !NSWindowStyleMask::Miniaturizable; + } + + if !attrs.enabled_buttons.contains(WindowButtons::CLOSE) { + masks &= !NSWindowStyleMask::Closable; + } + + if attrs.platform_specific.fullsize_content_view { + masks |= NSWindowStyleMask::FullSizeContentView; + } + + let window: Option> = unsafe { + msg_send_id![ + super(mtm.alloc().set_ivars(())), + initWithContentRect: frame, + styleMask: masks, + backing: NSBackingStoreType::NSBackingStoreBuffered, + defer: false, + ] + }; + let window = window?; + + // It is very important for correct memory management that we + // disable the extra release that would otherwise happen when + // calling `close` on the window. + unsafe { window.setReleasedWhenClosed(false) }; + + window.setTitle(&NSString::from_str(&attrs.title)); + window.setAcceptsMouseMovedEvents(true); + + if let Some(identifier) = &attrs.platform_specific.tabbing_identifier { + window.setTabbingIdentifier(&NSString::from_str(identifier)); + window.setTabbingMode(NSWindowTabbingMode::Preferred); + } + + if attrs.content_protected { + window.setSharingType(NSWindowSharingType::NSWindowSharingNone); + } + + if attrs.platform_specific.titlebar_transparent { + window.setTitlebarAppearsTransparent(true); + } + if attrs.platform_specific.title_hidden { + window.setTitleVisibility(NSWindowTitleVisibility::NSWindowTitleHidden); + } + if attrs.platform_specific.titlebar_buttons_hidden { + for titlebar_button in &[ + #[allow(deprecated)] + NSWindowFullScreenButton, + NSWindowButton::NSWindowMiniaturizeButton, + NSWindowButton::NSWindowCloseButton, + NSWindowButton::NSWindowZoomButton, + ] { + if let Some(button) = window.standardWindowButton(*titlebar_button) { + button.setHidden(true); + } + } + } + if attrs.platform_specific.movable_by_window_background { + window.setMovableByWindowBackground(true); + } + + if !attrs.enabled_buttons.contains(WindowButtons::MAXIMIZE) { + if let Some(button) = window.standardWindowButton(NSWindowButton::NSWindowZoomButton) { + button.setEnabled(false); + } + } + + if !attrs.platform_specific.has_shadow { + window.setHasShadow(false); + } + if attrs.position.is_none() { + window.center(); + } + + let view = WinitView::new( + app_delegate, + &window, + attrs.platform_specific.accepts_first_mouse, + attrs.platform_specific.option_as_alt, + ); + + // The default value of `setWantsBestResolutionOpenGLSurface:` was `false` until + // macos 10.14 and `true` after 10.15, we should set it to `YES` or `NO` to avoid + // always the default system value in favour of the user's code + #[allow(deprecated)] + view.setWantsBestResolutionOpenGLSurface(!attrs.platform_specific.disallow_hidpi); + + // On Mojave, views automatically become layer-backed shortly after being added to + // a window. Changing the layer-backedness of a view breaks the association between + // the view and its associated OpenGL context. To work around this, on Mojave we + // explicitly make the view layer-backed up front so that AppKit doesn't do it + // itself and break the association with its context. + if unsafe { NSAppKitVersionNumber }.floor() > NSAppKitVersionNumber10_12 { + view.setWantsLayer(true); + } + + // Configure the new view as the "key view" for the window + window.setContentView(Some(&view)); + window.setInitialFirstResponder(Some(&view)); + + if attrs.transparent { + window.setOpaque(false); + // See `set_transparent` for details on why we do this. + window.setBackgroundColor(unsafe { Some(&NSColor::clearColor()) }); + } + + // register for drag and drop operations. + window + .registerForDraggedTypes(&NSArray::from_id_slice(&[ + unsafe { NSFilenamesPboardType }.copy() + ])); + + Some(window) + }) +} + +impl WindowDelegate { + pub(super) fn new( + app_delegate: &ApplicationDelegate, + attrs: WindowAttributes, + mtm: MainThreadMarker, + ) -> Result, RootOsError> { + let window = new_window(app_delegate, &attrs, mtm) + .ok_or_else(|| os_error!(OsError::CreationError("couldn't create `NSWindow`")))?; + + #[cfg(feature = "rwh_06")] + match attrs.parent_window.map(|handle| handle.0) { + Some(rwh_06::RawWindowHandle::AppKit(handle)) => { + // SAFETY: Caller ensures the pointer is valid or NULL + // Unwrap is fine, since the pointer comes from `NonNull`. + let parent_view: Retained = + unsafe { Retained::retain(handle.ns_view.as_ptr().cast()) }.unwrap(); + let parent = parent_view.window().ok_or_else(|| { + os_error!(OsError::CreationError("parent view should be installed in a window")) + })?; + + // SAFETY: We know that there are no parent -> child -> parent cycles since the only + // place in `winit` where we allow making a window a child window is + // right here, just after it's been created. + unsafe { + parent.addChildWindow_ordered(&window, NSWindowOrderingMode::NSWindowAbove) + }; + }, + Some(raw) => panic!("invalid raw window handle {raw:?} on macOS"), + None => (), + } + + let resize_increments = + match attrs.resize_increments.map(|i| i.to_logical(window.backingScaleFactor() as _)) { + Some(LogicalSize { width, height }) if width >= 1. && height >= 1. => { + NSSize::new(width, height) + }, + _ => NSSize::new(1., 1.), + }; + + let scale_factor = window.backingScaleFactor() as _; + + if let Some(appearance) = theme_to_appearance(attrs.preferred_theme) { + unsafe { window.setAppearance(Some(&appearance)) }; + } + + let delegate = mtm.alloc().set_ivars(State { + app_delegate: app_delegate.retain(), + window: window.retain(), + previous_position: Cell::new(flip_window_screen_coordinates(window.frame())), + previous_scale_factor: Cell::new(scale_factor), + resize_increments: Cell::new(resize_increments), + decorations: Cell::new(attrs.decorations), + resizable: Cell::new(attrs.resizable), + maximized: Cell::new(attrs.maximized), + save_presentation_opts: Cell::new(None), + initial_fullscreen: Cell::new(attrs.fullscreen.is_some()), + fullscreen: RefCell::new(None), + target_fullscreen: RefCell::new(None), + in_fullscreen_transition: Cell::new(false), + standard_frame: Cell::new(None), + is_simple_fullscreen: Cell::new(false), + saved_style: Cell::new(None), + is_borderless_game: Cell::new(attrs.platform_specific.borderless_game), + }); + let delegate: Retained = unsafe { msg_send_id![super(delegate), init] }; + + if scale_factor != 1.0 { + let delegate = delegate.clone(); + RunLoop::main(mtm).queue_closure(move || { + delegate.handle_scale_factor_changed(scale_factor); + }); + } + window.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); + + // Listen for theme change event. + // + // SAFETY: The observer is un-registered in the `Drop` of the delegate. + unsafe { + window.addObserver_forKeyPath_options_context( + &delegate, + ns_string!("effectiveAppearance"), + NSKeyValueObservingOptions::NSKeyValueObservingOptionNew + | NSKeyValueObservingOptions::NSKeyValueObservingOptionOld, + ptr::null_mut(), + ) + }; + + if attrs.blur { + delegate.set_blur(attrs.blur); + } + + if let Some(dim) = attrs.min_inner_size { + delegate.set_min_inner_size(Some(dim)); + } + if let Some(dim) = attrs.max_inner_size { + delegate.set_max_inner_size(Some(dim)); + } + + delegate.set_window_level(attrs.window_level); + + delegate.set_cursor(attrs.cursor); + + // XXX Send `Focused(false)` right after creating the window delegate, so we won't + // obscure the real focused events on the startup. + delegate.queue_event(WindowEvent::Focused(false)); + + // Set fullscreen mode after we setup everything + delegate.set_fullscreen(attrs.fullscreen.map(Into::into)); + + // Setting the window as key has to happen *after* we set the fullscreen + // state, since otherwise we'll briefly see the window at normal size + // before it transitions. + if attrs.visible { + if attrs.active { + // Tightly linked with `app_state::window_activation_hack` + window.makeKeyAndOrderFront(None); + } else { + window.orderFront(None); + } + } + + if attrs.maximized { + delegate.set_maximized(attrs.maximized); + } + + Ok(delegate) + } + + #[track_caller] + pub(super) fn view(&self) -> Retained { + // SAFETY: The view inside WinitWindow is always `WinitView` + unsafe { Retained::cast(self.window().contentView().unwrap()) } + } + + #[track_caller] + pub(super) fn window(&self) -> &WinitWindow { + &self.ivars().window + } + + #[track_caller] + pub(crate) fn id(&self) -> WindowId { + self.window().id() + } + + pub(crate) fn queue_event(&self, event: WindowEvent) { + self.ivars().app_delegate.maybe_queue_window_event(self.window().id(), event); + } + + fn handle_scale_factor_changed(&self, scale_factor: CGFloat) { + let app_delegate = &self.ivars().app_delegate; + let window = self.window(); + + let content_size = window.contentRectForFrameRect(window.frame()).size; + let content_size = LogicalSize::new(content_size.width, content_size.height); + + let suggested_size = content_size.to_physical(scale_factor); + let new_inner_size = Arc::new(Mutex::new(suggested_size)); + app_delegate.handle_window_event(window.id(), WindowEvent::ScaleFactorChanged { + scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&new_inner_size)), + }); + let physical_size = *new_inner_size.lock().unwrap(); + drop(new_inner_size); + + if physical_size != suggested_size { + let logical_size = physical_size.to_logical(scale_factor); + let size = NSSize::new(logical_size.width, logical_size.height); + window.setContentSize(size); + } + app_delegate.handle_window_event(window.id(), WindowEvent::Resized(physical_size)); + } + + fn emit_move_event(&self) { + let position = flip_window_screen_coordinates(self.window().frame()); + if self.ivars().previous_position.get() == position { + return; + } + self.ivars().previous_position.set(position); + + let position = + LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor()); + self.queue_event(WindowEvent::Moved(position)); + } + + fn set_style_mask(&self, mask: NSWindowStyleMask) { + self.window().setStyleMask(mask); + // If we don't do this, key handling will break + // (at least until the window is clicked again/etc.) + let _ = self.window().makeFirstResponder(Some(&self.view())); + } + + pub fn set_title(&self, title: &str) { + self.window().setTitle(&NSString::from_str(title)) + } + + pub fn set_transparent(&self, transparent: bool) { + // This is just a hint for Quartz, it doesn't actually speculate with window alpha. + // Providing a wrong value here could result in visual artifacts, when the window is + // transparent. + self.window().setOpaque(!transparent); + + // AppKit draws the window with a background color by default, which is usually really + // nice, but gets in the way when we want to allow the contents of the window to be + // transparent, as in that case, the transparent contents will just be drawn on top of + // the background color. As such, to allow the window to be transparent, we must also set + // the background color to one with an empty alpha channel. + let color = if transparent { + unsafe { NSColor::clearColor() } + } else { + unsafe { NSColor::windowBackgroundColor() } + }; + + self.window().setBackgroundColor(Some(&color)); + } + + pub fn set_blur(&self, blur: bool) { + // NOTE: in general we want to specify the blur radius, but the choice of 80 + // should be a reasonable default. + let radius = if blur { 80 } else { 0 }; + let window_number = unsafe { self.window().windowNumber() }; + unsafe { + ffi::CGSSetWindowBackgroundBlurRadius( + ffi::CGSMainConnectionID(), + window_number, + radius, + ); + } + } + + pub fn set_visible(&self, visible: bool) { + match visible { + true => self.window().makeKeyAndOrderFront(None), + false => self.window().orderOut(None), + } + } + + #[inline] + pub fn is_visible(&self) -> Option { + Some(self.window().isVisible()) + } + + pub fn request_redraw(&self) { + self.ivars().app_delegate.queue_redraw(self.window().id()); + } + + #[inline] + pub fn pre_present_notify(&self) {} + + pub fn outer_position(&self) -> Result, NotSupportedError> { + let position = flip_window_screen_coordinates(self.window().frame()); + Ok(LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor())) + } + + pub fn inner_position(&self) -> Result, NotSupportedError> { + let content_rect = self.window().contentRectForFrameRect(self.window().frame()); + let position = flip_window_screen_coordinates(content_rect); + Ok(LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor())) + } + + pub fn set_outer_position(&self, position: Position) { + let position = position.to_logical(self.scale_factor()); + let point = flip_window_screen_coordinates(NSRect::new( + NSPoint::new(position.x, position.y), + self.window().frame().size, + )); + unsafe { self.window().setFrameOrigin(point) }; + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + let content_rect = self.window().contentRectForFrameRect(self.window().frame()); + let logical = LogicalSize::new(content_rect.size.width, content_rect.size.height); + logical.to_physical(self.scale_factor()) + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + let frame = self.window().frame(); + let logical = LogicalSize::new(frame.size.width, frame.size.height); + logical.to_physical(self.scale_factor()) + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + let scale_factor = self.scale_factor(); + let size = size.to_logical(scale_factor); + self.window().setContentSize(NSSize::new(size.width, size.height)); + None + } + + pub fn set_min_inner_size(&self, dimensions: Option) { + let dimensions = + dimensions.unwrap_or(Size::Logical(LogicalSize { width: 0.0, height: 0.0 })); + let min_size = dimensions.to_logical::(self.scale_factor()); + + let min_size = NSSize::new(min_size.width, min_size.height); + unsafe { self.window().setContentMinSize(min_size) }; + + // If necessary, resize the window to match constraint + let mut current_size = self.window().contentRectForFrameRect(self.window().frame()).size; + if current_size.width < min_size.width { + current_size.width = min_size.width; + } + if current_size.height < min_size.height { + current_size.height = min_size.height; + } + self.window().setContentSize(current_size); + } + + pub fn set_max_inner_size(&self, dimensions: Option) { + let dimensions = dimensions.unwrap_or(Size::Logical(LogicalSize { + width: f32::MAX as f64, + height: f32::MAX as f64, + })); + let scale_factor = self.scale_factor(); + let max_size = dimensions.to_logical::(scale_factor); + + let max_size = NSSize::new(max_size.width, max_size.height); + unsafe { self.window().setContentMaxSize(max_size) }; + + // If necessary, resize the window to match constraint + let mut current_size = self.window().contentRectForFrameRect(self.window().frame()).size; + if max_size.width < current_size.width { + current_size.width = max_size.width; + } + if max_size.height < current_size.height { + current_size.height = max_size.height; + } + self.window().setContentSize(current_size); + } + + pub fn resize_increments(&self) -> Option> { + let increments = self.ivars().resize_increments.get(); + let (w, h) = (increments.width, increments.height); + if w > 1.0 || h > 1.0 { + Some(LogicalSize::new(w, h).to_physical(self.scale_factor())) + } else { + None + } + } + + pub fn set_resize_increments(&self, increments: Option) { + // XXX the resize increments are only used during live resizes. + self.ivars().resize_increments.set( + increments + .map(|increments| { + let logical = increments.to_logical::(self.scale_factor()); + NSSize::new(logical.width.max(1.0), logical.height.max(1.0)) + }) + .unwrap_or_else(|| NSSize::new(1.0, 1.0)), + ); + } + + pub(crate) fn set_resize_increments_inner(&self, size: NSSize) { + // It was concluded (#2411) that there is never a use-case for + // "outer" resize increments, hence we set "inner" ones here. + // ("outer" in macOS being just resizeIncrements, and "inner" - contentResizeIncrements) + // This is consistent with X11 size hints behavior + self.window().setContentResizeIncrements(size); + } + + #[inline] + pub fn set_resizable(&self, resizable: bool) { + self.ivars().resizable.set(resizable); + let fullscreen = self.ivars().fullscreen.borrow().is_some(); + if !fullscreen { + let mut mask = self.window().styleMask(); + if resizable { + mask |= NSWindowStyleMask::Resizable; + } else { + mask &= !NSWindowStyleMask::Resizable; + } + self.set_style_mask(mask); + } + // Otherwise, we don't change the mask until we exit fullscreen. + } + + #[inline] + pub fn is_resizable(&self) -> bool { + self.window().isResizable() + } + + #[inline] + pub fn set_enabled_buttons(&self, buttons: WindowButtons) { + let mut mask = self.window().styleMask(); + + if buttons.contains(WindowButtons::CLOSE) { + mask |= NSWindowStyleMask::Closable; + } else { + mask &= !NSWindowStyleMask::Closable; + } + + if buttons.contains(WindowButtons::MINIMIZE) { + mask |= NSWindowStyleMask::Miniaturizable; + } else { + mask &= !NSWindowStyleMask::Miniaturizable; + } + + // This must happen before the button's "enabled" status has been set, + // hence we do it synchronously. + self.set_style_mask(mask); + + // We edit the button directly instead of using `NSResizableWindowMask`, + // since that mask also affect the resizability of the window (which is + // controllable by other means in `winit`). + if let Some(button) = self.window().standardWindowButton(NSWindowButton::NSWindowZoomButton) + { + button.setEnabled(buttons.contains(WindowButtons::MAXIMIZE)); + } + } + + #[inline] + pub fn enabled_buttons(&self) -> WindowButtons { + let mut buttons = WindowButtons::empty(); + if self.window().isMiniaturizable() { + buttons |= WindowButtons::MINIMIZE; + } + if self + .window() + .standardWindowButton(NSWindowButton::NSWindowZoomButton) + .map(|b| b.isEnabled()) + .unwrap_or(true) + { + buttons |= WindowButtons::MAXIMIZE; + } + if self.window().hasCloseBox() { + buttons |= WindowButtons::CLOSE; + } + buttons + } + + pub fn set_cursor(&self, cursor: Cursor) { + let view = self.view(); + + let cursor = match cursor { + Cursor::Icon(icon) => cursor_from_icon(icon), + Cursor::Custom(cursor) => cursor.inner.0, + }; + + if view.cursor_icon() == cursor { + return; + } + + view.set_cursor_icon(cursor); + self.window().invalidateCursorRectsForView(&view); + } + + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let associate_mouse_cursor = match mode { + CursorGrabMode::Locked => false, + CursorGrabMode::None => true, + CursorGrabMode::Confined => { + return Err(ExternalError::NotSupported(NotSupportedError::new())) + }, + }; + + // TODO: Do this for real https://stackoverflow.com/a/40922095/5435443 + CGDisplay::associate_mouse_and_mouse_cursor_position(associate_mouse_cursor) + .map_err(|status| ExternalError::Os(os_error!(OsError::CGError(status)))) + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + let view = self.view(); + let state_changed = view.set_cursor_visible(visible); + if state_changed { + self.window().invalidateCursorRectsForView(&view); + } + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + self.window().backingScaleFactor() as _ + } + + #[inline] + pub fn set_cursor_position(&self, cursor_position: Position) -> Result<(), ExternalError> { + let physical_window_position = self.inner_position().unwrap(); + let scale_factor = self.scale_factor(); + let window_position = physical_window_position.to_logical::(scale_factor); + let logical_cursor_position = cursor_position.to_logical::(scale_factor); + let point = CGPoint { + x: logical_cursor_position.x + window_position.x, + y: logical_cursor_position.y + window_position.y, + }; + CGDisplay::warp_mouse_cursor_position(point) + .map_err(|e| ExternalError::Os(os_error!(OsError::CGError(e))))?; + CGDisplay::associate_mouse_and_mouse_cursor_position(true) + .map_err(|e| ExternalError::Os(os_error!(OsError::CGError(e))))?; + + Ok(()) + } + + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + let mtm = MainThreadMarker::from(self); + let event = + NSApplication::sharedApplication(mtm).currentEvent().ok_or(ExternalError::Ignored)?; + self.window().performWindowDragWithEvent(&event); + Ok(()) + } + + #[inline] + pub fn drag_resize_window(&self, _direction: ResizeDirection) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + self.window().setIgnoresMouseEvents(!hittest); + Ok(()) + } + + pub(crate) fn is_zoomed(&self) -> bool { + // because `isZoomed` doesn't work if the window's borderless, + // we make it resizable temporarily. + let curr_mask = self.window().styleMask(); + + let required = NSWindowStyleMask::Titled | NSWindowStyleMask::Resizable; + let needs_temp_mask = !curr_mask.contains(required); + if needs_temp_mask { + self.set_style_mask(required); + } + + let is_zoomed = self.window().isZoomed(); + + // Roll back temp styles + if needs_temp_mask { + self.set_style_mask(curr_mask); + } + + is_zoomed + } + + fn saved_style(&self) -> NSWindowStyleMask { + let base_mask = + self.ivars().saved_style.take().unwrap_or_else(|| self.window().styleMask()); + if self.ivars().resizable.get() { + base_mask | NSWindowStyleMask::Resizable + } else { + base_mask & !NSWindowStyleMask::Resizable + } + } + + /// This is called when the window is exiting fullscreen, whether by the + /// user clicking on the green fullscreen button or programmatically by + /// `toggleFullScreen:` + pub(crate) fn restore_state_from_fullscreen(&self) { + self.ivars().fullscreen.replace(None); + + let maximized = self.ivars().maximized.get(); + let mask = self.saved_style(); + + self.set_style_mask(mask); + self.set_maximized(maximized); + } + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + let is_minimized = self.window().isMiniaturized(); + if is_minimized == minimized { + return; + } + + if minimized { + self.window().miniaturize(Some(self)); + } else { + unsafe { self.window().deminiaturize(Some(self)) }; + } + } + + #[inline] + pub fn is_minimized(&self) -> Option { + Some(self.window().isMiniaturized()) + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + let mtm = MainThreadMarker::from(self); + let is_zoomed = self.is_zoomed(); + if is_zoomed == maximized { + return; + }; + + // Save the standard frame sized if it is not zoomed + if !is_zoomed { + self.ivars().standard_frame.set(Some(self.window().frame())); + } + + self.ivars().maximized.set(maximized); + + if self.ivars().fullscreen.borrow().is_some() { + // Handle it in window_did_exit_fullscreen + return; + } + + if self.window().styleMask().contains(NSWindowStyleMask::Resizable) { + // Just use the native zoom if resizable + self.window().zoom(None); + } else { + // if it's not resizable, we set the frame directly + let new_rect = if maximized { + let screen = NSScreen::mainScreen(mtm).expect("no screen found"); + screen.visibleFrame() + } else { + self.ivars().standard_frame.get().unwrap_or(DEFAULT_STANDARD_FRAME) + }; + self.window().setFrame_display(new_rect, false); + } + } + + #[inline] + pub(crate) fn fullscreen(&self) -> Option { + self.ivars().fullscreen.borrow().clone() + } + + #[inline] + pub fn is_maximized(&self) -> bool { + self.is_zoomed() + } + + #[inline] + pub(crate) fn set_fullscreen(&self, fullscreen: Option) { + let mtm = MainThreadMarker::from(self); + let app = NSApplication::sharedApplication(mtm); + + if self.ivars().is_simple_fullscreen.get() { + return; + } + if self.ivars().in_fullscreen_transition.get() { + // We can't set fullscreen here. + // Set fullscreen after transition. + self.ivars().target_fullscreen.replace(Some(fullscreen)); + return; + } + let old_fullscreen = self.ivars().fullscreen.borrow().clone(); + if fullscreen == old_fullscreen { + return; + } + + // If the fullscreen is on a different monitor, we must move the window + // to that monitor before we toggle fullscreen (as `toggleFullScreen` + // does not take a screen parameter, but uses the current screen) + if let Some(ref fullscreen) = fullscreen { + let new_screen = match fullscreen { + Fullscreen::Borderless(Some(monitor)) => monitor.clone(), + Fullscreen::Borderless(None) => { + if let Some(monitor) = self.current_monitor_inner() { + monitor + } else { + return; + } + }, + Fullscreen::Exclusive(video_mode) => video_mode.monitor(), + } + .ns_screen(mtm) + .unwrap(); + + let old_screen = self.window().screen().unwrap(); + if old_screen != new_screen { + unsafe { self.window().setFrameOrigin(new_screen.frame().origin) }; + } + } + + if let Some(Fullscreen::Exclusive(ref video_mode)) = fullscreen { + // Note: `enterFullScreenMode:withOptions:` seems to do the exact + // same thing as we're doing here (captures the display, sets the + // video mode, and hides the menu bar and dock), with the exception + // of that I couldn't figure out how to set the display mode with + // it. I think `enterFullScreenMode:withOptions:` is still using the + // older display mode API where display modes were of the type + // `CFDictionary`, but this has changed, so we can't obtain the + // correct parameter for this any longer. Apple's code samples for + // this function seem to just pass in "YES" for the display mode + // parameter, which is not consistent with the docs saying that it + // takes a `NSDictionary`.. + + let display_id = video_mode.monitor().native_identifier(); + + let mut fade_token = ffi::kCGDisplayFadeReservationInvalidToken; + + if matches!(old_fullscreen, Some(Fullscreen::Borderless(_))) { + self.ivars().save_presentation_opts.replace(Some(app.presentationOptions())); + } + + unsafe { + // Fade to black (and wait for the fade to complete) to hide the + // flicker from capturing the display and switching display mode + if ffi::CGAcquireDisplayFadeReservation(5.0, &mut fade_token) + == ffi::kCGErrorSuccess + { + ffi::CGDisplayFade( + fade_token, + 0.3, + ffi::kCGDisplayBlendNormal, + ffi::kCGDisplayBlendSolidColor, + 0.0, + 0.0, + 0.0, + ffi::TRUE, + ); + } + + assert_eq!(ffi::CGDisplayCapture(display_id), ffi::kCGErrorSuccess); + } + + unsafe { + let result = ffi::CGDisplaySetDisplayMode( + display_id, + video_mode.native_mode.0, + std::ptr::null(), + ); + assert!(result == ffi::kCGErrorSuccess, "failed to set video mode"); + + // After the display has been configured, fade back in + // asynchronously + if fade_token != ffi::kCGDisplayFadeReservationInvalidToken { + ffi::CGDisplayFade( + fade_token, + 0.6, + ffi::kCGDisplayBlendSolidColor, + ffi::kCGDisplayBlendNormal, + 0.0, + 0.0, + 0.0, + ffi::FALSE, + ); + ffi::CGReleaseDisplayFadeReservation(fade_token); + } + } + } + + self.ivars().fullscreen.replace(fullscreen.clone()); + + fn toggle_fullscreen(window: &WinitWindow) { + // Window level must be restored from `CGShieldingWindowLevel() + // + 1` back to normal in order for `toggleFullScreen` to do + // anything + window.setLevel(ffi::kCGNormalWindowLevel as NSWindowLevel); + window.toggleFullScreen(None); + } + + match (old_fullscreen, fullscreen) { + (None, Some(fullscreen)) => { + // `toggleFullScreen` doesn't work if the `StyleMask` is none, so we + // set a normal style temporarily. The previous state will be + // restored in `WindowDelegate::window_did_exit_fullscreen`. + let curr_mask = self.window().styleMask(); + let required = NSWindowStyleMask::Titled | NSWindowStyleMask::Resizable; + if !curr_mask.contains(required) { + self.set_style_mask(required); + self.ivars().saved_style.set(Some(curr_mask)); + } + + // In borderless games, we want to disable the dock and menu bar + // by setting the presentation options. We do this here rather than in + // `window:willUseFullScreenPresentationOptions` because for some reason + // the menu bar remains interactable despite being hidden. + if self.is_borderless_game() && matches!(fullscreen, Fullscreen::Borderless(_)) { + let presentation_options = NSApplicationPresentationOptions::NSApplicationPresentationHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar; + app.setPresentationOptions(presentation_options); + } + + toggle_fullscreen(self.window()); + }, + (Some(Fullscreen::Borderless(_)), None) => { + // State is restored by `window_did_exit_fullscreen` + toggle_fullscreen(self.window()); + }, + (Some(Fullscreen::Exclusive(ref video_mode)), None) => { + restore_and_release_display(&video_mode.monitor()); + toggle_fullscreen(self.window()); + }, + (Some(Fullscreen::Borderless(_)), Some(Fullscreen::Exclusive(_))) => { + // If we're already in fullscreen mode, calling + // `CGDisplayCapture` will place the shielding window on top of + // our window, which results in a black display and is not what + // we want. So, we must place our window on top of the shielding + // window. Unfortunately, this also makes our window be on top + // of the menu bar, and this looks broken, so we must make sure + // that the menu bar is disabled. This is done in the window + // delegate in `window:willUseFullScreenPresentationOptions:`. + self.ivars().save_presentation_opts.set(Some(app.presentationOptions())); + + let presentation_options = + NSApplicationPresentationOptions::NSApplicationPresentationFullScreen + | NSApplicationPresentationOptions::NSApplicationPresentationHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar; + app.setPresentationOptions(presentation_options); + + let window_level = unsafe { ffi::CGShieldingWindowLevel() } as NSWindowLevel + 1; + self.window().setLevel(window_level); + }, + (Some(Fullscreen::Exclusive(ref video_mode)), Some(Fullscreen::Borderless(_))) => { + let presentation_options = self.ivars().save_presentation_opts.get().unwrap_or( + NSApplicationPresentationOptions::NSApplicationPresentationFullScreen + | NSApplicationPresentationOptions::NSApplicationPresentationAutoHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationAutoHideMenuBar + ); + app.setPresentationOptions(presentation_options); + + restore_and_release_display(&video_mode.monitor()); + + // Restore the normal window level following the Borderless fullscreen + // `CGShieldingWindowLevel() + 1` hack. + self.window().setLevel(ffi::kCGNormalWindowLevel as NSWindowLevel); + }, + _ => {}, + }; + } + + #[inline] + pub fn set_decorations(&self, decorations: bool) { + if decorations == self.ivars().decorations.get() { + return; + } + + self.ivars().decorations.set(decorations); + + let fullscreen = self.ivars().fullscreen.borrow().is_some(); + let resizable = self.ivars().resizable.get(); + + // If we're in fullscreen mode, we wait to apply decoration changes + // until we're in `window_did_exit_fullscreen`. + if fullscreen { + return; + } + + let new_mask = { + let mut new_mask = if decorations { + NSWindowStyleMask::Closable + | NSWindowStyleMask::Miniaturizable + | NSWindowStyleMask::Resizable + | NSWindowStyleMask::Titled + } else { + NSWindowStyleMask::Borderless | NSWindowStyleMask::Resizable + }; + if !resizable { + new_mask &= !NSWindowStyleMask::Resizable; + } + new_mask + }; + self.set_style_mask(new_mask); + } + + #[inline] + pub fn is_decorated(&self) -> bool { + self.ivars().decorations.get() + } + + #[inline] + pub fn set_window_level(&self, level: WindowLevel) { + let level = match level { + WindowLevel::AlwaysOnTop => ffi::kCGFloatingWindowLevel as NSWindowLevel, + WindowLevel::AlwaysOnBottom => (ffi::kCGNormalWindowLevel - 1) as NSWindowLevel, + WindowLevel::Normal => ffi::kCGNormalWindowLevel as NSWindowLevel, + }; + self.window().setLevel(level); + } + + #[inline] + pub fn set_window_icon(&self, _icon: Option) { + // macOS doesn't have window icons. Though, there is + // `setRepresentedFilename`, but that's semantically distinct and should + // only be used when the window is in some way representing a specific + // file/directory. For instance, Terminal.app uses this for the CWD. + // Anyway, that should eventually be implemented as + // `WindowAttributesExt::with_represented_file` or something, and doesn't + // have anything to do with `set_window_icon`. + // https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/WinPanel/Tasks/SettingWindowTitle.html + } + + #[inline] + pub fn set_ime_cursor_area(&self, spot: Position, size: Size) { + let scale_factor = self.scale_factor(); + let logical_spot = spot.to_logical(scale_factor); + let logical_spot = NSPoint::new(logical_spot.x, logical_spot.y); + + let size = size.to_logical(scale_factor); + let size = NSSize::new(size.width, size.height); + + self.view().set_ime_cursor_area(logical_spot, size); + } + + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + self.view().set_ime_allowed(allowed); + } + + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + + #[inline] + pub fn focus_window(&self) { + let mtm = MainThreadMarker::from(self); + let is_minimized = self.window().isMiniaturized(); + let is_visible = self.window().isVisible(); + + if !is_minimized && is_visible { + #[allow(deprecated)] + NSApplication::sharedApplication(mtm).activateIgnoringOtherApps(true); + self.window().makeKeyAndOrderFront(None); + } + } + + #[inline] + pub fn request_user_attention(&self, request_type: Option) { + let mtm = MainThreadMarker::from(self); + let ns_request_type = request_type.map(|ty| match ty { + UserAttentionType::Critical => NSRequestUserAttentionType::NSCriticalRequest, + UserAttentionType::Informational => NSRequestUserAttentionType::NSInformationalRequest, + }); + if let Some(ty) = ns_request_type { + NSApplication::sharedApplication(mtm).requestUserAttention(ty); + } + } + + #[inline] + // Allow directly accessing the current monitor internally without unwrapping. + pub(crate) fn current_monitor_inner(&self) -> Option { + let display_id = get_display_id(&*self.window().screen()?); + if let Some(monitor) = MonitorHandle::new(display_id) { + Some(monitor) + } else { + // NOTE: Display ID was just fetched from live NSScreen, but can still result in `None` + // with certain Thunderbolt docked monitors. + warn!(display_id, "got screen with invalid display ID"); + None + } + } + + #[inline] + pub fn current_monitor(&self) -> Option { + self.current_monitor_inner() + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + monitor::available_monitors() + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + let monitor = monitor::primary_monitor(); + Some(monitor) + } + + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut window_handle = rwh_04::AppKitHandle::empty(); + window_handle.ns_window = self.window() as *const WinitWindow as *mut _; + window_handle.ns_view = Retained::as_ptr(&self.view()) as *mut _; + rwh_04::RawWindowHandle::AppKit(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut window_handle = rwh_05::AppKitWindowHandle::empty(); + window_handle.ns_window = self.window() as *const WinitWindow as *mut _; + window_handle.ns_view = Retained::as_ptr(&self.view()) as *mut _; + rwh_05::RawWindowHandle::AppKit(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::AppKit(rwh_05::AppKitDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> rwh_06::RawWindowHandle { + let window_handle = rwh_06::AppKitWindowHandle::new({ + let ptr = Retained::as_ptr(&self.view()) as *mut _; + std::ptr::NonNull::new(ptr).expect("Retained should never be null") + }); + rwh_06::RawWindowHandle::AppKit(window_handle) + } + + fn toggle_style_mask(&self, mask: NSWindowStyleMask, on: bool) { + let current_style_mask = self.window().styleMask(); + if on { + self.set_style_mask(current_style_mask | mask); + } else { + self.set_style_mask(current_style_mask & !mask); + } + } + + #[inline] + pub fn has_focus(&self) -> bool { + self.window().isKeyWindow() + } + + pub fn theme(&self) -> Option { + unsafe { self.window().appearance() } + .map(|appearance| appearance_to_theme(&appearance)) + .or_else(|| { + let mtm = MainThreadMarker::from(self); + let app = NSApplication::sharedApplication(mtm); + + if app.respondsToSelector(sel!(effectiveAppearance)) { + Some(super::window_delegate::appearance_to_theme(&app.effectiveAppearance())) + } else { + Some(Theme::Light) + } + }) + } + + pub fn set_theme(&self, theme: Option) { + unsafe { self.window().setAppearance(theme_to_appearance(theme).as_deref()) }; + } + + #[inline] + pub fn set_content_protected(&self, protected: bool) { + self.window().setSharingType(if protected { + NSWindowSharingType::NSWindowSharingNone + } else { + NSWindowSharingType::NSWindowSharingReadOnly + }) + } + + pub fn title(&self) -> String { + self.window().title().to_string() + } + + pub fn reset_dead_keys(&self) { + // (Artur) I couldn't find a way to implement this. + } +} + +fn restore_and_release_display(monitor: &MonitorHandle) { + let available_monitors = monitor::available_monitors(); + if available_monitors.contains(monitor) { + unsafe { + ffi::CGRestorePermanentDisplayConfiguration(); + assert_eq!(ffi::CGDisplayRelease(monitor.native_identifier()), ffi::kCGErrorSuccess); + }; + } else { + warn!( + monitor = monitor.name(), + "Tried to restore exclusive fullscreen on a monitor that is no longer available" + ); + } +} + +impl WindowExtMacOS for WindowDelegate { + #[inline] + fn simple_fullscreen(&self) -> bool { + self.ivars().is_simple_fullscreen.get() + } + + #[inline] + fn set_simple_fullscreen(&self, fullscreen: bool) -> bool { + let mtm = MainThreadMarker::from(self); + + let app = NSApplication::sharedApplication(mtm); + let is_native_fullscreen = self.ivars().fullscreen.borrow().is_some(); + let is_simple_fullscreen = self.ivars().is_simple_fullscreen.get(); + + // Do nothing if native fullscreen is active. + if is_native_fullscreen + || (fullscreen && is_simple_fullscreen) + || (!fullscreen && !is_simple_fullscreen) + { + return false; + } + + if fullscreen { + // Remember the original window's settings + // Exclude title bar + self.ivars() + .standard_frame + .set(Some(self.window().contentRectForFrameRect(self.window().frame()))); + self.ivars().saved_style.set(Some(self.window().styleMask())); + self.ivars().save_presentation_opts.set(Some(app.presentationOptions())); + + // Tell our window's state that we're in fullscreen + self.ivars().is_simple_fullscreen.set(true); + + // Simulate pre-Lion fullscreen by hiding the dock and menu bar + let presentation_options = if self.is_borderless_game() { + NSApplicationPresentationOptions::NSApplicationPresentationHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar + } else { + NSApplicationPresentationOptions::NSApplicationPresentationAutoHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationAutoHideMenuBar + }; + app.setPresentationOptions(presentation_options); + + // Hide the titlebar + self.toggle_style_mask(NSWindowStyleMask::Titled, false); + + // Set the window frame to the screen frame size + let screen = self.window().screen().expect("expected screen to be available"); + self.window().setFrame_display(screen.frame(), true); + + // Fullscreen windows can't be resized, minimized, or moved + self.toggle_style_mask(NSWindowStyleMask::Miniaturizable, false); + self.toggle_style_mask(NSWindowStyleMask::Resizable, false); + self.window().setMovable(false); + } else { + let new_mask = self.saved_style(); + self.ivars().is_simple_fullscreen.set(false); + + let save_presentation_opts = self.ivars().save_presentation_opts.get(); + let frame = self.ivars().standard_frame.get().unwrap_or(DEFAULT_STANDARD_FRAME); + + if let Some(presentation_opts) = save_presentation_opts { + app.setPresentationOptions(presentation_opts); + } + + self.window().setFrame_display(frame, true); + self.window().setMovable(true); + self.set_style_mask(new_mask); + } + + true + } + + #[inline] + fn has_shadow(&self) -> bool { + self.window().hasShadow() + } + + #[inline] + fn set_has_shadow(&self, has_shadow: bool) { + self.window().setHasShadow(has_shadow) + } + + #[inline] + fn set_tabbing_identifier(&self, identifier: &str) { + self.window().setTabbingIdentifier(&NSString::from_str(identifier)) + } + + #[inline] + fn tabbing_identifier(&self) -> String { + self.window().tabbingIdentifier().to_string() + } + + #[inline] + fn select_next_tab(&self) { + self.window().selectNextTab(None) + } + + #[inline] + fn select_previous_tab(&self) { + unsafe { self.window().selectPreviousTab(None) } + } + + #[inline] + fn select_tab_at_index(&self, index: usize) { + if let Some(group) = self.window().tabGroup() { + if let Some(windows) = unsafe { self.window().tabbedWindows() } { + if index < windows.len() { + group.setSelectedWindow(Some(&windows[index])); + } + } + } + } + + #[inline] + fn num_tabs(&self) -> usize { + unsafe { self.window().tabbedWindows() }.map(|windows| windows.len()).unwrap_or(1) + } + + fn is_document_edited(&self) -> bool { + self.window().isDocumentEdited() + } + + fn set_document_edited(&self, edited: bool) { + self.window().setDocumentEdited(edited) + } + + fn set_option_as_alt(&self, option_as_alt: OptionAsAlt) { + self.view().set_option_as_alt(option_as_alt); + } + + fn option_as_alt(&self) -> OptionAsAlt { + self.view().option_as_alt() + } + + fn set_borderless_game(&self, borderless_game: bool) { + self.ivars().is_borderless_game.set(borderless_game); + } + + fn is_borderless_game(&self) -> bool { + self.ivars().is_borderless_game.get() + } +} + +const DEFAULT_STANDARD_FRAME: NSRect = + NSRect::new(NSPoint::new(50.0, 50.0), NSSize::new(800.0, 600.0)); + +fn dark_appearance_name() -> &'static NSString { + // Don't use the static `NSAppearanceNameDarkAqua` to allow linking on macOS < 10.14 + ns_string!("NSAppearanceNameDarkAqua") +} + +pub fn appearance_to_theme(appearance: &NSAppearance) -> Theme { + let best_match = appearance.bestMatchFromAppearancesWithNames(&NSArray::from_id_slice(&[ + unsafe { NSAppearanceNameAqua.copy() }, + dark_appearance_name().copy(), + ])); + if let Some(best_match) = best_match { + if *best_match == *dark_appearance_name() { + Theme::Dark + } else { + Theme::Light + } + } else { + warn!(?appearance, "failed to determine the theme of the appearance"); + // Default to light in this case + Theme::Light + } +} + +fn theme_to_appearance(theme: Option) -> Option> { + let appearance = match theme? { + Theme::Light => unsafe { NSAppearance::appearanceNamed(NSAppearanceNameAqua) }, + Theme::Dark => NSAppearance::appearanceNamed(dark_appearance_name()), + }; + if let Some(appearance) = appearance { + Some(appearance) + } else { + warn!(?theme, "could not find appearance for theme"); + // Assume system appearance in this case + None + } +} diff --git a/vendor/winit/src/platform_impl/mod.rs b/vendor/winit/src/platform_impl/mod.rs new file mode 100644 index 00000000..3bfce688 --- /dev/null +++ b/vendor/winit/src/platform_impl/mod.rs @@ -0,0 +1,78 @@ +use crate::monitor::{MonitorHandle as RootMonitorHandle, VideoModeHandle as RootVideoModeHandle}; +use crate::window::Fullscreen as RootFullscreen; + +#[cfg(android_platform)] +mod android; +#[cfg(ios_platform)] +mod ios; +#[cfg(any(x11_platform, wayland_platform))] +mod linux; +#[cfg(macos_platform)] +mod macos; +#[cfg(orbital_platform)] +mod orbital; +#[cfg(web_platform)] +mod web; +#[cfg(windows_platform)] +mod windows; + +#[cfg(android_platform)] +use android as platform; +#[cfg(ios_platform)] +use ios as platform; +#[cfg(any(x11_platform, wayland_platform))] +use linux as platform; +#[cfg(macos_platform)] +use macos as platform; +#[cfg(orbital_platform)] +use orbital as platform; +#[cfg(web_platform)] +use web as platform; +#[cfg(windows_platform)] +use windows as platform; + +pub use self::platform::*; + +/// Helper for converting between platform-specific and generic +/// [`VideoModeHandle`]/[`MonitorHandle`] +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum Fullscreen { + Exclusive(VideoModeHandle), + Borderless(Option), +} + +impl From for Fullscreen { + fn from(f: RootFullscreen) -> Self { + match f { + RootFullscreen::Exclusive(mode) => Self::Exclusive(mode.video_mode), + RootFullscreen::Borderless(Some(handle)) => Self::Borderless(Some(handle.inner)), + RootFullscreen::Borderless(None) => Self::Borderless(None), + } + } +} + +impl From for RootFullscreen { + fn from(f: Fullscreen) -> Self { + match f { + Fullscreen::Exclusive(video_mode) => { + Self::Exclusive(RootVideoModeHandle { video_mode }) + }, + Fullscreen::Borderless(Some(inner)) => { + Self::Borderless(Some(RootMonitorHandle { inner })) + }, + Fullscreen::Borderless(None) => Self::Borderless(None), + } + } +} + +#[cfg(all( + not(ios_platform), + not(windows_platform), + not(macos_platform), + not(android_platform), + not(x11_platform), + not(wayland_platform), + not(web_platform), + not(orbital_platform), +))] +compile_error!("The platform you're compiling for is not supported by winit"); diff --git a/vendor/winit/src/platform_impl/orbital/event_loop.rs b/vendor/winit/src/platform_impl/orbital/event_loop.rs new file mode 100644 index 00000000..91f76e75 --- /dev/null +++ b/vendor/winit/src/platform_impl/orbital/event_loop.rs @@ -0,0 +1,829 @@ +use std::cell::Cell; +use std::collections::VecDeque; +use std::marker::PhantomData; +use std::sync::{mpsc, Arc, Mutex}; +use std::time::Instant; +use std::{mem, slice}; + +use bitflags::bitflags; +use orbclient::{ + ButtonEvent, EventOption, FocusEvent, HoverEvent, KeyEvent, MouseEvent, MouseRelativeEvent, + MoveEvent, QuitEvent, ResizeEvent, ScrollEvent, TextInputEvent, +}; +use smol_str::SmolStr; + +use crate::error::EventLoopError; +use crate::event::{self, Ime, Modifiers, StartCause}; +use crate::event_loop::{self, ControlFlow, DeviceEvents}; +use crate::keyboard::{ + Key, KeyCode, KeyLocation, ModifiersKeys, ModifiersState, NamedKey, NativeKey, NativeKeyCode, + PhysicalKey, +}; +use crate::window::{ + CustomCursor as RootCustomCursor, CustomCursorSource, Theme, WindowId as RootWindowId, +}; + +use super::{ + DeviceId, KeyEventExtra, MonitorHandle, OsError, PlatformSpecificEventLoopAttributes, + RedoxSocket, TimeSocket, WindowId, WindowProperties, +}; + +fn convert_scancode(scancode: u8) -> (PhysicalKey, Option) { + // Key constants from https://docs.rs/orbclient/latest/orbclient/event/index.html + let (key_code, named_key_opt) = match scancode { + orbclient::K_A => (KeyCode::KeyA, None), + orbclient::K_B => (KeyCode::KeyB, None), + orbclient::K_C => (KeyCode::KeyC, None), + orbclient::K_D => (KeyCode::KeyD, None), + orbclient::K_E => (KeyCode::KeyE, None), + orbclient::K_F => (KeyCode::KeyF, None), + orbclient::K_G => (KeyCode::KeyG, None), + orbclient::K_H => (KeyCode::KeyH, None), + orbclient::K_I => (KeyCode::KeyI, None), + orbclient::K_J => (KeyCode::KeyJ, None), + orbclient::K_K => (KeyCode::KeyK, None), + orbclient::K_L => (KeyCode::KeyL, None), + orbclient::K_M => (KeyCode::KeyM, None), + orbclient::K_N => (KeyCode::KeyN, None), + orbclient::K_O => (KeyCode::KeyO, None), + orbclient::K_P => (KeyCode::KeyP, None), + orbclient::K_Q => (KeyCode::KeyQ, None), + orbclient::K_R => (KeyCode::KeyR, None), + orbclient::K_S => (KeyCode::KeyS, None), + orbclient::K_T => (KeyCode::KeyT, None), + orbclient::K_U => (KeyCode::KeyU, None), + orbclient::K_V => (KeyCode::KeyV, None), + orbclient::K_W => (KeyCode::KeyW, None), + orbclient::K_X => (KeyCode::KeyX, None), + orbclient::K_Y => (KeyCode::KeyY, None), + orbclient::K_Z => (KeyCode::KeyZ, None), + orbclient::K_0 => (KeyCode::Digit0, None), + orbclient::K_1 => (KeyCode::Digit1, None), + orbclient::K_2 => (KeyCode::Digit2, None), + orbclient::K_3 => (KeyCode::Digit3, None), + orbclient::K_4 => (KeyCode::Digit4, None), + orbclient::K_5 => (KeyCode::Digit5, None), + orbclient::K_6 => (KeyCode::Digit6, None), + orbclient::K_7 => (KeyCode::Digit7, None), + orbclient::K_8 => (KeyCode::Digit8, None), + orbclient::K_9 => (KeyCode::Digit9, None), + + orbclient::K_ALT => (KeyCode::AltLeft, Some(NamedKey::Alt)), + orbclient::K_ALT_GR => (KeyCode::AltRight, Some(NamedKey::AltGraph)), + orbclient::K_BACKSLASH => (KeyCode::Backslash, None), + orbclient::K_BKSP => (KeyCode::Backspace, Some(NamedKey::Backspace)), + orbclient::K_BRACE_CLOSE => (KeyCode::BracketRight, None), + orbclient::K_BRACE_OPEN => (KeyCode::BracketLeft, None), + orbclient::K_CAPS => (KeyCode::CapsLock, Some(NamedKey::CapsLock)), + orbclient::K_COMMA => (KeyCode::Comma, None), + orbclient::K_CTRL => (KeyCode::ControlLeft, Some(NamedKey::Control)), + orbclient::K_DEL => (KeyCode::Delete, Some(NamedKey::Delete)), + orbclient::K_DOWN => (KeyCode::ArrowDown, Some(NamedKey::ArrowDown)), + orbclient::K_END => (KeyCode::End, Some(NamedKey::End)), + orbclient::K_ENTER => (KeyCode::Enter, Some(NamedKey::Enter)), + orbclient::K_EQUALS => (KeyCode::Equal, None), + orbclient::K_ESC => (KeyCode::Escape, Some(NamedKey::Escape)), + orbclient::K_F1 => (KeyCode::F1, Some(NamedKey::F1)), + orbclient::K_F2 => (KeyCode::F2, Some(NamedKey::F2)), + orbclient::K_F3 => (KeyCode::F3, Some(NamedKey::F3)), + orbclient::K_F4 => (KeyCode::F4, Some(NamedKey::F4)), + orbclient::K_F5 => (KeyCode::F5, Some(NamedKey::F5)), + orbclient::K_F6 => (KeyCode::F6, Some(NamedKey::F6)), + orbclient::K_F7 => (KeyCode::F7, Some(NamedKey::F7)), + orbclient::K_F8 => (KeyCode::F8, Some(NamedKey::F8)), + orbclient::K_F9 => (KeyCode::F9, Some(NamedKey::F9)), + orbclient::K_F10 => (KeyCode::F10, Some(NamedKey::F10)), + orbclient::K_F11 => (KeyCode::F11, Some(NamedKey::F11)), + orbclient::K_F12 => (KeyCode::F12, Some(NamedKey::F12)), + orbclient::K_HOME => (KeyCode::Home, Some(NamedKey::Home)), + orbclient::K_LEFT => (KeyCode::ArrowLeft, Some(NamedKey::ArrowLeft)), + orbclient::K_LEFT_SHIFT => (KeyCode::ShiftLeft, Some(NamedKey::Shift)), + orbclient::K_MINUS => (KeyCode::Minus, None), + orbclient::K_NUM_0 => (KeyCode::Numpad0, None), + orbclient::K_NUM_1 => (KeyCode::Numpad1, None), + orbclient::K_NUM_2 => (KeyCode::Numpad2, None), + orbclient::K_NUM_3 => (KeyCode::Numpad3, None), + orbclient::K_NUM_4 => (KeyCode::Numpad4, None), + orbclient::K_NUM_5 => (KeyCode::Numpad5, None), + orbclient::K_NUM_6 => (KeyCode::Numpad6, None), + orbclient::K_NUM_7 => (KeyCode::Numpad7, None), + orbclient::K_NUM_8 => (KeyCode::Numpad8, None), + orbclient::K_NUM_9 => (KeyCode::Numpad9, None), + orbclient::K_PERIOD => (KeyCode::Period, None), + orbclient::K_PGDN => (KeyCode::PageDown, Some(NamedKey::PageDown)), + orbclient::K_PGUP => (KeyCode::PageUp, Some(NamedKey::PageUp)), + orbclient::K_QUOTE => (KeyCode::Quote, None), + orbclient::K_RIGHT => (KeyCode::ArrowRight, Some(NamedKey::ArrowRight)), + orbclient::K_RIGHT_SHIFT => (KeyCode::ShiftRight, Some(NamedKey::Shift)), + orbclient::K_SEMICOLON => (KeyCode::Semicolon, None), + orbclient::K_SLASH => (KeyCode::Slash, None), + orbclient::K_SPACE => (KeyCode::Space, Some(NamedKey::Space)), + orbclient::K_SUPER => (KeyCode::SuperLeft, Some(NamedKey::Super)), + orbclient::K_TAB => (KeyCode::Tab, Some(NamedKey::Tab)), + orbclient::K_TICK => (KeyCode::Backquote, None), + orbclient::K_UP => (KeyCode::ArrowUp, Some(NamedKey::ArrowUp)), + orbclient::K_VOLUME_DOWN => (KeyCode::AudioVolumeDown, Some(NamedKey::AudioVolumeDown)), + orbclient::K_VOLUME_TOGGLE => (KeyCode::AudioVolumeMute, Some(NamedKey::AudioVolumeMute)), + orbclient::K_VOLUME_UP => (KeyCode::AudioVolumeUp, Some(NamedKey::AudioVolumeUp)), + + _ => return (PhysicalKey::Unidentified(NativeKeyCode::Unidentified), None), + }; + (PhysicalKey::Code(key_code), named_key_opt) +} + +fn element_state(pressed: bool) -> event::ElementState { + if pressed { + event::ElementState::Pressed + } else { + event::ElementState::Released + } +} + +bitflags! { + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] + struct KeyboardModifierState: u8 { + const LSHIFT = 1 << 0; + const RSHIFT = 1 << 1; + const LCTRL = 1 << 2; + const RCTRL = 1 << 3; + const LALT = 1 << 4; + const RALT = 1 << 5; + const LSUPER = 1 << 6; + const RSUPER = 1 << 7; + } +} + +bitflags! { + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] + struct MouseButtonState: u8 { + const LEFT = 1 << 0; + const MIDDLE = 1 << 1; + const RIGHT = 1 << 2; + } +} + +#[derive(Default)] +struct EventState { + keyboard: KeyboardModifierState, + mouse: MouseButtonState, + resize_opt: Option<(u32, u32)>, +} + +impl EventState { + fn character_all_modifiers(&self, character: char) -> char { + // Modify character if Ctrl is pressed + #[allow(clippy::collapsible_if)] + if self.keyboard.contains(KeyboardModifierState::LCTRL) + || self.keyboard.contains(KeyboardModifierState::RCTRL) + { + if character.is_ascii_lowercase() { + return ((character as u8 - b'a') + 1) as char; + } + // TODO: more control key variants? + } + + // Return character as-is if no special handling required + character + } + + fn key(&mut self, key: PhysicalKey, pressed: bool) { + let code = match key { + PhysicalKey::Code(code) => code, + _ => return, + }; + + match code { + KeyCode::ShiftLeft => self.keyboard.set(KeyboardModifierState::LSHIFT, pressed), + KeyCode::ShiftRight => self.keyboard.set(KeyboardModifierState::RSHIFT, pressed), + KeyCode::ControlLeft => self.keyboard.set(KeyboardModifierState::LCTRL, pressed), + KeyCode::ControlRight => self.keyboard.set(KeyboardModifierState::RCTRL, pressed), + KeyCode::AltLeft => self.keyboard.set(KeyboardModifierState::LALT, pressed), + KeyCode::AltRight => self.keyboard.set(KeyboardModifierState::RALT, pressed), + KeyCode::SuperLeft => self.keyboard.set(KeyboardModifierState::LSUPER, pressed), + KeyCode::SuperRight => self.keyboard.set(KeyboardModifierState::RSUPER, pressed), + _ => (), + } + } + + fn mouse( + &mut self, + left: bool, + middle: bool, + right: bool, + ) -> Option<(event::MouseButton, event::ElementState)> { + if self.mouse.contains(MouseButtonState::LEFT) != left { + self.mouse.set(MouseButtonState::LEFT, left); + return Some((event::MouseButton::Left, element_state(left))); + } + + if self.mouse.contains(MouseButtonState::MIDDLE) != middle { + self.mouse.set(MouseButtonState::MIDDLE, middle); + return Some((event::MouseButton::Middle, element_state(middle))); + } + + if self.mouse.contains(MouseButtonState::RIGHT) != right { + self.mouse.set(MouseButtonState::RIGHT, right); + return Some((event::MouseButton::Right, element_state(right))); + } + + None + } + + fn modifiers(&self) -> Modifiers { + let mut state = ModifiersState::empty(); + let mut pressed_mods = ModifiersKeys::empty(); + + if self.keyboard.intersects(KeyboardModifierState::LSHIFT | KeyboardModifierState::RSHIFT) { + state |= ModifiersState::SHIFT; + } + + pressed_mods + .set(ModifiersKeys::LSHIFT, self.keyboard.contains(KeyboardModifierState::LSHIFT)); + pressed_mods + .set(ModifiersKeys::RSHIFT, self.keyboard.contains(KeyboardModifierState::RSHIFT)); + + if self.keyboard.intersects(KeyboardModifierState::LCTRL | KeyboardModifierState::RCTRL) { + state |= ModifiersState::CONTROL; + } + + pressed_mods + .set(ModifiersKeys::LCONTROL, self.keyboard.contains(KeyboardModifierState::LCTRL)); + pressed_mods + .set(ModifiersKeys::RCONTROL, self.keyboard.contains(KeyboardModifierState::RCTRL)); + + if self.keyboard.intersects(KeyboardModifierState::LALT | KeyboardModifierState::RALT) { + state |= ModifiersState::ALT; + } + + pressed_mods.set(ModifiersKeys::LALT, self.keyboard.contains(KeyboardModifierState::LALT)); + pressed_mods.set(ModifiersKeys::RALT, self.keyboard.contains(KeyboardModifierState::RALT)); + + if self.keyboard.intersects(KeyboardModifierState::LSUPER | KeyboardModifierState::RSUPER) { + state |= ModifiersState::SUPER + } + + pressed_mods + .set(ModifiersKeys::LSUPER, self.keyboard.contains(KeyboardModifierState::LSUPER)); + pressed_mods + .set(ModifiersKeys::RSUPER, self.keyboard.contains(KeyboardModifierState::RSUPER)); + + Modifiers { state, pressed_mods } + } +} + +pub struct EventLoop { + windows: Vec<(Arc, EventState)>, + window_target: event_loop::ActiveEventLoop, + user_events_sender: mpsc::Sender, + user_events_receiver: mpsc::Receiver, +} + +impl EventLoop { + pub(crate) fn new(_: &PlatformSpecificEventLoopAttributes) -> Result { + let (user_events_sender, user_events_receiver) = mpsc::channel(); + + let event_socket = Arc::new( + RedoxSocket::event() + .map_err(OsError::new) + .map_err(|error| EventLoopError::Os(os_error!(error)))?, + ); + + let wake_socket = Arc::new( + TimeSocket::open() + .map_err(OsError::new) + .map_err(|error| EventLoopError::Os(os_error!(error)))?, + ); + + event_socket + .write(&syscall::Event { + id: wake_socket.0.fd, + flags: syscall::EventFlags::EVENT_READ, + data: wake_socket.0.fd, + }) + .map_err(OsError::new) + .map_err(|error| EventLoopError::Os(os_error!(error)))?; + + Ok(Self { + windows: Vec::new(), + window_target: event_loop::ActiveEventLoop { + p: ActiveEventLoop { + control_flow: Cell::new(ControlFlow::default()), + exit: Cell::new(false), + creates: Mutex::new(VecDeque::new()), + redraws: Arc::new(Mutex::new(VecDeque::new())), + destroys: Arc::new(Mutex::new(VecDeque::new())), + event_socket, + wake_socket, + }, + _marker: PhantomData, + }, + user_events_sender, + user_events_receiver, + }) + } + + fn process_event( + window_id: WindowId, + event_option: EventOption, + event_state: &mut EventState, + mut event_handler: F, + ) where + F: FnMut(event::Event), + { + match event_option { + EventOption::Key(KeyEvent { character, scancode, pressed }) => { + // Convert scancode + let (physical_key, named_key_opt) = convert_scancode(scancode); + + // Get previous modifiers and update modifiers based on physical key + let modifiers_before = event_state.keyboard; + event_state.key(physical_key, pressed); + + // Default to unidentified key with no text + let mut logical_key = Key::Unidentified(NativeKey::Unidentified); + let mut key_without_modifiers = logical_key.clone(); + let mut text = None; + let mut text_with_all_modifiers = None; + + // Set key and text based on character + if character != '\0' { + let mut tmp = [0u8; 4]; + let character_str = character.encode_utf8(&mut tmp); + // The key with Shift and Caps Lock applied (but not Ctrl) + logical_key = Key::Character(character_str.into()); + // The key without Shift or Caps Lock applied + key_without_modifiers = + Key::Character(SmolStr::from_iter(character.to_lowercase())); + if pressed { + // The key with Shift and Caps Lock applied (but not Ctrl) + text = Some(character_str.into()); + // The key with Shift, Caps Lock, and Ctrl applied + let character_all_modifiers = + event_state.character_all_modifiers(character); + text_with_all_modifiers = + Some(character_all_modifiers.encode_utf8(&mut tmp).into()) + } + }; + + // Override key if a named key was found (this is to allow Enter to replace '\n') + if let Some(named_key) = named_key_opt { + logical_key = Key::Named(named_key); + key_without_modifiers = logical_key.clone(); + } + + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::KeyboardInput { + device_id: event::DeviceId(DeviceId), + event: event::KeyEvent { + logical_key, + physical_key, + location: KeyLocation::Standard, + state: element_state(pressed), + repeat: false, + text, + platform_specific: KeyEventExtra { + key_without_modifiers, + text_with_all_modifiers, + }, + }, + is_synthetic: false, + }, + }); + + // If the state of the modifiers has changed, send the event. + if modifiers_before != event_state.keyboard { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::ModifiersChanged(event_state.modifiers()), + }) + } + }, + EventOption::TextInput(TextInputEvent { character }) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Ime(Ime::Preedit("".into(), None)), + }); + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Ime(Ime::Commit(character.into())), + }); + }, + EventOption::Mouse(MouseEvent { x, y }) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::CursorMoved { + device_id: event::DeviceId(DeviceId), + position: (x, y).into(), + }, + }); + }, + EventOption::MouseRelative(MouseRelativeEvent { dx, dy }) => { + event_handler(event::Event::DeviceEvent { + device_id: event::DeviceId(DeviceId), + event: event::DeviceEvent::MouseMotion { delta: (dx as f64, dy as f64) }, + }); + }, + EventOption::Button(ButtonEvent { left, middle, right }) => { + while let Some((button, state)) = event_state.mouse(left, middle, right) { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::MouseInput { + device_id: event::DeviceId(DeviceId), + state, + button, + }, + }); + } + }, + EventOption::Scroll(ScrollEvent { x, y }) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::MouseWheel { + device_id: event::DeviceId(DeviceId), + delta: event::MouseScrollDelta::LineDelta(x as f32, y as f32), + phase: event::TouchPhase::Moved, + }, + }); + }, + EventOption::Quit(QuitEvent {}) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::CloseRequested, + }); + }, + EventOption::Focus(FocusEvent { focused }) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Focused(focused), + }); + }, + EventOption::Move(MoveEvent { x, y }) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Moved((x, y).into()), + }); + }, + EventOption::Resize(ResizeEvent { width, height }) => { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Resized((width, height).into()), + }); + + // Acknowledge resize after event loop. + event_state.resize_opt = Some((width, height)); + }, + // TODO: Screen, Clipboard, Drop + EventOption::Hover(HoverEvent { entered }) => { + if entered { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::CursorEntered { + device_id: event::DeviceId(DeviceId), + }, + }); + } else { + event_handler(event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::CursorLeft { + device_id: event::DeviceId(DeviceId), + }, + }); + } + }, + other => { + tracing::warn!("unhandled event: {:?}", other); + }, + } + } + + pub fn run(mut self, mut event_handler_inner: F) -> Result<(), EventLoopError> + where + F: FnMut(event::Event, &event_loop::ActiveEventLoop), + { + let mut event_handler = + move |event: event::Event, window_target: &event_loop::ActiveEventLoop| { + event_handler_inner(event, window_target); + }; + + let mut start_cause = StartCause::Init; + + loop { + event_handler(event::Event::NewEvents(start_cause), &self.window_target); + + if start_cause == StartCause::Init { + event_handler(event::Event::Resumed, &self.window_target); + } + + // Handle window creates. + while let Some(window) = { + let mut creates = self.window_target.p.creates.lock().unwrap(); + creates.pop_front() + } { + let window_id = WindowId { fd: window.fd as u64 }; + + let mut buf: [u8; 4096] = [0; 4096]; + let path = window.fpath(&mut buf).expect("failed to read properties"); + let properties = WindowProperties::new(path); + + self.windows.push((window, EventState::default())); + + // Send resize event on create to indicate first size. + event_handler( + event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Resized((properties.w, properties.h).into()), + }, + &self.window_target, + ); + + // Send resize event on create to indicate first position. + event_handler( + event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::Moved((properties.x, properties.y).into()), + }, + &self.window_target, + ); + } + + // Handle window destroys. + while let Some(destroy_id) = { + let mut destroys = self.window_target.p.destroys.lock().unwrap(); + destroys.pop_front() + } { + event_handler( + event::Event::WindowEvent { + window_id: RootWindowId(destroy_id), + event: event::WindowEvent::Destroyed, + }, + &self.window_target, + ); + + self.windows.retain(|(window, _event_state)| window.fd as u64 != destroy_id.fd); + } + + // Handle window events. + let mut i = 0; + // While loop is used here because the same window may be processed more than once. + while let Some((window, event_state)) = self.windows.get_mut(i) { + let window_id = WindowId { fd: window.fd as u64 }; + + let mut event_buf = [0u8; 16 * mem::size_of::()]; + let count = + syscall::read(window.fd, &mut event_buf).expect("failed to read window events"); + // Safety: orbclient::Event is a packed struct designed to be transferred over a + // socket. + let events = unsafe { + slice::from_raw_parts( + event_buf.as_ptr() as *const orbclient::Event, + count / mem::size_of::(), + ) + }; + + for orbital_event in events { + Self::process_event( + window_id, + orbital_event.to_option(), + event_state, + |event| event_handler(event, &self.window_target), + ); + } + + if count == event_buf.len() { + // If event buf was full, process same window again to ensure all events are + // drained. + continue; + } + + // Acknowledge the latest resize event. + if let Some((w, h)) = event_state.resize_opt.take() { + window + .write(format!("S,{w},{h}").as_bytes()) + .expect("failed to acknowledge resize"); + + // Require redraw after resize. + let mut redraws = self.window_target.p.redraws.lock().unwrap(); + if !redraws.contains(&window_id) { + redraws.push_back(window_id); + } + } + + // Move to next window. + i += 1; + } + + while let Ok(event) = self.user_events_receiver.try_recv() { + event_handler(event::Event::UserEvent(event), &self.window_target); + } + + // To avoid deadlocks the redraws lock is not held during event processing. + while let Some(window_id) = { + let mut redraws = self.window_target.p.redraws.lock().unwrap(); + redraws.pop_front() + } { + event_handler( + event::Event::WindowEvent { + window_id: RootWindowId(window_id), + event: event::WindowEvent::RedrawRequested, + }, + &self.window_target, + ); + } + + event_handler(event::Event::AboutToWait, &self.window_target); + + if self.window_target.p.exiting() { + break; + } + + let requested_resume = match self.window_target.p.control_flow() { + ControlFlow::Poll => { + start_cause = StartCause::Poll; + continue; + }, + ControlFlow::Wait => None, + ControlFlow::WaitUntil(instant) => Some(instant), + }; + + // Re-using wake socket caused extra wake events before because there were leftover + // timeouts, and then new timeouts were added each time a spurious timeout expired. + let timeout_socket = TimeSocket::open().unwrap(); + + self.window_target + .p + .event_socket + .write(&syscall::Event { + id: timeout_socket.0.fd, + flags: syscall::EventFlags::EVENT_READ, + data: 0, + }) + .unwrap(); + + let start = Instant::now(); + if let Some(instant) = requested_resume { + let mut time = timeout_socket.current_time().unwrap(); + + if let Some(duration) = instant.checked_duration_since(start) { + time.tv_sec += duration.as_secs() as i64; + time.tv_nsec += duration.subsec_nanos() as i32; + // Normalize timespec so tv_nsec is not greater than one second. + while time.tv_nsec >= 1_000_000_000 { + time.tv_sec += 1; + time.tv_nsec -= 1_000_000_000; + } + } + + timeout_socket.timeout(&time).unwrap(); + } + + // Wait for event if needed. + let mut event = syscall::Event::default(); + self.window_target.p.event_socket.read(&mut event).unwrap(); + + // TODO: handle spurious wakeups (redraw caused wakeup but redraw already handled) + match requested_resume { + Some(requested_resume) if event.id == timeout_socket.0.fd => { + // If the event is from the special timeout socket, report that resume + // time was reached. + start_cause = StartCause::ResumeTimeReached { start, requested_resume }; + }, + _ => { + // Normal window event or spurious timeout. + start_cause = StartCause::WaitCancelled { start, requested_resume }; + }, + } + } + + event_handler(event::Event::LoopExiting, &self.window_target); + + Ok(()) + } + + pub fn window_target(&self) -> &event_loop::ActiveEventLoop { + &self.window_target + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { + user_events_sender: self.user_events_sender.clone(), + wake_socket: self.window_target.p.wake_socket.clone(), + } + } +} + +pub struct EventLoopProxy { + user_events_sender: mpsc::Sender, + wake_socket: Arc, +} + +impl EventLoopProxy { + pub fn send_event(&self, event: T) -> Result<(), event_loop::EventLoopClosed> { + self.user_events_sender + .send(event) + .map_err(|mpsc::SendError(x)| event_loop::EventLoopClosed(x))?; + + self.wake_socket.wake().unwrap(); + + Ok(()) + } +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + Self { + user_events_sender: self.user_events_sender.clone(), + wake_socket: self.wake_socket.clone(), + } + } +} + +impl Unpin for EventLoopProxy {} + +pub struct ActiveEventLoop { + control_flow: Cell, + exit: Cell, + pub(super) creates: Mutex>>, + pub(super) redraws: Arc>>, + pub(super) destroys: Arc>>, + pub(super) event_socket: Arc, + pub(super) wake_socket: Arc, +} + +impl ActiveEventLoop { + pub fn create_custom_cursor(&self, source: CustomCursorSource) -> RootCustomCursor { + let _ = source.inner; + RootCustomCursor { inner: super::PlatformCustomCursor } + } + + pub fn primary_monitor(&self) -> Option { + Some(MonitorHandle) + } + + pub fn available_monitors(&self) -> VecDeque { + let mut v = VecDeque::with_capacity(1); + v.push_back(MonitorHandle); + v + } + + #[inline] + pub fn listen_device_events(&self, _allowed: DeviceEvents) {} + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Orbital(rwh_05::OrbitalDisplayHandle::empty()) + } + + #[inline] + pub fn system_theme(&self) -> Option { + None + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Orbital(rwh_06::OrbitalDisplayHandle::new())) + } + + pub fn set_control_flow(&self, control_flow: ControlFlow) { + self.control_flow.set(control_flow) + } + + pub fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub(crate) fn exit(&self) { + self.exit.set(true); + } + + pub(crate) fn exiting(&self) -> bool { + self.exit.get() + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle + } +} + +#[derive(Clone)] +pub(crate) struct OwnedDisplayHandle; + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::OrbitalDisplayHandle::empty().into() + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::OrbitalDisplayHandle::new().into()) + } +} diff --git a/vendor/winit/src/platform_impl/orbital/mod.rs b/vendor/winit/src/platform_impl/orbital/mod.rs new file mode 100644 index 00000000..2d152513 --- /dev/null +++ b/vendor/winit/src/platform_impl/orbital/mod.rs @@ -0,0 +1,255 @@ +#![cfg(target_os = "redox")] + +use std::fmt::{self, Display, Formatter}; +use std::str; +use std::sync::Arc; + +use smol_str::SmolStr; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::keyboard::Key; + +pub(crate) use self::event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy, OwnedDisplayHandle}; +mod event_loop; + +pub use self::window::Window; +mod window; + +struct RedoxSocket { + fd: usize, +} + +impl RedoxSocket { + fn event() -> syscall::Result { + Self::open_raw("event:") + } + + fn orbital(properties: &WindowProperties<'_>) -> syscall::Result { + Self::open_raw(&format!("{properties}")) + } + + // Paths should be checked to ensure they are actually sockets and not normal files. If a + // non-socket path is used, it could cause read and write to not function as expected. For + // example, the seek would change in a potentially unpredictable way if either read or write + // were called at the same time by multiple threads. + fn open_raw(path: &str) -> syscall::Result { + let fd = syscall::open(path, syscall::O_RDWR | syscall::O_CLOEXEC)?; + Ok(Self { fd }) + } + + fn read(&self, buf: &mut [u8]) -> syscall::Result<()> { + let count = syscall::read(self.fd, buf)?; + if count == buf.len() { + Ok(()) + } else { + Err(syscall::Error::new(syscall::EINVAL)) + } + } + + fn write(&self, buf: &[u8]) -> syscall::Result<()> { + let count = syscall::write(self.fd, buf)?; + if count == buf.len() { + Ok(()) + } else { + Err(syscall::Error::new(syscall::EINVAL)) + } + } + + fn fpath<'a>(&self, buf: &'a mut [u8]) -> syscall::Result<&'a str> { + let count = syscall::fpath(self.fd, buf)?; + str::from_utf8(&buf[..count]).map_err(|_err| syscall::Error::new(syscall::EINVAL)) + } +} + +impl Drop for RedoxSocket { + fn drop(&mut self) { + let _ = syscall::close(self.fd); + } +} + +pub struct TimeSocket(RedoxSocket); + +impl TimeSocket { + fn open() -> syscall::Result { + RedoxSocket::open_raw("time:4").map(Self) + } + + // Read current time. + fn current_time(&self) -> syscall::Result { + let mut timespec = syscall::TimeSpec::default(); + self.0.read(&mut timespec)?; + Ok(timespec) + } + + // Write a timeout. + fn timeout(&self, timespec: &syscall::TimeSpec) -> syscall::Result<()> { + self.0.write(timespec) + } + + // Wake immediately. + fn wake(&self) -> syscall::Result<()> { + // Writing a default TimeSpec will always trigger a time event. + self.timeout(&syscall::TimeSpec::default()) + } +} + +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PlatformSpecificEventLoopAttributes {} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct WindowId { + fd: u64, +} + +impl WindowId { + pub const fn dummy() -> Self { + WindowId { fd: u64::MAX } + } +} + +impl From for u64 { + fn from(id: WindowId) -> Self { + id.fd + } +} + +impl From for WindowId { + fn from(fd: u64) -> Self { + Self { fd } + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct DeviceId; + +impl DeviceId { + pub const fn dummy() -> Self { + DeviceId + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct PlatformSpecificWindowAttributes; + +struct WindowProperties<'a> { + flags: &'a str, + x: i32, + y: i32, + w: u32, + h: u32, + title: &'a str, +} + +impl<'a> WindowProperties<'a> { + fn new(path: &'a str) -> Self { + // orbital:flags/x/y/w/h/t + let mut parts = path.splitn(6, '/'); + let flags = parts.next().unwrap_or(""); + let x = parts.next().map_or(0, |part| part.parse::().unwrap_or(0)); + let y = parts.next().map_or(0, |part| part.parse::().unwrap_or(0)); + let w = parts.next().map_or(0, |part| part.parse::().unwrap_or(0)); + let h = parts.next().map_or(0, |part| part.parse::().unwrap_or(0)); + let title = parts.next().unwrap_or(""); + Self { flags, x, y, w, h, title } + } +} + +impl fmt::Display for WindowProperties<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "orbital:{}/{}/{}/{}/{}/{}", + self.flags, self.x, self.y, self.w, self.h, self.title + ) + } +} + +#[derive(Clone, Debug)] +pub struct OsError(Arc); + +impl OsError { + fn new(error: syscall::Error) -> Self { + Self(Arc::new(error)) + } +} + +impl Display for OsError { + fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> { + self.0.fmt(fmt) + } +} + +pub(crate) use crate::cursor::{ + NoCustomCursor as PlatformCustomCursor, NoCustomCursor as PlatformCustomCursorSource, +}; +pub(crate) use crate::icon::NoIcon as PlatformIcon; + +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct MonitorHandle; + +impl MonitorHandle { + pub fn name(&self) -> Option { + Some("Redox Device".to_owned()) + } + + pub fn size(&self) -> PhysicalSize { + PhysicalSize::new(0, 0) // TODO + } + + pub fn position(&self) -> PhysicalPosition { + (0, 0).into() + } + + pub fn scale_factor(&self) -> f64 { + 1.0 // TODO + } + + pub fn refresh_rate_millihertz(&self) -> Option { + // FIXME no way to get real refresh rate for now. + None + } + + pub fn video_modes(&self) -> impl Iterator { + let size = self.size().into(); + // FIXME this is not the real refresh rate + // (it is guaranteed to support 32 bit color though) + std::iter::once(VideoModeHandle { + size, + bit_depth: 32, + refresh_rate_millihertz: 60000, + monitor: self.clone(), + }) + } +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct VideoModeHandle { + size: (u32, u32), + bit_depth: u16, + refresh_rate_millihertz: u32, + monitor: MonitorHandle, +} + +impl VideoModeHandle { + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone() + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra { + pub key_without_modifiers: Key, + pub text_with_all_modifiers: Option, +} diff --git a/vendor/winit/src/platform_impl/orbital/window.rs b/vendor/winit/src/platform_impl/orbital/window.rs new file mode 100644 index 00000000..3e676c36 --- /dev/null +++ b/vendor/winit/src/platform_impl/orbital/window.rs @@ -0,0 +1,509 @@ +use std::collections::VecDeque; +use std::sync::{Arc, Mutex}; + +use crate::cursor::Cursor; +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::platform_impl::Fullscreen; +use crate::window::ImePurpose; +use crate::{error, window}; + +use super::{ + ActiveEventLoop, MonitorHandle, OsError, RedoxSocket, TimeSocket, WindowId, WindowProperties, +}; + +// These values match the values uses in the `window_new` function in orbital: +// https://gitlab.redox-os.org/redox-os/orbital/-/blob/master/src/scheme.rs +const ORBITAL_FLAG_ASYNC: char = 'a'; +const ORBITAL_FLAG_BACK: char = 'b'; +const ORBITAL_FLAG_FRONT: char = 'f'; +const ORBITAL_FLAG_HIDDEN: char = 'h'; +const ORBITAL_FLAG_BORDERLESS: char = 'l'; +const ORBITAL_FLAG_MAXIMIZED: char = 'm'; +const ORBITAL_FLAG_RESIZABLE: char = 'r'; +const ORBITAL_FLAG_TRANSPARENT: char = 't'; + +pub struct Window { + window_socket: Arc, + redraws: Arc>>, + destroys: Arc>>, + wake_socket: Arc, +} + +impl Window { + pub(crate) fn new( + el: &ActiveEventLoop, + attrs: window::WindowAttributes, + ) -> Result { + let scale = MonitorHandle.scale_factor(); + + let (x, y) = if let Some(pos) = attrs.position { + pos.to_physical::(scale).into() + } else { + // These coordinates are a special value to center the window. + (-1, -1) + }; + + let (w, h): (u32, u32) = if let Some(size) = attrs.inner_size { + size.to_physical::(scale).into() + } else { + (1024, 768) + }; + + // TODO: min/max inner_size + + // Async by default. + let mut flag_str = ORBITAL_FLAG_ASYNC.to_string(); + + if attrs.maximized { + flag_str.push(ORBITAL_FLAG_MAXIMIZED); + } + + if attrs.resizable { + flag_str.push(ORBITAL_FLAG_RESIZABLE); + } + + // TODO: fullscreen + + if attrs.transparent { + flag_str.push(ORBITAL_FLAG_TRANSPARENT); + } + + if !attrs.decorations { + flag_str.push(ORBITAL_FLAG_BORDERLESS); + } + + if !attrs.visible { + flag_str.push(ORBITAL_FLAG_HIDDEN); + } + + match attrs.window_level { + window::WindowLevel::AlwaysOnBottom => { + flag_str.push(ORBITAL_FLAG_BACK); + }, + window::WindowLevel::Normal => {}, + window::WindowLevel::AlwaysOnTop => { + flag_str.push(ORBITAL_FLAG_FRONT); + }, + } + + // TODO: window_icon + + // Open window. + let window = RedoxSocket::orbital(&WindowProperties { + flags: &flag_str, + x, + y, + w, + h, + title: &attrs.title, + }) + .expect("failed to open window"); + + // Add to event socket. + el.event_socket + .write(&syscall::Event { + id: window.fd, + flags: syscall::EventFlags::EVENT_READ, + data: window.fd, + }) + .unwrap(); + + let window_socket = Arc::new(window); + + // Notify event thread that this window was created, it will send some default events. + { + let mut creates = el.creates.lock().unwrap(); + creates.push_back(window_socket.clone()); + } + + el.wake_socket.wake().unwrap(); + + Ok(Self { + window_socket, + redraws: el.redraws.clone(), + destroys: el.destroys.clone(), + wake_socket: el.wake_socket.clone(), + }) + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) { + f(self) + } + + pub(crate) fn maybe_wait_on_main(&self, f: impl FnOnce(&Self) -> R + Send) -> R { + f(self) + } + + fn get_flag(&self, flag: char) -> Result { + let mut buf: [u8; 4096] = [0; 4096]; + let path = self + .window_socket + .fpath(&mut buf) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + let properties = WindowProperties::new(path); + Ok(properties.flags.contains(flag)) + } + + fn set_flag(&self, flag: char, value: bool) -> Result<(), error::ExternalError> { + self.window_socket + .write(format!("F,{flag},{}", if value { 1 } else { 0 }).as_bytes()) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + Ok(()) + } + + #[inline] + pub fn id(&self) -> WindowId { + WindowId { fd: self.window_socket.fd as u64 } + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + Some(MonitorHandle) + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + let mut v = VecDeque::with_capacity(1); + v.push_back(MonitorHandle); + v + } + + #[inline] + pub fn current_monitor(&self) -> Option { + Some(MonitorHandle) + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + MonitorHandle.scale_factor() + } + + #[inline] + pub fn request_redraw(&self) { + let window_id = self.id(); + let mut redraws = self.redraws.lock().unwrap(); + if !redraws.contains(&window_id) { + redraws.push_back(window_id); + + self.wake_socket.wake().unwrap(); + } + } + + #[inline] + pub fn pre_present_notify(&self) {} + + #[inline] + pub fn reset_dead_keys(&self) { + // TODO? + } + + #[inline] + pub fn inner_position(&self) -> Result, error::NotSupportedError> { + let mut buf: [u8; 4096] = [0; 4096]; + let path = self.window_socket.fpath(&mut buf).expect("failed to read properties"); + let properties = WindowProperties::new(path); + Ok((properties.x, properties.y).into()) + } + + #[inline] + pub fn outer_position(&self) -> Result, error::NotSupportedError> { + // TODO: adjust for window decorations + self.inner_position() + } + + #[inline] + pub fn set_outer_position(&self, position: Position) { + // TODO: adjust for window decorations + let (x, y): (i32, i32) = position.to_physical::(self.scale_factor()).into(); + self.window_socket.write(format!("P,{x},{y}").as_bytes()).expect("failed to set position"); + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + let mut buf: [u8; 4096] = [0; 4096]; + let path = self.window_socket.fpath(&mut buf).expect("failed to read properties"); + let properties = WindowProperties::new(path); + (properties.w, properties.h).into() + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + let (w, h): (u32, u32) = size.to_physical::(self.scale_factor()).into(); + self.window_socket.write(format!("S,{w},{h}").as_bytes()).expect("failed to set size"); + None + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + // TODO: adjust for window decorations + self.inner_size() + } + + #[inline] + pub fn set_min_inner_size(&self, _: Option) {} + + #[inline] + pub fn set_max_inner_size(&self, _: Option) {} + + #[inline] + pub fn title(&self) -> String { + let mut buf: [u8; 4096] = [0; 4096]; + let path = self.window_socket.fpath(&mut buf).expect("failed to read properties"); + let properties = WindowProperties::new(path); + properties.title.to_string() + } + + #[inline] + pub fn set_title(&self, title: &str) { + self.window_socket.write(format!("T,{title}").as_bytes()).expect("failed to set title"); + } + + #[inline] + pub fn set_transparent(&self, transparent: bool) { + let _ = self.set_flag(ORBITAL_FLAG_TRANSPARENT, transparent); + } + + #[inline] + pub fn set_blur(&self, _blur: bool) {} + + #[inline] + pub fn set_visible(&self, visible: bool) { + let _ = self.set_flag(ORBITAL_FLAG_HIDDEN, !visible); + } + + #[inline] + pub fn is_visible(&self) -> Option { + Some(!self.get_flag(ORBITAL_FLAG_HIDDEN).unwrap_or(false)) + } + + #[inline] + pub fn resize_increments(&self) -> Option> { + None + } + + #[inline] + pub fn set_resize_increments(&self, _increments: Option) {} + + #[inline] + pub fn set_resizable(&self, resizeable: bool) { + let _ = self.set_flag(ORBITAL_FLAG_RESIZABLE, resizeable); + } + + #[inline] + pub fn is_resizable(&self) -> bool { + self.get_flag(ORBITAL_FLAG_RESIZABLE).unwrap_or(false) + } + + #[inline] + pub fn set_minimized(&self, _minimized: bool) {} + + #[inline] + pub fn is_minimized(&self) -> Option { + None + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + let _ = self.set_flag(ORBITAL_FLAG_MAXIMIZED, maximized); + } + + #[inline] + pub fn is_maximized(&self) -> bool { + self.get_flag(ORBITAL_FLAG_MAXIMIZED).unwrap_or(false) + } + + #[inline] + pub(crate) fn set_fullscreen(&self, _monitor: Option) {} + + #[inline] + pub(crate) fn fullscreen(&self) -> Option { + None + } + + #[inline] + pub fn set_decorations(&self, decorations: bool) { + let _ = self.set_flag(ORBITAL_FLAG_BORDERLESS, !decorations); + } + + #[inline] + pub fn is_decorated(&self) -> bool { + !self.get_flag(ORBITAL_FLAG_BORDERLESS).unwrap_or(false) + } + + #[inline] + pub fn set_window_level(&self, level: window::WindowLevel) { + match level { + window::WindowLevel::AlwaysOnBottom => { + let _ = self.set_flag(ORBITAL_FLAG_BACK, true); + }, + window::WindowLevel::Normal => { + let _ = self.set_flag(ORBITAL_FLAG_BACK, false); + let _ = self.set_flag(ORBITAL_FLAG_FRONT, false); + }, + window::WindowLevel::AlwaysOnTop => { + let _ = self.set_flag(ORBITAL_FLAG_FRONT, true); + }, + } + } + + #[inline] + pub fn set_window_icon(&self, _window_icon: Option) {} + + #[inline] + pub fn set_ime_cursor_area(&self, _position: Position, _size: Size) {} + + #[inline] + pub fn set_ime_allowed(&self, _allowed: bool) {} + + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + + #[inline] + pub fn focus_window(&self) {} + + #[inline] + pub fn request_user_attention(&self, _request_type: Option) {} + + #[inline] + pub fn set_cursor(&self, _: Cursor) {} + + #[inline] + pub fn set_cursor_position(&self, _: Position) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported(error::NotSupportedError::new())) + } + + #[inline] + pub fn set_cursor_grab( + &self, + mode: window::CursorGrabMode, + ) -> Result<(), error::ExternalError> { + let (grab, relative) = match mode { + window::CursorGrabMode::None => (false, false), + window::CursorGrabMode::Confined => (true, false), + window::CursorGrabMode::Locked => (true, true), + }; + self.window_socket + .write(format!("M,G,{}", if grab { 1 } else { 0 }).as_bytes()) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + self.window_socket + .write(format!("M,R,{}", if relative { 1 } else { 0 }).as_bytes()) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + Ok(()) + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + let _ = self.window_socket.write(format!("M,C,{}", if visible { 1 } else { 0 }).as_bytes()); + } + + #[inline] + pub fn drag_window(&self) -> Result<(), error::ExternalError> { + self.window_socket + .write(b"D") + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + Ok(()) + } + + #[inline] + pub fn drag_resize_window( + &self, + direction: window::ResizeDirection, + ) -> Result<(), error::ExternalError> { + let arg = match direction { + window::ResizeDirection::East => "R", + window::ResizeDirection::North => "T", + window::ResizeDirection::NorthEast => "T,R", + window::ResizeDirection::NorthWest => "T,L", + window::ResizeDirection::South => "B", + window::ResizeDirection::SouthEast => "B,R", + window::ResizeDirection::SouthWest => "B,L", + window::ResizeDirection::West => "L", + }; + self.window_socket + .write(format!("D,{arg}").as_bytes()) + .map_err(|err| error::ExternalError::Os(os_error!(OsError::new(err))))?; + Ok(()) + } + + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + + #[inline] + pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported(error::NotSupportedError::new())) + } + + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut handle = rwh_04::OrbitalHandle::empty(); + handle.window = self.window_socket.fd as *mut _; + rwh_04::RawWindowHandle::Orbital(handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut handle = rwh_05::OrbitalWindowHandle::empty(); + handle.window = self.window_socket.fd as *mut _; + rwh_05::RawWindowHandle::Orbital(handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Orbital(rwh_05::OrbitalDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> Result { + let handle = rwh_06::OrbitalWindowHandle::new({ + let window = self.window_socket.fd as *mut _; + std::ptr::NonNull::new(window).expect("orbital fd should never be null") + }); + Ok(rwh_06::RawWindowHandle::Orbital(handle)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Orbital(rwh_06::OrbitalDisplayHandle::new())) + } + + #[inline] + pub fn set_enabled_buttons(&self, _buttons: window::WindowButtons) {} + + #[inline] + pub fn enabled_buttons(&self) -> window::WindowButtons { + window::WindowButtons::all() + } + + #[inline] + pub fn theme(&self) -> Option { + None + } + + #[inline] + pub fn has_focus(&self) -> bool { + false + } + + #[inline] + pub fn set_theme(&self, _theme: Option) {} + + pub fn set_content_protected(&self, _protected: bool) {} +} + +impl Drop for Window { + fn drop(&mut self) { + { + let mut destroys = self.destroys.lock().unwrap(); + destroys.push_back(self.id()); + } + + self.wake_socket.wake().unwrap(); + } +} diff --git a/vendor/winit/src/platform_impl/web/async/abortable.rs b/vendor/winit/src/platform_impl/web/async/abortable.rs new file mode 100644 index 00000000..671ae879 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/async/abortable.rs @@ -0,0 +1,96 @@ +use std::error::Error; +use std::fmt::{self, Display, Formatter}; +use std::future::Future; +use std::pin::Pin; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::task::{Context, Poll}; + +use pin_project::pin_project; + +use super::AtomicWaker; + +#[pin_project] +pub struct Abortable { + #[pin] + future: F, + shared: Arc, +} + +impl Abortable { + pub fn new(handle: AbortHandle, future: F) -> Self { + Self { future, shared: handle.0 } + } +} + +impl Future for Abortable { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.shared.aborted.load(Ordering::Relaxed) { + return Poll::Ready(Err(Aborted)); + } + + if let Poll::Ready(value) = self.as_mut().project().future.poll(cx) { + return Poll::Ready(Ok(value)); + } + + self.shared.waker.register(cx.waker()); + + if self.shared.aborted.load(Ordering::Relaxed) { + return Poll::Ready(Err(Aborted)); + } + + Poll::Pending + } +} + +#[derive(Debug)] +struct Shared { + waker: AtomicWaker, + aborted: AtomicBool, +} + +#[derive(Clone, Debug)] +pub struct AbortHandle(Arc); + +impl AbortHandle { + pub fn new() -> Self { + Self(Arc::new(Shared { waker: AtomicWaker::new(), aborted: AtomicBool::new(false) })) + } + + pub fn abort(&self) { + self.0.aborted.store(true, Ordering::Relaxed); + self.0.waker.wake() + } +} + +#[derive(Debug)] +pub struct DropAbortHandle(AbortHandle); + +impl DropAbortHandle { + pub fn new(handle: AbortHandle) -> Self { + Self(handle) + } + + pub fn handle(&self) -> AbortHandle { + self.0.clone() + } +} + +impl Drop for DropAbortHandle { + fn drop(&mut self) { + self.0.abort() + } +} + +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] +pub struct Aborted; + +impl Display for Aborted { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "`Abortable` future has been aborted") + } +} + +impl Error for Aborted {} diff --git a/vendor/winit/src/platform_impl/web/async/atomic_waker.rs b/vendor/winit/src/platform_impl/web/async/atomic_waker.rs new file mode 100644 index 00000000..87b83612 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/async/atomic_waker.rs @@ -0,0 +1,35 @@ +use std::cell::RefCell; +use std::ops::Deref; +use std::task::Waker; + +#[derive(Debug)] +pub struct AtomicWaker(RefCell>); + +impl AtomicWaker { + pub const fn new() -> Self { + Self(RefCell::new(None)) + } + + pub fn register(&self, waker: &Waker) { + let mut this = self.0.borrow_mut(); + + if let Some(old_waker) = this.deref() { + if old_waker.will_wake(waker) { + return; + } + } + + *this = Some(waker.clone()); + } + + pub fn wake(&self) { + if let Some(waker) = self.0.borrow_mut().take() { + waker.wake(); + } + } +} + +// SAFETY: Wasm without the `atomics` target feature is single-threaded. +unsafe impl Send for AtomicWaker {} +// SAFETY: Wasm without the `atomics` target feature is single-threaded. +unsafe impl Sync for AtomicWaker {} diff --git a/vendor/winit/src/platform_impl/web/async/channel.rs b/vendor/winit/src/platform_impl/web/async/channel.rs new file mode 100644 index 00000000..11a7a479 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/async/channel.rs @@ -0,0 +1,113 @@ +use std::future; +use std::rc::Rc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::{self, RecvError, SendError, TryRecvError}; +use std::sync::{Arc, Mutex}; +use std::task::Poll; + +use super::AtomicWaker; + +pub fn channel() -> (Sender, Receiver) { + let (sender, receiver) = mpsc::channel(); + let shared = Arc::new(Shared { closed: AtomicBool::new(false), waker: AtomicWaker::new() }); + + let sender = + Sender(Arc::new(SenderInner { sender: Mutex::new(sender), shared: Arc::clone(&shared) })); + let receiver = Receiver { receiver: Rc::new(receiver), shared }; + + (sender, receiver) +} + +pub struct Sender(Arc>); + +struct SenderInner { + // We need to wrap it into a `Mutex` to make it `Sync`. So the sender can't + // be accessed on the main thread, as it could block. Additionally we need + // to wrap `Sender` in an `Arc` to make it cloneable on the main thread without + // having to block. + sender: Mutex>, + shared: Arc, +} + +impl Sender { + pub fn send(&self, event: T) -> Result<(), SendError> { + self.0.sender.lock().unwrap().send(event)?; + self.0.shared.waker.wake(); + + Ok(()) + } +} + +impl SenderInner { + fn close(&self) { + self.shared.closed.store(true, Ordering::Relaxed); + self.shared.waker.wake(); + } +} + +impl Clone for Sender { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } +} + +impl Drop for SenderInner { + fn drop(&mut self) { + self.close(); + } +} + +pub struct Receiver { + receiver: Rc>, + shared: Arc, +} + +impl Receiver { + pub async fn next(&self) -> Result { + future::poll_fn(|cx| match self.receiver.try_recv() { + Ok(event) => Poll::Ready(Ok(event)), + Err(TryRecvError::Empty) => { + self.shared.waker.register(cx.waker()); + + match self.receiver.try_recv() { + Ok(event) => Poll::Ready(Ok(event)), + Err(TryRecvError::Empty) => { + if self.shared.closed.load(Ordering::Relaxed) { + Poll::Ready(Err(RecvError)) + } else { + Poll::Pending + } + }, + Err(TryRecvError::Disconnected) => Poll::Ready(Err(RecvError)), + } + }, + Err(TryRecvError::Disconnected) => Poll::Ready(Err(RecvError)), + }) + .await + } + + pub fn try_recv(&self) -> Result, RecvError> { + match self.receiver.try_recv() { + Ok(value) => Ok(Some(value)), + Err(TryRecvError::Empty) => Ok(None), + Err(TryRecvError::Disconnected) => Err(RecvError), + } + } +} + +impl Clone for Receiver { + fn clone(&self) -> Self { + Self { receiver: Rc::clone(&self.receiver), shared: Arc::clone(&self.shared) } + } +} + +impl Drop for Receiver { + fn drop(&mut self) { + self.shared.closed.store(true, Ordering::Relaxed); + } +} + +struct Shared { + closed: AtomicBool, + waker: AtomicWaker, +} diff --git a/vendor/winit/src/platform_impl/web/async/concurrent_queue.rs b/vendor/winit/src/platform_impl/web/async/concurrent_queue.rs new file mode 100644 index 00000000..70afb3dd --- /dev/null +++ b/vendor/winit/src/platform_impl/web/async/concurrent_queue.rs @@ -0,0 +1,52 @@ +use std::cell::{Cell, RefCell}; + +#[derive(Debug)] +pub struct ConcurrentQueue { + queue: RefCell>, + closed: Cell, +} + +pub enum PushError { + #[allow(dead_code)] + Full(T), + Closed(T), +} + +pub enum PopError { + Empty, + Closed, +} + +impl ConcurrentQueue { + pub fn unbounded() -> Self { + Self { queue: RefCell::new(Vec::new()), closed: Cell::new(false) } + } + + pub fn push(&self, value: T) -> Result<(), PushError> { + if self.closed.get() { + return Err(PushError::Closed(value)); + } + + self.queue.borrow_mut().push(value); + Ok(()) + } + + pub fn pop(&self) -> Result { + self.queue.borrow_mut().pop().ok_or_else(|| { + if self.closed.get() { + PopError::Closed + } else { + PopError::Empty + } + }) + } + + pub fn close(&self) -> bool { + !self.closed.replace(true) + } +} + +// SAFETY: Wasm without the `atomics` target feature is single-threaded. +unsafe impl Send for ConcurrentQueue {} +// SAFETY: Wasm without the `atomics` target feature is single-threaded. +unsafe impl Sync for ConcurrentQueue {} diff --git a/vendor/winit/src/platform_impl/web/async/dispatcher.rs b/vendor/winit/src/platform_impl/web/async/dispatcher.rs new file mode 100644 index 00000000..10ab345e --- /dev/null +++ b/vendor/winit/src/platform_impl/web/async/dispatcher.rs @@ -0,0 +1,106 @@ +use super::super::main_thread::MainThreadMarker; +use super::{channel, Receiver, Sender, Wrapper}; +use std::cell::Ref; +use std::sync::{Arc, Condvar, Mutex}; + +pub struct Dispatcher(Wrapper>, Closure>); + +struct Closure(Box); + +impl Dispatcher { + #[track_caller] + pub fn new(main_thread: MainThreadMarker, value: T) -> Option<(Self, DispatchRunner)> { + let (sender, receiver) = channel::>(); + + Wrapper::new( + main_thread, + value, + |value, Closure(closure)| { + // SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do + // anything funny with it here. See `Self::queue()`. + closure(value.borrow().as_ref().unwrap()) + }, + { + let receiver = receiver.clone(); + move |value| async move { + while let Ok(Closure(closure)) = receiver.next().await { + // SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't + // do anything funny with it here. See + // `Self::queue()`. + closure(value.borrow().as_ref().unwrap()) + } + } + }, + sender, + |sender, closure| { + // SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do + // anything funny with it here. See `Self::queue()`. + sender.send(closure).unwrap() + }, + ) + .map(|wrapper| (Self(wrapper.clone()), DispatchRunner { wrapper, receiver })) + } + + pub fn value(&self) -> Option> { + self.0.value() + } + + pub fn dispatch(&self, f: impl 'static + FnOnce(&T) + Send) { + if let Some(value) = self.0.value() { + f(&value) + } else { + self.0.send(Closure(Box::new(f))) + } + } + + pub fn queue(&self, f: impl FnOnce(&T) -> R + Send) -> R { + if let Some(value) = self.0.value() { + f(&value) + } else { + let pair = Arc::new((Mutex::new(None), Condvar::new())); + let closure = Box::new({ + let pair = pair.clone(); + move |value: &T| { + *pair.0.lock().unwrap() = Some(f(value)); + pair.1.notify_one(); + } + }) as Box; + // SAFETY: The `transmute` is necessary because `Closure` requires `'static`. This is + // safe because this function won't return until `f` has finished executing. See + // `Self::new()`. + let closure = Closure(unsafe { + std::mem::transmute::< + Box, + Box, + >(closure) + }); + + self.0.send(closure); + + let mut started = pair.0.lock().unwrap(); + + while started.is_none() { + started = pair.1.wait(started).unwrap(); + } + + started.take().unwrap() + } + } +} + +pub struct DispatchRunner { + wrapper: Wrapper>, Closure>, + receiver: Receiver>, +} + +impl DispatchRunner { + pub fn run(&self) { + while let Some(Closure(closure)) = + self.receiver.try_recv().expect("should only be closed when `Dispatcher` is dropped") + { + // SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do anything + // funny with it here. See `Self::queue()`. + closure(&self.wrapper.value().expect("don't call this outside the main thread")) + } + } +} diff --git a/vendor/winit/src/platform_impl/web/async/mod.rs b/vendor/winit/src/platform_impl/web/async/mod.rs new file mode 100644 index 00000000..4681cf92 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/async/mod.rs @@ -0,0 +1,19 @@ +mod abortable; +#[cfg(not(target_feature = "atomics"))] +mod atomic_waker; +mod channel; +#[cfg(not(target_feature = "atomics"))] +mod concurrent_queue; +mod dispatcher; +mod notifier; +mod waker; +mod wrapper; + +pub use self::abortable::{AbortHandle, Abortable, DropAbortHandle}; +pub use self::channel::{channel, Receiver, Sender}; +pub use self::dispatcher::{DispatchRunner, Dispatcher}; +pub use self::notifier::{Notified, Notifier}; +pub use self::waker::{Waker, WakerSpawner}; +use self::wrapper::Wrapper; +use atomic_waker::AtomicWaker; +use concurrent_queue::{ConcurrentQueue, PushError}; diff --git a/vendor/winit/src/platform_impl/web/async/notifier.rs b/vendor/winit/src/platform_impl/web/async/notifier.rs new file mode 100644 index 00000000..81655d18 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/async/notifier.rs @@ -0,0 +1,72 @@ +use std::future::Future; +use std::pin::Pin; +use std::sync::{Arc, OnceLock}; +use std::task::{Context, Poll, Waker}; + +use super::{ConcurrentQueue, PushError}; + +#[derive(Debug)] +pub struct Notifier(Arc>); + +impl Notifier { + pub fn new() -> Self { + Self(Arc::new(Inner { queue: ConcurrentQueue::unbounded(), value: OnceLock::new() })) + } + + pub fn notify(self, value: T) { + if self.0.value.set(value).is_err() { + unreachable!("value set before") + } + + self.0.queue.close(); + + while let Ok(waker) = self.0.queue.pop() { + waker.wake() + } + } + + pub fn notified(&self) -> Notified { + Notified(Some(Arc::clone(&self.0))) + } +} + +#[derive(Clone, Debug)] +pub struct Notified(Option>>); + +impl Future for Notified { + type Output = T; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.0.take().expect("`Receiver` polled after completion"); + + if this.value.get().is_none() { + match this.queue.push(cx.waker().clone()) { + Ok(()) => { + if this.value.get().is_none() { + self.0 = Some(this); + return Poll::Pending; + } + }, + Err(PushError::Closed(_)) => (), + Err(PushError::Full(_)) => { + unreachable!("found full queue despite using unbounded queue") + }, + } + } + + let (Ok(Some(value)) | Err(Some(value))) = Arc::try_unwrap(this) + .map(|mut inner| inner.value.take()) + .map_err(|this| this.value.get().cloned()) + else { + unreachable!("found no value despite being ready") + }; + + Poll::Ready(value) + } +} + +#[derive(Debug)] +struct Inner { + queue: ConcurrentQueue, + value: OnceLock, +} diff --git a/vendor/winit/src/platform_impl/web/async/waker.rs b/vendor/winit/src/platform_impl/web/async/waker.rs new file mode 100644 index 00000000..40b316dc --- /dev/null +++ b/vendor/winit/src/platform_impl/web/async/waker.rs @@ -0,0 +1,131 @@ +use std::future; +use std::num::NonZeroUsize; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::Arc; +use std::task::Poll; + +use super::super::main_thread::MainThreadMarker; +use super::{AtomicWaker, Wrapper}; + +pub struct WakerSpawner(Wrapper, Sender, NonZeroUsize>); + +pub struct Waker(Wrapper, Sender, NonZeroUsize>); + +struct Handler { + value: T, + handler: fn(&T, NonZeroUsize, bool), +} + +#[derive(Clone)] +struct Sender(Arc); + +impl WakerSpawner { + #[track_caller] + pub fn new( + main_thread: MainThreadMarker, + value: T, + handler: fn(&T, NonZeroUsize, bool), + ) -> Option { + let inner = Arc::new(Inner { + counter: AtomicUsize::new(0), + waker: AtomicWaker::new(), + closed: AtomicBool::new(false), + }); + + let handler = Handler { value, handler }; + + let sender = Sender(Arc::clone(&inner)); + + let wrapper = Wrapper::new( + main_thread, + handler, + |handler, count| { + let handler = handler.borrow(); + let handler = handler.as_ref().unwrap(); + (handler.handler)(&handler.value, count, true); + }, + { + let inner = Arc::clone(&inner); + + move |handler| async move { + while let Some(count) = future::poll_fn(|cx| { + let count = inner.counter.swap(0, Ordering::Relaxed); + + match NonZeroUsize::new(count) { + Some(count) => Poll::Ready(Some(count)), + None => { + inner.waker.register(cx.waker()); + + let count = inner.counter.swap(0, Ordering::Relaxed); + + match NonZeroUsize::new(count) { + Some(count) => Poll::Ready(Some(count)), + None => { + if inner.closed.load(Ordering::Relaxed) { + return Poll::Ready(None); + } + + Poll::Pending + }, + } + }, + } + }) + .await + { + let handler = handler.borrow(); + let handler = handler.as_ref().unwrap(); + (handler.handler)(&handler.value, count, false); + } + } + }, + sender, + |inner, _| { + inner.0.counter.fetch_add(1, Ordering::Relaxed); + inner.0.waker.wake(); + }, + )?; + + Some(Self(wrapper)) + } + + pub fn waker(&self) -> Waker { + Waker(self.0.clone()) + } + + pub fn fetch(&self) -> usize { + debug_assert!( + MainThreadMarker::new().is_some(), + "this should only be called from the main thread" + ); + + self.0.with_sender_data(|inner| inner.0.counter.swap(0, Ordering::Relaxed)) + } +} + +impl Drop for WakerSpawner { + fn drop(&mut self) { + self.0.with_sender_data(|inner| { + inner.0.closed.store(true, Ordering::Relaxed); + inner.0.waker.wake(); + }); + } +} + +impl Waker { + pub fn wake(&self) { + self.0.send(NonZeroUsize::MIN) + } +} + +impl Clone for Waker { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +struct Inner { + counter: AtomicUsize, + waker: AtomicWaker, + closed: AtomicBool, +} diff --git a/vendor/winit/src/platform_impl/web/async/wrapper.rs b/vendor/winit/src/platform_impl/web/async/wrapper.rs new file mode 100644 index 00000000..2c7f6940 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/async/wrapper.rs @@ -0,0 +1,89 @@ +use super::super::main_thread::MainThreadMarker; +use std::cell::{Ref, RefCell}; +use std::future::Future; +use std::marker::PhantomData; +use std::sync::Arc; + +// Unsafe wrapper type that allows us to use `T` when it's not `Send` from other threads. +// `value` **must** only be accessed on the main thread. +pub struct Wrapper { + value: Value, + handler: fn(&RefCell>, E), + sender_data: S, + sender_handler: fn(&S, E), +} + +struct Value { + // SAFETY: + // This value must not be accessed if not on the main thread. + // + // - We wrap this in an `Arc` to allow it to be safely cloned without accessing the value. + // - The `RefCell` lets us mutably access in the main thread but is safe to drop in any thread + // because it has no `Drop` behavior. + // - The `Option` lets us safely drop `T` only in the main thread. + value: Arc>>, + // Prevent's `Send` or `Sync` to be automatically implemented. + local: PhantomData<*const ()>, +} + +// SAFETY: See `Self::value`. +unsafe impl Send for Value {} +// SAFETY: See `Self::value`. +unsafe impl Sync for Value {} + +impl Wrapper { + #[track_caller] + pub fn new>( + _: MainThreadMarker, + value: V, + handler: fn(&RefCell>, E), + receiver: impl 'static + FnOnce(Arc>>) -> R, + sender_data: S, + sender_handler: fn(&S, E), + ) -> Option { + let value = Arc::new(RefCell::new(Some(value))); + + wasm_bindgen_futures::spawn_local({ + let value = Arc::clone(&value); + async move { + receiver(Arc::clone(&value)).await; + drop(value.borrow_mut().take().unwrap()); + } + }); + + Some(Self { + value: Value { value, local: PhantomData }, + handler, + sender_data, + sender_handler, + }) + } + + pub fn send(&self, event: E) { + if MainThreadMarker::new().is_some() { + (self.handler)(&self.value.value, event) + } else { + (self.sender_handler)(&self.sender_data, event) + } + } + + pub fn value(&self) -> Option> { + MainThreadMarker::new() + .map(|_| Ref::map(self.value.value.borrow(), |value| value.as_ref().unwrap())) + } + + pub fn with_sender_data(&self, f: impl FnOnce(&S) -> T) -> T { + f(&self.sender_data) + } +} + +impl Clone for Wrapper { + fn clone(&self) -> Self { + Self { + value: Value { value: self.value.value.clone(), local: PhantomData }, + handler: self.handler, + sender_data: self.sender_data.clone(), + sender_handler: self.sender_handler, + } + } +} diff --git a/vendor/winit/src/platform_impl/web/cursor.rs b/vendor/winit/src/platform_impl/web/cursor.rs new file mode 100644 index 00000000..214ff45a --- /dev/null +++ b/vendor/winit/src/platform_impl/web/cursor.rs @@ -0,0 +1,730 @@ +use std::cell::RefCell; +use std::future::{self, Future}; +use std::hash::{Hash, Hasher}; +use std::mem; +use std::ops::{Deref, DerefMut}; +use std::pin::Pin; +use std::rc::Rc; +use std::sync::Arc; +use std::task::{ready, Context, Poll, Waker}; +use std::time::Duration; + +use cursor_icon::CursorIcon; +use js_sys::{Array, Object}; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::JsFuture; +use web_sys::{ + Blob, Document, DomException, HtmlCanvasElement, HtmlImageElement, ImageBitmap, + ImageBitmapOptions, ImageBitmapRenderingContext, ImageData, PremultiplyAlpha, Url, Window, +}; + +use super::backend::Style; +use super::main_thread::{MainThreadMarker, MainThreadSafe}; +use super::r#async::{AbortHandle, Abortable, DropAbortHandle, Notified, Notifier}; +use super::ActiveEventLoop; +use crate::cursor::{BadImage, Cursor, CursorImage, CustomCursor as RootCustomCursor}; +use crate::platform::web::CustomCursorError; + +#[derive(Debug)] +pub(crate) enum CustomCursorSource { + Image(CursorImage), + Url { url: String, hotspot_x: u16, hotspot_y: u16 }, + Animation { duration: Duration, cursors: Vec }, +} + +impl CustomCursorSource { + pub fn from_rgba( + rgba: Vec, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + ) -> Result { + Ok(CustomCursorSource::Image(CursorImage::from_rgba( + rgba, width, height, hotspot_x, hotspot_y, + )?)) + } +} + +#[derive(Clone, Debug)] +pub struct CustomCursor { + pub(crate) animation: bool, + state: Arc>>, +} + +impl Hash for CustomCursor { + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.state).hash(state); + } +} + +impl PartialEq for CustomCursor { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.state, &other.state) + } +} + +impl Eq for CustomCursor {} + +impl CustomCursor { + pub(crate) fn new(event_loop: &ActiveEventLoop, source: CustomCursorSource) -> Self { + match source { + CustomCursorSource::Image(image) => Self::build_spawn( + event_loop, + from_rgba(event_loop.runner.window(), event_loop.runner.document().clone(), &image), + false, + ), + CustomCursorSource::Url { url, hotspot_x, hotspot_y } => Self::build_spawn( + event_loop, + from_url(UrlType::Plain(url), hotspot_x, hotspot_y), + false, + ), + CustomCursorSource::Animation { duration, cursors } => Self::build_spawn( + event_loop, + from_animation( + event_loop.runner.main_thread(), + duration, + cursors.into_iter().map(|cursor| cursor.inner), + ), + true, + ), + } + } + + fn build_spawn(window_target: &ActiveEventLoop, task: F, animation: bool) -> CustomCursor + where + F: 'static + Future>, + S: Into, + { + let handle = AbortHandle::new(); + let this = CustomCursor { + animation, + state: Arc::new(MainThreadSafe::new( + window_target.runner.main_thread(), + RefCell::new(ImageState::Loading { + notifier: Notifier::new(), + _handle: DropAbortHandle::new(handle.clone()), + }), + )), + }; + let weak = Arc::downgrade(&this.state); + let main_thread = window_target.runner.main_thread(); + + let task = Abortable::new(handle, { + async move { + let result = task.await; + + let this = weak.upgrade().expect("`CursorHandler` invalidated without aborting"); + let mut this = this.get(main_thread).borrow_mut(); + + match result { + Ok(new_state) => { + let ImageState::Loading { notifier, .. } = + mem::replace(this.deref_mut(), new_state.into()) + else { + unreachable!("found invalid state"); + }; + notifier.notify(Ok(())); + }, + Err(error) => { + let ImageState::Loading { notifier, .. } = + mem::replace(this.deref_mut(), ImageState::Failed(error.clone())) + else { + unreachable!("found invalid state"); + }; + notifier.notify(Err(error)); + }, + } + } + }); + + wasm_bindgen_futures::spawn_local(async move { + let _ = task.await; + }); + + this + } + + pub(crate) fn new_async( + event_loop: &ActiveEventLoop, + source: CustomCursorSource, + ) -> CustomCursorFuture { + let CustomCursor { animation, state } = Self::new(event_loop, source); + let binding = state.get(event_loop.runner.main_thread()).borrow(); + let ImageState::Loading { notifier, .. } = binding.deref() else { + unreachable!("found invalid state") + }; + let notified = notifier.notified(); + drop(binding); + + CustomCursorFuture { notified, animation, state: Some(state) } + } +} + +#[derive(Debug)] +pub struct CustomCursorFuture { + notified: Notified>, + animation: bool, + state: Option>>>, +} + +impl Future for CustomCursorFuture { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.state.is_none() { + panic!("`CustomCursorFuture` polled after completion") + } + + let result = ready!(Pin::new(&mut self.notified).poll(cx)); + let state = self.state.take().expect("`CustomCursorFuture` polled after completion"); + + Poll::Ready(result.map(|_| CustomCursor { animation: self.animation, state })) + } +} + +#[derive(Debug)] +pub struct CursorHandler(Rc>); + +#[derive(Debug)] +struct Inner { + main_thread: MainThreadMarker, + canvas: HtmlCanvasElement, + style: Style, + visible: bool, + cursor: SelectedCursor, +} + +impl CursorHandler { + pub(crate) fn new( + main_thread: MainThreadMarker, + canvas: HtmlCanvasElement, + style: Style, + ) -> Self { + Self(Rc::new(RefCell::new(Inner { + main_thread, + canvas, + style, + visible: true, + cursor: SelectedCursor::default(), + }))) + } + + pub fn set_cursor(&self, cursor: Cursor) { + let mut this = self.0.borrow_mut(); + + match cursor { + Cursor::Icon(icon) => { + if let SelectedCursor::Icon(old_icon) + | SelectedCursor::Loading { previous: Previous::Icon(old_icon), .. } = + &this.cursor + { + if *old_icon == icon { + return; + } + } + + this.cursor = SelectedCursor::Icon(icon); + this.set_style(); + }, + Cursor::Custom(cursor) => { + let cursor = cursor.inner; + + if let SelectedCursor::Loading { cursor: old_cursor, .. } + | SelectedCursor::Image(old_cursor) + | SelectedCursor::Animation { cursor: old_cursor, .. } = &this.cursor + { + if *old_cursor == cursor { + return; + } + } + + let state = cursor.state.get(this.main_thread).borrow(); + + match state.deref() { + ImageState::Loading { notifier, .. } => { + let notified = notifier.notified(); + let handle = DropAbortHandle::new(AbortHandle::new()); + let task = Abortable::new(handle.handle(), { + let weak = Rc::downgrade(&self.0); + async move { + let _ = notified.await; + let handler = weak + .upgrade() + .expect("`CursorHandler` invalidated without aborting"); + handler.borrow_mut().notify(); + } + }); + wasm_bindgen_futures::spawn_local(async move { + let _ = task.await; + }); + + drop(state); + this.cursor = SelectedCursor::Loading { + cursor, + previous: mem::take(&mut this.cursor).into(), + _handle: handle, + }; + }, + ImageState::Failed(error) => { + tracing::error!( + "trying to load custom cursor that has failed to load: {error}" + ) + }, + ImageState::Image(_) => { + drop(state); + this.cursor = SelectedCursor::Image(cursor); + this.set_style(); + }, + ImageState::Animation(animation) => { + let canvas: &CanvasAnimateExt = this.canvas.unchecked_ref(); + let animation = canvas.animate_with_keyframe_animation_options( + Some(&animation.keyframes), + &animation.options, + ); + drop(state); + + if !this.visible { + animation.cancel(); + } + + this.cursor = SelectedCursor::Animation { + animation: AnimationDropper(animation), + cursor, + }; + this.set_style(); + }, + }; + }, + } + } + + pub fn set_cursor_visible(&self, visible: bool) { + let mut this = self.0.borrow_mut(); + + if !visible && this.visible { + this.visible = false; + this.style.set("cursor", "none"); + + if let SelectedCursor::Animation { animation, .. } = &this.cursor { + animation.0.cancel(); + } + } else if visible && !this.visible { + this.visible = true; + this.set_style(); + } + } +} + +impl Inner { + fn set_style(&self) { + if self.visible { + match &self.cursor { + SelectedCursor::Icon(icon) + | SelectedCursor::Loading { previous: Previous::Icon(icon), .. } => { + if let CursorIcon::Default = icon { + self.style.remove("cursor") + } else { + self.style.set("cursor", icon.name()) + } + }, + SelectedCursor::Loading { previous: Previous::Image(cursor), .. } + | SelectedCursor::Image(cursor) => { + match cursor.state.get(self.main_thread).borrow().deref() { + ImageState::Image(Image { style, .. }) => self.style.set("cursor", style), + _ => unreachable!("found invalid saved state"), + } + }, + SelectedCursor::Loading { + previous: Previous::Animation { animation, .. }, .. + } + | SelectedCursor::Animation { animation, .. } => { + self.style.remove("cursor"); + animation.0.play() + }, + } + } + } + + fn notify(&mut self) { + let SelectedCursor::Loading { cursor, previous, .. } = mem::take(&mut self.cursor) else { + unreachable!("found wrong state") + }; + + let state = cursor.state.get(self.main_thread).borrow(); + match state.deref() { + ImageState::Image(_) => { + drop(state); + self.cursor = SelectedCursor::Image(cursor); + self.set_style(); + }, + ImageState::Animation(animation) => { + let canvas: &CanvasAnimateExt = self.canvas.unchecked_ref(); + let animation = canvas.animate_with_keyframe_animation_options( + Some(&animation.keyframes), + &animation.options, + ); + drop(state); + + if !self.visible { + animation.cancel(); + } + + self.cursor = + SelectedCursor::Animation { animation: AnimationDropper(animation), cursor }; + self.set_style(); + }, + ImageState::Failed(error) => { + tracing::error!("custom cursor failed to load: {error}"); + self.cursor = previous.into() + }, + ImageState::Loading { .. } => unreachable!("notified without being ready"), + } + } +} + +#[derive(Debug)] +enum SelectedCursor { + Icon(CursorIcon), + Loading { cursor: CustomCursor, previous: Previous, _handle: DropAbortHandle }, + Image(CustomCursor), + Animation { cursor: CustomCursor, animation: AnimationDropper }, +} + +impl Default for SelectedCursor { + fn default() -> Self { + Self::Icon(Default::default()) + } +} + +impl From for SelectedCursor { + fn from(previous: Previous) -> Self { + match previous { + Previous::Icon(icon) => Self::Icon(icon), + Previous::Image(cursor) => Self::Image(cursor), + Previous::Animation { cursor, animation } => Self::Animation { cursor, animation }, + } + } +} + +#[derive(Debug)] +enum Previous { + Icon(CursorIcon), + Image(CustomCursor), + Animation { cursor: CustomCursor, animation: AnimationDropper }, +} + +impl From for Previous { + fn from(value: SelectedCursor) -> Self { + match value { + SelectedCursor::Icon(icon) => Self::Icon(icon), + SelectedCursor::Loading { previous, .. } => previous, + SelectedCursor::Image(image) => Self::Image(image), + SelectedCursor::Animation { cursor, animation } => { + Self::Animation { cursor, animation } + }, + } + } +} + +#[derive(Debug)] +enum ImageState { + Loading { notifier: Notifier>, _handle: DropAbortHandle }, + Failed(CustomCursorError), + Image(Image), + Animation(Animation), +} + +#[derive(Debug)] +struct Image { + style: String, + _object_url: Option, + _image: HtmlImageElement, +} + +impl From for ImageState { + fn from(image: Image) -> Self { + Self::Image(image) + } +} + +#[derive(Debug)] +struct Animation { + keyframes: Array, + options: KeyframeAnimationOptions, + _images: Vec, +} + +impl From for ImageState { + fn from(animation: Animation) -> Self { + Self::Animation(animation) + } +} + +#[derive(Debug)] +enum UrlType { + Plain(String), + Object(ObjectUrl), +} + +impl UrlType { + fn url(&self) -> &str { + match &self { + UrlType::Plain(url) => url, + UrlType::Object(object_url) => &object_url.0, + } + } +} + +#[derive(Debug)] +struct ObjectUrl(String); + +impl Drop for ObjectUrl { + fn drop(&mut self) { + Url::revoke_object_url(&self.0).expect("unexpected exception in `URL.revokeObjectURL()`"); + } +} + +#[derive(Debug)] +struct AnimationDropper(WebAnimation); + +impl Drop for AnimationDropper { + fn drop(&mut self) { + self.0.cancel() + } +} + +fn from_rgba( + window: &Window, + document: Document, + image: &CursorImage, +) -> impl Future> { + // 1. Create an `ImageData` from the RGBA data. + // 2. Create an `ImageBitmap` from the `ImageData`. + // 3. Draw `ImageBitmap` on an `HTMLCanvasElement`. + // 4. Create a `Blob` from the `HTMLCanvasElement`. + // 5. Create an object URL from the `Blob`. + // 6. Decode the image on an `HTMLImageElement` from the URL. + + // 1. Create an `ImageData` from the RGBA data. + // Adapted from https://github.com/rust-windowing/softbuffer/blob/ab7688e2ed2e2eca51b3c4e1863a5bd7fe85800e/src/web.rs#L196-L223 + #[cfg(target_feature = "atomics")] + // Can't share `SharedArrayBuffer` with `ImageData`. + let result = { + use js_sys::{Uint8Array, Uint8ClampedArray}; + use wasm_bindgen::JsValue; + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(js_namespace = ImageData)] + type ImageDataExt; + #[wasm_bindgen(catch, constructor, js_class = ImageData)] + fn new(array: Uint8ClampedArray, sw: u32) -> Result; + } + + let array = Uint8Array::new_with_length(image.rgba.len() as u32); + array.copy_from(&image.rgba); + let array = Uint8ClampedArray::new(&array); + ImageDataExt::new(array, image.width as u32) + .map(JsValue::from) + .map(ImageData::unchecked_from_js) + }; + #[cfg(not(target_feature = "atomics"))] + let result = ImageData::new_with_u8_clamped_array( + wasm_bindgen::Clamped(&image.rgba), + image.width as u32, + ); + let image_data = result.expect("found wrong image size"); + + // 2. Create an `ImageBitmap` from the `ImageData`. + // + // We call `createImageBitmap()` before spawning the future, + // to not have to clone the image buffer. + let options = ImageBitmapOptions::new(); + options.set_premultiply_alpha(PremultiplyAlpha::None); + let bitmap = JsFuture::from( + window + .create_image_bitmap_with_image_data_and_image_bitmap_options(&image_data, &options) + .expect("unexpected exception in `createImageBitmap()`"), + ); + + let CursorImage { width, height, hotspot_x, hotspot_y, .. } = *image; + async move { + let bitmap: ImageBitmap = + bitmap.await.expect("found invalid state in `ImageData`").unchecked_into(); + + let canvas: HtmlCanvasElement = + document.create_element("canvas").expect("invalid tag name").unchecked_into(); + #[allow(clippy::disallowed_methods)] + canvas.set_width(width as u32); + #[allow(clippy::disallowed_methods)] + canvas.set_height(height as u32); + + // 3. Draw `ImageBitmap` on an `HTMLCanvasElement`. + let context: ImageBitmapRenderingContext = canvas + .get_context("bitmaprenderer") + .expect("unexpected exception in `HTMLCanvasElement.getContext()`") + .expect("`bitmaprenderer` context unsupported") + .unchecked_into(); + context.transfer_from_image_bitmap(&bitmap); + drop(bitmap); + drop(context); + + // 4. Create a `Blob` from the `HTMLCanvasElement`. + // + // To keep the `Closure` alive until `HTMLCanvasElement.toBlob()` is done, + // we do the whole `Waker` strategy. Commonly on `Drop` the callback is aborted, + // but it would increase complexity and isn't possible in this case. + // Keep in mind that `HTMLCanvasElement.toBlob()` can call the callback immediately. + let value = Rc::new(RefCell::new(None)); + let waker = Rc::new(RefCell::>::new(None)); + let callback = Closure::once({ + let value = value.clone(); + let waker = waker.clone(); + move |blob: Option| { + *value.borrow_mut() = Some(blob); + if let Some(waker) = waker.borrow_mut().take() { + waker.wake(); + } + } + }); + canvas + .to_blob(callback.as_ref().unchecked_ref()) + .expect("failed with `SecurityError` despite only source coming from memory"); + let blob = future::poll_fn(|cx| { + if let Some(blob) = value.borrow_mut().take() { + Poll::Ready(blob) + } else { + *waker.borrow_mut() = Some(cx.waker().clone()); + Poll::Pending + } + }) + .await; + drop(canvas); + + let Some(blob) = blob else { + return Err(CustomCursorError::Blob); + }; + + // 5. Create an object URL from the `Blob`. + let url = Url::create_object_url_with_blob(&blob) + .expect("unexpected exception in `URL.createObjectURL()`"); + let url = UrlType::Object(ObjectUrl(url)); + + from_url(url, hotspot_x, hotspot_y).await + } +} + +async fn from_url( + url: UrlType, + hotspot_x: u16, + hotspot_y: u16, +) -> Result { + // 6. Decode the image on an `HTMLImageElement` from the URL. + let image = HtmlImageElement::new().expect("unexpected exception in `new HtmlImageElement`"); + image.set_src(url.url()); + let result = JsFuture::from(image.decode()).await; + + if let Err(error) = result { + debug_assert!(error.has_type::()); + let error: DomException = error.unchecked_into(); + debug_assert_eq!(error.name(), "EncodingError"); + let error = error.message(); + + return Err(CustomCursorError::Decode(error)); + } + + Ok(Image { + style: format!("url({}) {hotspot_x} {hotspot_y}, auto", url.url()), + _object_url: match url { + UrlType::Plain(_) => None, + UrlType::Object(object_url) => Some(object_url), + }, + _image: image, + }) +} + +#[allow(clippy::await_holding_refcell_ref)] // false-positive +async fn from_animation( + main_thread: MainThreadMarker, + duration: Duration, + cursors: impl ExactSizeIterator, +) -> Result { + let keyframes = Array::new(); + let mut images = Vec::with_capacity(cursors.len()); + + for cursor in cursors { + let state = cursor.state.get(main_thread).borrow(); + + match state.deref() { + ImageState::Loading { notifier, .. } => { + let notified = notifier.notified(); + drop(state); + notified.await?; + }, + ImageState::Failed(error) => return Err(error.clone()), + ImageState::Image(_) => drop(state), + ImageState::Animation(_) => unreachable!("check in `CustomCursorSource` failed"), + } + + let state = cursor.state.get(main_thread).borrow(); + let style = match state.deref() { + ImageState::Image(Image { style, .. }) => style, + _ => unreachable!("found invalid state"), + }; + + let keyframe: Keyframe = Object::new().unchecked_into(); + keyframe.set_cursor(style); + keyframes.push(&keyframe); + drop(state); + + images.push(cursor); + } + + keyframes.push(&keyframes.get(0)); + + let options: KeyframeAnimationOptions = Object::new().unchecked_into(); + options.set_duration(duration.as_millis() as f64); + options.set_iterations(f64::INFINITY); + + Ok(Animation { keyframes, options, _images: images }) +} + +#[wasm_bindgen] +extern "C" { + type CanvasAnimateExt; + + #[wasm_bindgen(method, js_name = animate)] + fn animate_with_keyframe_animation_options( + this: &CanvasAnimateExt, + keyframes: Option<&Object>, + options: &KeyframeAnimationOptions, + ) -> WebAnimation; + + #[derive(Debug)] + type WebAnimation; + + #[wasm_bindgen(method)] + fn cancel(this: &WebAnimation); + + #[wasm_bindgen(method)] + fn play(this: &WebAnimation); + + #[wasm_bindgen(extends = Object)] + type Keyframe; + + #[wasm_bindgen(method, setter, js_name = cursor)] + fn set_cursor(this: &Keyframe, value: &str); + + #[derive(Debug)] + #[wasm_bindgen(extends = Object)] + type KeyframeAnimationOptions; + + #[wasm_bindgen(method, setter, js_name = duration)] + fn set_duration(this: &KeyframeAnimationOptions, value: f64); + + #[wasm_bindgen(method, setter, js_name = iterations)] + fn set_iterations(this: &KeyframeAnimationOptions, value: f64); +} diff --git a/vendor/winit/src/platform_impl/web/device.rs b/vendor/winit/src/platform_impl/web/device.rs new file mode 100644 index 00000000..91d08586 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/device.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId(pub i32); + +impl DeviceId { + pub const fn dummy() -> Self { + Self(0) + } +} diff --git a/vendor/winit/src/platform_impl/web/error.rs b/vendor/winit/src/platform_impl/web/error.rs new file mode 100644 index 00000000..6995f2bc --- /dev/null +++ b/vendor/winit/src/platform_impl/web/error.rs @@ -0,0 +1,10 @@ +use std::fmt; + +#[derive(Debug)] +pub struct OsError(pub String); + +impl fmt::Display for OsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/vendor/winit/src/platform_impl/web/event_loop/mod.rs b/vendor/winit/src/platform_impl/web/event_loop/mod.rs new file mode 100644 index 00000000..23fe2e00 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/event_loop/mod.rs @@ -0,0 +1,119 @@ +use std::marker::PhantomData; +use std::sync::mpsc::{self, Receiver, Sender}; + +use crate::error::EventLoopError; +use crate::event::Event; +use crate::event_loop::ActiveEventLoop as RootActiveEventLoop; +use crate::platform::web::{ActiveEventLoopExtWebSys, PollStrategy, WaitUntilStrategy}; + +use super::{backend, device, window}; + +mod proxy; +pub(crate) mod runner; +mod state; +mod window_target; + +pub(crate) use proxy::EventLoopProxy; +pub(crate) use window_target::{ActiveEventLoop, OwnedDisplayHandle}; + +pub struct EventLoop { + elw: RootActiveEventLoop, + user_event_sender: Sender, + user_event_receiver: Receiver, +} + +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PlatformSpecificEventLoopAttributes {} + +impl EventLoop { + pub(crate) fn new(_: &PlatformSpecificEventLoopAttributes) -> Result { + let (user_event_sender, user_event_receiver) = mpsc::channel(); + let elw = RootActiveEventLoop { p: ActiveEventLoop::new(), _marker: PhantomData }; + Ok(EventLoop { elw, user_event_sender, user_event_receiver }) + } + + pub fn run(self, mut event_handler: F) -> ! + where + F: FnMut(Event, &RootActiveEventLoop), + { + let target = RootActiveEventLoop { p: self.elw.p.clone(), _marker: PhantomData }; + + // SAFETY: Don't use `move` to make sure we leak the `event_handler` and `target`. + let handler: Box)> = Box::new(|event| { + let event = match event.map_nonuser_event() { + Ok(event) => event, + Err(Event::UserEvent(())) => Event::UserEvent( + self.user_event_receiver + .try_recv() + .expect("handler woken up without user event"), + ), + Err(_) => unreachable!(), + }; + event_handler(event, &target) + }); + // SAFETY: The `transmute` is necessary because `run()` requires `'static`. This is safe + // because this function will never return and all resources not cleaned up by the point we + // `throw` will leak, making this actually `'static`. + let handler = unsafe { + std::mem::transmute::)>, Box) + 'static>>( + handler, + ) + }; + self.elw.p.run(handler, false); + + // Throw an exception to break out of Rust execution and use unreachable to tell the + // compiler this function won't return, giving it a return type of '!' + backend::throw( + "Using exceptions for control flow, don't mind me. This isn't actually an error!", + ); + + unreachable!(); + } + + pub fn spawn(self, mut event_handler: F) + where + F: 'static + FnMut(Event, &RootActiveEventLoop), + { + let target = RootActiveEventLoop { p: self.elw.p.clone(), _marker: PhantomData }; + + self.elw.p.run( + Box::new(move |event| { + let event = match event.map_nonuser_event() { + Ok(event) => event, + Err(Event::UserEvent(())) => Event::UserEvent( + self.user_event_receiver + .try_recv() + .expect("handler woken up without user event"), + ), + Err(_) => unreachable!(), + }; + event_handler(event, &target) + }), + true, + ); + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy::new(self.elw.p.waker(), self.user_event_sender.clone()) + } + + pub fn window_target(&self) -> &RootActiveEventLoop { + &self.elw + } + + pub fn set_poll_strategy(&self, strategy: PollStrategy) { + self.elw.set_poll_strategy(strategy); + } + + pub fn poll_strategy(&self) -> PollStrategy { + self.elw.poll_strategy() + } + + pub fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) { + self.elw.set_wait_until_strategy(strategy); + } + + pub fn wait_until_strategy(&self) -> WaitUntilStrategy { + self.elw.wait_until_strategy() + } +} diff --git a/vendor/winit/src/platform_impl/web/event_loop/proxy.rs b/vendor/winit/src/platform_impl/web/event_loop/proxy.rs new file mode 100644 index 00000000..bd8e7146 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/event_loop/proxy.rs @@ -0,0 +1,29 @@ +use std::rc::Weak; +use std::sync::mpsc::{SendError, Sender}; + +use super::runner::Execution; +use crate::event_loop::EventLoopClosed; +use crate::platform_impl::platform::r#async::Waker; + +pub struct EventLoopProxy { + runner: Waker>, + sender: Sender, +} + +impl EventLoopProxy { + pub fn new(runner: Waker>, sender: Sender) -> Self { + Self { runner, sender } + } + + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.sender.send(event).map_err(|SendError(event)| EventLoopClosed(event))?; + self.runner.wake(); + Ok(()) + } +} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + Self { runner: self.runner.clone(), sender: self.sender.clone() } + } +} diff --git a/vendor/winit/src/platform_impl/web/event_loop/runner.rs b/vendor/winit/src/platform_impl/web/event_loop/runner.rs new file mode 100644 index 00000000..d9ba3d37 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/event_loop/runner.rs @@ -0,0 +1,808 @@ +use std::cell::{Cell, RefCell}; +use std::collections::{HashSet, VecDeque}; +use std::iter; +use std::num::NonZeroUsize; +use std::ops::Deref; +use std::rc::{Rc, Weak}; + +use wasm_bindgen::prelude::Closure; +use wasm_bindgen::JsCast; +use web_sys::{Document, KeyboardEvent, PageTransitionEvent, PointerEvent, WheelEvent}; +use web_time::{Duration, Instant}; + +use super::super::main_thread::MainThreadMarker; +use super::super::DeviceId; +use super::backend; +use super::state::State; +use crate::dpi::PhysicalSize; +use crate::event::{ + DeviceEvent, DeviceId as RootDeviceId, ElementState, Event, RawKeyEvent, StartCause, + WindowEvent, +}; +use crate::event_loop::{ControlFlow, DeviceEvents}; +use crate::platform::web::{PollStrategy, WaitUntilStrategy}; +use crate::platform_impl::platform::backend::EventListenerHandle; +use crate::platform_impl::platform::r#async::{DispatchRunner, Waker, WakerSpawner}; +use crate::platform_impl::platform::window::Inner; +use crate::window::WindowId; + +pub struct Shared(Rc); + +pub(super) type EventHandler = dyn FnMut(Event<()>); + +impl Clone for Shared { + fn clone(&self) -> Self { + Shared(self.0.clone()) + } +} + +type OnEventHandle = RefCell>>; + +pub struct Execution { + main_thread: MainThreadMarker, + proxy_spawner: WakerSpawner>, + control_flow: Cell, + poll_strategy: Cell, + wait_until_strategy: Cell, + exit: Cell, + runner: RefCell, + suspended: Cell, + event_loop_recreation: Cell, + events: RefCell>, + id: RefCell, + window: web_sys::Window, + document: Document, + #[allow(clippy::type_complexity)] + all_canvases: RefCell>, DispatchRunner)>>, + redraw_pending: RefCell>, + destroy_pending: RefCell>, + page_transition_event_handle: RefCell>, + device_events: Cell, + on_mouse_move: OnEventHandle, + on_wheel: OnEventHandle, + on_mouse_press: OnEventHandle, + on_mouse_release: OnEventHandle, + on_key_press: OnEventHandle, + on_key_release: OnEventHandle, + on_visibility_change: OnEventHandle, +} + +enum RunnerEnum { + /// The `EventLoop` is created but not being run. + Pending, + /// The `EventLoop` is being run. + Running(Runner), + /// The `EventLoop` is exited after being started with `EventLoop::run_app`. Since + /// `EventLoop::run_app` takes ownership of the `EventLoop`, we can be certain + /// that this event loop will never be run again. + Destroyed, +} + +impl RunnerEnum { + fn maybe_runner(&self) -> Option<&Runner> { + match self { + RunnerEnum::Running(runner) => Some(runner), + _ => None, + } + } +} + +struct Runner { + state: State, + event_handler: Box, +} + +impl Runner { + pub fn new(event_handler: Box) -> Self { + Runner { state: State::Init, event_handler } + } + + /// Returns the corresponding `StartCause` for the current `state`, or `None` + /// when in `Exit` state. + fn maybe_start_cause(&self) -> Option { + Some(match self.state { + State::Init => StartCause::Init, + State::Poll { .. } => StartCause::Poll, + State::Wait { start } => StartCause::WaitCancelled { start, requested_resume: None }, + State::WaitUntil { start, end, .. } => { + StartCause::WaitCancelled { start, requested_resume: Some(end) } + }, + State::Exit => return None, + }) + } + + fn handle_single_event(&mut self, runner: &Shared, event: impl Into) { + match event.into() { + EventWrapper::Event(event) => (self.event_handler)(event), + EventWrapper::ScaleChange { canvas, size, scale } => { + if let Some(canvas) = canvas.upgrade() { + canvas.borrow().handle_scale_change( + runner, + |event| (self.event_handler)(event), + size, + scale, + ) + } + }, + } + } +} + +impl Shared { + pub fn new() -> Self { + let main_thread = MainThreadMarker::new().expect("only callable from inside the `Window`"); + #[allow(clippy::disallowed_methods)] + let window = web_sys::window().expect("only callable from inside the `Window`"); + #[allow(clippy::disallowed_methods)] + let document = window.document().expect("Failed to obtain document"); + + Shared(Rc::::new_cyclic(|weak| { + let proxy_spawner = + WakerSpawner::new(main_thread, weak.clone(), |runner, count, local| { + if let Some(runner) = runner.upgrade() { + Shared(runner).send_user_events(count, local) + } + }) + .expect("`EventLoop` has to be created in the main thread"); + + Execution { + main_thread, + proxy_spawner, + control_flow: Cell::new(ControlFlow::default()), + poll_strategy: Cell::new(PollStrategy::default()), + wait_until_strategy: Cell::new(WaitUntilStrategy::default()), + exit: Cell::new(false), + runner: RefCell::new(RunnerEnum::Pending), + suspended: Cell::new(false), + event_loop_recreation: Cell::new(false), + events: RefCell::new(VecDeque::new()), + window, + document, + id: RefCell::new(0), + all_canvases: RefCell::new(Vec::new()), + redraw_pending: RefCell::new(HashSet::new()), + destroy_pending: RefCell::new(VecDeque::new()), + page_transition_event_handle: RefCell::new(None), + device_events: Cell::default(), + on_mouse_move: RefCell::new(None), + on_wheel: RefCell::new(None), + on_mouse_press: RefCell::new(None), + on_mouse_release: RefCell::new(None), + on_key_press: RefCell::new(None), + on_key_release: RefCell::new(None), + on_visibility_change: RefCell::new(None), + } + })) + } + + pub fn main_thread(&self) -> MainThreadMarker { + self.0.main_thread + } + + pub fn window(&self) -> &web_sys::Window { + &self.0.window + } + + pub fn document(&self) -> &Document { + &self.0.document + } + + pub fn add_canvas( + &self, + id: WindowId, + canvas: Weak>, + runner: DispatchRunner, + ) { + self.0.all_canvases.borrow_mut().push((id, canvas, runner)); + } + + pub fn notify_destroy_window(&self, id: WindowId) { + self.0.destroy_pending.borrow_mut().push_back(id); + } + + // Set the event callback to use for the event loop runner + // This the event callback is a fairly thin layer over the user-provided callback that closes + // over a RootActiveEventLoop reference + pub fn set_listener(&self, event_handler: Box) { + { + let mut runner = self.0.runner.borrow_mut(); + assert!(matches!(*runner, RunnerEnum::Pending)); + *runner = RunnerEnum::Running(Runner::new(event_handler)); + } + self.init(); + + *self.0.page_transition_event_handle.borrow_mut() = Some(backend::on_page_transition( + self.window().clone(), + { + let runner = self.clone(); + move |event: PageTransitionEvent| { + if event.persisted() { + runner.0.suspended.set(false); + runner.send_event(Event::Resumed); + } + } + }, + { + let runner = self.clone(); + move |event: PageTransitionEvent| { + runner.0.suspended.set(true); + if event.persisted() { + runner.send_event(Event::Suspended); + } else { + runner.handle_unload(); + } + } + }, + )); + + let runner = self.clone(); + let window = self.window().clone(); + *self.0.on_mouse_move.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "pointermove", + Closure::new(move |event: PointerEvent| { + if !runner.device_events() { + return; + } + + // chorded button event + let device_id = RootDeviceId(DeviceId(event.pointer_id())); + + if let Some(button) = backend::event::mouse_button(&event) { + let state = if backend::event::mouse_buttons(&event).contains(button.into()) { + ElementState::Pressed + } else { + ElementState::Released + }; + + runner.send_event(Event::DeviceEvent { + device_id, + event: DeviceEvent::Button { button: button.to_id(), state }, + }); + + return; + } + + // pointer move event + let mut delta = backend::event::MouseDelta::init(&window, &event); + runner.send_events(backend::event::pointer_move_event(event).flat_map(|event| { + let delta = delta.delta(&event).to_physical(backend::scale_factor(&window)); + + let x_motion = (delta.x != 0.0).then_some(Event::DeviceEvent { + device_id, + event: DeviceEvent::Motion { axis: 0, value: delta.x }, + }); + + let y_motion = (delta.y != 0.0).then_some(Event::DeviceEvent { + device_id, + event: DeviceEvent::Motion { axis: 1, value: delta.y }, + }); + + x_motion.into_iter().chain(y_motion).chain(iter::once(Event::DeviceEvent { + device_id, + event: DeviceEvent::MouseMotion { delta: (delta.x, delta.y) }, + })) + })); + }), + )); + let runner = self.clone(); + let window = self.window().clone(); + *self.0.on_wheel.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "wheel", + Closure::new(move |event: WheelEvent| { + if !runner.device_events() { + return; + } + + if let Some(delta) = backend::event::mouse_scroll_delta(&window, &event) { + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(DeviceId(0)), + event: DeviceEvent::MouseWheel { delta }, + }); + } + }), + )); + let runner = self.clone(); + *self.0.on_mouse_press.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "pointerdown", + Closure::new(move |event: PointerEvent| { + if !runner.device_events() { + return; + } + + let button = backend::event::mouse_button(&event).expect("no mouse button pressed"); + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(DeviceId(event.pointer_id())), + event: DeviceEvent::Button { + button: button.to_id(), + state: ElementState::Pressed, + }, + }); + }), + )); + let runner = self.clone(); + *self.0.on_mouse_release.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "pointerup", + Closure::new(move |event: PointerEvent| { + if !runner.device_events() { + return; + } + + let button = backend::event::mouse_button(&event).expect("no mouse button pressed"); + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(DeviceId(event.pointer_id())), + event: DeviceEvent::Button { + button: button.to_id(), + state: ElementState::Released, + }, + }); + }), + )); + let runner = self.clone(); + *self.0.on_key_press.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "keydown", + Closure::new(move |event: KeyboardEvent| { + if !runner.device_events() { + return; + } + + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(DeviceId::dummy()), + event: DeviceEvent::Key(RawKeyEvent { + physical_key: backend::event::key_code(&event), + state: ElementState::Pressed, + }), + }); + }), + )); + let runner = self.clone(); + *self.0.on_key_release.borrow_mut() = Some(EventListenerHandle::new( + self.window().clone(), + "keyup", + Closure::new(move |event: KeyboardEvent| { + if !runner.device_events() { + return; + } + + runner.send_event(Event::DeviceEvent { + device_id: RootDeviceId(DeviceId::dummy()), + event: DeviceEvent::Key(RawKeyEvent { + physical_key: backend::event::key_code(&event), + state: ElementState::Released, + }), + }); + }), + )); + let runner = self.clone(); + *self.0.on_visibility_change.borrow_mut() = Some(EventListenerHandle::new( + // Safari <14 doesn't support the `visibilitychange` event on `Window`. + self.document().clone(), + "visibilitychange", + Closure::new(move |_| { + if !runner.0.suspended.get() { + for (id, canvas, _) in &*runner.0.all_canvases.borrow() { + if let Some(canvas) = canvas.upgrade() { + let is_visible = backend::is_visible(runner.document()); + // only fire if: + // - not visible and intersects + // - not visible and we don't know if it intersects yet + // - visible and intersects + if let (false, Some(true) | None) | (true, Some(true)) = + (is_visible, canvas.borrow().is_intersecting) + { + runner.send_event(Event::WindowEvent { + window_id: *id, + event: WindowEvent::Occluded(!is_visible), + }); + } + } + } + } + }), + )); + } + + // Generate a strictly increasing ID + // This is used to differentiate windows when handling events + pub fn generate_id(&self) -> u32 { + let mut id = self.0.id.borrow_mut(); + *id += 1; + + *id + } + + pub fn request_redraw(&self, id: WindowId) { + self.0.redraw_pending.borrow_mut().insert(id); + self.send_events::(iter::empty()); + } + + pub fn init(&self) { + // NB: For consistency all platforms must emit a 'resumed' event even though web + // applications don't themselves have a formal suspend/resume lifecycle. + self.run_until_cleared([Event::NewEvents(StartCause::Init), Event::Resumed].into_iter()); + } + + // Run the polling logic for the Poll ControlFlow, which involves clearing the queue + pub fn poll(&self) { + let start_cause = Event::NewEvents(StartCause::Poll); + self.run_until_cleared(iter::once(start_cause)); + } + + // Run the logic for waking from a WaitUntil, which involves clearing the queue + // Generally there shouldn't be events built up when this is called + pub fn resume_time_reached(&self, start: Instant, requested_resume: Instant) { + let start_cause = + Event::NewEvents(StartCause::ResumeTimeReached { start, requested_resume }); + self.run_until_cleared(iter::once(start_cause)); + } + + // Add an event to the event loop runner, from the user or an event handler + // + // It will determine if the event should be immediately sent to the user or buffered for later + pub(crate) fn send_event>(&self, event: E) { + self.send_events(iter::once(event)); + } + + // Add a series of user events to the event loop runner + // + // This will schedule the event loop to wake up instead of waking it up immediately if its not + // running. + pub(crate) fn send_user_events(&self, count: NonZeroUsize, local: bool) { + // If the event loop is closed, it should discard any new events + if self.is_closed() { + return; + } + + if local { + // If the loop is not running and triggered locally, queue on next microtick. + if let Ok(RunnerEnum::Running(_)) = + self.0.runner.try_borrow().as_ref().map(Deref::deref) + { + self.window().queue_microtask( + &Closure::once_into_js({ + let this = Rc::downgrade(&self.0); + move || { + if let Some(shared) = this.upgrade() { + Shared(shared).send_events( + iter::repeat(Event::UserEvent(())).take(count.get()), + ) + } + } + }) + .unchecked_into(), + ); + + return; + } + } + + self.send_events(iter::repeat(Event::UserEvent(())).take(count.get())) + } + + // Add a series of events to the event loop runner + // + // It will determine if the event should be immediately sent to the user or buffered for later + pub(crate) fn send_events>(&self, events: impl IntoIterator) { + // If the event loop is closed, it should discard any new events + if self.is_closed() { + return; + } + // If we can run the event processing right now, or need to queue this and wait for later + let mut process_immediately = true; + match self.0.runner.try_borrow().as_ref().map(Deref::deref) { + Ok(RunnerEnum::Running(ref runner)) => { + // If we're currently polling, queue this and wait for the poll() method to be + // called. + if let State::Poll { .. } = runner.state { + process_immediately = false; + } + }, + Ok(RunnerEnum::Pending) => { + // The runner still hasn't been attached: queue this event and wait for it to be + process_immediately = false; + }, + // Some other code is mutating the runner, which most likely means + // the event loop is running and busy. So we queue this event for + // it to be processed later. + Err(_) => { + process_immediately = false; + }, + // This is unreachable since `self.is_closed() == true`. + Ok(RunnerEnum::Destroyed) => unreachable!(), + } + if !process_immediately { + // Queue these events to look at later + self.0.events.borrow_mut().extend(events.into_iter().map(Into::into)); + return; + } + // At this point, we know this is a fresh set of events + // Now we determine why new events are incoming, and handle the events + let start_cause = match (self.0.runner.borrow().maybe_runner()) + .unwrap_or_else(|| { + unreachable!("The runner cannot process events when it is not attached") + }) + .maybe_start_cause() + { + Some(c) => c, + // If we're in the exit state, don't do event processing + None => return, + }; + // Take the start event, then the events provided to this function, and run an iteration of + // the event loop + let start_event = Event::NewEvents(start_cause); + let events = + iter::once(EventWrapper::from(start_event)).chain(events.into_iter().map(Into::into)); + self.run_until_cleared(events); + } + + // Process the destroy-pending windows. This should only be called from + // `run_until_cleared`, somewhere between emitting `NewEvents` and `AboutToWait`. + fn process_destroy_pending_windows(&self) { + while let Some(id) = self.0.destroy_pending.borrow_mut().pop_front() { + self.0.all_canvases.borrow_mut().retain(|&(item_id, ..)| item_id != id); + self.handle_event(Event::WindowEvent { + window_id: id, + event: crate::event::WindowEvent::Destroyed, + }); + self.0.redraw_pending.borrow_mut().remove(&id); + } + } + + // Given the set of new events, run the event loop until the main events and redraw events are + // cleared + // + // This will also process any events that have been queued or that are queued during processing + fn run_until_cleared>(&self, events: impl Iterator) { + for event in events { + self.handle_event(event.into()); + } + self.process_destroy_pending_windows(); + + // Collect all of the redraw events to avoid double-locking the RefCell + let redraw_events: Vec = self.0.redraw_pending.borrow_mut().drain().collect(); + for window_id in redraw_events { + self.handle_event(Event::WindowEvent { + window_id, + event: WindowEvent::RedrawRequested, + }); + } + + self.handle_event(Event::AboutToWait); + + self.apply_control_flow(); + // If the event loop is closed, it has been closed this iteration and now the closing + // event should be emitted + if self.is_closed() { + self.handle_loop_destroyed(); + } + } + + fn handle_unload(&self) { + self.exit(); + self.apply_control_flow(); + // We don't call `handle_loop_destroyed` here because we don't need to + // perform cleanup when the web browser is going to destroy the page. + self.handle_event(Event::LoopExiting); + } + + // handle_event takes in events and either queues them or applies a callback + // + // It should only ever be called from `run_until_cleared`. + fn handle_event(&self, event: impl Into) { + if self.is_closed() { + self.exit(); + } + match *self.0.runner.borrow_mut() { + RunnerEnum::Running(ref mut runner) => { + runner.handle_single_event(self, event); + }, + // If an event is being handled without a runner somehow, add it to the event queue so + // it will eventually be processed + RunnerEnum::Pending => self.0.events.borrow_mut().push_back(event.into()), + // If the Runner has been destroyed, there is nothing to do. + RunnerEnum::Destroyed => return, + } + + let is_closed = self.exiting(); + + // Don't take events out of the queue if the loop is closed or the runner doesn't exist + // If the runner doesn't exist and this method recurses, it will recurse infinitely + if !is_closed && self.0.runner.borrow().maybe_runner().is_some() { + // Pre-fetch window commands to avoid having to wait until the next event loop cycle + // and potentially block other threads in the meantime. + for (_, window, runner) in self.0.all_canvases.borrow().iter() { + if let Some(window) = window.upgrade() { + runner.run(); + drop(window) + } + } + + // Take an event out of the queue and handle it + // Make sure not to let the borrow_mut live during the next handle_event + let event = { + let mut events = self.0.events.borrow_mut(); + + // Pre-fetch `UserEvent`s to avoid having to wait until the next event loop cycle. + events.extend( + iter::repeat(Event::UserEvent(())) + .take(self.0.proxy_spawner.fetch()) + .map(EventWrapper::from), + ); + + events.pop_front() + }; + if let Some(event) = event { + self.handle_event(event); + } + } + } + + // Apply the new ControlFlow that has been selected by the user + // Start any necessary timeouts etc + fn apply_control_flow(&self) { + let new_state = if self.exiting() { + State::Exit + } else { + match self.control_flow() { + ControlFlow::Poll => { + let cloned = self.clone(); + State::Poll { + _request: backend::Schedule::new( + self.poll_strategy(), + self.window(), + move || cloned.poll(), + ), + } + }, + ControlFlow::Wait => State::Wait { start: Instant::now() }, + ControlFlow::WaitUntil(end) => { + let start = Instant::now(); + + let delay = if end <= start { Duration::from_millis(0) } else { end - start }; + + let cloned = self.clone(); + + State::WaitUntil { + start, + end, + _timeout: backend::Schedule::new_with_duration( + self.wait_until_strategy(), + self.window(), + move || cloned.resume_time_reached(start, end), + delay, + ), + } + }, + } + }; + + if let RunnerEnum::Running(ref mut runner) = *self.0.runner.borrow_mut() { + runner.state = new_state; + } + } + + fn handle_loop_destroyed(&self) { + self.handle_event(Event::LoopExiting); + let all_canvases = std::mem::take(&mut *self.0.all_canvases.borrow_mut()); + *self.0.page_transition_event_handle.borrow_mut() = None; + *self.0.on_mouse_move.borrow_mut() = None; + *self.0.on_wheel.borrow_mut() = None; + *self.0.on_mouse_press.borrow_mut() = None; + *self.0.on_mouse_release.borrow_mut() = None; + *self.0.on_key_press.borrow_mut() = None; + *self.0.on_key_release.borrow_mut() = None; + *self.0.on_visibility_change.borrow_mut() = None; + // Dropping the `Runner` drops the event handler closure, which will in + // turn drop all `Window`s moved into the closure. + *self.0.runner.borrow_mut() = RunnerEnum::Destroyed; + for (_, canvas, _) in all_canvases { + // In case any remaining `Window`s are still not dropped, we will need + // to explicitly remove the event handlers associated with their canvases. + if let Some(canvas) = canvas.upgrade() { + let mut canvas = canvas.borrow_mut(); + canvas.remove_listeners(); + } + } + // At this point, the `self.0` `Rc` should only be strongly referenced + // by the following: + // * `self`, i.e. the item which triggered this event loop wakeup, which is usually a + // `wasm-bindgen` `Closure`, which will be dropped after returning to the JS glue code. + // * The `ActiveEventLoop` leaked inside `EventLoop::run_app` due to the JS exception thrown + // at the end. + // * For each undropped `Window`: + // * The `register_redraw_request` closure. + // * The `destroy_fn` closure. + if self.0.event_loop_recreation.get() { + crate::event_loop::EventLoopBuilder::<()>::allow_event_loop_recreation(); + } + } + + // Check if the event loop is currently closed + fn is_closed(&self) -> bool { + match self.0.runner.try_borrow().as_ref().map(Deref::deref) { + Ok(RunnerEnum::Running(runner)) => runner.state.exiting(), + // The event loop is not closed since it is not initialized. + Ok(RunnerEnum::Pending) => false, + // The event loop is closed since it has been destroyed. + Ok(RunnerEnum::Destroyed) => true, + // Some other code is mutating the runner, which most likely means + // the event loop is running and busy. + Err(_) => false, + } + } + + pub fn listen_device_events(&self, allowed: DeviceEvents) { + self.0.device_events.set(allowed) + } + + fn device_events(&self) -> bool { + match self.0.device_events.get() { + DeviceEvents::Always => true, + DeviceEvents::WhenFocused => { + self.0.all_canvases.borrow().iter().any(|(_, canvas, _)| { + if let Some(canvas) = canvas.upgrade() { + canvas.borrow().has_focus.get() + } else { + false + } + }) + }, + DeviceEvents::Never => false, + } + } + + pub fn event_loop_recreation(&self, allow: bool) { + self.0.event_loop_recreation.set(allow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.0.control_flow.get() + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.0.control_flow.set(control_flow) + } + + pub(crate) fn exit(&self) { + self.0.exit.set(true) + } + + pub(crate) fn exiting(&self) -> bool { + self.0.exit.get() + } + + pub(crate) fn set_poll_strategy(&self, strategy: PollStrategy) { + self.0.poll_strategy.set(strategy) + } + + pub(crate) fn poll_strategy(&self) -> PollStrategy { + self.0.poll_strategy.get() + } + + pub(crate) fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) { + self.0.wait_until_strategy.set(strategy) + } + + pub(crate) fn wait_until_strategy(&self) -> WaitUntilStrategy { + self.0.wait_until_strategy.get() + } + + pub(crate) fn waker(&self) -> Waker> { + self.0.proxy_spawner.waker() + } +} + +pub(crate) enum EventWrapper { + Event(Event<()>), + ScaleChange { canvas: Weak>, size: PhysicalSize, scale: f64 }, +} + +impl From> for EventWrapper { + fn from(value: Event<()>) -> Self { + Self::Event(value) + } +} diff --git a/vendor/winit/src/platform_impl/web/event_loop/state.rs b/vendor/winit/src/platform_impl/web/event_loop/state.rs new file mode 100644 index 00000000..d06e30ed --- /dev/null +++ b/vendor/winit/src/platform_impl/web/event_loop/state.rs @@ -0,0 +1,18 @@ +use super::backend; + +use web_time::Instant; + +#[derive(Debug)] +pub enum State { + Init, + WaitUntil { _timeout: backend::Schedule, start: Instant, end: Instant }, + Wait { start: Instant }, + Poll { _request: backend::Schedule }, + Exit, +} + +impl State { + pub fn exiting(&self) -> bool { + matches!(self, State::Exit) + } +} diff --git a/vendor/winit/src/platform_impl/web/event_loop/window_target.rs b/vendor/winit/src/platform_impl/web/event_loop/window_target.rs new file mode 100644 index 00000000..d741f098 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/event_loop/window_target.rs @@ -0,0 +1,685 @@ +use std::cell::{Cell, RefCell}; +use std::clone::Clone; +use std::collections::vec_deque::IntoIter as VecDequeIter; +use std::collections::VecDeque; +use std::iter; +use std::rc::{Rc, Weak}; + +use web_sys::Element; + +use super::super::monitor::MonitorHandle; +use super::super::KeyEventExtra; +use super::device::DeviceId; +use super::runner::{EventWrapper, Execution}; +use super::window::WindowId; +use super::{backend, runner}; +use crate::event::{ + DeviceId as RootDeviceId, ElementState, Event, KeyEvent, Touch, TouchPhase, WindowEvent, +}; +use crate::event_loop::{ControlFlow, DeviceEvents}; +use crate::keyboard::ModifiersState; +use crate::platform::web::{CustomCursorFuture, PollStrategy, WaitUntilStrategy}; +use crate::platform_impl::platform::cursor::CustomCursor; +use crate::platform_impl::platform::r#async::Waker; +use crate::window::{ + CustomCursor as RootCustomCursor, CustomCursorSource, Theme, WindowId as RootWindowId, +}; + +#[derive(Default)] +struct ModifiersShared(Rc>); + +impl ModifiersShared { + fn set(&self, new: ModifiersState) { + self.0.set(new) + } + + fn get(&self) -> ModifiersState { + self.0.get() + } +} + +impl Clone for ModifiersShared { + fn clone(&self) -> Self { + Self(Rc::clone(&self.0)) + } +} + +#[derive(Clone)] +pub struct ActiveEventLoop { + pub(crate) runner: runner::Shared, + modifiers: ModifiersShared, +} + +impl ActiveEventLoop { + pub fn new() -> Self { + Self { runner: runner::Shared::new(), modifiers: ModifiersShared::default() } + } + + pub fn run(&self, event_handler: Box, event_loop_recreation: bool) { + self.runner.event_loop_recreation(event_loop_recreation); + self.runner.set_listener(event_handler); + } + + pub fn generate_id(&self) -> WindowId { + WindowId(self.runner.generate_id()) + } + + pub fn create_custom_cursor(&self, source: CustomCursorSource) -> RootCustomCursor { + RootCustomCursor { inner: CustomCursor::new(self, source.inner) } + } + + pub fn create_custom_cursor_async(&self, source: CustomCursorSource) -> CustomCursorFuture { + CustomCursorFuture(CustomCursor::new_async(self, source.inner)) + } + + pub fn register(&self, canvas: &Rc>, id: WindowId) { + let canvas_clone = canvas.clone(); + let mut canvas = canvas.borrow_mut(); + #[cfg(any(feature = "rwh_04", feature = "rwh_05"))] + canvas.set_attribute("data-raw-handle", &id.0.to_string()); + + canvas.on_touch_start(); + + let runner = self.runner.clone(); + let has_focus = canvas.has_focus.clone(); + let modifiers = self.modifiers.clone(); + canvas.on_blur(move || { + has_focus.set(false); + + let clear_modifiers = (!modifiers.get().is_empty()).then(|| { + modifiers.set(ModifiersState::empty()); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(ModifiersState::empty().into()), + } + }); + + runner.send_events(clear_modifiers.into_iter().chain(iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Focused(false), + }))); + }); + + let runner = self.runner.clone(); + let has_focus = canvas.has_focus.clone(); + canvas.on_focus(move || { + if !has_focus.replace(true) { + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Focused(true), + }); + } + }); + + // It is possible that at this point the canvas has + // been focused before the callback can be called. + let focused = canvas + .document() + .active_element() + .filter(|element| { + let canvas: &Element = canvas.raw(); + element == canvas + }) + .is_some(); + + if focused { + canvas.has_focus.set(true); + self.runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Focused(true), + }) + } + + let runner = self.runner.clone(); + let modifiers = self.modifiers.clone(); + canvas.on_keyboard_press( + move |physical_key, logical_key, text, location, repeat, active_modifiers| { + let modifiers_changed = (modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + let device_id = RootDeviceId(DeviceId::dummy()); + + runner.send_events( + iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::KeyboardInput { + device_id, + event: KeyEvent { + physical_key, + logical_key, + text, + location, + state: ElementState::Pressed, + repeat, + platform_specific: KeyEventExtra, + }, + is_synthetic: false, + }, + }) + .chain(modifiers_changed), + ); + }, + ); + + let runner = self.runner.clone(); + let modifiers = self.modifiers.clone(); + canvas.on_keyboard_release( + move |physical_key, logical_key, text, location, repeat, active_modifiers| { + let modifiers_changed = (modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + let device_id = RootDeviceId(DeviceId::dummy()); + + runner.send_events( + iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::KeyboardInput { + device_id, + event: KeyEvent { + physical_key, + logical_key, + text, + location, + state: ElementState::Released, + repeat, + platform_specific: KeyEventExtra, + }, + is_synthetic: false, + }, + }) + .chain(modifiers_changed), + ) + }, + ); + + let has_focus = canvas.has_focus.clone(); + canvas.on_cursor_leave({ + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, pointer_id| { + let focus = (has_focus.get() && modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + let pointer = pointer_id.map(|pointer_id| Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorLeft { + device_id: RootDeviceId(DeviceId(pointer_id)), + }, + }); + + if focus.is_some() || pointer.is_some() { + runner.send_events(focus.into_iter().chain(pointer)) + } + } + }); + + canvas.on_cursor_enter({ + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, pointer_id| { + let focus = (has_focus.get() && modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + let pointer = pointer_id.map(|pointer_id| Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorEntered { + device_id: RootDeviceId(DeviceId(pointer_id)), + }, + }); + + if focus.is_some() || pointer.is_some() { + runner.send_events(focus.into_iter().chain(pointer)) + } + } + }); + + canvas.on_cursor_move( + { + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, pointer_id, events| { + let modifiers = + (has_focus.get() && modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + runner.send_events(modifiers.into_iter().chain(events.flat_map(|position| { + let device_id = RootDeviceId(DeviceId(pointer_id)); + + iter::once(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorMoved { device_id, position }, + }) + }))); + } + }, + { + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, device_id, events| { + let modifiers = + (has_focus.get() && modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + runner.send_events(modifiers.into_iter().chain(events.map( + |(location, force)| Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Touch(Touch { + id: device_id as u64, + device_id: RootDeviceId(DeviceId(device_id)), + phase: TouchPhase::Moved, + force: Some(force), + location, + }), + }, + ))); + } + }, + { + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, + pointer_id, + position: crate::dpi::PhysicalPosition, + buttons, + button| { + let modifiers = + (has_focus.get() && modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + let device_id = RootDeviceId(DeviceId(pointer_id)); + + let state = if buttons.contains(button.into()) { + ElementState::Pressed + } else { + ElementState::Released + }; + + // A chorded button event may come in without any prior CursorMoved events, + // therefore we should send a CursorMoved event to make sure that the + // user code has the correct cursor position. + runner.send_events(modifiers.into_iter().chain([ + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorMoved { device_id, position }, + }, + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::MouseInput { device_id, state, button }, + }, + ])); + } + }, + ); + + canvas.on_mouse_press( + { + let runner = self.runner.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, pointer_id, position, button| { + let modifiers = (modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + let device_id: RootDeviceId = RootDeviceId(DeviceId(pointer_id)); + + // A mouse down event may come in without any prior CursorMoved events, + // therefore we should send a CursorMoved event to make sure that the + // user code has the correct cursor position. + runner.send_events(modifiers.into_iter().chain([ + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorMoved { device_id, position }, + }, + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::MouseInput { + device_id, + state: ElementState::Pressed, + button, + }, + }, + ])); + } + }, + { + let runner = self.runner.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, device_id, location, force| { + let modifiers = (modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + runner.send_events(modifiers.into_iter().chain(iter::once( + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Touch(Touch { + id: device_id as u64, + device_id: RootDeviceId(DeviceId(device_id)), + phase: TouchPhase::Started, + force: Some(force), + location, + }), + }, + ))) + } + }, + ); + + canvas.on_mouse_release( + { + let runner = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, pointer_id, position, button| { + let modifiers = + (has_focus.get() && modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + let device_id: RootDeviceId = RootDeviceId(DeviceId(pointer_id)); + + // A mouse up event may come in without any prior CursorMoved events, + // therefore we should send a CursorMoved event to make sure that the + // user code has the correct cursor position. + runner.send_events(modifiers.into_iter().chain([ + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::CursorMoved { device_id, position }, + }, + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::MouseInput { + device_id, + state: ElementState::Released, + button, + }, + }, + ])); + } + }, + { + let runner_touch = self.runner.clone(); + let has_focus = has_focus.clone(); + let modifiers = self.modifiers.clone(); + + move |active_modifiers, device_id, location, force| { + let modifiers = + (has_focus.get() && modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + runner_touch.send_events(modifiers.into_iter().chain(iter::once( + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Touch(Touch { + id: device_id as u64, + device_id: RootDeviceId(DeviceId(device_id)), + phase: TouchPhase::Ended, + force: Some(force), + location, + }), + }, + ))); + } + }, + ); + + let runner = self.runner.clone(); + let modifiers = self.modifiers.clone(); + canvas.on_mouse_wheel(move |pointer_id, delta, active_modifiers| { + let modifiers_changed = + (has_focus.get() && modifiers.get() != active_modifiers).then(|| { + modifiers.set(active_modifiers); + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(active_modifiers.into()), + } + }); + + runner.send_events(modifiers_changed.into_iter().chain(iter::once( + Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::MouseWheel { + device_id: RootDeviceId(DeviceId(pointer_id)), + delta, + phase: TouchPhase::Moved, + }, + }, + ))); + }); + + let runner = self.runner.clone(); + canvas.on_touch_cancel(move |device_id, location, force| { + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Touch(Touch { + id: device_id as u64, + device_id: RootDeviceId(DeviceId(device_id)), + phase: TouchPhase::Cancelled, + force: Some(force), + location, + }), + }); + }); + + let runner = self.runner.clone(); + canvas.on_dark_mode(move |is_dark_mode| { + let theme = if is_dark_mode { Theme::Dark } else { Theme::Light }; + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ThemeChanged(theme), + }); + }); + + canvas.on_resize_scale( + { + let runner = self.runner.clone(); + let canvas = canvas_clone.clone(); + + move |size, scale| { + runner.send_event(EventWrapper::ScaleChange { + canvas: Rc::downgrade(&canvas), + size, + scale, + }) + } + }, + { + let runner = self.runner.clone(); + let canvas = canvas_clone.clone(); + + move |new_size| { + let canvas = canvas.borrow(); + canvas.set_current_size(new_size); + if canvas.old_size() != new_size { + canvas.set_old_size(new_size); + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Resized(new_size), + }); + canvas.request_animation_frame(); + } + } + }, + ); + + let runner = self.runner.clone(); + canvas.on_intersection(move |is_intersecting| { + // only fire if visible while skipping the first event if it's intersecting + if backend::is_visible(runner.document()) + && !(is_intersecting && canvas_clone.borrow().is_intersecting.is_none()) + { + runner.send_event(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::Occluded(!is_intersecting), + }); + } + + canvas_clone.borrow_mut().is_intersecting = Some(is_intersecting); + }); + + let runner = self.runner.clone(); + canvas.on_animation_frame(move || runner.request_redraw(RootWindowId(id))); + + canvas.on_context_menu(); + } + + pub fn available_monitors(&self) -> VecDequeIter { + VecDeque::new().into_iter() + } + + pub fn primary_monitor(&self) -> Option { + None + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Web(rwh_05::WebDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Web(rwh_06::WebDisplayHandle::new())) + } + + pub fn listen_device_events(&self, allowed: DeviceEvents) { + self.runner.listen_device_events(allowed) + } + + pub fn system_theme(&self) -> Option { + backend::is_dark_mode(self.runner.window()).map(|is_dark_mode| { + if is_dark_mode { + Theme::Dark + } else { + Theme::Light + } + }) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.runner.set_control_flow(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.runner.control_flow() + } + + pub(crate) fn exit(&self) { + self.runner.exit() + } + + pub(crate) fn exiting(&self) -> bool { + self.runner.exiting() + } + + pub(crate) fn set_poll_strategy(&self, strategy: PollStrategy) { + self.runner.set_poll_strategy(strategy) + } + + pub(crate) fn poll_strategy(&self) -> PollStrategy { + self.runner.poll_strategy() + } + + pub(crate) fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) { + self.runner.set_wait_until_strategy(strategy) + } + + pub(crate) fn wait_until_strategy(&self) -> WaitUntilStrategy { + self.runner.wait_until_strategy() + } + + pub(crate) fn waker(&self) -> Waker> { + self.runner.waker() + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle + } +} + +#[derive(Clone)] +pub(crate) struct OwnedDisplayHandle; + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::WebDisplayHandle::empty().into() + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::WebDisplayHandle::new().into()) + } +} diff --git a/vendor/winit/src/platform_impl/web/keyboard.rs b/vendor/winit/src/platform_impl/web/keyboard.rs new file mode 100644 index 00000000..6f8d69c7 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/keyboard.rs @@ -0,0 +1,522 @@ +use smol_str::SmolStr; + +use crate::keyboard::{Key, KeyCode, NamedKey, NativeKey, NativeKeyCode, PhysicalKey}; + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub(crate) struct KeyEventExtra; + +impl Key { + pub(crate) fn from_key_attribute_value(kav: &str) -> Self { + Key::Named(match kav { + "Unidentified" => return Key::Unidentified(NativeKey::Web(SmolStr::new(kav))), + "Dead" => return Key::Dead(None), + "Alt" => NamedKey::Alt, + "AltGraph" => NamedKey::AltGraph, + "CapsLock" => NamedKey::CapsLock, + "Control" => NamedKey::Control, + "Fn" => NamedKey::Fn, + "FnLock" => NamedKey::FnLock, + "NumLock" => NamedKey::NumLock, + "ScrollLock" => NamedKey::ScrollLock, + "Shift" => NamedKey::Shift, + "Symbol" => NamedKey::Symbol, + "SymbolLock" => NamedKey::SymbolLock, + "Hyper" => NamedKey::Hyper, + "Meta" => NamedKey::Super, + "Enter" => NamedKey::Enter, + "Tab" => NamedKey::Tab, + " " => NamedKey::Space, + "ArrowDown" => NamedKey::ArrowDown, + "ArrowLeft" => NamedKey::ArrowLeft, + "ArrowRight" => NamedKey::ArrowRight, + "ArrowUp" => NamedKey::ArrowUp, + "End" => NamedKey::End, + "Home" => NamedKey::Home, + "PageDown" => NamedKey::PageDown, + "PageUp" => NamedKey::PageUp, + "Backspace" => NamedKey::Backspace, + "Clear" => NamedKey::Clear, + "Copy" => NamedKey::Copy, + "CrSel" => NamedKey::CrSel, + "Cut" => NamedKey::Cut, + "Delete" => NamedKey::Delete, + "EraseEof" => NamedKey::EraseEof, + "ExSel" => NamedKey::ExSel, + "Insert" => NamedKey::Insert, + "Paste" => NamedKey::Paste, + "Redo" => NamedKey::Redo, + "Undo" => NamedKey::Undo, + "Accept" => NamedKey::Accept, + "Again" => NamedKey::Again, + "Attn" => NamedKey::Attn, + "Cancel" => NamedKey::Cancel, + "ContextMenu" => NamedKey::ContextMenu, + "Escape" => NamedKey::Escape, + "Execute" => NamedKey::Execute, + "Find" => NamedKey::Find, + "Help" => NamedKey::Help, + "Pause" => NamedKey::Pause, + "Play" => NamedKey::Play, + "Props" => NamedKey::Props, + "Select" => NamedKey::Select, + "ZoomIn" => NamedKey::ZoomIn, + "ZoomOut" => NamedKey::ZoomOut, + "BrightnessDown" => NamedKey::BrightnessDown, + "BrightnessUp" => NamedKey::BrightnessUp, + "Eject" => NamedKey::Eject, + "LogOff" => NamedKey::LogOff, + "Power" => NamedKey::Power, + "PowerOff" => NamedKey::PowerOff, + "PrintScreen" => NamedKey::PrintScreen, + "Hibernate" => NamedKey::Hibernate, + "Standby" => NamedKey::Standby, + "WakeUp" => NamedKey::WakeUp, + "AllCandidates" => NamedKey::AllCandidates, + "Alphanumeric" => NamedKey::Alphanumeric, + "CodeInput" => NamedKey::CodeInput, + "Compose" => NamedKey::Compose, + "Convert" => NamedKey::Convert, + "FinalMode" => NamedKey::FinalMode, + "GroupFirst" => NamedKey::GroupFirst, + "GroupLast" => NamedKey::GroupLast, + "GroupNext" => NamedKey::GroupNext, + "GroupPrevious" => NamedKey::GroupPrevious, + "ModeChange" => NamedKey::ModeChange, + "NextCandidate" => NamedKey::NextCandidate, + "NonConvert" => NamedKey::NonConvert, + "PreviousCandidate" => NamedKey::PreviousCandidate, + "Process" => NamedKey::Process, + "SingleCandidate" => NamedKey::SingleCandidate, + "HangulMode" => NamedKey::HangulMode, + "HanjaMode" => NamedKey::HanjaMode, + "JunjaMode" => NamedKey::JunjaMode, + "Eisu" => NamedKey::Eisu, + "Hankaku" => NamedKey::Hankaku, + "Hiragana" => NamedKey::Hiragana, + "HiraganaKatakana" => NamedKey::HiraganaKatakana, + "KanaMode" => NamedKey::KanaMode, + "KanjiMode" => NamedKey::KanjiMode, + "Katakana" => NamedKey::Katakana, + "Romaji" => NamedKey::Romaji, + "Zenkaku" => NamedKey::Zenkaku, + "ZenkakuHankaku" => NamedKey::ZenkakuHankaku, + "Soft1" => NamedKey::Soft1, + "Soft2" => NamedKey::Soft2, + "Soft3" => NamedKey::Soft3, + "Soft4" => NamedKey::Soft4, + "ChannelDown" => NamedKey::ChannelDown, + "ChannelUp" => NamedKey::ChannelUp, + "Close" => NamedKey::Close, + "MailForward" => NamedKey::MailForward, + "MailReply" => NamedKey::MailReply, + "MailSend" => NamedKey::MailSend, + "MediaClose" => NamedKey::MediaClose, + "MediaFastForward" => NamedKey::MediaFastForward, + "MediaPause" => NamedKey::MediaPause, + "MediaPlay" => NamedKey::MediaPlay, + "MediaPlayPause" => NamedKey::MediaPlayPause, + "MediaRecord" => NamedKey::MediaRecord, + "MediaRewind" => NamedKey::MediaRewind, + "MediaStop" => NamedKey::MediaStop, + "MediaTrackNext" => NamedKey::MediaTrackNext, + "MediaTrackPrevious" => NamedKey::MediaTrackPrevious, + "New" => NamedKey::New, + "Open" => NamedKey::Open, + "Print" => NamedKey::Print, + "Save" => NamedKey::Save, + "SpellCheck" => NamedKey::SpellCheck, + "Key11" => NamedKey::Key11, + "Key12" => NamedKey::Key12, + "AudioBalanceLeft" => NamedKey::AudioBalanceLeft, + "AudioBalanceRight" => NamedKey::AudioBalanceRight, + "AudioBassBoostDown" => NamedKey::AudioBassBoostDown, + "AudioBassBoostToggle" => NamedKey::AudioBassBoostToggle, + "AudioBassBoostUp" => NamedKey::AudioBassBoostUp, + "AudioFaderFront" => NamedKey::AudioFaderFront, + "AudioFaderRear" => NamedKey::AudioFaderRear, + "AudioSurroundModeNext" => NamedKey::AudioSurroundModeNext, + "AudioTrebleDown" => NamedKey::AudioTrebleDown, + "AudioTrebleUp" => NamedKey::AudioTrebleUp, + "AudioVolumeDown" => NamedKey::AudioVolumeDown, + "AudioVolumeUp" => NamedKey::AudioVolumeUp, + "AudioVolumeMute" => NamedKey::AudioVolumeMute, + "MicrophoneToggle" => NamedKey::MicrophoneToggle, + "MicrophoneVolumeDown" => NamedKey::MicrophoneVolumeDown, + "MicrophoneVolumeUp" => NamedKey::MicrophoneVolumeUp, + "MicrophoneVolumeMute" => NamedKey::MicrophoneVolumeMute, + "SpeechCorrectionList" => NamedKey::SpeechCorrectionList, + "SpeechInputToggle" => NamedKey::SpeechInputToggle, + "LaunchApplication1" => NamedKey::LaunchApplication1, + "LaunchApplication2" => NamedKey::LaunchApplication2, + "LaunchCalendar" => NamedKey::LaunchCalendar, + "LaunchContacts" => NamedKey::LaunchContacts, + "LaunchMail" => NamedKey::LaunchMail, + "LaunchMediaPlayer" => NamedKey::LaunchMediaPlayer, + "LaunchMusicPlayer" => NamedKey::LaunchMusicPlayer, + "LaunchPhone" => NamedKey::LaunchPhone, + "LaunchScreenSaver" => NamedKey::LaunchScreenSaver, + "LaunchSpreadsheet" => NamedKey::LaunchSpreadsheet, + "LaunchWebBrowser" => NamedKey::LaunchWebBrowser, + "LaunchWebCam" => NamedKey::LaunchWebCam, + "LaunchWordProcessor" => NamedKey::LaunchWordProcessor, + "BrowserBack" => NamedKey::BrowserBack, + "BrowserFavorites" => NamedKey::BrowserFavorites, + "BrowserForward" => NamedKey::BrowserForward, + "BrowserHome" => NamedKey::BrowserHome, + "BrowserRefresh" => NamedKey::BrowserRefresh, + "BrowserSearch" => NamedKey::BrowserSearch, + "BrowserStop" => NamedKey::BrowserStop, + "AppSwitch" => NamedKey::AppSwitch, + "Call" => NamedKey::Call, + "Camera" => NamedKey::Camera, + "CameraFocus" => NamedKey::CameraFocus, + "EndCall" => NamedKey::EndCall, + "GoBack" => NamedKey::GoBack, + "GoHome" => NamedKey::GoHome, + "HeadsetHook" => NamedKey::HeadsetHook, + "LastNumberRedial" => NamedKey::LastNumberRedial, + "Notification" => NamedKey::Notification, + "MannerMode" => NamedKey::MannerMode, + "VoiceDial" => NamedKey::VoiceDial, + "TV" => NamedKey::TV, + "TV3DMode" => NamedKey::TV3DMode, + "TVAntennaCable" => NamedKey::TVAntennaCable, + "TVAudioDescription" => NamedKey::TVAudioDescription, + "TVAudioDescriptionMixDown" => NamedKey::TVAudioDescriptionMixDown, + "TVAudioDescriptionMixUp" => NamedKey::TVAudioDescriptionMixUp, + "TVContentsMenu" => NamedKey::TVContentsMenu, + "TVDataService" => NamedKey::TVDataService, + "TVInput" => NamedKey::TVInput, + "TVInputComponent1" => NamedKey::TVInputComponent1, + "TVInputComponent2" => NamedKey::TVInputComponent2, + "TVInputComposite1" => NamedKey::TVInputComposite1, + "TVInputComposite2" => NamedKey::TVInputComposite2, + "TVInputHDMI1" => NamedKey::TVInputHDMI1, + "TVInputHDMI2" => NamedKey::TVInputHDMI2, + "TVInputHDMI3" => NamedKey::TVInputHDMI3, + "TVInputHDMI4" => NamedKey::TVInputHDMI4, + "TVInputVGA1" => NamedKey::TVInputVGA1, + "TVMediaContext" => NamedKey::TVMediaContext, + "TVNetwork" => NamedKey::TVNetwork, + "TVNumberEntry" => NamedKey::TVNumberEntry, + "TVPower" => NamedKey::TVPower, + "TVRadioService" => NamedKey::TVRadioService, + "TVSatellite" => NamedKey::TVSatellite, + "TVSatelliteBS" => NamedKey::TVSatelliteBS, + "TVSatelliteCS" => NamedKey::TVSatelliteCS, + "TVSatelliteToggle" => NamedKey::TVSatelliteToggle, + "TVTerrestrialAnalog" => NamedKey::TVTerrestrialAnalog, + "TVTerrestrialDigital" => NamedKey::TVTerrestrialDigital, + "TVTimer" => NamedKey::TVTimer, + "AVRInput" => NamedKey::AVRInput, + "AVRPower" => NamedKey::AVRPower, + "ColorF0Red" => NamedKey::ColorF0Red, + "ColorF1Green" => NamedKey::ColorF1Green, + "ColorF2Yellow" => NamedKey::ColorF2Yellow, + "ColorF3Blue" => NamedKey::ColorF3Blue, + "ColorF4Grey" => NamedKey::ColorF4Grey, + "ColorF5Brown" => NamedKey::ColorF5Brown, + "ClosedCaptionToggle" => NamedKey::ClosedCaptionToggle, + "Dimmer" => NamedKey::Dimmer, + "DisplaySwap" => NamedKey::DisplaySwap, + "DVR" => NamedKey::DVR, + "Exit" => NamedKey::Exit, + "FavoriteClear0" => NamedKey::FavoriteClear0, + "FavoriteClear1" => NamedKey::FavoriteClear1, + "FavoriteClear2" => NamedKey::FavoriteClear2, + "FavoriteClear3" => NamedKey::FavoriteClear3, + "FavoriteRecall0" => NamedKey::FavoriteRecall0, + "FavoriteRecall1" => NamedKey::FavoriteRecall1, + "FavoriteRecall2" => NamedKey::FavoriteRecall2, + "FavoriteRecall3" => NamedKey::FavoriteRecall3, + "FavoriteStore0" => NamedKey::FavoriteStore0, + "FavoriteStore1" => NamedKey::FavoriteStore1, + "FavoriteStore2" => NamedKey::FavoriteStore2, + "FavoriteStore3" => NamedKey::FavoriteStore3, + "Guide" => NamedKey::Guide, + "GuideNextDay" => NamedKey::GuideNextDay, + "GuidePreviousDay" => NamedKey::GuidePreviousDay, + "Info" => NamedKey::Info, + "InstantReplay" => NamedKey::InstantReplay, + "Link" => NamedKey::Link, + "ListProgram" => NamedKey::ListProgram, + "LiveContent" => NamedKey::LiveContent, + "Lock" => NamedKey::Lock, + "MediaApps" => NamedKey::MediaApps, + "MediaAudioTrack" => NamedKey::MediaAudioTrack, + "MediaLast" => NamedKey::MediaLast, + "MediaSkipBackward" => NamedKey::MediaSkipBackward, + "MediaSkipForward" => NamedKey::MediaSkipForward, + "MediaStepBackward" => NamedKey::MediaStepBackward, + "MediaStepForward" => NamedKey::MediaStepForward, + "MediaTopMenu" => NamedKey::MediaTopMenu, + "NavigateIn" => NamedKey::NavigateIn, + "NavigateNext" => NamedKey::NavigateNext, + "NavigateOut" => NamedKey::NavigateOut, + "NavigatePrevious" => NamedKey::NavigatePrevious, + "NextFavoriteChannel" => NamedKey::NextFavoriteChannel, + "NextUserProfile" => NamedKey::NextUserProfile, + "OnDemand" => NamedKey::OnDemand, + "Pairing" => NamedKey::Pairing, + "PinPDown" => NamedKey::PinPDown, + "PinPMove" => NamedKey::PinPMove, + "PinPToggle" => NamedKey::PinPToggle, + "PinPUp" => NamedKey::PinPUp, + "PlaySpeedDown" => NamedKey::PlaySpeedDown, + "PlaySpeedReset" => NamedKey::PlaySpeedReset, + "PlaySpeedUp" => NamedKey::PlaySpeedUp, + "RandomToggle" => NamedKey::RandomToggle, + "RcLowBattery" => NamedKey::RcLowBattery, + "RecordSpeedNext" => NamedKey::RecordSpeedNext, + "RfBypass" => NamedKey::RfBypass, + "ScanChannelsToggle" => NamedKey::ScanChannelsToggle, + "ScreenModeNext" => NamedKey::ScreenModeNext, + "Settings" => NamedKey::Settings, + "SplitScreenToggle" => NamedKey::SplitScreenToggle, + "STBInput" => NamedKey::STBInput, + "STBPower" => NamedKey::STBPower, + "Subtitle" => NamedKey::Subtitle, + "Teletext" => NamedKey::Teletext, + "VideoModeNext" => NamedKey::VideoModeNext, + "Wink" => NamedKey::Wink, + "ZoomToggle" => NamedKey::ZoomToggle, + "F1" => NamedKey::F1, + "F2" => NamedKey::F2, + "F3" => NamedKey::F3, + "F4" => NamedKey::F4, + "F5" => NamedKey::F5, + "F6" => NamedKey::F6, + "F7" => NamedKey::F7, + "F8" => NamedKey::F8, + "F9" => NamedKey::F9, + "F10" => NamedKey::F10, + "F11" => NamedKey::F11, + "F12" => NamedKey::F12, + "F13" => NamedKey::F13, + "F14" => NamedKey::F14, + "F15" => NamedKey::F15, + "F16" => NamedKey::F16, + "F17" => NamedKey::F17, + "F18" => NamedKey::F18, + "F19" => NamedKey::F19, + "F20" => NamedKey::F20, + "F21" => NamedKey::F21, + "F22" => NamedKey::F22, + "F23" => NamedKey::F23, + "F24" => NamedKey::F24, + "F25" => NamedKey::F25, + "F26" => NamedKey::F26, + "F27" => NamedKey::F27, + "F28" => NamedKey::F28, + "F29" => NamedKey::F29, + "F30" => NamedKey::F30, + "F31" => NamedKey::F31, + "F32" => NamedKey::F32, + "F33" => NamedKey::F33, + "F34" => NamedKey::F34, + "F35" => NamedKey::F35, + string => return Key::Character(SmolStr::new(string)), + }) + } +} + +impl PhysicalKey { + pub fn from_key_code_attribute_value(kcav: &str) -> Self { + PhysicalKey::Code(match kcav { + "Backquote" => KeyCode::Backquote, + "Backslash" => KeyCode::Backslash, + "BracketLeft" => KeyCode::BracketLeft, + "BracketRight" => KeyCode::BracketRight, + "Comma" => KeyCode::Comma, + "Digit0" => KeyCode::Digit0, + "Digit1" => KeyCode::Digit1, + "Digit2" => KeyCode::Digit2, + "Digit3" => KeyCode::Digit3, + "Digit4" => KeyCode::Digit4, + "Digit5" => KeyCode::Digit5, + "Digit6" => KeyCode::Digit6, + "Digit7" => KeyCode::Digit7, + "Digit8" => KeyCode::Digit8, + "Digit9" => KeyCode::Digit9, + "Equal" => KeyCode::Equal, + "IntlBackslash" => KeyCode::IntlBackslash, + "IntlRo" => KeyCode::IntlRo, + "IntlYen" => KeyCode::IntlYen, + "KeyA" => KeyCode::KeyA, + "KeyB" => KeyCode::KeyB, + "KeyC" => KeyCode::KeyC, + "KeyD" => KeyCode::KeyD, + "KeyE" => KeyCode::KeyE, + "KeyF" => KeyCode::KeyF, + "KeyG" => KeyCode::KeyG, + "KeyH" => KeyCode::KeyH, + "KeyI" => KeyCode::KeyI, + "KeyJ" => KeyCode::KeyJ, + "KeyK" => KeyCode::KeyK, + "KeyL" => KeyCode::KeyL, + "KeyM" => KeyCode::KeyM, + "KeyN" => KeyCode::KeyN, + "KeyO" => KeyCode::KeyO, + "KeyP" => KeyCode::KeyP, + "KeyQ" => KeyCode::KeyQ, + "KeyR" => KeyCode::KeyR, + "KeyS" => KeyCode::KeyS, + "KeyT" => KeyCode::KeyT, + "KeyU" => KeyCode::KeyU, + "KeyV" => KeyCode::KeyV, + "KeyW" => KeyCode::KeyW, + "KeyX" => KeyCode::KeyX, + "KeyY" => KeyCode::KeyY, + "KeyZ" => KeyCode::KeyZ, + "Minus" => KeyCode::Minus, + "Period" => KeyCode::Period, + "Quote" => KeyCode::Quote, + "Semicolon" => KeyCode::Semicolon, + "Slash" => KeyCode::Slash, + "AltLeft" => KeyCode::AltLeft, + "AltRight" => KeyCode::AltRight, + "Backspace" => KeyCode::Backspace, + "CapsLock" => KeyCode::CapsLock, + "ContextMenu" => KeyCode::ContextMenu, + "ControlLeft" => KeyCode::ControlLeft, + "ControlRight" => KeyCode::ControlRight, + "Enter" => KeyCode::Enter, + "MetaLeft" => KeyCode::SuperLeft, + "MetaRight" => KeyCode::SuperRight, + "ShiftLeft" => KeyCode::ShiftLeft, + "ShiftRight" => KeyCode::ShiftRight, + "Space" => KeyCode::Space, + "Tab" => KeyCode::Tab, + "Convert" => KeyCode::Convert, + "KanaMode" => KeyCode::KanaMode, + "Lang1" => KeyCode::Lang1, + "Lang2" => KeyCode::Lang2, + "Lang3" => KeyCode::Lang3, + "Lang4" => KeyCode::Lang4, + "Lang5" => KeyCode::Lang5, + "NonConvert" => KeyCode::NonConvert, + "Delete" => KeyCode::Delete, + "End" => KeyCode::End, + "Help" => KeyCode::Help, + "Home" => KeyCode::Home, + "Insert" => KeyCode::Insert, + "PageDown" => KeyCode::PageDown, + "PageUp" => KeyCode::PageUp, + "ArrowDown" => KeyCode::ArrowDown, + "ArrowLeft" => KeyCode::ArrowLeft, + "ArrowRight" => KeyCode::ArrowRight, + "ArrowUp" => KeyCode::ArrowUp, + "NumLock" => KeyCode::NumLock, + "Numpad0" => KeyCode::Numpad0, + "Numpad1" => KeyCode::Numpad1, + "Numpad2" => KeyCode::Numpad2, + "Numpad3" => KeyCode::Numpad3, + "Numpad4" => KeyCode::Numpad4, + "Numpad5" => KeyCode::Numpad5, + "Numpad6" => KeyCode::Numpad6, + "Numpad7" => KeyCode::Numpad7, + "Numpad8" => KeyCode::Numpad8, + "Numpad9" => KeyCode::Numpad9, + "NumpadAdd" => KeyCode::NumpadAdd, + "NumpadBackspace" => KeyCode::NumpadBackspace, + "NumpadClear" => KeyCode::NumpadClear, + "NumpadClearEntry" => KeyCode::NumpadClearEntry, + "NumpadComma" => KeyCode::NumpadComma, + "NumpadDecimal" => KeyCode::NumpadDecimal, + "NumpadDivide" => KeyCode::NumpadDivide, + "NumpadEnter" => KeyCode::NumpadEnter, + "NumpadEqual" => KeyCode::NumpadEqual, + "NumpadHash" => KeyCode::NumpadHash, + "NumpadMemoryAdd" => KeyCode::NumpadMemoryAdd, + "NumpadMemoryClear" => KeyCode::NumpadMemoryClear, + "NumpadMemoryRecall" => KeyCode::NumpadMemoryRecall, + "NumpadMemoryStore" => KeyCode::NumpadMemoryStore, + "NumpadMemorySubtract" => KeyCode::NumpadMemorySubtract, + "NumpadMultiply" => KeyCode::NumpadMultiply, + "NumpadParenLeft" => KeyCode::NumpadParenLeft, + "NumpadParenRight" => KeyCode::NumpadParenRight, + "NumpadStar" => KeyCode::NumpadStar, + "NumpadSubtract" => KeyCode::NumpadSubtract, + "Escape" => KeyCode::Escape, + "Fn" => KeyCode::Fn, + "FnLock" => KeyCode::FnLock, + "PrintScreen" => KeyCode::PrintScreen, + "ScrollLock" => KeyCode::ScrollLock, + "Pause" => KeyCode::Pause, + "BrowserBack" => KeyCode::BrowserBack, + "BrowserFavorites" => KeyCode::BrowserFavorites, + "BrowserForward" => KeyCode::BrowserForward, + "BrowserHome" => KeyCode::BrowserHome, + "BrowserRefresh" => KeyCode::BrowserRefresh, + "BrowserSearch" => KeyCode::BrowserSearch, + "BrowserStop" => KeyCode::BrowserStop, + "Eject" => KeyCode::Eject, + "LaunchApp1" => KeyCode::LaunchApp1, + "LaunchApp2" => KeyCode::LaunchApp2, + "LaunchMail" => KeyCode::LaunchMail, + "MediaPlayPause" => KeyCode::MediaPlayPause, + "MediaSelect" => KeyCode::MediaSelect, + "MediaStop" => KeyCode::MediaStop, + "MediaTrackNext" => KeyCode::MediaTrackNext, + "MediaTrackPrevious" => KeyCode::MediaTrackPrevious, + "Power" => KeyCode::Power, + "Sleep" => KeyCode::Sleep, + "AudioVolumeDown" => KeyCode::AudioVolumeDown, + "AudioVolumeMute" => KeyCode::AudioVolumeMute, + "AudioVolumeUp" => KeyCode::AudioVolumeUp, + "WakeUp" => KeyCode::WakeUp, + "Hyper" => KeyCode::Hyper, + "Turbo" => KeyCode::Turbo, + "Abort" => KeyCode::Abort, + "Resume" => KeyCode::Resume, + "Suspend" => KeyCode::Suspend, + "Again" => KeyCode::Again, + "Copy" => KeyCode::Copy, + "Cut" => KeyCode::Cut, + "Find" => KeyCode::Find, + "Open" => KeyCode::Open, + "Paste" => KeyCode::Paste, + "Props" => KeyCode::Props, + "Select" => KeyCode::Select, + "Undo" => KeyCode::Undo, + "Hiragana" => KeyCode::Hiragana, + "Katakana" => KeyCode::Katakana, + "F1" => KeyCode::F1, + "F2" => KeyCode::F2, + "F3" => KeyCode::F3, + "F4" => KeyCode::F4, + "F5" => KeyCode::F5, + "F6" => KeyCode::F6, + "F7" => KeyCode::F7, + "F8" => KeyCode::F8, + "F9" => KeyCode::F9, + "F10" => KeyCode::F10, + "F11" => KeyCode::F11, + "F12" => KeyCode::F12, + "F13" => KeyCode::F13, + "F14" => KeyCode::F14, + "F15" => KeyCode::F15, + "F16" => KeyCode::F16, + "F17" => KeyCode::F17, + "F18" => KeyCode::F18, + "F19" => KeyCode::F19, + "F20" => KeyCode::F20, + "F21" => KeyCode::F21, + "F22" => KeyCode::F22, + "F23" => KeyCode::F23, + "F24" => KeyCode::F24, + "F25" => KeyCode::F25, + "F26" => KeyCode::F26, + "F27" => KeyCode::F27, + "F28" => KeyCode::F28, + "F29" => KeyCode::F29, + "F30" => KeyCode::F30, + "F31" => KeyCode::F31, + "F32" => KeyCode::F32, + "F33" => KeyCode::F33, + "F34" => KeyCode::F34, + "F35" => KeyCode::F35, + _ => return PhysicalKey::Unidentified(NativeKeyCode::Unidentified), + }) + } +} diff --git a/vendor/winit/src/platform_impl/web/main_thread.rs b/vendor/winit/src/platform_impl/web/main_thread.rs new file mode 100644 index 00000000..59a2ac59 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/main_thread.rs @@ -0,0 +1,96 @@ +use std::fmt::{self, Debug, Formatter}; +use std::marker::PhantomData; +use std::mem; +use std::sync::OnceLock; + +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; + +use super::r#async::{self, Sender}; + +thread_local! { + static MAIN_THREAD: bool = { + #[wasm_bindgen] + extern "C" { + #[derive(Clone)] + type Global; + + #[wasm_bindgen(method, getter, js_name = Window)] + fn window(this: &Global) -> JsValue; + } + + let global: Global = js_sys::global().unchecked_into(); + !global.window().is_undefined() + }; +} + +#[derive(Clone, Copy, Debug)] +pub struct MainThreadMarker(PhantomData<*const ()>); + +impl MainThreadMarker { + pub fn new() -> Option { + MAIN_THREAD.with(|is| is.then_some(Self(PhantomData))) + } +} + +pub struct MainThreadSafe(Option); + +impl MainThreadSafe { + pub fn new(_: MainThreadMarker, value: T) -> Self { + DROP_HANDLER.get_or_init(|| { + let (sender, receiver) = r#async::channel(); + wasm_bindgen_futures::spawn_local( + async move { while receiver.next().await.is_ok() {} }, + ); + + sender + }); + + Self(Some(value)) + } + + pub fn into_inner(mut self, _: MainThreadMarker) -> T { + self.0.take().expect("already taken or dropped") + } + + pub fn get(&self, _: MainThreadMarker) -> &T { + self.0.as_ref().expect("already taken or dropped") + } +} + +impl Debug for MainThreadSafe { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if MainThreadMarker::new().is_some() { + f.debug_tuple("MainThreadSafe").field(&self.0).finish() + } else { + f.debug_struct("MainThreadSafe").finish_non_exhaustive() + } + } +} + +impl Drop for MainThreadSafe { + fn drop(&mut self) { + if let Some(value) = self.0.take() { + if mem::needs_drop::() && MainThreadMarker::new().is_none() { + DROP_HANDLER + .get() + .expect("drop handler not initialized when setting canvas") + .send(DropBox(Box::new(value))) + .expect("sender dropped in main thread") + } + } + } +} + +unsafe impl Send for MainThreadSafe {} +unsafe impl Sync for MainThreadSafe {} + +static DROP_HANDLER: OnceLock> = OnceLock::new(); + +struct DropBox(#[allow(dead_code)] Box); + +unsafe impl Send for DropBox {} +unsafe impl Sync for DropBox {} + +trait Any {} +impl Any for T {} diff --git a/vendor/winit/src/platform_impl/web/mod.rs b/vendor/winit/src/platform_impl/web/mod.rs new file mode 100644 index 00000000..969d8bb1 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/mod.rs @@ -0,0 +1,50 @@ +// Brief introduction to the internals of the web backend: +// The web backend used to support both wasm-bindgen and stdweb as methods of binding to the +// environment. Because they are both supporting the same underlying APIs, the actual web bindings +// are cordoned off into backend abstractions, which present the thinnest unifying layer possible. +// +// When adding support for new events or interactions with the browser, first consult trusted +// documentation (such as MDN) to ensure it is well-standardised and supported across many browsers. +// Once you have decided on the relevant web APIs, add support to both backends. +// +// The backend is used by the rest of the module to implement Winit's business logic, which forms +// the rest of the code. 'device', 'error', 'monitor', and 'window' define web-specific structures +// for winit's cross-platform structures. They are all relatively simple translations. +// +// The event_loop module handles listening for and processing events. 'Proxy' implements +// EventLoopProxy and 'WindowTarget' implements ActiveEventLoop. WindowTarget also handles +// registering the event handlers. The 'Execution' struct in the 'runner' module handles taking +// incoming events (from the registered handlers) and ensuring they are passed to the user in a +// compliant way. + +// TODO: FP, remove when is fixed. +#![allow(clippy::empty_docs)] + +mod r#async; +mod cursor; +mod device; +mod error; +mod event_loop; +mod keyboard; +mod main_thread; +mod monitor; +mod web_sys; +mod window; + +pub use self::device::DeviceId; +pub use self::error::OsError; +pub(crate) use self::event_loop::{ + ActiveEventLoop, EventLoop, EventLoopProxy, OwnedDisplayHandle, + PlatformSpecificEventLoopAttributes, +}; +pub use self::monitor::{MonitorHandle, VideoModeHandle}; +pub use self::window::{PlatformSpecificWindowAttributes, Window, WindowId}; + +pub(crate) use self::keyboard::KeyEventExtra; +use self::web_sys as backend; +pub(crate) use crate::icon::NoIcon as PlatformIcon; +pub(crate) use crate::platform_impl::Fullscreen; +pub(crate) use cursor::{ + CustomCursor as PlatformCustomCursor, CustomCursorFuture, + CustomCursorSource as PlatformCustomCursorSource, +}; diff --git a/vendor/winit/src/platform_impl/web/monitor.rs b/vendor/winit/src/platform_impl/web/monitor.rs new file mode 100644 index 00000000..1870284a --- /dev/null +++ b/vendor/winit/src/platform_impl/web/monitor.rs @@ -0,0 +1,53 @@ +use std::iter::Empty; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct MonitorHandle; + +impl MonitorHandle { + pub fn scale_factor(&self) -> f64 { + unreachable!() + } + + pub fn position(&self) -> PhysicalPosition { + unreachable!() + } + + pub fn name(&self) -> Option { + unreachable!() + } + + pub fn refresh_rate_millihertz(&self) -> Option { + unreachable!() + } + + pub fn size(&self) -> PhysicalSize { + unreachable!() + } + + pub fn video_modes(&self) -> Empty { + unreachable!() + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct VideoModeHandle; + +impl VideoModeHandle { + pub fn size(&self) -> PhysicalSize { + unreachable!(); + } + + pub fn bit_depth(&self) -> u16 { + unreachable!(); + } + + pub fn refresh_rate_millihertz(&self) -> u32 { + unreachable!(); + } + + pub fn monitor(&self) -> MonitorHandle { + unreachable!(); + } +} diff --git a/vendor/winit/src/platform_impl/web/web_sys/animation_frame.rs b/vendor/winit/src/platform_impl/web/web_sys/animation_frame.rs new file mode 100644 index 00000000..17aa8f68 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/web_sys/animation_frame.rs @@ -0,0 +1,60 @@ +use std::cell::Cell; +use std::rc::Rc; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::JsCast; + +pub struct AnimationFrameHandler { + window: web_sys::Window, + closure: Closure, + handle: Rc>>, +} + +impl AnimationFrameHandler { + pub fn new(window: web_sys::Window) -> Self { + let handle = Rc::new(Cell::new(None)); + let closure = Closure::new({ + let handle = handle.clone(); + move || handle.set(None) + }); + + Self { window, closure, handle } + } + + pub fn on_animation_frame(&mut self, mut f: F) + where + F: 'static + FnMut(), + { + let handle = self.handle.clone(); + self.closure = Closure::new(move || { + handle.set(None); + f(); + }) + } + + pub fn request(&self) { + if let Some(handle) = self.handle.take() { + self.window.cancel_animation_frame(handle).expect("Failed to cancel animation frame"); + } + + let handle = self + .window + .request_animation_frame(self.closure.as_ref().unchecked_ref()) + .expect("Failed to request animation frame"); + + self.handle.set(Some(handle)); + } + + pub fn cancel(&mut self) { + if let Some(handle) = self.handle.take() { + self.window.cancel_animation_frame(handle).expect("Failed to cancel animation frame"); + } + } +} + +impl Drop for AnimationFrameHandler { + fn drop(&mut self) { + if let Some(handle) = self.handle.take() { + self.window.cancel_animation_frame(handle).expect("Failed to cancel animation frame"); + } + } +} diff --git a/vendor/winit/src/platform_impl/web/web_sys/canvas.rs b/vendor/winit/src/platform_impl/web/web_sys/canvas.rs new file mode 100644 index 00000000..84dde9f7 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/web_sys/canvas.rs @@ -0,0 +1,565 @@ +use std::cell::Cell; +use std::ops::Deref; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; + +use smol_str::SmolStr; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::JsCast; +use web_sys::{ + CssStyleDeclaration, Document, Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, + PointerEvent, WheelEvent, +}; + +use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize}; +use crate::error::OsError as RootOE; +use crate::event::{Force, InnerSizeWriter, MouseButton, MouseScrollDelta}; +use crate::keyboard::{Key, KeyLocation, ModifiersState, PhysicalKey}; +use crate::platform_impl::OsError; +use crate::window::{WindowAttributes, WindowId as RootWindowId}; + +use super::super::cursor::CursorHandler; +use super::super::main_thread::MainThreadMarker; +use super::super::WindowId; +use super::animation_frame::AnimationFrameHandler; +use super::event_handle::EventListenerHandle; +use super::intersection_handle::IntersectionObserverHandle; +use super::media_query_handle::MediaQueryListHandle; +use super::pointer::PointerHandler; +use super::{event, fullscreen, ButtonsState, ResizeScaleHandle}; + +#[allow(dead_code)] +pub struct Canvas { + common: Common, + id: WindowId, + pub has_focus: Rc>, + pub prevent_default: Rc>, + pub is_intersecting: Option, + on_touch_start: Option>, + on_focus: Option>, + on_blur: Option>, + on_keyboard_release: Option>, + on_keyboard_press: Option>, + on_mouse_wheel: Option>, + on_dark_mode: Option, + pointer_handler: PointerHandler, + on_resize_scale: Option, + on_intersect: Option, + animation_frame_handler: AnimationFrameHandler, + on_touch_end: Option>, + on_context_menu: Option>, + pub cursor: CursorHandler, +} + +pub struct Common { + pub window: web_sys::Window, + pub document: Document, + /// Note: resizing the HTMLCanvasElement should go through `backend::set_canvas_size` to ensure + /// the DPI factor is maintained. Note: this is read-only because we use a pointer to this + /// for [`WindowHandle`][rwh_06::WindowHandle]. + raw: Rc, + style: Style, + old_size: Rc>>, + current_size: Rc>>, +} + +#[derive(Clone, Debug)] +pub struct Style { + read: CssStyleDeclaration, + write: CssStyleDeclaration, +} + +impl Canvas { + pub(crate) fn create( + main_thread: MainThreadMarker, + id: WindowId, + window: web_sys::Window, + document: Document, + attr: &mut WindowAttributes, + ) -> Result { + let canvas = match attr.platform_specific.canvas.take().map(|canvas| { + Arc::try_unwrap(canvas) + .map(|canvas| canvas.into_inner(main_thread)) + .unwrap_or_else(|canvas| canvas.get(main_thread).clone()) + }) { + Some(canvas) => canvas, + None => document + .create_element("canvas") + .map_err(|_| os_error!(OsError("Failed to create canvas element".to_owned())))? + .unchecked_into(), + }; + + if attr.platform_specific.append && !document.contains(Some(&canvas)) { + document + .body() + .expect("Failed to get body from document") + .append_child(&canvas) + .expect("Failed to append canvas to body"); + } + + // A tabindex is needed in order to capture local keyboard events. + // A "0" value means that the element should be focusable in + // sequential keyboard navigation, but its order is defined by the + // document's source order. + // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex + if attr.platform_specific.focusable { + canvas + .set_attribute("tabindex", "0") + .map_err(|_| os_error!(OsError("Failed to set a tabindex".to_owned())))?; + } + + let style = Style::new(&window, &canvas); + + let cursor = CursorHandler::new(main_thread, canvas.clone(), style.clone()); + + let common = Common { + window: window.clone(), + document: document.clone(), + raw: Rc::new(canvas.clone()), + style, + old_size: Rc::default(), + current_size: Rc::default(), + }; + + if let Some(size) = attr.inner_size { + let size = size.to_logical(super::scale_factor(&common.window)); + super::set_canvas_size(&common.document, &common.raw, &common.style, size); + } + + if let Some(size) = attr.min_inner_size { + let size = size.to_logical(super::scale_factor(&common.window)); + super::set_canvas_min_size(&common.document, &common.raw, &common.style, Some(size)); + } + + if let Some(size) = attr.max_inner_size { + let size = size.to_logical(super::scale_factor(&common.window)); + super::set_canvas_max_size(&common.document, &common.raw, &common.style, Some(size)); + } + + if let Some(position) = attr.position { + let position = position.to_logical(super::scale_factor(&common.window)); + super::set_canvas_position(&common.document, &common.raw, &common.style, position); + } + + if attr.fullscreen.is_some() { + fullscreen::request_fullscreen(&document, &canvas); + } + + if attr.active { + let _ = common.raw.focus(); + } + + Ok(Canvas { + common, + id, + has_focus: Rc::new(Cell::new(false)), + prevent_default: Rc::new(Cell::new(attr.platform_specific.prevent_default)), + is_intersecting: None, + on_touch_start: None, + on_blur: None, + on_focus: None, + on_keyboard_release: None, + on_keyboard_press: None, + on_mouse_wheel: None, + on_dark_mode: None, + pointer_handler: PointerHandler::new(), + on_resize_scale: None, + on_intersect: None, + animation_frame_handler: AnimationFrameHandler::new(window), + on_touch_end: None, + on_context_menu: None, + cursor, + }) + } + + pub fn set_cursor_lock(&self, lock: bool) -> Result<(), RootOE> { + if lock { + self.raw().request_pointer_lock(); + } else { + self.common.document.exit_pointer_lock(); + } + Ok(()) + } + + pub fn set_attribute(&self, attribute: &str, value: &str) { + self.common + .raw + .set_attribute(attribute, value) + .unwrap_or_else(|err| panic!("error: {err:?}\nSet attribute: {attribute}")) + } + + pub fn position(&self) -> LogicalPosition { + let bounds = self.common.raw.get_bounding_client_rect(); + let mut position = LogicalPosition { x: bounds.x(), y: bounds.y() }; + + if self.document().contains(Some(self.raw())) && self.style().get("display") != "none" { + position.x += super::style_size_property(self.style(), "border-left-width") + + super::style_size_property(self.style(), "padding-left"); + position.y += super::style_size_property(self.style(), "border-top-width") + + super::style_size_property(self.style(), "padding-top"); + } + + position + } + + #[inline] + pub fn old_size(&self) -> PhysicalSize { + self.common.old_size.get() + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + self.common.current_size.get() + } + + #[inline] + pub fn set_old_size(&self, size: PhysicalSize) { + self.common.old_size.set(size) + } + + #[inline] + pub fn set_current_size(&self, size: PhysicalSize) { + self.common.current_size.set(size) + } + + #[inline] + pub fn window(&self) -> &web_sys::Window { + &self.common.window + } + + #[inline] + pub fn document(&self) -> &Document { + &self.common.document + } + + #[inline] + pub fn raw(&self) -> &HtmlCanvasElement { + &self.common.raw + } + + #[inline] + pub fn style(&self) -> &Style { + &self.common.style + } + + pub fn on_touch_start(&mut self) { + let prevent_default = Rc::clone(&self.prevent_default); + self.on_touch_start = Some(self.common.add_event("touchstart", move |event: Event| { + if prevent_default.get() { + event.prevent_default(); + } + })); + } + + pub fn on_blur(&mut self, mut handler: F) + where + F: 'static + FnMut(), + { + self.on_blur = Some(self.common.add_event("blur", move |_: FocusEvent| { + handler(); + })); + } + + pub fn on_focus(&mut self, mut handler: F) + where + F: 'static + FnMut(), + { + self.on_focus = Some(self.common.add_event("focus", move |_: FocusEvent| { + handler(); + })); + } + + pub fn on_keyboard_release(&mut self, mut handler: F) + where + F: 'static + FnMut(PhysicalKey, Key, Option, KeyLocation, bool, ModifiersState), + { + let prevent_default = Rc::clone(&self.prevent_default); + self.on_keyboard_release = + Some(self.common.add_event("keyup", move |event: KeyboardEvent| { + if prevent_default.get() { + event.prevent_default(); + } + let key = event::key(&event); + let modifiers = event::keyboard_modifiers(&event); + handler( + event::key_code(&event), + key, + event::key_text(&event), + event::key_location(&event), + event.repeat(), + modifiers, + ); + })); + } + + pub fn on_keyboard_press(&mut self, mut handler: F) + where + F: 'static + FnMut(PhysicalKey, Key, Option, KeyLocation, bool, ModifiersState), + { + let prevent_default = Rc::clone(&self.prevent_default); + self.on_keyboard_press = + Some(self.common.add_event("keydown", move |event: KeyboardEvent| { + if prevent_default.get() { + event.prevent_default(); + } + let key = event::key(&event); + let modifiers = event::keyboard_modifiers(&event); + handler( + event::key_code(&event), + key, + event::key_text(&event), + event::key_location(&event), + event.repeat(), + modifiers, + ); + })); + } + + pub fn on_cursor_leave(&mut self, handler: F) + where + F: 'static + FnMut(ModifiersState, Option), + { + self.pointer_handler.on_cursor_leave(&self.common, handler) + } + + pub fn on_cursor_enter(&mut self, handler: F) + where + F: 'static + FnMut(ModifiersState, Option), + { + self.pointer_handler.on_cursor_enter(&self.common, handler) + } + + pub fn on_mouse_release(&mut self, mouse_handler: M, touch_handler: T) + where + M: 'static + FnMut(ModifiersState, i32, PhysicalPosition, MouseButton), + T: 'static + FnMut(ModifiersState, i32, PhysicalPosition, Force), + { + self.pointer_handler.on_mouse_release(&self.common, mouse_handler, touch_handler) + } + + pub fn on_mouse_press(&mut self, mouse_handler: M, touch_handler: T) + where + M: 'static + FnMut(ModifiersState, i32, PhysicalPosition, MouseButton), + T: 'static + FnMut(ModifiersState, i32, PhysicalPosition, Force), + { + self.pointer_handler.on_mouse_press( + &self.common, + mouse_handler, + touch_handler, + Rc::clone(&self.prevent_default), + ) + } + + pub fn on_cursor_move(&mut self, mouse_handler: M, touch_handler: T, button_handler: B) + where + M: 'static + FnMut(ModifiersState, i32, &mut dyn Iterator>), + T: 'static + + FnMut(ModifiersState, i32, &mut dyn Iterator, Force)>), + B: 'static + FnMut(ModifiersState, i32, PhysicalPosition, ButtonsState, MouseButton), + { + self.pointer_handler.on_cursor_move( + &self.common, + mouse_handler, + touch_handler, + button_handler, + Rc::clone(&self.prevent_default), + ) + } + + pub fn on_touch_cancel(&mut self, handler: F) + where + F: 'static + FnMut(i32, PhysicalPosition, Force), + { + self.pointer_handler.on_touch_cancel(&self.common, handler) + } + + pub fn on_mouse_wheel(&mut self, mut handler: F) + where + F: 'static + FnMut(i32, MouseScrollDelta, ModifiersState), + { + let window = self.common.window.clone(); + let prevent_default = Rc::clone(&self.prevent_default); + self.on_mouse_wheel = Some(self.common.add_event("wheel", move |event: WheelEvent| { + if prevent_default.get() { + event.prevent_default(); + } + + if let Some(delta) = event::mouse_scroll_delta(&window, &event) { + let modifiers = event::mouse_modifiers(&event); + handler(0, delta, modifiers); + } + })); + } + + pub fn on_dark_mode(&mut self, mut handler: F) + where + F: 'static + FnMut(bool), + { + self.on_dark_mode = Some(MediaQueryListHandle::new( + &self.common.window, + "(prefers-color-scheme: dark)", + move |mql| handler(mql.matches()), + )); + } + + pub(crate) fn on_resize_scale(&mut self, scale_handler: S, size_handler: R) + where + S: 'static + Fn(PhysicalSize, f64), + R: 'static + Fn(PhysicalSize), + { + self.on_resize_scale = Some(ResizeScaleHandle::new( + self.window().clone(), + self.document().clone(), + self.raw().clone(), + self.style().clone(), + scale_handler, + size_handler, + )); + } + + pub(crate) fn on_intersection(&mut self, handler: F) + where + F: 'static + FnMut(bool), + { + self.on_intersect = Some(IntersectionObserverHandle::new(self.raw(), handler)); + } + + pub(crate) fn on_animation_frame(&mut self, f: F) + where + F: 'static + FnMut(), + { + self.animation_frame_handler.on_animation_frame(f) + } + + pub(crate) fn on_context_menu(&mut self) { + let prevent_default = Rc::clone(&self.prevent_default); + self.on_context_menu = + Some(self.common.add_event("contextmenu", move |event: PointerEvent| { + if prevent_default.get() { + event.prevent_default(); + } + })); + } + + pub fn request_fullscreen(&self) { + fullscreen::request_fullscreen(self.document(), self.raw()); + } + + pub fn exit_fullscreen(&self) { + fullscreen::exit_fullscreen(self.document(), self.raw()); + } + + pub fn is_fullscreen(&self) -> bool { + fullscreen::is_fullscreen(self.document(), self.raw()) + } + + pub fn request_animation_frame(&self) { + self.animation_frame_handler.request(); + } + + pub(crate) fn handle_scale_change( + &self, + runner: &super::super::event_loop::runner::Shared, + event_handler: impl FnOnce(crate::event::Event<()>), + current_size: PhysicalSize, + scale: f64, + ) { + // First, we send the `ScaleFactorChanged` event: + self.set_current_size(current_size); + let new_size = { + let new_size = Arc::new(Mutex::new(current_size)); + event_handler(crate::event::Event::WindowEvent { + window_id: RootWindowId(self.id), + event: crate::event::WindowEvent::ScaleFactorChanged { + scale_factor: scale, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&new_size)), + }, + }); + + let new_size = *new_size.lock().unwrap(); + new_size + }; + + if current_size != new_size { + // Then we resize the canvas to the new size, a new + // `Resized` event will be sent by the `ResizeObserver`: + let new_size = new_size.to_logical(scale); + super::set_canvas_size(self.document(), self.raw(), self.style(), new_size); + + // Set the size might not trigger the event because the calculation is inaccurate. + self.on_resize_scale + .as_ref() + .expect("expected Window to still be active") + .notify_resize(); + } else if self.old_size() != new_size { + // Then we at least send a resized event. + self.set_old_size(new_size); + runner.send_event(crate::event::Event::WindowEvent { + window_id: RootWindowId(self.id), + event: crate::event::WindowEvent::Resized(new_size), + }) + } + } + + pub fn remove_listeners(&mut self) { + self.on_touch_start = None; + self.on_focus = None; + self.on_blur = None; + self.on_keyboard_release = None; + self.on_keyboard_press = None; + self.on_mouse_wheel = None; + self.on_dark_mode = None; + self.pointer_handler.remove_listeners(); + self.on_resize_scale = None; + self.on_intersect = None; + self.animation_frame_handler.cancel(); + self.on_touch_end = None; + self.on_context_menu = None; + } +} + +impl Common { + pub fn add_event( + &self, + event_name: &'static str, + handler: F, + ) -> EventListenerHandle + where + E: 'static + AsRef + wasm_bindgen::convert::FromWasmAbi, + F: 'static + FnMut(E), + { + EventListenerHandle::new(self.raw.deref().clone(), event_name, Closure::new(handler)) + } + + pub fn raw(&self) -> &HtmlCanvasElement { + &self.raw + } +} + +impl Style { + fn new(window: &web_sys::Window, canvas: &HtmlCanvasElement) -> Self { + #[allow(clippy::disallowed_methods)] + let read = window + .get_computed_style(canvas) + .expect("Failed to obtain computed style") + // this can't fail: we aren't using a pseudo-element + .expect("Invalid pseudo-element"); + + #[allow(clippy::disallowed_methods)] + let write = canvas.style(); + + Self { read, write } + } + + pub(crate) fn get(&self, property: &str) -> String { + self.read.get_property_value(property).expect("Invalid property") + } + + pub(crate) fn remove(&self, property: &str) { + self.write.remove_property(property).expect("Property is read only"); + } + + pub(crate) fn set(&self, property: &str, value: &str) { + self.write.set_property(property, value).expect("Property is read only"); + } +} diff --git a/vendor/winit/src/platform_impl/web/web_sys/event.rs b/vendor/winit/src/platform_impl/web/web_sys/event.rs new file mode 100644 index 00000000..ddd12576 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/web_sys/event.rs @@ -0,0 +1,270 @@ +use crate::event::{MouseButton, MouseScrollDelta}; +use crate::keyboard::{Key, KeyLocation, ModifiersState, NamedKey, PhysicalKey}; + +use dpi::{LogicalPosition, PhysicalPosition, Position}; +use smol_str::SmolStr; +use std::cell::OnceCell; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{KeyboardEvent, MouseEvent, PointerEvent, WheelEvent}; + +use super::Engine; + +bitflags::bitflags! { + // https://www.w3.org/TR/pointerevents3/#the-buttons-property + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct ButtonsState: u16 { + const LEFT = 0b00001; + const RIGHT = 0b00010; + const MIDDLE = 0b00100; + const BACK = 0b01000; + const FORWARD = 0b10000; + } +} + +impl From for MouseButton { + fn from(value: ButtonsState) -> Self { + match value { + ButtonsState::LEFT => MouseButton::Left, + ButtonsState::RIGHT => MouseButton::Right, + ButtonsState::MIDDLE => MouseButton::Middle, + ButtonsState::BACK => MouseButton::Back, + ButtonsState::FORWARD => MouseButton::Forward, + _ => MouseButton::Other(value.bits()), + } + } +} + +impl From for ButtonsState { + fn from(value: MouseButton) -> Self { + match value { + MouseButton::Left => ButtonsState::LEFT, + MouseButton::Right => ButtonsState::RIGHT, + MouseButton::Middle => ButtonsState::MIDDLE, + MouseButton::Back => ButtonsState::BACK, + MouseButton::Forward => ButtonsState::FORWARD, + MouseButton::Other(value) => ButtonsState::from_bits_retain(value), + } + } +} + +pub fn mouse_buttons(event: &MouseEvent) -> ButtonsState { + ButtonsState::from_bits_retain(event.buttons()) +} + +pub fn mouse_button(event: &MouseEvent) -> Option { + // https://www.w3.org/TR/pointerevents3/#the-button-property + match event.button() { + -1 => None, + 0 => Some(MouseButton::Left), + 1 => Some(MouseButton::Middle), + 2 => Some(MouseButton::Right), + 3 => Some(MouseButton::Back), + 4 => Some(MouseButton::Forward), + i => { + Some(MouseButton::Other(i.try_into().expect("unexpected negative mouse button value"))) + }, + } +} + +impl MouseButton { + pub fn to_id(self) -> u32 { + match self { + MouseButton::Left => 0, + MouseButton::Right => 1, + MouseButton::Middle => 2, + MouseButton::Back => 3, + MouseButton::Forward => 4, + MouseButton::Other(value) => value.into(), + } + } +} + +pub fn mouse_position(event: &MouseEvent) -> LogicalPosition { + #[wasm_bindgen] + extern "C" { + type MouseEventExt; + + #[wasm_bindgen(method, getter, js_name = offsetX)] + fn offset_x(this: &MouseEventExt) -> f64; + + #[wasm_bindgen(method, getter, js_name = offsetY)] + fn offset_y(this: &MouseEventExt) -> f64; + } + + let event: &MouseEventExt = event.unchecked_ref(); + + LogicalPosition { x: event.offset_x(), y: event.offset_y() } +} + +// TODO: Remove this when Firefox supports correct movement values in coalesced events and browsers +// have agreed on what coordinate space `movementX/Y` is using. +// See . +// See . +pub enum MouseDelta { + Chromium, + Gecko { old_position: LogicalPosition, old_delta: LogicalPosition }, + Other, +} + +impl MouseDelta { + pub fn init(window: &web_sys::Window, event: &PointerEvent) -> Self { + match super::engine(window) { + Some(Engine::Chromium) => Self::Chromium, + // Firefox has wrong movement values in coalesced events. + Some(Engine::Gecko) if has_coalesced_events_support(event) => Self::Gecko { + old_position: mouse_position(event), + old_delta: LogicalPosition::new( + event.movement_x() as f64, + event.movement_y() as f64, + ), + }, + _ => Self::Other, + } + } + + pub fn delta(&mut self, event: &MouseEvent) -> Position { + match self { + MouseDelta::Chromium => { + PhysicalPosition::new(event.movement_x(), event.movement_y()).into() + }, + MouseDelta::Gecko { old_position, old_delta } => { + let new_position = mouse_position(event); + let x = new_position.x - old_position.x + old_delta.x; + let y = new_position.y - old_position.y + old_delta.y; + *old_position = new_position; + *old_delta = LogicalPosition::new(0., 0.); + LogicalPosition::new(x, y).into() + }, + MouseDelta::Other => { + LogicalPosition::new(event.movement_x(), event.movement_y()).into() + }, + } + } +} + +pub fn mouse_scroll_delta( + window: &web_sys::Window, + event: &WheelEvent, +) -> Option { + let x = -event.delta_x(); + let y = -event.delta_y(); + + match event.delta_mode() { + WheelEvent::DOM_DELTA_LINE => Some(MouseScrollDelta::LineDelta(x as f32, y as f32)), + WheelEvent::DOM_DELTA_PIXEL => { + let delta = LogicalPosition::new(x, y).to_physical(super::scale_factor(window)); + Some(MouseScrollDelta::PixelDelta(delta)) + }, + _ => None, + } +} + +pub fn key_code(event: &KeyboardEvent) -> PhysicalKey { + let code = event.code(); + PhysicalKey::from_key_code_attribute_value(&code) +} + +pub fn key(event: &KeyboardEvent) -> Key { + Key::from_key_attribute_value(&event.key()) +} + +pub fn key_text(event: &KeyboardEvent) -> Option { + let key = event.key(); + let key = Key::from_key_attribute_value(&key); + match &key { + Key::Character(text) => Some(text.clone()), + Key::Named(NamedKey::Tab) => Some(SmolStr::new("\t")), + Key::Named(NamedKey::Enter) => Some(SmolStr::new("\r")), + Key::Named(NamedKey::Space) => Some(SmolStr::new(" ")), + _ => None, + } + .map(SmolStr::new) +} + +pub fn key_location(event: &KeyboardEvent) -> KeyLocation { + match event.location() { + KeyboardEvent::DOM_KEY_LOCATION_LEFT => KeyLocation::Left, + KeyboardEvent::DOM_KEY_LOCATION_RIGHT => KeyLocation::Right, + KeyboardEvent::DOM_KEY_LOCATION_NUMPAD => KeyLocation::Numpad, + KeyboardEvent::DOM_KEY_LOCATION_STANDARD => KeyLocation::Standard, + location => { + tracing::warn!("Unexpected key location: {location}"); + KeyLocation::Standard + }, + } +} + +pub fn keyboard_modifiers(event: &KeyboardEvent) -> ModifiersState { + let mut state = ModifiersState::empty(); + + if event.shift_key() { + state |= ModifiersState::SHIFT; + } + if event.ctrl_key() { + state |= ModifiersState::CONTROL; + } + if event.alt_key() { + state |= ModifiersState::ALT; + } + if event.meta_key() { + state |= ModifiersState::SUPER; + } + + state +} + +pub fn mouse_modifiers(event: &MouseEvent) -> ModifiersState { + let mut state = ModifiersState::empty(); + + if event.shift_key() { + state |= ModifiersState::SHIFT; + } + if event.ctrl_key() { + state |= ModifiersState::CONTROL; + } + if event.alt_key() { + state |= ModifiersState::ALT; + } + if event.meta_key() { + state |= ModifiersState::SUPER; + } + + state +} + +pub fn pointer_move_event(event: PointerEvent) -> impl Iterator { + // make a single iterator depending on the availability of coalesced events + if has_coalesced_events_support(&event) { + None.into_iter().chain( + Some(event.get_coalesced_events().into_iter().map(PointerEvent::unchecked_from_js)) + .into_iter() + .flatten(), + ) + } else { + Some(event).into_iter().chain(None.into_iter().flatten()) + } +} + +// TODO: Remove when Safari supports `getCoalescedEvents`. +// See . +pub fn has_coalesced_events_support(event: &PointerEvent) -> bool { + thread_local! { + static COALESCED_EVENTS_SUPPORT: OnceCell = const { OnceCell::new() }; + } + + COALESCED_EVENTS_SUPPORT.with(|support| { + *support.get_or_init(|| { + #[wasm_bindgen] + extern "C" { + type PointerCoalescedEventsSupport; + + #[wasm_bindgen(method, getter, js_name = getCoalescedEvents)] + fn has_get_coalesced_events(this: &PointerCoalescedEventsSupport) -> JsValue; + } + + let support: &PointerCoalescedEventsSupport = event.unchecked_ref(); + !support.has_get_coalesced_events().is_undefined() + }) + }) +} diff --git a/vendor/winit/src/platform_impl/web/web_sys/event_handle.rs b/vendor/winit/src/platform_impl/web/web_sys/event_handle.rs new file mode 100644 index 00000000..4474af3c --- /dev/null +++ b/vendor/winit/src/platform_impl/web/web_sys/event_handle.rs @@ -0,0 +1,38 @@ +use wasm_bindgen::prelude::Closure; +use wasm_bindgen::JsCast; +use web_sys::EventTarget; + +pub struct EventListenerHandle { + target: EventTarget, + event_type: &'static str, + listener: Closure, +} + +impl EventListenerHandle { + pub fn new(target: U, event_type: &'static str, listener: Closure) -> Self + where + U: Into, + { + let target = target.into(); + target + .add_event_listener_with_callback(event_type, listener.as_ref().unchecked_ref()) + .expect("Failed to add event listener"); + EventListenerHandle { target, event_type, listener } + } +} + +impl Drop for EventListenerHandle { + fn drop(&mut self) { + self.target + .remove_event_listener_with_callback( + self.event_type, + self.listener.as_ref().unchecked_ref(), + ) + .unwrap_or_else(|e| { + web_sys::console::error_2( + &format!("Error removing event listener {}", self.event_type).into(), + &e, + ) + }); + } +} diff --git a/vendor/winit/src/platform_impl/web/web_sys/fullscreen.rs b/vendor/winit/src/platform_impl/web/web_sys/fullscreen.rs new file mode 100644 index 00000000..867d34a6 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/web_sys/fullscreen.rs @@ -0,0 +1,103 @@ +use std::cell::OnceCell; + +use js_sys::Promise; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{Document, Element, HtmlCanvasElement}; + +pub fn request_fullscreen(document: &Document, canvas: &HtmlCanvasElement) { + if is_fullscreen(document, canvas) { + return; + } + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(extends = HtmlCanvasElement)] + type RequestFullscreen; + + #[wasm_bindgen(method, js_name = requestFullscreen)] + fn request_fullscreen(this: &RequestFullscreen) -> Promise; + + #[wasm_bindgen(method, js_name = webkitRequestFullscreen)] + fn webkit_request_fullscreen(this: &RequestFullscreen); + } + + let canvas: &RequestFullscreen = canvas.unchecked_ref(); + + if has_fullscreen_api_support(canvas) { + thread_local! { + static REJECT_HANDLER: Closure = Closure::new(|_| ()); + } + REJECT_HANDLER.with(|handler| { + let _ = canvas.request_fullscreen().catch(handler); + }); + } else { + canvas.webkit_request_fullscreen(); + } +} + +pub fn is_fullscreen(document: &Document, canvas: &HtmlCanvasElement) -> bool { + #[wasm_bindgen] + extern "C" { + type FullscreenElement; + + #[wasm_bindgen(method, getter, js_name = webkitFullscreenElement)] + fn webkit_fullscreen_element(this: &FullscreenElement) -> Option; + } + + let element = if has_fullscreen_api_support(canvas) { + #[allow(clippy::disallowed_methods)] + document.fullscreen_element() + } else { + let document: &FullscreenElement = document.unchecked_ref(); + document.webkit_fullscreen_element() + }; + + match element { + Some(element) => { + let canvas: &Element = canvas; + canvas == &element + }, + None => false, + } +} + +pub fn exit_fullscreen(document: &Document, canvas: &HtmlCanvasElement) { + #[wasm_bindgen] + extern "C" { + type ExitFullscreen; + + #[wasm_bindgen(method, js_name = webkitExitFullscreen)] + fn webkit_exit_fullscreen(this: &ExitFullscreen); + } + + if has_fullscreen_api_support(canvas) { + #[allow(clippy::disallowed_methods)] + document.exit_fullscreen() + } else { + let document: &ExitFullscreen = document.unchecked_ref(); + document.webkit_exit_fullscreen() + } +} + +fn has_fullscreen_api_support(canvas: &HtmlCanvasElement) -> bool { + thread_local! { + static FULLSCREEN_API_SUPPORT: OnceCell = const { OnceCell::new() }; + } + + FULLSCREEN_API_SUPPORT.with(|support| { + *support.get_or_init(|| { + #[wasm_bindgen] + extern "C" { + type CanvasFullScreenApiSupport; + + #[wasm_bindgen(method, getter, js_name = requestFullscreen)] + fn has_request_fullscreen(this: &CanvasFullScreenApiSupport) -> JsValue; + } + + let support: &CanvasFullScreenApiSupport = canvas.unchecked_ref(); + !support.has_request_fullscreen().is_undefined() + }) + }) +} diff --git a/vendor/winit/src/platform_impl/web/web_sys/intersection_handle.rs b/vendor/winit/src/platform_impl/web/web_sys/intersection_handle.rs new file mode 100644 index 00000000..9f63e84c --- /dev/null +++ b/vendor/winit/src/platform_impl/web/web_sys/intersection_handle.rs @@ -0,0 +1,33 @@ +use js_sys::Array; +use wasm_bindgen::prelude::Closure; +use wasm_bindgen::JsCast; +use web_sys::{Element, IntersectionObserver, IntersectionObserverEntry}; + +pub(super) struct IntersectionObserverHandle { + observer: IntersectionObserver, + _closure: Closure, +} + +impl IntersectionObserverHandle { + pub fn new(element: &Element, mut callback: F) -> Self + where + F: 'static + FnMut(bool), + { + let closure = Closure::new(move |entries: Array| { + let entry: IntersectionObserverEntry = entries.get(0).unchecked_into(); + callback(entry.is_intersecting()); + }); + let observer = IntersectionObserver::new(closure.as_ref().unchecked_ref()) + // we don't provide any `options` + .expect("Invalid `options`"); + observer.observe(element); + + Self { observer, _closure: closure } + } +} + +impl Drop for IntersectionObserverHandle { + fn drop(&mut self) { + self.observer.disconnect() + } +} diff --git a/vendor/winit/src/platform_impl/web/web_sys/media_query_handle.rs b/vendor/winit/src/platform_impl/web/web_sys/media_query_handle.rs new file mode 100644 index 00000000..766157a6 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/web_sys/media_query_handle.rs @@ -0,0 +1,48 @@ +use wasm_bindgen::prelude::Closure; +use wasm_bindgen::JsCast; +use web_sys::MediaQueryList; + +pub(super) struct MediaQueryListHandle { + mql: MediaQueryList, + closure: Closure, +} + +impl MediaQueryListHandle { + pub fn new(window: &web_sys::Window, media_query: &str, mut listener: F) -> Self + where + F: 'static + FnMut(&MediaQueryList), + { + let mql = window + .match_media(media_query) + .expect("Failed to parse media query") + .expect("Found empty media query"); + + let closure = Closure::new({ + let mql = mql.clone(); + move || listener(&mql) + }); + // TODO: Replace obsolete `addListener()` with `addEventListener()` and use + // `MediaQueryListEvent` instead of cloning the `MediaQueryList`. + // Requires Safari v14. + mql.add_listener_with_opt_callback(Some(closure.as_ref().unchecked_ref())) + .expect("Invalid listener"); + + Self { mql, closure } + } + + pub fn mql(&self) -> &MediaQueryList { + &self.mql + } +} + +impl Drop for MediaQueryListHandle { + fn drop(&mut self) { + remove_listener(&self.mql, &self.closure); + } +} + +fn remove_listener(mql: &MediaQueryList, listener: &Closure) { + mql.remove_listener_with_opt_callback(Some(listener.as_ref().unchecked_ref())).unwrap_or_else( + |e| web_sys::console::error_2(&"Error removing media query listener".into(), &e), + ); +} diff --git a/vendor/winit/src/platform_impl/web/web_sys/mod.rs b/vendor/winit/src/platform_impl/web/web_sys/mod.rs new file mode 100644 index 00000000..08962b49 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/web_sys/mod.rs @@ -0,0 +1,233 @@ +mod animation_frame; +mod canvas; +pub mod event; +mod event_handle; +mod fullscreen; +mod intersection_handle; +mod media_query_handle; +mod pointer; +mod resize_scaling; +mod schedule; + +use std::sync::OnceLock; + +pub use self::canvas::{Canvas, Style}; +pub use self::event::ButtonsState; +pub use self::event_handle::EventListenerHandle; +pub use self::resize_scaling::ResizeScaleHandle; +pub use self::schedule::Schedule; + +use crate::dpi::{LogicalPosition, LogicalSize}; +use js_sys::Array; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsCast; +use web_sys::{ + Document, HtmlCanvasElement, Navigator, PageTransitionEvent, VisibilityState, Window, +}; + +pub fn throw(msg: &str) { + wasm_bindgen::throw_str(msg); +} + +pub struct PageTransitionEventHandle { + _show_listener: event_handle::EventListenerHandle, + _hide_listener: event_handle::EventListenerHandle, +} + +pub fn on_page_transition( + window: web_sys::Window, + show_handler: impl FnMut(PageTransitionEvent) + 'static, + hide_handler: impl FnMut(PageTransitionEvent) + 'static, +) -> PageTransitionEventHandle { + let show_closure = Closure::new(show_handler); + let hide_closure = Closure::new(hide_handler); + + let show_listener = + event_handle::EventListenerHandle::new(window.clone(), "pageshow", show_closure); + let hide_listener = event_handle::EventListenerHandle::new(window, "pagehide", hide_closure); + PageTransitionEventHandle { _show_listener: show_listener, _hide_listener: hide_listener } +} + +pub fn scale_factor(window: &web_sys::Window) -> f64 { + window.device_pixel_ratio() +} + +fn fix_canvas_size(style: &Style, mut size: LogicalSize) -> LogicalSize { + if style.get("box-sizing") == "border-box" { + size.width += style_size_property(style, "border-left-width") + + style_size_property(style, "border-right-width") + + style_size_property(style, "padding-left") + + style_size_property(style, "padding-right"); + size.height += style_size_property(style, "border-top-width") + + style_size_property(style, "border-bottom-width") + + style_size_property(style, "padding-top") + + style_size_property(style, "padding-bottom"); + } + + size +} + +pub fn set_canvas_size( + document: &Document, + raw: &HtmlCanvasElement, + style: &Style, + new_size: LogicalSize, +) { + if !document.contains(Some(raw)) || style.get("display") == "none" { + return; + } + + let new_size = fix_canvas_size(style, new_size); + + style.set("width", &format!("{}px", new_size.width)); + style.set("height", &format!("{}px", new_size.height)); +} + +pub fn set_canvas_min_size( + document: &Document, + raw: &HtmlCanvasElement, + style: &Style, + dimensions: Option>, +) { + if let Some(dimensions) = dimensions { + if !document.contains(Some(raw)) || style.get("display") == "none" { + return; + } + + let new_size = fix_canvas_size(style, dimensions); + + style.set("min-width", &format!("{}px", new_size.width)); + style.set("min-height", &format!("{}px", new_size.height)); + } else { + style.remove("min-width"); + style.remove("min-height"); + } +} + +pub fn set_canvas_max_size( + document: &Document, + raw: &HtmlCanvasElement, + style: &Style, + dimensions: Option>, +) { + if let Some(dimensions) = dimensions { + if !document.contains(Some(raw)) || style.get("display") == "none" { + return; + } + + let new_size = fix_canvas_size(style, dimensions); + + style.set("max-width", &format!("{}px", new_size.width)); + style.set("max-height", &format!("{}px", new_size.height)); + } else { + style.remove("max-width"); + style.remove("max-height"); + } +} + +pub fn set_canvas_position( + document: &Document, + raw: &HtmlCanvasElement, + style: &Style, + mut position: LogicalPosition, +) { + if document.contains(Some(raw)) && style.get("display") != "none" { + position.x -= style_size_property(style, "margin-left") + + style_size_property(style, "border-left-width") + + style_size_property(style, "padding-left"); + position.y -= style_size_property(style, "margin-top") + + style_size_property(style, "border-top-width") + + style_size_property(style, "padding-top"); + } + + style.set("position", "fixed"); + style.set("left", &format!("{}px", position.x)); + style.set("top", &format!("{}px", position.y)); +} + +/// This function will panic if the element is not inserted in the DOM +/// or is not a CSS property that represents a size in pixel. +pub fn style_size_property(style: &Style, property: &str) -> f64 { + let prop = style.get(property); + prop.strip_suffix("px") + .expect("Element was not inserted into the DOM or is not a size in pixel") + .parse() + .expect("CSS property is not a size in pixel") +} + +pub fn is_dark_mode(window: &web_sys::Window) -> Option { + window.match_media("(prefers-color-scheme: dark)").ok().flatten().map(|media| media.matches()) +} + +pub fn is_visible(document: &Document) -> bool { + document.visibility_state() == VisibilityState::Visible +} + +pub type RawCanvasType = HtmlCanvasElement; + +#[derive(Clone, Copy)] +pub enum Engine { + Chromium, + Gecko, + WebKit, +} + +pub fn engine(window: &Window) -> Option { + static ENGINE: OnceLock> = OnceLock::new(); + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(extends = Navigator)] + type NavigatorExt; + + #[wasm_bindgen(method, getter, js_name = userAgentData)] + fn user_agent_data(this: &NavigatorExt) -> Option; + + type NavigatorUaData; + + #[wasm_bindgen(method, getter)] + fn brands(this: &NavigatorUaData) -> Array; + + type NavigatorUaBrandVersion; + + #[wasm_bindgen(method, getter)] + fn brand(this: &NavigatorUaBrandVersion) -> String; + } + + *ENGINE.get_or_init(|| { + let navigator: NavigatorExt = window.navigator().unchecked_into(); + + if let Some(data) = navigator.user_agent_data() { + for brand in data + .brands() + .iter() + .map(NavigatorUaBrandVersion::unchecked_from_js) + .map(|brand| brand.brand()) + { + match brand.as_str() { + "Chromium" => return Some(Engine::Chromium), + // TODO: verify when Firefox actually implements it. + "Gecko" => return Some(Engine::Gecko), + // TODO: verify when Safari actually implements it. + "WebKit" => return Some(Engine::WebKit), + _ => (), + } + } + + None + } else { + let data = navigator.user_agent().ok()?; + + if data.contains("Chrome/") { + Some(Engine::Chromium) + } else if data.contains("Gecko/") { + Some(Engine::Gecko) + } else if data.contains("AppleWebKit/") { + Some(Engine::WebKit) + } else { + None + } + } + }) +} diff --git a/vendor/winit/src/platform_impl/web/web_sys/pointer.rs b/vendor/winit/src/platform_impl/web/web_sys/pointer.rs new file mode 100644 index 00000000..3ee168bb --- /dev/null +++ b/vendor/winit/src/platform_impl/web/web_sys/pointer.rs @@ -0,0 +1,244 @@ +use std::cell::Cell; +use std::rc::Rc; + +use super::canvas::Common; +use super::event; +use super::event_handle::EventListenerHandle; +use crate::dpi::PhysicalPosition; +use crate::event::{Force, MouseButton}; +use crate::keyboard::ModifiersState; + +use event::ButtonsState; +use web_sys::PointerEvent; + +#[allow(dead_code)] +pub(super) struct PointerHandler { + on_cursor_leave: Option>, + on_cursor_enter: Option>, + on_cursor_move: Option>, + on_pointer_press: Option>, + on_pointer_release: Option>, + on_touch_cancel: Option>, +} + +impl PointerHandler { + pub fn new() -> Self { + Self { + on_cursor_leave: None, + on_cursor_enter: None, + on_cursor_move: None, + on_pointer_press: None, + on_pointer_release: None, + on_touch_cancel: None, + } + } + + pub fn on_cursor_leave(&mut self, canvas_common: &Common, mut handler: F) + where + F: 'static + FnMut(ModifiersState, Option), + { + self.on_cursor_leave = + Some(canvas_common.add_event("pointerout", move |event: PointerEvent| { + let modifiers = event::mouse_modifiers(&event); + + // touch events are handled separately + // handling them here would produce duplicate mouse events, inconsistent with + // other platforms. + let pointer_id = (event.pointer_type() != "touch").then(|| event.pointer_id()); + + handler(modifiers, pointer_id); + })); + } + + pub fn on_cursor_enter(&mut self, canvas_common: &Common, mut handler: F) + where + F: 'static + FnMut(ModifiersState, Option), + { + self.on_cursor_enter = + Some(canvas_common.add_event("pointerover", move |event: PointerEvent| { + let modifiers = event::mouse_modifiers(&event); + + // touch events are handled separately + // handling them here would produce duplicate mouse events, inconsistent with + // other platforms. + let pointer_id = (event.pointer_type() != "touch").then(|| event.pointer_id()); + + handler(modifiers, pointer_id); + })); + } + + pub fn on_mouse_release( + &mut self, + canvas_common: &Common, + mut mouse_handler: M, + mut touch_handler: T, + ) where + M: 'static + FnMut(ModifiersState, i32, PhysicalPosition, MouseButton), + T: 'static + FnMut(ModifiersState, i32, PhysicalPosition, Force), + { + let window = canvas_common.window.clone(); + self.on_pointer_release = + Some(canvas_common.add_event("pointerup", move |event: PointerEvent| { + let modifiers = event::mouse_modifiers(&event); + + match event.pointer_type().as_str() { + "touch" => touch_handler( + modifiers, + event.pointer_id(), + event::mouse_position(&event).to_physical(super::scale_factor(&window)), + Force::Normalized(event.pressure() as f64), + ), + _ => mouse_handler( + modifiers, + event.pointer_id(), + event::mouse_position(&event).to_physical(super::scale_factor(&window)), + event::mouse_button(&event).expect("no mouse button released"), + ), + } + })); + } + + pub fn on_mouse_press( + &mut self, + canvas_common: &Common, + mut mouse_handler: M, + mut touch_handler: T, + prevent_default: Rc>, + ) where + M: 'static + FnMut(ModifiersState, i32, PhysicalPosition, MouseButton), + T: 'static + FnMut(ModifiersState, i32, PhysicalPosition, Force), + { + let window = canvas_common.window.clone(); + let canvas = canvas_common.raw().clone(); + self.on_pointer_press = + Some(canvas_common.add_event("pointerdown", move |event: PointerEvent| { + if prevent_default.get() { + // prevent text selection + event.prevent_default(); + // but still focus element + let _ = canvas.focus(); + } + + let modifiers = event::mouse_modifiers(&event); + let pointer_type = &event.pointer_type(); + + match pointer_type.as_str() { + "touch" => { + touch_handler( + modifiers, + event.pointer_id(), + event::mouse_position(&event).to_physical(super::scale_factor(&window)), + Force::Normalized(event.pressure() as f64), + ); + }, + _ => { + mouse_handler( + modifiers, + event.pointer_id(), + event::mouse_position(&event).to_physical(super::scale_factor(&window)), + event::mouse_button(&event).expect("no mouse button pressed"), + ); + + if pointer_type == "mouse" { + // Error is swallowed here since the error would occur every time the + // mouse is clicked when the cursor is + // grabbed, and there is probably not a + // situation where this could fail, that we + // care if it fails. + let _e = canvas.set_pointer_capture(event.pointer_id()); + } + }, + } + })); + } + + pub fn on_cursor_move( + &mut self, + canvas_common: &Common, + mut mouse_handler: M, + mut touch_handler: T, + mut button_handler: B, + prevent_default: Rc>, + ) where + M: 'static + FnMut(ModifiersState, i32, &mut dyn Iterator>), + T: 'static + + FnMut(ModifiersState, i32, &mut dyn Iterator, Force)>), + B: 'static + FnMut(ModifiersState, i32, PhysicalPosition, ButtonsState, MouseButton), + { + let window = canvas_common.window.clone(); + let canvas = canvas_common.raw().clone(); + self.on_cursor_move = + Some(canvas_common.add_event("pointermove", move |event: PointerEvent| { + let modifiers = event::mouse_modifiers(&event); + + let id = event.pointer_id(); + + // chorded button event + if let Some(button) = event::mouse_button(&event) { + if prevent_default.get() { + // prevent text selection + event.prevent_default(); + // but still focus element + let _ = canvas.focus(); + } + + button_handler( + modifiers, + id, + event::mouse_position(&event).to_physical(super::scale_factor(&window)), + event::mouse_buttons(&event), + button, + ); + + return; + } + + // pointer move event + let scale = super::scale_factor(&window); + match event.pointer_type().as_str() { + "touch" => touch_handler( + modifiers, + id, + &mut event::pointer_move_event(event).map(|event| { + ( + event::mouse_position(&event).to_physical(scale), + Force::Normalized(event.pressure() as f64), + ) + }), + ), + _ => mouse_handler( + modifiers, + id, + &mut event::pointer_move_event(event) + .map(|event| event::mouse_position(&event).to_physical(scale)), + ), + }; + })); + } + + pub fn on_touch_cancel(&mut self, canvas_common: &Common, mut handler: F) + where + F: 'static + FnMut(i32, PhysicalPosition, Force), + { + let window = canvas_common.window.clone(); + self.on_touch_cancel = + Some(canvas_common.add_event("pointercancel", move |event: PointerEvent| { + if event.pointer_type() == "touch" { + handler( + event.pointer_id(), + event::mouse_position(&event).to_physical(super::scale_factor(&window)), + Force::Normalized(event.pressure() as f64), + ); + } + })); + } + + pub fn remove_listeners(&mut self) { + self.on_cursor_leave = None; + self.on_cursor_enter = None; + self.on_cursor_move = None; + self.on_pointer_press = None; + self.on_pointer_release = None; + self.on_touch_cancel = None; + } +} diff --git a/vendor/winit/src/platform_impl/web/web_sys/resize_scaling.rs b/vendor/winit/src/platform_impl/web/web_sys/resize_scaling.rs new file mode 100644 index 00000000..4d10b3ac --- /dev/null +++ b/vendor/winit/src/platform_impl/web/web_sys/resize_scaling.rs @@ -0,0 +1,302 @@ +use js_sys::{Array, Object}; +use tracing::warn; +use wasm_bindgen::prelude::{wasm_bindgen, Closure}; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{ + Document, HtmlCanvasElement, MediaQueryList, ResizeObserver, ResizeObserverBoxOptions, + ResizeObserverEntry, ResizeObserverOptions, ResizeObserverSize, Window, +}; + +use crate::dpi::{LogicalSize, PhysicalSize}; + +use super::super::backend; +use super::canvas::Style; +use super::media_query_handle::MediaQueryListHandle; + +use std::cell::{Cell, RefCell}; +use std::rc::Rc; + +pub struct ResizeScaleHandle(Rc); + +impl ResizeScaleHandle { + pub(crate) fn new( + window: Window, + document: Document, + canvas: HtmlCanvasElement, + style: Style, + scale_handler: S, + resize_handler: R, + ) -> Self + where + S: 'static + Fn(PhysicalSize, f64), + R: 'static + Fn(PhysicalSize), + { + Self(ResizeScaleInternal::new( + window, + document, + canvas, + style, + scale_handler, + resize_handler, + )) + } + + pub(crate) fn notify_resize(&self) { + self.0.notify() + } +} + +/// This is a helper type to help manage the `MediaQueryList` used for detecting +/// changes of the `devicePixelRatio`. +struct ResizeScaleInternal { + window: Window, + document: Document, + canvas: HtmlCanvasElement, + style: Style, + mql: RefCell, + observer: ResizeObserver, + _observer_closure: Closure, + scale_handler: Box, f64)>, + resize_handler: Box)>, + notify_scale: Cell, +} + +impl ResizeScaleInternal { + fn new( + window: Window, + document: Document, + canvas: HtmlCanvasElement, + style: Style, + scale_handler: S, + resize_handler: R, + ) -> Rc + where + S: 'static + Fn(PhysicalSize, f64), + R: 'static + Fn(PhysicalSize), + { + Rc::::new_cyclic(|weak_self| { + let mql = Self::create_mql(&window, { + let weak_self = weak_self.clone(); + move |mql| { + if let Some(rc_self) = weak_self.upgrade() { + Self::handle_scale(rc_self, mql); + } + } + }); + + let weak_self = weak_self.clone(); + let observer_closure = Closure::new(move |entries: Array, _| { + if let Some(this) = weak_self.upgrade() { + let size = this.process_entry(entries); + + if this.notify_scale.replace(false) { + let scale = backend::scale_factor(&this.window); + (this.scale_handler)(size, scale) + } else { + (this.resize_handler)(size) + } + } + }); + let observer = Self::create_observer(&canvas, observer_closure.as_ref()); + + Self { + window, + document, + canvas, + style, + mql: RefCell::new(mql), + observer, + _observer_closure: observer_closure, + scale_handler: Box::new(scale_handler), + resize_handler: Box::new(resize_handler), + notify_scale: Cell::new(false), + } + }) + } + + fn create_mql(window: &Window, closure: F) -> MediaQueryListHandle + where + F: 'static + FnMut(&MediaQueryList), + { + let current_scale = super::scale_factor(window); + // TODO: Remove `-webkit-device-pixel-ratio`. Requires Safari v16. + let media_query = format!( + "(resolution: {current_scale}dppx), + (-webkit-device-pixel-ratio: {current_scale})", + ); + let mql = MediaQueryListHandle::new(window, &media_query, closure); + debug_assert!( + mql.mql().matches(), + "created media query doesn't match, {current_scale} != {}", + super::scale_factor(window) + ); + mql + } + + fn create_observer(canvas: &HtmlCanvasElement, closure: &JsValue) -> ResizeObserver { + let observer = ResizeObserver::new(closure.as_ref().unchecked_ref()) + .expect("Failed to create `ResizeObserver`"); + + // Safari doesn't support `devicePixelContentBoxSize` + if has_device_pixel_support() { + let options = ResizeObserverOptions::new(); + options.set_box(ResizeObserverBoxOptions::DevicePixelContentBox); + observer.observe_with_options(canvas, &options); + } else { + observer.observe(canvas); + } + + observer + } + + fn notify(&self) { + if !self.document.contains(Some(&self.canvas)) || self.style.get("display") == "none" { + let size = PhysicalSize::new(0, 0); + + if self.notify_scale.replace(false) { + let scale = backend::scale_factor(&self.window); + (self.scale_handler)(size, scale) + } else { + (self.resize_handler)(size) + } + + return; + } + + // Safari doesn't support `devicePixelContentBoxSize` + if has_device_pixel_support() { + self.observer.unobserve(&self.canvas); + self.observer.observe(&self.canvas); + + return; + } + + let mut size = LogicalSize::new( + backend::style_size_property(&self.style, "width"), + backend::style_size_property(&self.style, "height"), + ); + + if self.style.get("box-sizing") == "border-box" { + size.width -= backend::style_size_property(&self.style, "border-left-width") + + backend::style_size_property(&self.style, "border-right-width") + + backend::style_size_property(&self.style, "padding-left") + + backend::style_size_property(&self.style, "padding-right"); + size.height -= backend::style_size_property(&self.style, "border-top-width") + + backend::style_size_property(&self.style, "border-bottom-width") + + backend::style_size_property(&self.style, "padding-top") + + backend::style_size_property(&self.style, "padding-bottom"); + } + + let size = size.to_physical(backend::scale_factor(&self.window)); + + if self.notify_scale.replace(false) { + let scale = backend::scale_factor(&self.window); + (self.scale_handler)(size, scale) + } else { + (self.resize_handler)(size) + } + } + + fn handle_scale(self: Rc, mql: &MediaQueryList) { + let weak_self = Rc::downgrade(&self); + let scale = super::scale_factor(&self.window); + + // TODO: confirm/reproduce this problem, see: + // . + // This should never happen, but if it does then apparently the scale factor didn't change. + if mql.matches() { + warn!( + "media query tracking scale factor was triggered without a change:\nMedia Query: \ + {}\nCurrent Scale: {scale}", + mql.media(), + ); + return; + } + + let new_mql = Self::create_mql(&self.window, move |mql| { + if let Some(rc_self) = weak_self.upgrade() { + Self::handle_scale(rc_self, mql); + } + }); + self.mql.replace(new_mql); + + self.notify_scale.set(true); + self.notify(); + } + + fn process_entry(&self, entries: Array) -> PhysicalSize { + let entry: ResizeObserverEntry = entries.get(0).unchecked_into(); + + // Safari doesn't support `devicePixelContentBoxSize` + if !has_device_pixel_support() { + let rect = entry.content_rect(); + + return LogicalSize::new(rect.width(), rect.height()) + .to_physical(backend::scale_factor(&self.window)); + } + + let entry: ResizeObserverSize = + entry.device_pixel_content_box_size().get(0).unchecked_into(); + + let writing_mode = self.style.get("writing-mode"); + + // means the canvas is not inserted into the DOM + if writing_mode.is_empty() { + debug_assert_eq!(entry.inline_size(), 0.); + debug_assert_eq!(entry.block_size(), 0.); + + return PhysicalSize::new(0, 0); + } + + let horizontal = match writing_mode.as_str() { + _ if writing_mode.starts_with("horizontal") => true, + _ if writing_mode.starts_with("vertical") | writing_mode.starts_with("sideways") => { + false + }, + // deprecated values + "lr" | "lr-tb" | "rl" => true, + "tb" | "tb-lr" | "tb-rl" => false, + _ => { + warn!("unrecognized `writing-mode`, assuming horizontal"); + true + }, + }; + + if horizontal { + PhysicalSize::new(entry.inline_size() as u32, entry.block_size() as u32) + } else { + PhysicalSize::new(entry.block_size() as u32, entry.inline_size() as u32) + } + } +} + +impl Drop for ResizeScaleInternal { + fn drop(&mut self) { + self.observer.disconnect(); + } +} + +// TODO: Remove when Safari supports `devicePixelContentBoxSize`. +// See . +pub fn has_device_pixel_support() -> bool { + thread_local! { + static DEVICE_PIXEL_SUPPORT: bool = { + #[wasm_bindgen] + extern "C" { + type ResizeObserverEntryExt; + + #[wasm_bindgen(js_class = ResizeObserverEntry, static_method_of = ResizeObserverEntryExt, getter)] + fn prototype() -> Object; + } + + let prototype = ResizeObserverEntryExt::prototype(); + let descriptor = Object::get_own_property_descriptor( + &prototype, + &JsValue::from_str("devicePixelContentBoxSize"), + ); + !descriptor.is_undefined() + }; + } + + DEVICE_PIXEL_SUPPORT.with(|support| *support) +} diff --git a/vendor/winit/src/platform_impl/web/web_sys/schedule.rs b/vendor/winit/src/platform_impl/web/web_sys/schedule.rs new file mode 100644 index 00000000..eb970684 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/web_sys/schedule.rs @@ -0,0 +1,342 @@ +use js_sys::{Array, Function, Object, Promise, Reflect}; +use std::cell::OnceCell; +use std::time::Duration; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{ + AbortController, AbortSignal, Blob, BlobPropertyBag, MessageChannel, MessagePort, Url, Worker, +}; + +use crate::platform::web::{PollStrategy, WaitUntilStrategy}; + +#[derive(Debug)] +pub struct Schedule { + _closure: Closure, + inner: Inner, +} + +#[derive(Debug)] +enum Inner { + Scheduler { + controller: AbortController, + }, + IdleCallback { + window: web_sys::Window, + handle: u32, + }, + Timeout { + window: web_sys::Window, + handle: i32, + port: MessagePort, + _timeout_closure: Closure, + }, + Worker(MessagePort), +} + +impl Schedule { + pub fn new(strategy: PollStrategy, window: &web_sys::Window, f: F) -> Schedule + where + F: 'static + FnMut(), + { + if strategy == PollStrategy::Scheduler && has_scheduler_support(window) { + Self::new_scheduler(window, f, None) + } else if strategy == PollStrategy::IdleCallback && has_idle_callback_support(window) { + Self::new_idle_callback(window.clone(), f) + } else { + Self::new_timeout(window.clone(), f, None) + } + } + + pub fn new_with_duration( + strategy: WaitUntilStrategy, + window: &web_sys::Window, + f: F, + duration: Duration, + ) -> Schedule + where + F: 'static + FnMut(), + { + match strategy { + WaitUntilStrategy::Scheduler => { + if has_scheduler_support(window) { + Self::new_scheduler(window, f, Some(duration)) + } else { + Self::new_timeout(window.clone(), f, Some(duration)) + } + }, + WaitUntilStrategy::Worker => Self::new_worker(f, duration), + } + } + + fn new_scheduler(window: &web_sys::Window, f: F, duration: Option) -> Schedule + where + F: 'static + FnMut(), + { + let window: &WindowSupportExt = window.unchecked_ref(); + let scheduler = window.scheduler(); + + let closure = Closure::new(f); + let mut options = SchedulerPostTaskOptions::new(); + let controller = AbortController::new().expect("Failed to create `AbortController`"); + options.signal(&controller.signal()); + + if let Some(duration) = duration { + // `Duration::as_millis()` always rounds down (because of truncation), we want to round + // up instead. This makes sure that the we never wake up **before** the given time. + let duration = duration + .as_secs() + .checked_mul(1000) + .and_then(|secs| secs.checked_add(duration_millis_ceil(duration).into())) + .unwrap_or(u64::MAX); + + options.delay(duration as f64); + } + + thread_local! { + static REJECT_HANDLER: Closure = Closure::new(|_| ()); + } + REJECT_HANDLER.with(|handler| { + let _ = scheduler + .post_task_with_options(closure.as_ref().unchecked_ref(), &options) + .catch(handler); + }); + + Schedule { _closure: closure, inner: Inner::Scheduler { controller } } + } + + fn new_idle_callback(window: web_sys::Window, f: F) -> Schedule + where + F: 'static + FnMut(), + { + let closure = Closure::new(f); + let handle = window + .request_idle_callback(closure.as_ref().unchecked_ref()) + .expect("Failed to request idle callback"); + + Schedule { _closure: closure, inner: Inner::IdleCallback { window, handle } } + } + + fn new_timeout(window: web_sys::Window, f: F, duration: Option) -> Schedule + where + F: 'static + FnMut(), + { + let channel = MessageChannel::new().unwrap(); + let closure = Closure::new(f); + let port_1 = channel.port1(); + port_1.set_onmessage(Some(closure.as_ref().unchecked_ref())); + port_1.start(); + + let port_2 = channel.port2(); + let timeout_closure = Closure::new(move || { + port_2.post_message(&JsValue::UNDEFINED).expect("Failed to send message") + }); + let handle = if let Some(duration) = duration { + // `Duration::as_millis()` always rounds down (because of truncation), we want to round + // up instead. This makes sure that the we never wake up **before** the given time. + let duration = duration + .as_secs() + .try_into() + .ok() + .and_then(|secs: i32| secs.checked_mul(1000)) + .and_then(|secs: i32| { + let millis: i32 = duration_millis_ceil(duration) + .try_into() + .expect("millis are somehow bigger then 1K"); + secs.checked_add(millis) + }) + .unwrap_or(i32::MAX); + + window.set_timeout_with_callback_and_timeout_and_arguments_0( + timeout_closure.as_ref().unchecked_ref(), + duration, + ) + } else { + window.set_timeout_with_callback(timeout_closure.as_ref().unchecked_ref()) + } + .expect("Failed to set timeout"); + + Schedule { + _closure: closure, + inner: Inner::Timeout { + window, + handle, + port: port_1, + _timeout_closure: timeout_closure, + }, + } + } + + fn new_worker(f: F, duration: Duration) -> Schedule + where + F: 'static + FnMut(), + { + thread_local! { + static URL: ScriptUrl = ScriptUrl::new(include_str!("worker.min.js")); + static WORKER: Worker = URL.with(|url| Worker::new(&url.0)).expect("`new Worker()` is not expected to fail with a local script"); + } + + let channel = MessageChannel::new().unwrap(); + let closure = Closure::new(f); + let port_1 = channel.port1(); + port_1.set_onmessage(Some(closure.as_ref().unchecked_ref())); + port_1.start(); + + // `Duration::as_millis()` always rounds down (because of truncation), we want to round + // up instead. This makes sure that the we never wake up **before** the given time. + let duration = duration + .as_secs() + .try_into() + .ok() + .and_then(|secs: u32| secs.checked_mul(1000)) + .and_then(|secs| secs.checked_add(duration_millis_ceil(duration))) + .unwrap_or(u32::MAX); + + WORKER + .with(|worker| { + let port_2 = channel.port2(); + worker.post_message_with_transfer( + &Array::of2(&port_2, &duration.into()), + &Array::of1(&port_2).into(), + ) + }) + .expect("`Worker.postMessage()` is not expected to fail"); + + Schedule { _closure: closure, inner: Inner::Worker(port_1) } + } +} + +impl Drop for Schedule { + fn drop(&mut self) { + match &self.inner { + Inner::Scheduler { controller, .. } => controller.abort(), + Inner::IdleCallback { window, handle, .. } => window.cancel_idle_callback(*handle), + Inner::Timeout { window, handle, port, .. } => { + window.clear_timeout_with_handle(*handle); + port.close(); + port.set_onmessage(None); + }, + Inner::Worker(port) => { + port.close(); + port.set_onmessage(None); + }, + } + } +} + +// TODO: Replace with `u32::div_ceil()` when we hit Rust v1.73. +fn duration_millis_ceil(duration: Duration) -> u32 { + let micros = duration.subsec_micros(); + + // From . + let d = micros / 1000; + let r = micros % 1000; + if r > 0 && 1000 > 0 { + d + 1 + } else { + d + } +} + +fn has_scheduler_support(window: &web_sys::Window) -> bool { + thread_local! { + static SCHEDULER_SUPPORT: OnceCell = const { OnceCell::new() }; + } + + SCHEDULER_SUPPORT.with(|support| { + *support.get_or_init(|| { + #[wasm_bindgen] + extern "C" { + type SchedulerSupport; + + #[wasm_bindgen(method, getter, js_name = scheduler)] + fn has_scheduler(this: &SchedulerSupport) -> JsValue; + } + + let support: &SchedulerSupport = window.unchecked_ref(); + + !support.has_scheduler().is_undefined() + }) + }) +} + +fn has_idle_callback_support(window: &web_sys::Window) -> bool { + thread_local! { + static IDLE_CALLBACK_SUPPORT: OnceCell = const { OnceCell::new() }; + } + + IDLE_CALLBACK_SUPPORT.with(|support| { + *support.get_or_init(|| { + #[wasm_bindgen] + extern "C" { + type IdleCallbackSupport; + + #[wasm_bindgen(method, getter, js_name = requestIdleCallback)] + fn has_request_idle_callback(this: &IdleCallbackSupport) -> JsValue; + } + + let support: &IdleCallbackSupport = window.unchecked_ref(); + !support.has_request_idle_callback().is_undefined() + }) + }) +} + +struct ScriptUrl(String); + +impl ScriptUrl { + fn new(script: &str) -> Self { + let sequence = Array::of1(&script.into()); + let property = BlobPropertyBag::new(); + property.set_type("text/javascript"); + let blob = Blob::new_with_str_sequence_and_options(&sequence, &property) + .expect("`new Blob()` should never throw"); + + let url = Url::create_object_url_with_blob(&blob) + .expect("`URL.createObjectURL()` should never throw"); + + Self(url) + } +} + +impl Drop for ScriptUrl { + fn drop(&mut self) { + Url::revoke_object_url(&self.0).expect("`URL.revokeObjectURL()` should never throw"); + } +} + +#[wasm_bindgen] +extern "C" { + type WindowSupportExt; + + #[wasm_bindgen(method, getter)] + fn scheduler(this: &WindowSupportExt) -> Scheduler; + + type Scheduler; + + #[wasm_bindgen(method, js_name = postTask)] + fn post_task_with_options( + this: &Scheduler, + callback: &Function, + options: &SchedulerPostTaskOptions, + ) -> Promise; + + type SchedulerPostTaskOptions; +} + +impl SchedulerPostTaskOptions { + fn new() -> Self { + Object::new().unchecked_into() + } + + fn delay(&mut self, val: f64) -> &mut Self { + let r = Reflect::set(self, &JsValue::from("delay"), &val.into()); + debug_assert!(r.is_ok(), "Failed to set `delay` property"); + self + } + + fn signal(&mut self, val: &AbortSignal) -> &mut Self { + let r = Reflect::set(self, &JsValue::from("signal"), &val.into()); + debug_assert!(r.is_ok(), "Failed to set `signal` property"); + self + } +} diff --git a/vendor/winit/src/platform_impl/web/web_sys/worker.js b/vendor/winit/src/platform_impl/web/web_sys/worker.js new file mode 100644 index 00000000..5a8411ef --- /dev/null +++ b/vendor/winit/src/platform_impl/web/web_sys/worker.js @@ -0,0 +1,10 @@ +onmessage = event => { + const [port, timeout] = event.data + const f = () => port.postMessage(undefined) + + if ('scheduler' in this) { + scheduler.postTask(f, { delay: timeout }) + } else { + setTimeout(f, timeout) + } +} diff --git a/vendor/winit/src/platform_impl/web/web_sys/worker.min.js b/vendor/winit/src/platform_impl/web/web_sys/worker.min.js new file mode 100644 index 00000000..fd394a73 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/web_sys/worker.min.js @@ -0,0 +1 @@ +onmessage=e=>{let[s,t]=e.data,a=()=>s.postMessage(void 0);"scheduler"in this?scheduler.postTask(a,{delay:t}):setTimeout(a,t)}; diff --git a/vendor/winit/src/platform_impl/web/window.rs b/vendor/winit/src/platform_impl/web/window.rs new file mode 100644 index 00000000..29ca7302 --- /dev/null +++ b/vendor/winit/src/platform_impl/web/window.rs @@ -0,0 +1,477 @@ +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOE}; +use crate::icon::Icon; +use crate::window::{ + Cursor, CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, + WindowAttributes, WindowButtons, WindowId as RootWI, WindowLevel, +}; + +use super::main_thread::{MainThreadMarker, MainThreadSafe}; +use super::monitor::MonitorHandle; +use super::r#async::Dispatcher; +use super::{backend, ActiveEventLoop, Fullscreen}; +use web_sys::HtmlCanvasElement; + +use std::cell::RefCell; +use std::collections::VecDeque; +use std::rc::Rc; +use std::sync::Arc; + +pub struct Window { + inner: Dispatcher, +} + +pub struct Inner { + id: WindowId, + pub window: web_sys::Window, + canvas: Rc>, + destroy_fn: Option>, +} + +impl Window { + pub(crate) fn new( + target: &ActiveEventLoop, + mut attr: WindowAttributes, + ) -> Result { + let id = target.generate_id(); + + let window = target.runner.window(); + let document = target.runner.document(); + let canvas = backend::Canvas::create( + target.runner.main_thread(), + id, + window.clone(), + document.clone(), + &mut attr, + )?; + let canvas = Rc::new(RefCell::new(canvas)); + + target.register(&canvas, id); + + let runner = target.runner.clone(); + let destroy_fn = Box::new(move || runner.notify_destroy_window(RootWI(id))); + + let inner = Inner { id, window: window.clone(), canvas, destroy_fn: Some(destroy_fn) }; + + inner.set_title(&attr.title); + inner.set_maximized(attr.maximized); + inner.set_visible(attr.visible); + inner.set_window_icon(attr.window_icon); + inner.set_cursor(attr.cursor); + + let canvas = Rc::downgrade(&inner.canvas); + let (dispatcher, runner) = Dispatcher::new(target.runner.main_thread(), inner).unwrap(); + target.runner.add_canvas(RootWI(id), canvas, runner); + + Ok(Window { inner: dispatcher }) + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Inner) + Send + 'static) { + self.inner.dispatch(f) + } + + pub(crate) fn maybe_wait_on_main(&self, f: impl FnOnce(&Inner) -> R + Send) -> R { + self.inner.queue(f) + } + + pub fn canvas(&self) -> Option { + self.inner.value().map(|inner| inner.canvas.borrow().raw().clone()) + } + + pub(crate) fn prevent_default(&self) -> bool { + self.inner.queue(|inner| inner.canvas.borrow().prevent_default.get()) + } + + pub(crate) fn set_prevent_default(&self, prevent_default: bool) { + self.inner.dispatch(move |inner| inner.canvas.borrow().prevent_default.set(prevent_default)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> Result { + self.inner + .value() + .map(|inner| { + let canvas = inner.canvas.borrow(); + // SAFETY: This will only work if the reference to `HtmlCanvasElement` stays valid. + let canvas: &wasm_bindgen::JsValue = canvas.raw(); + let window_handle = + rwh_06::WebCanvasWindowHandle::new(std::ptr::NonNull::from(canvas).cast()); + rwh_06::RawWindowHandle::WebCanvas(window_handle) + }) + .ok_or(rwh_06::HandleError::Unavailable) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub(crate) fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Web(rwh_06::WebDisplayHandle::new())) + } +} + +impl Inner { + pub fn set_title(&self, title: &str) { + self.canvas.borrow().set_attribute("alt", title) + } + + pub fn set_transparent(&self, _transparent: bool) {} + + pub fn set_blur(&self, _blur: bool) {} + + pub fn set_visible(&self, _visible: bool) { + // Intentionally a no-op + } + + #[inline] + pub fn is_visible(&self) -> Option { + None + } + + pub fn request_redraw(&self) { + self.canvas.borrow().request_animation_frame(); + } + + pub fn pre_present_notify(&self) {} + + pub fn outer_position(&self) -> Result, NotSupportedError> { + Ok(self.canvas.borrow().position().to_physical(self.scale_factor())) + } + + pub fn inner_position(&self) -> Result, NotSupportedError> { + // Note: the canvas element has no window decorations, so this is equal to `outer_position`. + self.outer_position() + } + + pub fn set_outer_position(&self, position: Position) { + let canvas = self.canvas.borrow(); + let position = position.to_logical::(self.scale_factor()); + + backend::set_canvas_position(canvas.document(), canvas.raw(), canvas.style(), position) + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + self.canvas.borrow().inner_size() + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + // Note: the canvas element has no window decorations, so this is equal to `inner_size`. + self.inner_size() + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + let size = size.to_logical(self.scale_factor()); + let canvas = self.canvas.borrow(); + backend::set_canvas_size(canvas.document(), canvas.raw(), canvas.style(), size); + None + } + + #[inline] + pub fn set_min_inner_size(&self, dimensions: Option) { + let dimensions = dimensions.map(|dimensions| dimensions.to_logical(self.scale_factor())); + let canvas = self.canvas.borrow(); + backend::set_canvas_min_size(canvas.document(), canvas.raw(), canvas.style(), dimensions) + } + + #[inline] + pub fn set_max_inner_size(&self, dimensions: Option) { + let dimensions = dimensions.map(|dimensions| dimensions.to_logical(self.scale_factor())); + let canvas = self.canvas.borrow(); + backend::set_canvas_max_size(canvas.document(), canvas.raw(), canvas.style(), dimensions) + } + + #[inline] + pub fn resize_increments(&self) -> Option> { + None + } + + #[inline] + pub fn set_resize_increments(&self, _increments: Option) { + // Intentionally a no-op: users can't resize canvas elements + } + + #[inline] + pub fn set_resizable(&self, _resizable: bool) { + // Intentionally a no-op: users can't resize canvas elements + } + + pub fn is_resizable(&self) -> bool { + true + } + + #[inline] + pub fn set_enabled_buttons(&self, _buttons: WindowButtons) {} + + #[inline] + pub fn enabled_buttons(&self) -> WindowButtons { + WindowButtons::all() + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + super::backend::scale_factor(&self.window) + } + + #[inline] + pub fn set_cursor(&self, cursor: Cursor) { + self.canvas.borrow_mut().cursor.set_cursor(cursor) + } + + #[inline] + pub fn set_cursor_position(&self, _position: Position) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let lock = match mode { + CursorGrabMode::None => false, + CursorGrabMode::Locked => true, + CursorGrabMode::Confined => { + return Err(ExternalError::NotSupported(NotSupportedError::new())) + }, + }; + + self.canvas.borrow().set_cursor_lock(lock).map_err(ExternalError::Os) + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + self.canvas.borrow_mut().cursor.set_cursor_visible(visible) + } + + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn drag_resize_window(&self, _direction: ResizeDirection) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn show_window_menu(&self, _position: Position) {} + + #[inline] + pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + + #[inline] + pub fn set_minimized(&self, _minimized: bool) { + // Intentionally a no-op, as canvases cannot be 'minimized' + } + + #[inline] + pub fn is_minimized(&self) -> Option { + // Canvas cannot be 'minimized' + Some(false) + } + + #[inline] + pub fn set_maximized(&self, _maximized: bool) { + // Intentionally a no-op, as canvases cannot be 'maximized' + } + + #[inline] + pub fn is_maximized(&self) -> bool { + // Canvas cannot be 'maximized' + false + } + + #[inline] + pub(crate) fn fullscreen(&self) -> Option { + if self.canvas.borrow().is_fullscreen() { + Some(Fullscreen::Borderless(None)) + } else { + None + } + } + + #[inline] + pub(crate) fn set_fullscreen(&self, fullscreen: Option) { + let canvas = &self.canvas.borrow(); + + if fullscreen.is_some() { + canvas.request_fullscreen(); + } else { + canvas.exit_fullscreen() + } + } + + #[inline] + pub fn set_decorations(&self, _decorations: bool) { + // Intentionally a no-op, no canvas decorations + } + + pub fn is_decorated(&self) -> bool { + true + } + + #[inline] + pub fn set_window_level(&self, _level: WindowLevel) { + // Intentionally a no-op, no window ordering + } + + #[inline] + pub fn set_window_icon(&self, _window_icon: Option) { + // Currently an intentional no-op + } + + #[inline] + pub fn set_ime_cursor_area(&self, _position: Position, _size: Size) { + // Currently a no-op as it does not seem there is good support for this on web + } + + #[inline] + pub fn set_ime_allowed(&self, _allowed: bool) { + // Currently not implemented + } + + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) { + // Currently not implemented + } + + #[inline] + pub fn focus_window(&self) { + let _ = self.canvas.borrow().raw().focus(); + } + + #[inline] + pub fn request_user_attention(&self, _request_type: Option) { + // Currently an intentional no-op + } + + #[inline] + pub fn current_monitor(&self) -> Option { + None + } + + #[inline] + pub fn available_monitors(&self) -> VecDeque { + VecDeque::new() + } + + #[inline] + pub fn primary_monitor(&self) -> Option { + None + } + + #[inline] + pub fn id(&self) -> WindowId { + self.id + } + + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut window_handle = rwh_04::WebHandle::empty(); + window_handle.id = self.id.0; + rwh_04::RawWindowHandle::Web(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut window_handle = rwh_05::WebWindowHandle::empty(); + window_handle.id = self.id.0; + rwh_05::RawWindowHandle::Web(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Web(rwh_05::WebDisplayHandle::empty()) + } + + #[inline] + pub fn set_theme(&self, _theme: Option) {} + + #[inline] + pub fn theme(&self) -> Option { + backend::is_dark_mode(&self.window).map(|is_dark_mode| { + if is_dark_mode { + Theme::Dark + } else { + Theme::Light + } + }) + } + + pub fn set_content_protected(&self, _protected: bool) {} + + #[inline] + pub fn has_focus(&self) -> bool { + self.canvas.borrow().has_focus.get() + } + + pub fn title(&self) -> String { + String::new() + } + + pub fn reset_dead_keys(&self) { + // Not supported + } +} + +impl Drop for Inner { + fn drop(&mut self) { + if let Some(destroy_fn) = self.destroy_fn.take() { + destroy_fn(); + } + } +} +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId(pub(crate) u32); + +impl WindowId { + pub const fn dummy() -> Self { + Self(0) + } +} + +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0 as u64 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id as u32) + } +} + +#[derive(Clone, Debug)] +pub struct PlatformSpecificWindowAttributes { + pub(crate) canvas: Option>>, + pub(crate) prevent_default: bool, + pub(crate) focusable: bool, + pub(crate) append: bool, +} + +impl PlatformSpecificWindowAttributes { + pub(crate) fn set_canvas(&mut self, canvas: Option) { + let Some(canvas) = canvas else { + self.canvas = None; + return; + }; + + let main_thread = MainThreadMarker::new() + .expect("received a `HtmlCanvasElement` outside the window context"); + + self.canvas = Some(Arc::new(MainThreadSafe::new(main_thread, canvas))); + } +} + +impl Default for PlatformSpecificWindowAttributes { + fn default() -> Self { + Self { canvas: None, prevent_default: true, focusable: true, append: false } + } +} diff --git a/vendor/winit/src/platform_impl/windows/dark_mode.rs b/vendor/winit/src/platform_impl/windows/dark_mode.rs new file mode 100644 index 00000000..1c0ffedc --- /dev/null +++ b/vendor/winit/src/platform_impl/windows/dark_mode.rs @@ -0,0 +1,171 @@ +/// This is a simple implementation of support for Windows Dark Mode, +/// which is inspired by the solution in https://github.com/ysc3839/win32-darkmode +use std::{ffi::c_void, ptr}; + +use crate::utils::Lazy; +use windows_sys::core::PCSTR; +use windows_sys::Win32::Foundation::{BOOL, HWND, NTSTATUS, S_OK}; +use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA}; +use windows_sys::Win32::System::SystemInformation::OSVERSIONINFOW; +use windows_sys::Win32::UI::Accessibility::{HCF_HIGHCONTRASTON, HIGHCONTRASTA}; +use windows_sys::Win32::UI::Controls::SetWindowTheme; +use windows_sys::Win32::UI::WindowsAndMessaging::{SystemParametersInfoA, SPI_GETHIGHCONTRAST}; + +use crate::window::Theme; + +use super::util; + +// PdfCraft patch: `pub(crate)`, for the Windows 11 check in `WM_DPICHANGED` (event_loop.rs). +pub(crate) static WIN10_BUILD_VERSION: Lazy> = Lazy::new(|| { + type RtlGetVersion = unsafe extern "system" fn(*mut OSVERSIONINFOW) -> NTSTATUS; + let handle = get_function!("ntdll.dll", RtlGetVersion); + + if let Some(rtl_get_version) = handle { + unsafe { + let mut vi = OSVERSIONINFOW { + dwOSVersionInfoSize: 0, + dwMajorVersion: 0, + dwMinorVersion: 0, + dwBuildNumber: 0, + dwPlatformId: 0, + szCSDVersion: [0; 128], + }; + + let status = (rtl_get_version)(&mut vi); + + if status >= 0 && vi.dwMajorVersion == 10 && vi.dwMinorVersion == 0 { + Some(vi.dwBuildNumber) + } else { + None + } + } + } else { + None + } +}); + +static DARK_MODE_SUPPORTED: Lazy = Lazy::new(|| { + // We won't try to do anything for windows versions < 17763 + // (Windows 10 October 2018 update) + match *WIN10_BUILD_VERSION { + Some(v) => v >= 17763, + None => false, + } +}); + +static DARK_THEME_NAME: Lazy> = Lazy::new(|| util::encode_wide("DarkMode_Explorer")); +static LIGHT_THEME_NAME: Lazy> = Lazy::new(|| util::encode_wide("")); + +/// Attempt to set a theme on a window, if necessary. +/// Returns the theme that was picked +pub fn try_theme(hwnd: HWND, preferred_theme: Option) -> Theme { + if *DARK_MODE_SUPPORTED { + let is_dark_mode = match preferred_theme { + Some(theme) => theme == Theme::Dark, + None => should_use_dark_mode(), + }; + + let theme = if is_dark_mode { Theme::Dark } else { Theme::Light }; + let theme_name = match theme { + Theme::Dark => DARK_THEME_NAME.as_ptr(), + Theme::Light => LIGHT_THEME_NAME.as_ptr(), + }; + + let status = unsafe { SetWindowTheme(hwnd, theme_name, ptr::null()) }; + + if status == S_OK && set_dark_mode_for_window(hwnd, is_dark_mode) { + return theme; + } + } + + Theme::Light +} + +fn set_dark_mode_for_window(hwnd: HWND, is_dark_mode: bool) -> bool { + // Uses Windows undocumented API SetWindowCompositionAttribute, + // as seen in win32-darkmode example linked at top of file. + + type SetWindowCompositionAttribute = + unsafe extern "system" fn(HWND, *mut WINDOWCOMPOSITIONATTRIBDATA) -> BOOL; + + #[allow(clippy::upper_case_acronyms)] + type WINDOWCOMPOSITIONATTRIB = u32; + const WCA_USEDARKMODECOLORS: WINDOWCOMPOSITIONATTRIB = 26; + + #[allow(non_snake_case)] + #[allow(clippy::upper_case_acronyms)] + #[repr(C)] + struct WINDOWCOMPOSITIONATTRIBDATA { + Attrib: WINDOWCOMPOSITIONATTRIB, + pvData: *mut c_void, + cbData: usize, + } + + static SET_WINDOW_COMPOSITION_ATTRIBUTE: Lazy> = + Lazy::new(|| get_function!("user32.dll", SetWindowCompositionAttribute)); + + if let Some(set_window_composition_attribute) = *SET_WINDOW_COMPOSITION_ATTRIBUTE { + unsafe { + // SetWindowCompositionAttribute needs a bigbool (i32), not bool. + let mut is_dark_mode_bigbool = BOOL::from(is_dark_mode); + + let mut data = WINDOWCOMPOSITIONATTRIBDATA { + Attrib: WCA_USEDARKMODECOLORS, + pvData: &mut is_dark_mode_bigbool as *mut _ as _, + cbData: std::mem::size_of_val(&is_dark_mode_bigbool) as _, + }; + + let status = set_window_composition_attribute(hwnd, &mut data); + + status != false.into() + } + } else { + false + } +} + +pub fn should_use_dark_mode() -> bool { + should_apps_use_dark_mode() && !is_high_contrast() +} + +fn should_apps_use_dark_mode() -> bool { + type ShouldAppsUseDarkMode = unsafe extern "system" fn() -> bool; + static SHOULD_APPS_USE_DARK_MODE: Lazy> = Lazy::new(|| unsafe { + const UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL: PCSTR = 132 as PCSTR; + + // We won't try to do anything for windows versions < 17763 + // (Windows 10 October 2018 update) + if !*DARK_MODE_SUPPORTED { + return None; + } + + let module = LoadLibraryA("uxtheme.dll\0".as_ptr().cast()); + + if module == 0 { + return None; + } + + let handle = GetProcAddress(module, UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL); + + handle.map(|handle| std::mem::transmute(handle)) + }); + + SHOULD_APPS_USE_DARK_MODE + .map(|should_apps_use_dark_mode| unsafe { (should_apps_use_dark_mode)() }) + .unwrap_or(false) +} + +fn is_high_contrast() -> bool { + let mut hc = HIGHCONTRASTA { cbSize: 0, dwFlags: 0, lpszDefaultScheme: ptr::null_mut() }; + + let ok = unsafe { + SystemParametersInfoA( + SPI_GETHIGHCONTRAST, + std::mem::size_of_val(&hc) as _, + &mut hc as *mut _ as _, + 0, + ) + }; + + ok != false.into() && util::has_flag(hc.dwFlags, HCF_HIGHCONTRASTON) +} diff --git a/vendor/winit/src/platform_impl/windows/definitions.rs b/vendor/winit/src/platform_impl/windows/definitions.rs new file mode 100644 index 00000000..c015ffc4 --- /dev/null +++ b/vendor/winit/src/platform_impl/windows/definitions.rs @@ -0,0 +1,148 @@ +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] + +use std::ffi::c_void; + +use windows_sys::core::{IUnknown, GUID, HRESULT}; +use windows_sys::Win32::Foundation::{BOOL, HWND, POINTL}; +use windows_sys::Win32::System::Com::{ + IAdviseSink, IDataObject, IEnumFORMATETC, IEnumSTATDATA, FORMATETC, STGMEDIUM, +}; + +#[repr(C)] +pub struct IUnknownVtbl { + pub QueryInterface: unsafe extern "system" fn( + This: *mut IUnknown, + riid: *const GUID, + ppvObject: *mut *mut c_void, + ) -> HRESULT, + pub AddRef: unsafe extern "system" fn(This: *mut IUnknown) -> u32, + pub Release: unsafe extern "system" fn(This: *mut IUnknown) -> u32, +} + +#[repr(C)] +pub struct IDataObjectVtbl { + pub parent: IUnknownVtbl, + pub GetData: unsafe extern "system" fn( + This: *mut IDataObject, + pformatetcIn: *const FORMATETC, + pmedium: *mut STGMEDIUM, + ) -> HRESULT, + pub GetDataHere: unsafe extern "system" fn( + This: *mut IDataObject, + pformatetc: *const FORMATETC, + pmedium: *mut STGMEDIUM, + ) -> HRESULT, + QueryGetData: + unsafe extern "system" fn(This: *mut IDataObject, pformatetc: *const FORMATETC) -> HRESULT, + pub GetCanonicalFormatEtc: unsafe extern "system" fn( + This: *mut IDataObject, + pformatetcIn: *const FORMATETC, + pformatetcOut: *mut FORMATETC, + ) -> HRESULT, + pub SetData: unsafe extern "system" fn( + This: *mut IDataObject, + pformatetc: *const FORMATETC, + pformatetcOut: *const FORMATETC, + fRelease: BOOL, + ) -> HRESULT, + pub EnumFormatEtc: unsafe extern "system" fn( + This: *mut IDataObject, + dwDirection: u32, + ppenumFormatEtc: *mut *mut IEnumFORMATETC, + ) -> HRESULT, + pub DAdvise: unsafe extern "system" fn( + This: *mut IDataObject, + pformatetc: *const FORMATETC, + advf: u32, + pAdvSInk: *const IAdviseSink, + pdwConnection: *mut u32, + ) -> HRESULT, + pub DUnadvise: unsafe extern "system" fn(This: *mut IDataObject, dwConnection: u32) -> HRESULT, + pub EnumDAdvise: unsafe extern "system" fn( + This: *mut IDataObject, + ppenumAdvise: *const *const IEnumSTATDATA, + ) -> HRESULT, +} + +#[repr(C)] +pub struct IDropTargetVtbl { + pub parent: IUnknownVtbl, + pub DragEnter: unsafe extern "system" fn( + This: *mut IDropTarget, + pDataObj: *const IDataObject, + grfKeyState: u32, + pt: *const POINTL, + pdwEffect: *mut u32, + ) -> HRESULT, + pub DragOver: unsafe extern "system" fn( + This: *mut IDropTarget, + grfKeyState: u32, + pt: *const POINTL, + pdwEffect: *mut u32, + ) -> HRESULT, + pub DragLeave: unsafe extern "system" fn(This: *mut IDropTarget) -> HRESULT, + pub Drop: unsafe extern "system" fn( + This: *mut IDropTarget, + pDataObj: *const IDataObject, + grfKeyState: u32, + pt: *const POINTL, + pdwEffect: *mut u32, + ) -> HRESULT, +} + +#[repr(C)] +pub struct IDropTarget { + pub lpVtbl: *const IDropTargetVtbl, +} + +#[repr(C)] +pub struct ITaskbarListVtbl { + pub parent: IUnknownVtbl, + pub HrInit: unsafe extern "system" fn(This: *mut ITaskbarList) -> HRESULT, + pub AddTab: unsafe extern "system" fn(This: *mut ITaskbarList, hwnd: HWND) -> HRESULT, + pub DeleteTab: unsafe extern "system" fn(This: *mut ITaskbarList, hwnd: HWND) -> HRESULT, + pub ActivateTab: unsafe extern "system" fn(This: *mut ITaskbarList, hwnd: HWND) -> HRESULT, + pub SetActiveAlt: unsafe extern "system" fn(This: *mut ITaskbarList, hwnd: HWND) -> HRESULT, +} + +#[repr(C)] +pub struct ITaskbarList { + pub lpVtbl: *const ITaskbarListVtbl, +} + +#[repr(C)] +pub struct ITaskbarList2Vtbl { + pub parent: ITaskbarListVtbl, + pub MarkFullscreenWindow: unsafe extern "system" fn( + This: *mut ITaskbarList2, + hwnd: HWND, + fFullscreen: BOOL, + ) -> HRESULT, +} + +#[repr(C)] +pub struct ITaskbarList2 { + pub lpVtbl: *const ITaskbarList2Vtbl, +} + +pub const CLSID_TaskbarList: GUID = GUID { + data1: 0x56fdf344, + data2: 0xfd6d, + data3: 0x11d0, + data4: [0x95, 0x8a, 0x00, 0x60, 0x97, 0xc9, 0xa0, 0x90], +}; + +pub const IID_ITaskbarList: GUID = GUID { + data1: 0x56fdf342, + data2: 0xfd6d, + data3: 0x11d0, + data4: [0x95, 0x8a, 0x00, 0x60, 0x97, 0xc9, 0xa0, 0x90], +}; + +pub const IID_ITaskbarList2: GUID = GUID { + data1: 0x602d4995, + data2: 0xb13a, + data3: 0x429b, + data4: [0xa6, 0x6e, 0x19, 0x35, 0xe4, 0x4f, 0x43, 0x17], +}; diff --git a/vendor/winit/src/platform_impl/windows/dpi.rs b/vendor/winit/src/platform_impl/windows/dpi.rs new file mode 100644 index 00000000..5880069c --- /dev/null +++ b/vendor/winit/src/platform_impl/windows/dpi.rs @@ -0,0 +1,112 @@ +#![allow(non_snake_case, unused_unsafe)] + +use std::sync::Once; + +use windows_sys::Win32::Foundation::{HWND, S_OK}; +use windows_sys::Win32::Graphics::Gdi::{ + GetDC, GetDeviceCaps, MonitorFromWindow, HMONITOR, LOGPIXELSX, MONITOR_DEFAULTTONEAREST, +}; +use windows_sys::Win32::UI::HiDpi::{ + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, + MDT_EFFECTIVE_DPI, PROCESS_PER_MONITOR_DPI_AWARE, +}; +use windows_sys::Win32::UI::WindowsAndMessaging::IsProcessDPIAware; + +use crate::platform_impl::platform::util::{ + ENABLE_NON_CLIENT_DPI_SCALING, GET_DPI_FOR_MONITOR, GET_DPI_FOR_WINDOW, SET_PROCESS_DPI_AWARE, + SET_PROCESS_DPI_AWARENESS, SET_PROCESS_DPI_AWARENESS_CONTEXT, +}; + +pub fn become_dpi_aware() { + static ENABLE_DPI_AWARENESS: Once = Once::new(); + ENABLE_DPI_AWARENESS.call_once(|| { + unsafe { + if let Some(SetProcessDpiAwarenessContext) = *SET_PROCESS_DPI_AWARENESS_CONTEXT { + // We are on Windows 10 Anniversary Update (1607) or later. + if SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) + == false.into() + { + // V2 only works with Windows 10 Creators Update (1703). Try using the older + // V1 if we can't set V2. + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); + } + } else if let Some(SetProcessDpiAwareness) = *SET_PROCESS_DPI_AWARENESS { + // We are on Windows 8.1 or later. + SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); + } else if let Some(SetProcessDPIAware) = *SET_PROCESS_DPI_AWARE { + // We are on Vista or later. + SetProcessDPIAware(); + } + } + }); +} + +pub fn enable_non_client_dpi_scaling(hwnd: HWND) { + unsafe { + if let Some(EnableNonClientDpiScaling) = *ENABLE_NON_CLIENT_DPI_SCALING { + EnableNonClientDpiScaling(hwnd); + } + } +} + +pub fn get_monitor_dpi(hmonitor: HMONITOR) -> Option { + unsafe { + if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR { + // We are on Windows 8.1 or later. + let mut dpi_x = 0; + let mut dpi_y = 0; + if GetDpiForMonitor(hmonitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) == S_OK { + // MSDN says that "the values of *dpiX and *dpiY are identical. You only need to + // record one of the values to determine the DPI and respond appropriately". + // https://msdn.microsoft.com/en-us/library/windows/desktop/dn280510(v=vs.85).aspx + return Some(dpi_x); + } + } + } + None +} + +pub const BASE_DPI: u32 = 96; +pub fn dpi_to_scale_factor(dpi: u32) -> f64 { + dpi as f64 / BASE_DPI as f64 +} + +pub unsafe fn hwnd_dpi(hwnd: HWND) -> u32 { + let hdc = unsafe { GetDC(hwnd) }; + if hdc == 0 { + panic!("[winit] `GetDC` returned null!"); + } + if let Some(GetDpiForWindow) = *GET_DPI_FOR_WINDOW { + // We are on Windows 10 Anniversary Update (1607) or later. + match unsafe { GetDpiForWindow(hwnd) } { + 0 => BASE_DPI, // 0 is returned if hwnd is invalid + dpi => dpi, + } + } else if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR { + // We are on Windows 8.1 or later. + let monitor = unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }; + if monitor == 0 { + return BASE_DPI; + } + + let mut dpi_x = 0; + let mut dpi_y = 0; + if unsafe { GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) } == S_OK { + dpi_x + } else { + BASE_DPI + } + } else { + // We are on Vista or later. + if unsafe { IsProcessDPIAware() } != false.into() { + // If the process is DPI aware, then scaling must be handled by the application using + // this DPI value. + unsafe { GetDeviceCaps(hdc, LOGPIXELSX as i32) as u32 } + } else { + // If the process is DPI unaware, then scaling is performed by the OS; we thus return + // 96 (scale factor 1.0) to prevent the window from being re-scaled by both the + // application and the WM. + BASE_DPI + } + } +} diff --git a/vendor/winit/src/platform_impl/windows/drop_handler.rs b/vendor/winit/src/platform_impl/windows/drop_handler.rs new file mode 100644 index 00000000..38cb5c54 --- /dev/null +++ b/vendor/winit/src/platform_impl/windows/drop_handler.rs @@ -0,0 +1,237 @@ +use std::ffi::{c_void, OsString}; +use std::os::windows::ffi::OsStringExt; +use std::path::PathBuf; +use std::ptr; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use windows_sys::core::{IUnknown, GUID, HRESULT}; +use windows_sys::Win32::Foundation::{DV_E_FORMATETC, HWND, POINTL, S_OK}; +use windows_sys::Win32::System::Com::{IDataObject, DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL}; +use windows_sys::Win32::System::Ole::{CF_HDROP, DROPEFFECT_COPY, DROPEFFECT_NONE}; +use windows_sys::Win32::UI::Shell::{DragFinish, DragQueryFileW, HDROP}; + +use tracing::debug; + +use crate::platform_impl::platform::definitions::{ + IDataObjectVtbl, IDropTarget, IDropTargetVtbl, IUnknownVtbl, +}; +use crate::platform_impl::platform::WindowId; + +use crate::event::Event; +use crate::window::WindowId as RootWindowId; + +#[repr(C)] +pub struct FileDropHandlerData { + pub interface: IDropTarget, + refcount: AtomicUsize, + window: HWND, + send_event: Box)>, + cursor_effect: u32, + hovered_is_valid: bool, /* If the currently hovered item is not valid there must not be any + * `HoveredFileCancelled` emitted */ +} + +pub struct FileDropHandler { + pub data: *mut FileDropHandlerData, +} + +#[allow(non_snake_case)] +impl FileDropHandler { + pub fn new(window: HWND, send_event: Box)>) -> FileDropHandler { + let data = Box::new(FileDropHandlerData { + interface: IDropTarget { lpVtbl: &DROP_TARGET_VTBL as *const IDropTargetVtbl }, + refcount: AtomicUsize::new(1), + window, + send_event, + cursor_effect: DROPEFFECT_NONE, + hovered_is_valid: false, + }); + FileDropHandler { data: Box::into_raw(data) } + } + + // Implement IUnknown + pub unsafe extern "system" fn QueryInterface( + _this: *mut IUnknown, + _riid: *const GUID, + _ppvObject: *mut *mut c_void, + ) -> HRESULT { + // This function doesn't appear to be required for an `IDropTarget`. + // An implementation would be nice however. + unimplemented!(); + } + + pub unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 { + let drop_handler_data = unsafe { Self::from_interface(this) }; + let count = drop_handler_data.refcount.fetch_add(1, Ordering::Release) + 1; + count as u32 + } + + pub unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 { + let drop_handler = unsafe { Self::from_interface(this) }; + let count = drop_handler.refcount.fetch_sub(1, Ordering::Release) - 1; + if count == 0 { + // Destroy the underlying data + drop(unsafe { Box::from_raw(drop_handler as *mut FileDropHandlerData) }); + } + count as u32 + } + + pub unsafe extern "system" fn DragEnter( + this: *mut IDropTarget, + pDataObj: *const IDataObject, + _grfKeyState: u32, + _pt: *const POINTL, + pdwEffect: *mut u32, + ) -> HRESULT { + use crate::event::WindowEvent::HoveredFile; + let drop_handler = unsafe { Self::from_interface(this) }; + let hdrop = unsafe { + Self::iterate_filenames(pDataObj, |filename| { + drop_handler.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(drop_handler.window)), + event: HoveredFile(filename), + }); + }) + }; + drop_handler.hovered_is_valid = hdrop.is_some(); + drop_handler.cursor_effect = + if drop_handler.hovered_is_valid { DROPEFFECT_COPY } else { DROPEFFECT_NONE }; + unsafe { + *pdwEffect = drop_handler.cursor_effect; + } + + S_OK + } + + pub unsafe extern "system" fn DragOver( + this: *mut IDropTarget, + _grfKeyState: u32, + _pt: *const POINTL, + pdwEffect: *mut u32, + ) -> HRESULT { + let drop_handler = unsafe { Self::from_interface(this) }; + unsafe { + *pdwEffect = drop_handler.cursor_effect; + } + + S_OK + } + + pub unsafe extern "system" fn DragLeave(this: *mut IDropTarget) -> HRESULT { + use crate::event::WindowEvent::HoveredFileCancelled; + let drop_handler = unsafe { Self::from_interface(this) }; + if drop_handler.hovered_is_valid { + drop_handler.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(drop_handler.window)), + event: HoveredFileCancelled, + }); + } + + S_OK + } + + pub unsafe extern "system" fn Drop( + this: *mut IDropTarget, + pDataObj: *const IDataObject, + _grfKeyState: u32, + _pt: *const POINTL, + _pdwEffect: *mut u32, + ) -> HRESULT { + use crate::event::WindowEvent::DroppedFile; + let drop_handler = unsafe { Self::from_interface(this) }; + let hdrop = unsafe { + Self::iterate_filenames(pDataObj, |filename| { + drop_handler.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(drop_handler.window)), + event: DroppedFile(filename), + }); + }) + }; + if let Some(hdrop) = hdrop { + unsafe { DragFinish(hdrop) }; + } + + S_OK + } + + unsafe fn from_interface<'a, InterfaceT>(this: *mut InterfaceT) -> &'a mut FileDropHandlerData { + unsafe { &mut *(this as *mut _) } + } + + unsafe fn iterate_filenames(data_obj: *const IDataObject, callback: F) -> Option + where + F: Fn(PathBuf), + { + let drop_format = FORMATETC { + cfFormat: CF_HDROP, + ptd: ptr::null_mut(), + dwAspect: DVASPECT_CONTENT, + lindex: -1, + tymed: TYMED_HGLOBAL as u32, + }; + + let mut medium = unsafe { std::mem::zeroed() }; + let get_data_fn = unsafe { (*(*data_obj).cast::()).GetData }; + let get_data_result = unsafe { get_data_fn(data_obj as *mut _, &drop_format, &mut medium) }; + if get_data_result >= 0 { + let hdrop = unsafe { medium.u.hGlobal as HDROP }; + + // The second parameter (0xFFFFFFFF) instructs the function to return the item count + let item_count = unsafe { DragQueryFileW(hdrop, 0xffffffff, ptr::null_mut(), 0) }; + + for i in 0..item_count { + // Get the length of the path string NOT including the terminating null character. + // Previously, this was using a fixed size array of MAX_PATH length, but the + // Windows API allows longer paths under certain circumstances. + let character_count = + unsafe { DragQueryFileW(hdrop, i, ptr::null_mut(), 0) as usize }; + let str_len = character_count + 1; + + // Fill path_buf with the null-terminated file name + let mut path_buf = Vec::with_capacity(str_len); + unsafe { + DragQueryFileW(hdrop, i, path_buf.as_mut_ptr(), str_len as u32); + path_buf.set_len(str_len); + } + + callback(OsString::from_wide(&path_buf[0..character_count]).into()); + } + + Some(hdrop) + } else if get_data_result == DV_E_FORMATETC { + // If the dropped item is not a file this error will occur. + // In this case it is OK to return without taking further action. + debug!("Error occurred while processing dropped/hovered item: item is not a file."); + None + } else { + debug!("Unexpected error occurred while processing dropped/hovered item."); + None + } + } +} + +impl FileDropHandlerData { + fn send_event(&self, event: Event<()>) { + (self.send_event)(event); + } +} + +impl Drop for FileDropHandler { + fn drop(&mut self) { + unsafe { + FileDropHandler::Release(self.data as *mut IUnknown); + } + } +} + +static DROP_TARGET_VTBL: IDropTargetVtbl = IDropTargetVtbl { + parent: IUnknownVtbl { + QueryInterface: FileDropHandler::QueryInterface, + AddRef: FileDropHandler::AddRef, + Release: FileDropHandler::Release, + }, + DragEnter: FileDropHandler::DragEnter, + DragOver: FileDropHandler::DragOver, + DragLeave: FileDropHandler::DragLeave, + Drop: FileDropHandler::Drop, +}; diff --git a/vendor/winit/src/platform_impl/windows/event_loop.rs b/vendor/winit/src/platform_impl/windows/event_loop.rs new file mode 100644 index 00000000..469658e4 --- /dev/null +++ b/vendor/winit/src/platform_impl/windows/event_loop.rs @@ -0,0 +1,2684 @@ +#![allow(non_snake_case)] + +mod runner; + +use std::cell::Cell; +use std::collections::VecDeque; +use std::ffi::c_void; +use std::marker::PhantomData; +use std::os::windows::io::{AsRawHandle as _, FromRawHandle as _, OwnedHandle, RawHandle}; +use std::rc::Rc; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::mpsc::{self, Receiver, Sender}; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::time::{Duration, Instant}; +use std::{mem, panic, ptr}; + +use crate::utils::Lazy; + +use windows_sys::Win32::Devices::HumanInterfaceDevice::MOUSE_MOVE_RELATIVE; +use windows_sys::Win32::Foundation::{ + GetLastError, FALSE, HANDLE, HWND, LPARAM, LRESULT, POINT, RECT, WAIT_FAILED, WPARAM, +}; +use windows_sys::Win32::Graphics::Gdi::{ + GetMonitorInfoW, MonitorFromRect, MonitorFromWindow, RedrawWindow, ScreenToClient, + ValidateRect, MONITORINFO, MONITOR_DEFAULTTONULL, RDW_INTERNALPAINT, SC_SCREENSAVE, +}; +use windows_sys::Win32::System::Ole::RevokeDragDrop; +use windows_sys::Win32::System::Threading::{ + CreateWaitableTimerExW, GetCurrentThreadId, SetWaitableTimer, + CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, INFINITE, TIMER_ALL_ACCESS, +}; +use windows_sys::Win32::UI::Controls::{HOVER_DEFAULT, WM_MOUSELEAVE}; +use windows_sys::Win32::UI::Input::Ime::{GCS_COMPSTR, GCS_RESULTSTR, ISC_SHOWUICOMPOSITIONWINDOW}; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ + ReleaseCapture, SetCapture, TrackMouseEvent, TME_LEAVE, TRACKMOUSEEVENT, +}; +use windows_sys::Win32::UI::Input::Pointer::{ + POINTER_FLAG_DOWN, POINTER_FLAG_UP, POINTER_FLAG_UPDATE, +}; +use windows_sys::Win32::UI::Input::Touch::{ + CloseTouchInputHandle, GetTouchInputInfo, TOUCHEVENTF_DOWN, TOUCHEVENTF_MOVE, TOUCHEVENTF_UP, + TOUCHINPUT, +}; +use windows_sys::Win32::UI::Input::{RAWINPUT, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE}; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GetClientRect, GetCursorPos, + GetMenu, LoadCursorW, MsgWaitForMultipleObjectsEx, PeekMessageW, PostMessageW, + RegisterClassExW, RegisterWindowMessageA, SetCursor, SetWindowPos, TranslateMessage, + CREATESTRUCTW, GIDC_ARRIVAL, GIDC_REMOVAL, GWL_STYLE, GWL_USERDATA, HTCAPTION, HTCLIENT, + MINMAXINFO, MNC_CLOSE, MSG, MWMO_INPUTAVAILABLE, NCCALCSIZE_PARAMS, PM_REMOVE, PT_PEN, + PT_TOUCH, QS_ALLINPUT, RI_MOUSE_HWHEEL, RI_MOUSE_WHEEL, SC_MINIMIZE, SC_RESTORE, + SIZE_MAXIMIZED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, WHEEL_DELTA, WINDOWPOS, + WMSZ_BOTTOM, WMSZ_BOTTOMLEFT, WMSZ_BOTTOMRIGHT, WMSZ_LEFT, WMSZ_RIGHT, WMSZ_TOP, WMSZ_TOPLEFT, + WMSZ_TOPRIGHT, WM_CAPTURECHANGED, WM_CLOSE, WM_CREATE, WM_DESTROY, WM_DPICHANGED, + WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, WM_GETMINMAXINFO, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, + WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION, WM_INPUT, WM_INPUT_DEVICE_CHANGE, WM_KEYDOWN, + WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, + WM_MENUCHAR, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCACTIVATE, WM_NCCALCSIZE, + WM_NCCREATE, WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_POINTERUP, + WM_POINTERUPDATE, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, + WM_SIZE, WM_SIZING, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED, + WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, WS_EX_LAYERED, + WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE, +}; + +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::error::EventLoopError; +use crate::event::{ + DeviceEvent, Event, Force, Ime, InnerSizeWriter, RawKeyEvent, Touch, TouchPhase, WindowEvent, +}; +use crate::event_loop::{ActiveEventLoop as RootAEL, ControlFlow, DeviceEvents, EventLoopClosed}; +use crate::keyboard::ModifiersState; +use crate::platform::pump_events::PumpStatus; +use crate::platform_impl::platform::dark_mode::{try_theme, WIN10_BUILD_VERSION}; +use crate::platform_impl::platform::dpi::{become_dpi_aware, dpi_to_scale_factor}; +use crate::platform_impl::platform::drop_handler::FileDropHandler; +use crate::platform_impl::platform::icon::WinCursor; +use crate::platform_impl::platform::ime::ImeContext; +use crate::platform_impl::platform::keyboard::KeyEventBuilder; +use crate::platform_impl::platform::keyboard_layout::LAYOUT_CACHE; +use crate::platform_impl::platform::monitor::{self, MonitorHandle}; +use crate::platform_impl::platform::window::InitData; +use crate::platform_impl::platform::window_state::{ + CursorFlags, ImeState, WindowFlags, WindowState, +}; +use crate::platform_impl::platform::{ + raw_input, util, wrap_device_id, Fullscreen, WindowId, DEVICE_ID, +}; +use crate::window::{ + CustomCursor as RootCustomCursor, CustomCursorSource, Theme, WindowId as RootWindowId, +}; +use runner::{EventLoopRunner, EventLoopRunnerShared}; + +use super::window::set_skip_taskbar; +use super::SelectedCursor; + +/// some backends like macos uses an uninhabited `Never` type, +/// on windows, `UserEvent`s are also dispatched through the +/// WNDPROC callback, and due to the re-entrant nature of the +/// callback, recursively delivered events must be queued in a +/// buffer, the current implementation put this queue in +/// `EventLoopRunner`, which is shared between the event pumping +/// loop and the callback. because it's hard to decide from the +/// outside whether a event needs to be buffered, I decided not +/// use `Event` for the shared runner state, but use unit +/// as a placeholder so user events can be buffered as usual, +/// the real `UserEvent` is pulled from the mpsc channel directly +/// when the placeholder event is delivered to the event handler +pub(crate) struct UserEventPlaceholder; + +// here below, the generic `EventLoopRunnerShared` is replaced with +// `EventLoopRunnerShared` so we can get rid +// of the generic parameter T in types which don't depend on T. +// this is the approach which requires minimum changes to current +// backend implementation. it should be considered transitional +// and should be refactored and cleaned up eventually, I hope. + +pub(crate) struct WindowData { + pub window_state: Arc>, + pub event_loop_runner: EventLoopRunnerShared, + pub key_event_builder: KeyEventBuilder, + pub _file_drop_handler: Option, + pub userdata_removed: Cell, + pub recurse_depth: Cell, +} + +impl WindowData { + fn send_event(&self, event: Event) { + self.event_loop_runner.send_event(event); + } + + fn window_state_lock(&self) -> MutexGuard<'_, WindowState> { + self.window_state.lock().unwrap() + } +} + +struct ThreadMsgTargetData { + event_loop_runner: EventLoopRunnerShared, +} + +impl ThreadMsgTargetData { + fn send_event(&self, event: Event) { + self.event_loop_runner.send_event(event); + } +} + +/// The result of a subclass procedure (the message handling callback) +#[derive(Clone, Copy)] +pub(crate) enum ProcResult { + DefWindowProc(WPARAM), + Value(isize), +} + +pub struct EventLoop { + user_event_sender: Sender, + user_event_receiver: Receiver, + window_target: RootAEL, + msg_hook: Option bool + 'static>>, + // It is a timer used on timed waits. + // It is created lazily in case if we have `ControlFlow::WaitUntil`. + // Keep it as a field to avoid recreating it on every `ControlFlow::WaitUntil`. + high_resolution_timer: Option, +} + +pub(crate) struct PlatformSpecificEventLoopAttributes { + pub(crate) any_thread: bool, + pub(crate) dpi_aware: bool, + pub(crate) msg_hook: Option bool + 'static>>, +} + +impl Default for PlatformSpecificEventLoopAttributes { + fn default() -> Self { + Self { any_thread: false, dpi_aware: true, msg_hook: None } + } +} + +pub struct ActiveEventLoop { + thread_id: u32, + thread_msg_target: HWND, + pub(crate) runner_shared: EventLoopRunnerShared, +} + +impl EventLoop { + pub(crate) fn new( + attributes: &mut PlatformSpecificEventLoopAttributes, + ) -> Result { + let thread_id = unsafe { GetCurrentThreadId() }; + + if !attributes.any_thread && thread_id != main_thread_id() { + panic!( + "Initializing the event loop outside of the main thread is a significant \ + cross-platform compatibility hazard. If you absolutely need to create an \ + EventLoop on a different thread, you can use the \ + `EventLoopBuilderExtWindows::any_thread` function." + ); + } + + if attributes.dpi_aware { + become_dpi_aware(); + } + + let thread_msg_target = create_event_target_window(); + + let runner_shared = Rc::new(EventLoopRunner::new(thread_msg_target)); + + let (user_event_sender, user_event_receiver) = mpsc::channel(); + insert_event_target_window_data(thread_msg_target, runner_shared.clone()); + raw_input::register_all_mice_and_keyboards_for_raw_input( + thread_msg_target, + Default::default(), + ); + + Ok(EventLoop { + user_event_sender, + user_event_receiver, + window_target: RootAEL { + p: ActiveEventLoop { thread_id, thread_msg_target, runner_shared }, + _marker: PhantomData, + }, + msg_hook: attributes.msg_hook.take(), + high_resolution_timer: None, + }) + } + + pub fn window_target(&self) -> &RootAEL { + &self.window_target + } + + pub fn run(mut self, event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &RootAEL), + { + self.run_on_demand(event_handler) + } + + pub fn run_on_demand(&mut self, mut event_handler: F) -> Result<(), EventLoopError> + where + F: FnMut(Event, &RootAEL), + { + { + let runner = &self.window_target.p.runner_shared; + + let event_loop_windows_ref = &self.window_target; + let user_event_receiver = &self.user_event_receiver; + // # Safety + // We make sure to call runner.clear_event_handler() before + // returning + unsafe { + runner.set_event_handler(move |event| { + // the shared `EventLoopRunner` is not parameterized + // `EventLoopProxy::send_event()` calls `PostMessage` + // to wakeup and dispatch a placeholder `UserEvent`, + // when we received the placeholder event here, the + // real UserEvent(T) should already be put in the + // mpsc channel and ready to be pulled + let event = match event.map_nonuser_event() { + Ok(non_user_event) => non_user_event, + Err(_user_event_placeholder) => Event::UserEvent( + user_event_receiver + .try_recv() + .expect("user event signaled but not received"), + ), + }; + event_handler(event, event_loop_windows_ref) + }); + } + } + + let exit_code = loop { + self.wait_for_messages(None); + // wait_for_messages calls user application before and after waiting + // so it may have decided to exit. + if let Some(code) = self.exit_code() { + break code; + } + + self.dispatch_peeked_messages(); + + if let Some(code) = self.exit_code() { + break code; + } + }; + + let runner = &self.window_target.p.runner_shared; + runner.loop_destroyed(); + + // # Safety + // We assume that this will effectively call `runner.clear_event_handler()` + // to meet the safety requirements for calling `runner.set_event_handler()` above. + runner.reset_runner(); + + if exit_code == 0 { + Ok(()) + } else { + Err(EventLoopError::ExitFailure(exit_code)) + } + } + + pub fn pump_events(&mut self, timeout: Option, mut event_handler: F) -> PumpStatus + where + F: FnMut(Event, &RootAEL), + { + { + let runner = &self.window_target.p.runner_shared; + let event_loop_windows_ref = &self.window_target; + let user_event_receiver = &self.user_event_receiver; + + // # Safety + // We make sure to call runner.clear_event_handler() before + // returning + // + // Note: we're currently assuming nothing can panic and unwind + // to leave the runner in an unsound state with an associated + // event handler. + unsafe { + runner.set_event_handler(move |event| { + let event = match event.map_nonuser_event() { + Ok(non_user_event) => non_user_event, + Err(_user_event_placeholder) => Event::UserEvent( + user_event_receiver + .recv() + .expect("user event signaled but not received"), + ), + }; + event_handler(event, event_loop_windows_ref) + }); + runner.wakeup(); + } + } + + if self.exit_code().is_none() { + self.wait_for_messages(timeout); + } + // wait_for_messages calls user application before and after waiting + // so it may have decided to exit. + if self.exit_code().is_none() { + self.dispatch_peeked_messages(); + } + + let runner = &self.window_target.p.runner_shared; + + let status = if let Some(code) = runner.exit_code() { + runner.loop_destroyed(); + + // Immediately reset the internal state for the loop to allow + // the loop to be run more than once. + runner.reset_runner(); + PumpStatus::Exit(code) + } else { + runner.prepare_wait(); + PumpStatus::Continue + }; + + // We wait until we've checked for an exit status before clearing the + // application callback, in case we need to dispatch a LoopExiting event + // + // # Safety + // This pairs up with our call to `runner.set_event_handler` and ensures + // the application's callback can't be held beyond its lifetime. + runner.clear_event_handler(); + + status + } + + /// Waits until new event messages arrive to be peeked. + /// Doesn't peek messages itself. + /// + /// Parameter timeout is optional. This method would wait for the smaller timeout + /// between the argument and a timeout from control flow. + fn wait_for_messages(&mut self, timeout: Option) { + let runner = &self.window_target.p.runner_shared; + + // We aim to be consistent with the MacOS backend which has a RunLoop + // observer that will dispatch AboutToWait when about to wait for + // events, and NewEvents after the RunLoop wakes up. + // + // We emulate similar behaviour by treating `MsgWaitForMultipleObjectsEx` as our wait + // point and wake up point (when it returns) and we drain all other + // pending messages via `PeekMessage` until we come back to "wait" via + // `MsgWaitForMultipleObjectsEx`. + // + runner.prepare_wait(); + wait_for_messages_impl(&mut self.high_resolution_timer, runner.control_flow(), timeout); + // Before we potentially exit, make sure to consistently emit an event for the wake up + runner.wakeup(); + } + + /// Dispatch all queued messages via `PeekMessageW` + fn dispatch_peeked_messages(&mut self) { + let runner = &self.window_target.p.runner_shared; + + // We generally want to continue dispatching all pending messages + // but we also allow dispatching to be interrupted as a means to + // ensure the `pump_events` won't indefinitely block an external + // event loop if there are too many pending events. This interrupt + // flag will be set after dispatching `RedrawRequested` events. + runner.interrupt_msg_dispatch.set(false); + + // # Safety + // The Windows API has no documented requirement for bitwise + // initializing a `MSG` struct (it can be uninitialized memory for the C + // API) and there's no API to construct or initialize a `MSG`. This + // is the simplest way avoid uninitialized memory in Rust + let mut msg: MSG = unsafe { mem::zeroed() }; + + loop { + unsafe { + if PeekMessageW(&mut msg, 0, 0, 0, PM_REMOVE) == false.into() { + break; + } + + let handled = if let Some(callback) = self.msg_hook.as_deref_mut() { + callback(&mut msg as *mut _ as *mut _) + } else { + false + }; + if !handled { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + + if let Err(payload) = runner.take_panic_error() { + runner.reset_runner(); + panic::resume_unwind(payload); + } + + if let Some(_code) = runner.exit_code() { + break; + } + + if runner.interrupt_msg_dispatch.get() { + break; + } + } + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { + target_window: self.window_target.p.thread_msg_target, + event_send: self.user_event_sender.clone(), + } + } + + fn exit_code(&self) -> Option { + self.window_target.p.exit_code() + } +} + +impl ActiveEventLoop { + #[inline(always)] + pub(crate) fn create_thread_executor(&self) -> EventLoopThreadExecutor { + EventLoopThreadExecutor { thread_id: self.thread_id, target_window: self.thread_msg_target } + } + + pub fn create_custom_cursor(&self, source: CustomCursorSource) -> RootCustomCursor { + let inner = match WinCursor::new(&source.inner.0) { + Ok(cursor) => cursor, + Err(err) => { + tracing::warn!("Failed to create custom cursor: {err}"); + WinCursor::Failed + }, + }; + + RootCustomCursor { inner } + } + + // TODO: Investigate opportunities for caching + pub fn available_monitors(&self) -> VecDeque { + monitor::available_monitors() + } + + pub fn primary_monitor(&self) -> Option { + let monitor = monitor::primary_monitor(); + Some(monitor) + } + + #[cfg(feature = "rwh_05")] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Windows(rwh_05::WindowsDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Windows(rwh_06::WindowsDisplayHandle::new())) + } + + pub fn listen_device_events(&self, allowed: DeviceEvents) { + raw_input::register_all_mice_and_keyboards_for_raw_input(self.thread_msg_target, allowed); + } + + pub fn system_theme(&self) -> Option { + Some(if super::dark_mode::should_use_dark_mode() { Theme::Dark } else { Theme::Light }) + } + + pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) { + self.runner_shared.set_control_flow(control_flow) + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.runner_shared.control_flow() + } + + pub(crate) fn exit(&self) { + self.runner_shared.set_exit_code(0) + } + + pub(crate) fn exiting(&self) -> bool { + self.runner_shared.exit_code().is_some() + } + + pub(crate) fn clear_exit(&self) { + self.runner_shared.clear_exit(); + } + + pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle { + OwnedDisplayHandle + } + + fn exit_code(&self) -> Option { + self.runner_shared.exit_code() + } +} + +#[derive(Clone)] +pub(crate) struct OwnedDisplayHandle; + +impl OwnedDisplayHandle { + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::WindowsDisplayHandle::empty().into() + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::WindowsDisplayHandle::new().into()) + } +} + +/// Returns the id of the main thread. +/// +/// Windows has no real API to check if the current executing thread is the "main thread", unlike +/// macOS. +/// +/// Windows will let us look up the current thread's id, but there's no API that lets us check what +/// the id of the main thread is. We would somehow need to get the main thread's id before a +/// developer could spin off any other threads inside of the main entrypoint in order to emulate the +/// capabilities of other platforms. +/// +/// We can get the id of the main thread by using CRT initialization. CRT initialization can be used +/// to setup global state within a program. The OS will call a list of function pointers which +/// assign values to a static variable. To have get a hold of the main thread id, we need to place +/// our function pointer inside of the `.CRT$XCU` section so it is called before the main +/// entrypoint. +/// +/// Full details of CRT initialization can be found here: +/// +fn main_thread_id() -> u32 { + static mut MAIN_THREAD_ID: u32 = 0; + + // Function pointer used in CRT initialization section to set the above static field's value. + + // Mark as used so this is not removable. + #[used] + #[allow(non_upper_case_globals)] + // Place the function pointer inside of CRT initialization section so it is loaded before + // main entrypoint. + // + // See: https://doc.rust-lang.org/stable/reference/abi.html#the-link_section-attribute + #[link_section = ".CRT$XCU"] + static INIT_MAIN_THREAD_ID: unsafe extern "C" fn() = { + unsafe extern "C" fn initer() { + unsafe { + MAIN_THREAD_ID = GetCurrentThreadId(); + } + } + initer + }; + + unsafe { MAIN_THREAD_ID } +} + +/// Returns the minimum `Option`, taking into account that `None` +/// equates to an infinite timeout, not a zero timeout (so can't just use +/// `Option::min`) +fn min_timeout(a: Option, b: Option) -> Option { + a.map_or(b, |a_timeout| b.map_or(Some(a_timeout), |b_timeout| Some(a_timeout.min(b_timeout)))) +} + +// Implementation taken from https://github.com/rust-lang/rust/blob/db5476571d9b27c862b95c1e64764b0ac8980e23/src/libstd/sys/windows/mod.rs +fn dur2timeout(dur: Duration) -> u32 { + // Note that a duration is a (u64, u32) (seconds, nanoseconds) pair, and the + // timeouts in windows APIs are typically u32 milliseconds. To translate, we + // have two pieces to take care of: + // + // * Nanosecond precision is rounded up + // * Greater than u32::MAX milliseconds (50 days) is rounded up to INFINITE (never time out). + dur.as_secs() + .checked_mul(1000) + .and_then(|ms| ms.checked_add((dur.subsec_nanos() as u64) / 1_000_000)) + .and_then( + |ms| { + if dur.subsec_nanos() % 1_000_000 > 0 { + ms.checked_add(1) + } else { + Some(ms) + } + }, + ) + .map(|ms| if ms > u32::MAX as u64 { INFINITE } else { ms as u32 }) + .unwrap_or(INFINITE) +} + +impl Drop for EventLoop { + fn drop(&mut self) { + unsafe { + DestroyWindow(self.window_target.p.thread_msg_target); + } + } +} + +/// Set upper limit for waiting time to avoid overflows. +/// I chose 50 days as a limit because it is used in dur2timeout. +const FIFTY_DAYS: Duration = Duration::from_secs(50_u64 * 24 * 60 * 60); +/// Waitable timers use 100 ns intervals to indicate due time. +/// +/// And there is no point waiting using other ways for such small timings +/// because they are even less precise (can overshoot by few ms). +const MIN_WAIT: Duration = Duration::from_nanos(100); + +fn create_high_resolution_timer() -> Option { + unsafe { + let handle: HANDLE = CreateWaitableTimerExW( + ptr::null(), + ptr::null(), + CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, + TIMER_ALL_ACCESS, + ); + // CREATE_WAITABLE_TIMER_HIGH_RESOLUTION is supported only after + // Win10 1803 but it is already default option for rustc + // (std uses it to implement `std::thread::sleep`). + if handle == 0 { + None + } else { + Some(OwnedHandle::from_raw_handle(handle as *mut c_void)) + } + } +} + +/// This function should not return error if parameters are valid +/// but there is no guarantee about that at MSDN docs +/// so we return result of GetLastError if fail. +/// +/// ## Safety +/// +/// timer must be a valid timer handle created by [create_high_resolution_timer]. +/// timeout divided by 100 nanoseconds must be more than 0 and less than i64::MAX. +unsafe fn set_high_resolution_timer(timer: RawHandle, timeout: Duration) -> Result<(), u32> { + const INTERVAL_NS: u32 = MIN_WAIT.subsec_nanos(); + const INTERVALS_IN_SEC: u64 = (Duration::from_secs(1).as_nanos() / INTERVAL_NS as u128) as u64; + let intervals_to_wait: u64 = + timeout.as_secs() * INTERVALS_IN_SEC + u64::from(timeout.subsec_nanos() / INTERVAL_NS); + debug_assert!(intervals_to_wait < i64::MAX as u64, "Must be called with smaller duration",); + // Use negative time to indicate relative time. + let due_time: i64 = -(intervals_to_wait as i64); + unsafe { + let set_result = SetWaitableTimer(timer as HANDLE, &due_time, 0, None, ptr::null(), FALSE); + if set_result != FALSE { + Ok(()) + } else { + Err(GetLastError()) + } + } +} + +/// Implementation detail of [EventLoop::wait_for_messages]. +/// +/// Does actual system-level waiting and doesn't process any messages itself, +/// including winits internal notifications about waiting and new messages arrival. +fn wait_for_messages_impl( + high_resolution_timer: &mut Option, + control_flow: ControlFlow, + timeout: Option, +) { + let timeout = { + let control_flow_timeout = match control_flow { + ControlFlow::Wait => None, + ControlFlow::Poll => Some(Duration::ZERO), + ControlFlow::WaitUntil(wait_deadline) => { + let start = Instant::now(); + Some(wait_deadline.saturating_duration_since(start)) + }, + }; + let timeout = min_timeout(timeout, control_flow_timeout); + if timeout == Some(Duration::ZERO) { + // Do not wait if we don't have time. + return; + } + // Now we decided to wait so need to do some clamping + // to avoid problems with overflow and calling WinAPI with invalid parameters. + timeout + .map(|t| t.min(FIFTY_DAYS)) + // If timeout is less than minimally supported by Windows, + // increase it to that minimum. Who want less than microsecond delays anyway? + .map(|t| t.max(MIN_WAIT)) + }; + + if timeout.is_some() && high_resolution_timer.is_none() { + *high_resolution_timer = create_high_resolution_timer(); + } + + let high_resolution_timer: Option = + high_resolution_timer.as_ref().map(OwnedHandle::as_raw_handle); + + let use_timer: bool; + if let (Some(handle), Some(timeout)) = (high_resolution_timer, timeout) { + let res = unsafe { + // Safety: handle can be Some only if we succeeded in creating high resolution + // timer. We properly clamped timeout so it can be used as argument + // to timer. + set_high_resolution_timer(handle, timeout) + }; + if let Err(error_code) = res { + // We successfully got timer but failed to set it? + // Should be some bug in our code. + tracing::trace!("Failed to set high resolution timer: last error {}", error_code); + use_timer = false; + } else { + use_timer = true; + } + } else { + use_timer = false; + } + + unsafe { + // Either: + // 1. User wants to wait indefinitely if timeout is not set. + // 2. We failed to get and set high resolution timer and we need something instead of it. + let wait_duration_ms = timeout.map(dur2timeout).unwrap_or(INFINITE); + + let (num_handles, raw_handles) = + if use_timer { (1, [high_resolution_timer.unwrap()]) } else { (0, [ptr::null_mut()]) }; + + // We must use `QS_ALLINPUT` to wake on accessibility messages. + let result = MsgWaitForMultipleObjectsEx( + num_handles, + raw_handles.as_ptr() as *const _, + wait_duration_ms, + QS_ALLINPUT, + MWMO_INPUTAVAILABLE, + ); + if result == WAIT_FAILED { + // Well, nothing smart to do in such case. + // Treat it as spurious wake up. + tracing::warn!("Failed to MsgWaitForMultipleObjectsEx: error code {}", GetLastError(),); + } + } +} + +pub(crate) struct EventLoopThreadExecutor { + thread_id: u32, + target_window: HWND, +} + +unsafe impl Send for EventLoopThreadExecutor {} +unsafe impl Sync for EventLoopThreadExecutor {} + +impl EventLoopThreadExecutor { + /// Check to see if we're in the parent event loop's thread. + pub(super) fn in_event_loop_thread(&self) -> bool { + let cur_thread_id = unsafe { GetCurrentThreadId() }; + self.thread_id == cur_thread_id + } + + /// Executes a function in the event loop thread. If we're already in the event loop thread, + /// we just call the function directly. + /// + /// The `Inserted` can be used to inject a `WindowState` for the callback to use. The state is + /// removed automatically if the callback receives a `WM_CLOSE` message for the window. + /// + /// Note that if you are using this to change some property of a window and updating + /// `WindowState` then you should call this within the lock of `WindowState`. Otherwise the + /// events may be sent to the other thread in different order to the one in which you set + /// `WindowState`, leaving them out of sync. + /// + /// Note that we use a FnMut instead of a FnOnce because we're too lazy to create an equivalent + /// to the unstable FnBox. + pub(super) fn execute_in_thread(&self, mut function: F) + where + F: FnMut() + Send + 'static, + { + unsafe { + if self.in_event_loop_thread() { + function(); + } else { + // We double-box because the first box is a fat pointer. + let boxed2: ThreadExecFn = Box::new(Box::new(function)); + + let raw = Box::into_raw(boxed2); + + let res = PostMessageW(self.target_window, EXEC_MSG_ID.get(), raw as usize, 0); + assert!(res != false.into(), "PostMessage failed; is the messages queue full?"); + } + } + } +} + +type ThreadExecFn = Box>; + +pub struct EventLoopProxy { + target_window: HWND, + event_send: Sender, +} +unsafe impl Send for EventLoopProxy {} + +impl Clone for EventLoopProxy { + fn clone(&self) -> Self { + Self { target_window: self.target_window, event_send: self.event_send.clone() } + } +} + +impl EventLoopProxy { + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.event_send + .send(event) + .map(|result| { + unsafe { PostMessageW(self.target_window, USER_EVENT_MSG_ID.get(), 0, 0) }; + result + }) + .map_err(|e| EventLoopClosed(e.0)) + } +} + +/// A lazily-initialized window message ID. +pub struct LazyMessageId { + /// The ID. + id: AtomicU32, + + /// The name of the message. + name: &'static str, +} + +/// An invalid custom window ID. +const INVALID_ID: u32 = 0x0; + +impl LazyMessageId { + /// Create a new `LazyId`. + const fn new(name: &'static str) -> Self { + Self { id: AtomicU32::new(INVALID_ID), name } + } + + /// Get the message ID. + pub fn get(&self) -> u32 { + // Load the ID. + let id = self.id.load(Ordering::Relaxed); + + if id != INVALID_ID { + return id; + } + + // Register the message. + // SAFETY: We are sure that the pointer is a valid C string ending with '\0'. + assert!(self.name.ends_with('\0')); + let new_id = unsafe { RegisterWindowMessageA(self.name.as_ptr()) }; + + assert_ne!( + new_id, + 0, + "RegisterWindowMessageA returned zero for '{}': {}", + self.name, + std::io::Error::last_os_error() + ); + + // Store the new ID. Since `RegisterWindowMessageA` returns the same value for any given + // string, the target value will always either be a). `INVALID_ID` or b). the + // correct ID. Therefore a compare-and-swap operation here (or really any + // consideration) is never necessary. + self.id.store(new_id, Ordering::Relaxed); + + new_id + } +} + +// Message sent by the `EventLoopProxy` when we want to wake up the thread. +// WPARAM and LPARAM are unused. +static USER_EVENT_MSG_ID: LazyMessageId = LazyMessageId::new("Winit::WakeupMsg\0"); +// Message sent when we want to execute a closure in the thread. +// WPARAM contains a Box> that must be retrieved with `Box::from_raw`, +// and LPARAM is unused. +static EXEC_MSG_ID: LazyMessageId = LazyMessageId::new("Winit::ExecMsg\0"); +// Message sent by a `Window` when it wants to be destroyed by the main thread. +// WPARAM and LPARAM are unused. +pub(crate) static DESTROY_MSG_ID: LazyMessageId = LazyMessageId::new("Winit::DestroyMsg\0"); +// WPARAM is a bool specifying the `WindowFlags::MARKER_RETAIN_STATE_ON_SIZE` flag. See the +// documentation in the `window_state` module for more information. +pub(crate) static SET_RETAIN_STATE_ON_SIZE_MSG_ID: LazyMessageId = + LazyMessageId::new("Winit::SetRetainMaximized\0"); +static THREAD_EVENT_TARGET_WINDOW_CLASS: Lazy> = + Lazy::new(|| util::encode_wide("Winit Thread Event Target")); +/// When the taskbar is created, it registers a message with the "TaskbarCreated" string and then +/// broadcasts this message to all top-level windows +pub(crate) static TASKBAR_CREATED: LazyMessageId = LazyMessageId::new("TaskbarCreated\0"); + +fn create_event_target_window() -> HWND { + use windows_sys::Win32::UI::WindowsAndMessaging::{CS_HREDRAW, CS_VREDRAW}; + unsafe { + let class = WNDCLASSEXW { + cbSize: mem::size_of::() as u32, + style: CS_HREDRAW | CS_VREDRAW, + lpfnWndProc: Some(thread_event_target_callback), + cbClsExtra: 0, + cbWndExtra: 0, + hInstance: util::get_instance_handle(), + hIcon: 0, + hCursor: 0, // must be null in order for cursor state to work properly + hbrBackground: 0, + lpszMenuName: ptr::null(), + lpszClassName: THREAD_EVENT_TARGET_WINDOW_CLASS.as_ptr(), + hIconSm: 0, + }; + + RegisterClassExW(&class); + } + + unsafe { + // WS_EX_TOOLWINDOW prevents this window from ever showing up in the taskbar, which + // we want to avoid. If you remove this style, this window won't show up in the + // taskbar *initially*, but it can show up at some later point. This can sometimes + // happen on its own after several hours have passed, although this has proven + // difficult to reproduce. Alternatively, it can be manually triggered by killing + // `explorer.exe` and then starting the process back up. + // It is unclear why the bug is triggered by waiting for several hours. + let window = CreateWindowExW( + WS_EX_NOACTIVATE | WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_TOOLWINDOW, + THREAD_EVENT_TARGET_WINDOW_CLASS.as_ptr(), + ptr::null(), + WS_OVERLAPPED, + 0, + 0, + 0, + 0, + 0, + 0, + util::get_instance_handle(), + ptr::null(), + ); + + super::set_window_long( + window, + GWL_STYLE, + // The window technically has to be visible to receive WM_PAINT messages (which are + // used for delivering events during resizes), but it isn't displayed to + // the user because of the LAYERED style. + (WS_VISIBLE | WS_POPUP) as isize, + ); + window + } +} + +fn insert_event_target_window_data( + thread_msg_target: HWND, + event_loop_runner: EventLoopRunnerShared, +) { + let userdata = ThreadMsgTargetData { event_loop_runner }; + let input_ptr = Box::into_raw(Box::new(userdata)); + + unsafe { super::set_window_long(thread_msg_target, GWL_USERDATA, input_ptr as isize) }; +} + +/// Capture mouse input, allowing `window` to receive mouse events when the cursor is outside of +/// the window. +unsafe fn capture_mouse(window: HWND, window_state: &mut WindowState) { + window_state.mouse.capture_count += 1; + unsafe { SetCapture(window) }; +} + +/// Release mouse input, stopping windows on this thread from receiving mouse input when the cursor +/// is outside the window. +unsafe fn release_mouse(mut window_state: MutexGuard<'_, WindowState>) { + window_state.mouse.capture_count = window_state.mouse.capture_count.saturating_sub(1); + if window_state.mouse.capture_count == 0 { + // ReleaseCapture() causes a WM_CAPTURECHANGED where we lock the window_state. + drop(window_state); + unsafe { ReleaseCapture() }; + } +} + +fn normalize_pointer_pressure(pressure: u32) -> Option { + match pressure { + 1..=1024 => Some(Force::Normalized(pressure as f64 / 1024.0)), + _ => None, + } +} + +/// Emit a `ModifiersChanged` event whenever modifiers have changed. +/// Returns the current modifier state +fn update_modifiers(window: HWND, userdata: &WindowData) { + use crate::event::WindowEvent::ModifiersChanged; + + let modifiers = { + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + layouts.get_agnostic_mods() + }; + + let mut window_state = userdata.window_state.lock().unwrap(); + if window_state.modifiers_state != modifiers { + window_state.modifiers_state = modifiers; + + // Drop lock + drop(window_state); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ModifiersChanged(modifiers.into()), + }); + } +} + +unsafe fn gain_active_focus(window: HWND, userdata: &WindowData) { + use crate::event::WindowEvent::Focused; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: Focused(true), + }); +} + +unsafe fn lose_active_focus(window: HWND, userdata: &WindowData) { + use crate::event::WindowEvent::{Focused, ModifiersChanged}; + + userdata.window_state_lock().modifiers_state = ModifiersState::empty(); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ModifiersChanged(ModifiersState::empty().into()), + }); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: Focused(false), + }); +} + +/// Any window whose callback is configured to this function will have its events propagated +/// through the events loop of the thread the window was created in. +// This is the callback that is called by `DispatchMessage` in the events loop. +// +// Returning 0 tells the Win32 API that the message has been processed. +// FIXME: detect WM_DWMCOMPOSITIONCHANGED and call DwmEnableBlurBehindWindow if necessary +pub(super) unsafe extern "system" fn public_window_callback( + window: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + let userdata = unsafe { super::get_window_long(window, GWL_USERDATA) }; + + let userdata_ptr = match (userdata, msg) { + (0, WM_NCCREATE) => { + let createstruct = unsafe { &mut *(lparam as *mut CREATESTRUCTW) }; + let initdata = unsafe { &mut *(createstruct.lpCreateParams as *mut InitData<'_>) }; + + let result = match unsafe { initdata.on_nccreate(window) } { + Some(userdata) => unsafe { + super::set_window_long(window, GWL_USERDATA, userdata as _); + DefWindowProcW(window, msg, wparam, lparam) + }, + None => -1, // failed to create the window + }; + + return result; + }, + // Getting here should quite frankly be impossible, + // but we'll make window creation fail here just in case. + (0, WM_CREATE) => return -1, + (_, WM_CREATE) => unsafe { + let createstruct = &mut *(lparam as *mut CREATESTRUCTW); + let initdata = createstruct.lpCreateParams; + let initdata = &mut *(initdata as *mut InitData<'_>); + + initdata.on_create(); + return DefWindowProcW(window, msg, wparam, lparam); + }, + (0, _) => return unsafe { DefWindowProcW(window, msg, wparam, lparam) }, + _ => userdata as *mut WindowData, + }; + + let (result, userdata_removed, recurse_depth) = { + let userdata = unsafe { &*(userdata_ptr) }; + + userdata.recurse_depth.set(userdata.recurse_depth.get() + 1); + + let result = unsafe { public_window_callback_inner(window, msg, wparam, lparam, userdata) }; + + let userdata_removed = userdata.userdata_removed.get(); + let recurse_depth = userdata.recurse_depth.get() - 1; + userdata.recurse_depth.set(recurse_depth); + + (result, userdata_removed, recurse_depth) + }; + + if userdata_removed && recurse_depth == 0 { + drop(unsafe { Box::from_raw(userdata_ptr) }); + } + + result +} + +unsafe fn public_window_callback_inner( + window: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + userdata: &WindowData, +) -> LRESULT { + let mut result = ProcResult::DefWindowProc(wparam); + + // Send new modifiers before sending key events. + let mods_changed_callback = || match msg { + WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP => { + update_modifiers(window, userdata); + result = ProcResult::Value(0); + }, + _ => (), + }; + userdata + .event_loop_runner + .catch_unwind(mods_changed_callback) + .unwrap_or_else(|| result = ProcResult::Value(-1)); + + let keyboard_callback = || { + use crate::event::WindowEvent::KeyboardInput; + let events = + userdata.key_event_builder.process_message(window, msg, wparam, lparam, &mut result); + for event in events { + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: KeyboardInput { + device_id: DEVICE_ID, + event: event.event, + is_synthetic: event.is_synthetic, + }, + }); + } + }; + userdata + .event_loop_runner + .catch_unwind(keyboard_callback) + .unwrap_or_else(|| result = ProcResult::Value(-1)); + + // I decided to bind the closure to `callback` and pass it to catch_unwind rather than passing + // the closure to catch_unwind directly so that the match body indentation wouldn't change and + // the git blame and history would be preserved. + let callback = || match msg { + WM_NCCALCSIZE => { + let window_flags = userdata.window_state_lock().window_flags; + if wparam == 0 || window_flags.contains(WindowFlags::MARKER_DECORATIONS) { + result = ProcResult::DefWindowProc(wparam); + return; + } + + let params = unsafe { &mut *(lparam as *mut NCCALCSIZE_PARAMS) }; + + if util::is_maximized(window) { + // Limit the window size when maximized to the current monitor. + // Otherwise it would include the non-existent decorations. + // + // Use `MonitorFromRect` instead of `MonitorFromWindow` to select + // the correct monitor here. + // See https://github.com/MicrosoftEdge/WebView2Feedback/issues/2549 + let monitor = unsafe { MonitorFromRect(¶ms.rgrc[0], MONITOR_DEFAULTTONULL) }; + if let Ok(monitor_info) = monitor::get_monitor_info(monitor) { + params.rgrc[0] = monitor_info.monitorInfo.rcWork; + } + } else if window_flags.contains(WindowFlags::MARKER_UNDECORATED_SHADOW) { + // Extend the client area to cover the whole non-client area. + // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize#remarks + // + // HACK(msiglreith): To add the drop shadow we slightly tweak the non-client area. + // This leads to a small black 1px border on the top. Adding a margin manually + // on all 4 borders would result in the caption getting drawn by the DWM. + // + // Another option would be to allow the DWM to paint inside the client area. + // Unfortunately this results in janky resize behavior, where the compositor is + // ahead of the window surface. Currently, there seems no option to achieve this + // with the Windows API. + params.rgrc[0].top += 1; + params.rgrc[0].bottom += 1; + } + + result = ProcResult::Value(0); + }, + + WM_ENTERSIZEMOVE => { + userdata + .window_state_lock() + .set_window_flags_in_place(|f| f.insert(WindowFlags::MARKER_IN_SIZE_MOVE)); + result = ProcResult::Value(0); + }, + + WM_EXITSIZEMOVE => { + let mut state = userdata.window_state_lock(); + if state.dragging { + state.dragging = false; + unsafe { PostMessageW(window, WM_LBUTTONUP, 0, lparam) }; + } + + state.set_window_flags_in_place(|f| f.remove(WindowFlags::MARKER_IN_SIZE_MOVE)); + result = ProcResult::Value(0); + }, + + WM_NCLBUTTONDOWN => { + if wparam == HTCAPTION as _ { + // Prevent the user event loop from pausing when left clicking the title bar. + // + // When the user interacts with the title bar, Windows enters the modal event + // loop. Currently, a left click causes a pause for about 500ms. Sending a dummy + // mouse-move event seems to cancel the modal loop early, preventing the pause. + // The application will never see this dummy event. + // + // The mouse coordinates are encoded into the lparam value, however the WM_MOUSEMOVE + // event is not using the same coordinate system of the WM_NCLBUTTONDOWN event. + // One uses client-area coordinates and the other is screen-coordinates. In any + // case, passing the lparam as-is with the dummy event does not seem the cancel + // the modal loop. + // + // However, passing in a value of 0 has been observed to always cancel the pause. + // + // Other notes: + // + // For some unknown reason, the cursor will blink when clicking the title bar. + // Cancelling the modal loop early causes the blink to happen *immediately*. + // Otherwise, the blank happens *after* the pause. + // + // When right-click the title bar, the system window menu is presented to the user, + // and the modal event loop begins. This dummy event does *not* prevent the freeze + // in the main event loop caused by that popup menu. + let lparam = 0; + unsafe { PostMessageW(window, WM_MOUSEMOVE, 0, lparam) }; + } + result = ProcResult::DefWindowProc(wparam); + }, + + WM_CLOSE => { + use crate::event::WindowEvent::CloseRequested; + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CloseRequested, + }); + result = ProcResult::Value(0); + }, + + WM_DESTROY => { + use crate::event::WindowEvent::Destroyed; + unsafe { RevokeDragDrop(window) }; + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: Destroyed, + }); + result = ProcResult::Value(0); + }, + + WM_NCDESTROY => { + unsafe { super::set_window_long(window, GWL_USERDATA, 0) }; + userdata.userdata_removed.set(true); + result = ProcResult::Value(0); + }, + + WM_PAINT => { + userdata.window_state_lock().redraw_requested = + userdata.event_loop_runner.should_buffer(); + + // We'll buffer only in response to `UpdateWindow`, if win32 decides to redraw the + // window outside the normal flow of the event loop. This way mark event as handled + // and request a normal redraw with `RedrawWindow`. + if !userdata.event_loop_runner.should_buffer() { + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::RedrawRequested, + }); + } + + // NOTE: calling `RedrawWindow` during `WM_PAINT` does nothing, since to mark + // `WM_PAINT` as handled we should call the `DefWindowProcW`. Call it and check whether + // user asked for redraw during `RedrawRequested` event handling and request it again + // after marking `WM_PAINT` as handled. + result = ProcResult::Value(unsafe { DefWindowProcW(window, msg, wparam, lparam) }); + if std::mem::take(&mut userdata.window_state_lock().redraw_requested) { + unsafe { RedrawWindow(window, ptr::null(), 0, RDW_INTERNALPAINT) }; + } + }, + WM_WINDOWPOSCHANGING => { + let mut window_state = userdata.window_state_lock(); + if let Some(ref mut fullscreen) = window_state.fullscreen { + let window_pos = unsafe { &mut *(lparam as *mut WINDOWPOS) }; + let new_rect = RECT { + left: window_pos.x, + top: window_pos.y, + right: window_pos.x + window_pos.cx, + bottom: window_pos.y + window_pos.cy, + }; + + const NOMOVE_OR_NOSIZE: u32 = SWP_NOMOVE | SWP_NOSIZE; + + let new_rect = if window_pos.flags & NOMOVE_OR_NOSIZE != 0 { + let cur_rect = util::WindowArea::Outer.get_rect(window).expect( + "Unexpected GetWindowRect failure; please report this error to \ + rust-windowing/winit", + ); + + match window_pos.flags & NOMOVE_OR_NOSIZE { + NOMOVE_OR_NOSIZE => None, + + SWP_NOMOVE => Some(RECT { + left: cur_rect.left, + top: cur_rect.top, + right: cur_rect.left + window_pos.cx, + bottom: cur_rect.top + window_pos.cy, + }), + + SWP_NOSIZE => Some(RECT { + left: window_pos.x, + top: window_pos.y, + right: window_pos.x - cur_rect.left + cur_rect.right, + bottom: window_pos.y - cur_rect.top + cur_rect.bottom, + }), + + _ => unreachable!(), + } + } else { + Some(new_rect) + }; + + if let Some(new_rect) = new_rect { + let new_monitor = unsafe { MonitorFromRect(&new_rect, MONITOR_DEFAULTTONULL) }; + match fullscreen { + Fullscreen::Borderless(ref mut fullscreen_monitor) => { + if new_monitor != 0 + && fullscreen_monitor + .as_ref() + .map(|monitor| new_monitor != monitor.hmonitor()) + .unwrap_or(true) + { + if let Ok(new_monitor_info) = monitor::get_monitor_info(new_monitor) + { + let new_monitor_rect = new_monitor_info.monitorInfo.rcMonitor; + window_pos.x = new_monitor_rect.left; + window_pos.y = new_monitor_rect.top; + window_pos.cx = new_monitor_rect.right - new_monitor_rect.left; + window_pos.cy = new_monitor_rect.bottom - new_monitor_rect.top; + } + *fullscreen_monitor = Some(MonitorHandle::new(new_monitor)); + } + }, + Fullscreen::Exclusive(ref video_mode) => { + let old_monitor = video_mode.monitor.hmonitor(); + if let Ok(old_monitor_info) = monitor::get_monitor_info(old_monitor) { + let old_monitor_rect = old_monitor_info.monitorInfo.rcMonitor; + window_pos.x = old_monitor_rect.left; + window_pos.y = old_monitor_rect.top; + window_pos.cx = old_monitor_rect.right - old_monitor_rect.left; + window_pos.cy = old_monitor_rect.bottom - old_monitor_rect.top; + } + }, + } + } + } + + result = ProcResult::Value(0); + }, + + // WM_MOVE supplies client area positions, so we send Moved here instead. + WM_WINDOWPOSCHANGED => { + use crate::event::WindowEvent::Moved; + + let windowpos = lparam as *const WINDOWPOS; + if unsafe { (*windowpos).flags & SWP_NOMOVE != SWP_NOMOVE } { + let physical_position = + unsafe { PhysicalPosition::new((*windowpos).x, (*windowpos).y) }; + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: Moved(physical_position), + }); + } + + // This is necessary for us to still get sent WM_SIZE. + result = ProcResult::DefWindowProc(wparam); + }, + + WM_SIZE => { + use crate::event::WindowEvent::Resized; + let w = super::loword(lparam as u32) as u32; + let h = super::hiword(lparam as u32) as u32; + + let physical_size = PhysicalSize::new(w, h); + let event = Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: Resized(physical_size), + }; + + { + let mut w = userdata.window_state_lock(); + // See WindowFlags::MARKER_RETAIN_STATE_ON_SIZE docs for info on why this `if` check + // exists. + if !w.window_flags().contains(WindowFlags::MARKER_RETAIN_STATE_ON_SIZE) { + let maximized = wparam == SIZE_MAXIMIZED as usize; + w.set_window_flags_in_place(|f| f.set(WindowFlags::MAXIMIZED, maximized)); + } + } + userdata.send_event(event); + result = ProcResult::Value(0); + }, + + WM_SIZING => { + /// Calculate the amount to add to round `value` to the nearest multiple of `increment`. + fn snap_to_nearest_increment_delta(value: i32, increment: i32) -> i32 { + let half_one = increment / 2; + let half_two = increment - half_one; + half_one - (value - half_two) % increment + } + + let scale_factor = userdata.window_state_lock().scale_factor; + let Some(inc) = userdata + .window_state_lock() + .resize_increments + .map(|inc| inc.to_physical(scale_factor)) + .filter(|inc| inc.width > 0 && inc.height > 0) + else { + result = ProcResult::Value(0); + return; + }; + + let side = wparam as u32; + // The desired new size of the window, decorations included. + let rect = unsafe { &mut *(lparam as *mut RECT) }; + + // We need to calculate the dimensions of the window decorations to get the true + // size of the window's contents + let adj_rect = userdata + .window_state_lock() + .window_flags + .adjust_rect(window, *rect) + .unwrap_or(*rect); + let deco_width = rect.left - adj_rect.left + adj_rect.right - rect.right; + let deco_height = rect.top - adj_rect.top + adj_rect.bottom - rect.bottom; + + let width = rect.right - rect.left - deco_width; + let height = rect.bottom - rect.top - deco_height; + + let mut width_delta = snap_to_nearest_increment_delta(width, inc.width); + let mut height_delta = snap_to_nearest_increment_delta(height, inc.height); + + // Windows won't bound check the value of `rect` after we're done here, so we + // have to check manually. If the width/height we snap to would go out of bounds, just + // set it equal to the min/max bound. + let min_size = + userdata.window_state_lock().min_size.map(|size| size.to_physical(scale_factor)); + let max_size = + userdata.window_state_lock().max_size.map(|size| size.to_physical(scale_factor)); + let final_width = width + width_delta; + let final_height = height + height_delta; + if let Some(min_size) = min_size { + if final_width < min_size.width { + width_delta += min_size.width - final_width; + } + if final_height < min_size.height { + height_delta += min_size.height - final_height; + } + } + if let Some(max_size) = max_size { + if final_width > max_size.width { + width_delta -= final_width - max_size.width; + } + if final_height > max_size.height { + height_delta -= final_height - max_size.height; + } + } + + match side { + WMSZ_LEFT | WMSZ_BOTTOMLEFT | WMSZ_TOPLEFT => { + rect.left -= width_delta; + }, + WMSZ_RIGHT | WMSZ_BOTTOMRIGHT | WMSZ_TOPRIGHT => { + rect.right += width_delta; + }, + _ => {}, + } + + match side { + WMSZ_TOP | WMSZ_TOPLEFT | WMSZ_TOPRIGHT => { + rect.top -= height_delta; + }, + WMSZ_BOTTOM | WMSZ_BOTTOMLEFT | WMSZ_BOTTOMRIGHT => { + rect.bottom += height_delta; + }, + _ => {}, + } + + result = ProcResult::DefWindowProc(wparam); + }, + + WM_MENUCHAR => { + result = ProcResult::Value((MNC_CLOSE << 16) as isize); + }, + + WM_IME_STARTCOMPOSITION => { + let ime_allowed = userdata.window_state_lock().ime_allowed; + if ime_allowed { + userdata.window_state_lock().ime_state = ImeState::Enabled; + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Enabled), + }); + } + + result = ProcResult::DefWindowProc(wparam); + }, + + WM_IME_COMPOSITION => { + let ime_allowed_and_composing = { + let w = userdata.window_state_lock(); + w.ime_allowed && w.ime_state != ImeState::Disabled + }; + // Windows Hangul IME sends WM_IME_COMPOSITION after WM_IME_ENDCOMPOSITION, so + // check whether composing. + if ime_allowed_and_composing { + let ime_context = unsafe { ImeContext::current(window) }; + + if lparam == 0 { + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + }); + } + + // Google Japanese Input and ATOK have both flags, so + // first, receive composing result if exist. + if (lparam as u32 & GCS_RESULTSTR) != 0 { + if let Some(text) = unsafe { ime_context.get_composed_text() } { + userdata.window_state_lock().ime_state = ImeState::Enabled; + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + }); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Commit(text)), + }); + } + } + + // Next, receive preedit range for next composing if exist. + if (lparam as u32 & GCS_COMPSTR) != 0 { + if let Some((text, first, last)) = + unsafe { ime_context.get_composing_text_and_cursor() } + { + userdata.window_state_lock().ime_state = ImeState::Preedit; + let cursor_range = first.map(|f| (f, last.unwrap_or(f))); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Preedit(text, cursor_range)), + }); + } + } + } + + // Not calling DefWindowProc to hide composing text drawn by IME. + result = ProcResult::Value(0); + }, + + WM_IME_ENDCOMPOSITION => { + let ime_allowed_or_composing = { + let w = userdata.window_state_lock(); + w.ime_allowed || w.ime_state != ImeState::Disabled + }; + if ime_allowed_or_composing { + if userdata.window_state_lock().ime_state == ImeState::Preedit { + // Windows Hangul IME sends WM_IME_COMPOSITION after WM_IME_ENDCOMPOSITION, so + // trying receiving composing result and commit if exists. + let ime_context = unsafe { ImeContext::current(window) }; + if let Some(text) = unsafe { ime_context.get_composed_text() } { + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Preedit(String::new(), None)), + }); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Commit(text)), + }); + } + } + + userdata.window_state_lock().ime_state = ImeState::Disabled; + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Ime(Ime::Disabled), + }); + } + + result = ProcResult::DefWindowProc(wparam); + }, + + WM_IME_SETCONTEXT => { + // IME UI visibility flags are in lparam. + let lparam = lparam & !(ISC_SHOWUICOMPOSITIONWINDOW as isize); + result = ProcResult::Value(unsafe { DefWindowProcW(window, msg, wparam, lparam) }); + }, + + // this is necessary for us to maintain minimize/restore state + WM_SYSCOMMAND => { + if wparam == SC_RESTORE as usize { + let mut w = userdata.window_state_lock(); + w.set_window_flags_in_place(|f| f.set(WindowFlags::MINIMIZED, false)); + } + if wparam == SC_MINIMIZE as usize { + let mut w = userdata.window_state_lock(); + w.set_window_flags_in_place(|f| f.set(WindowFlags::MINIMIZED, true)); + } + // Send `WindowEvent::Minimized` here if we decide to implement one + + if wparam == SC_SCREENSAVE as usize { + let window_state = userdata.window_state_lock(); + if window_state.fullscreen.is_some() { + result = ProcResult::Value(0); + return; + } + } + + result = ProcResult::DefWindowProc(wparam); + }, + + WM_MOUSEMOVE => { + use crate::event::WindowEvent::{CursorEntered, CursorLeft, CursorMoved}; + + let x = super::get_x_lparam(lparam as u32) as i32; + let y = super::get_y_lparam(lparam as u32) as i32; + let position = PhysicalPosition::new(x as f64, y as f64); + + let cursor_moved; + { + let mut w = userdata.window_state_lock(); + let mouse_was_inside_window = + w.mouse.cursor_flags().contains(CursorFlags::IN_WINDOW); + + match get_pointer_move_kind(window, mouse_was_inside_window, x, y) { + PointerMoveKind::Enter => { + w.mouse + .set_cursor_flags(window, |f| f.set(CursorFlags::IN_WINDOW, true)) + .ok(); + + drop(w); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CursorEntered { device_id: DEVICE_ID }, + }); + + // Calling TrackMouseEvent in order to receive mouse leave events. + unsafe { + TrackMouseEvent(&mut TRACKMOUSEEVENT { + cbSize: mem::size_of::() as u32, + dwFlags: TME_LEAVE, + hwndTrack: window, + dwHoverTime: HOVER_DEFAULT, + }) + }; + }, + PointerMoveKind::Leave => { + w.mouse + .set_cursor_flags(window, |f| f.set(CursorFlags::IN_WINDOW, false)) + .ok(); + + drop(w); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CursorLeft { device_id: DEVICE_ID }, + }); + }, + PointerMoveKind::None => drop(w), + } + + // handle spurious WM_MOUSEMOVE messages + // see https://devblogs.microsoft.com/oldnewthing/20031001-00/?p=42343 + // and http://debugandconquer.blogspot.com/2015/08/the-cause-of-spurious-mouse-move.html + let mut w = userdata.window_state_lock(); + cursor_moved = w.mouse.last_position != Some(position); + w.mouse.last_position = Some(position); + } + + if cursor_moved { + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CursorMoved { device_id: DEVICE_ID, position }, + }); + } + + result = ProcResult::Value(0); + }, + + WM_MOUSELEAVE => { + use crate::event::WindowEvent::CursorLeft; + { + let mut w = userdata.window_state_lock(); + w.mouse.set_cursor_flags(window, |f| f.set(CursorFlags::IN_WINDOW, false)).ok(); + } + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: CursorLeft { device_id: DEVICE_ID }, + }); + + result = ProcResult::Value(0); + }, + + WM_MOUSEWHEEL => { + use crate::event::MouseScrollDelta::LineDelta; + + let value = (wparam >> 16) as i16; + let value = value as f32 / WHEEL_DELTA as f32; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::MouseWheel { + device_id: DEVICE_ID, + delta: LineDelta(0.0, value), + phase: TouchPhase::Moved, + }, + }); + + result = ProcResult::Value(0); + }, + + WM_MOUSEHWHEEL => { + use crate::event::MouseScrollDelta::LineDelta; + + let value = (wparam >> 16) as i16; + let value = -value as f32 / WHEEL_DELTA as f32; // NOTE: inverted! See https://github.com/rust-windowing/winit/pull/2105/ + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::MouseWheel { + device_id: DEVICE_ID, + delta: LineDelta(value, 0.0), + phase: TouchPhase::Moved, + }, + }); + + result = ProcResult::Value(0); + }, + + WM_KEYDOWN | WM_SYSKEYDOWN => { + if msg == WM_SYSKEYDOWN { + result = ProcResult::DefWindowProc(wparam); + } + }, + + WM_KEYUP | WM_SYSKEYUP => { + if msg == WM_SYSKEYUP && unsafe { GetMenu(window) != 0 } { + // let Windows handle event if the window has a native menu, a modal event loop + // is started here on Alt key up. + result = ProcResult::DefWindowProc(wparam); + } + }, + + WM_LBUTTONDOWN => { + use crate::event::ElementState::Pressed; + use crate::event::MouseButton::Left; + use crate::event::WindowEvent::MouseInput; + + unsafe { capture_mouse(window, &mut userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { device_id: DEVICE_ID, state: Pressed, button: Left }, + }); + result = ProcResult::Value(0); + }, + + WM_LBUTTONUP => { + use crate::event::ElementState::Released; + use crate::event::MouseButton::Left; + use crate::event::WindowEvent::MouseInput; + + unsafe { release_mouse(userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { device_id: DEVICE_ID, state: Released, button: Left }, + }); + result = ProcResult::Value(0); + }, + + WM_RBUTTONDOWN => { + use crate::event::ElementState::Pressed; + use crate::event::MouseButton::Right; + use crate::event::WindowEvent::MouseInput; + + unsafe { capture_mouse(window, &mut userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { device_id: DEVICE_ID, state: Pressed, button: Right }, + }); + result = ProcResult::Value(0); + }, + + WM_RBUTTONUP => { + use crate::event::ElementState::Released; + use crate::event::MouseButton::Right; + use crate::event::WindowEvent::MouseInput; + + unsafe { release_mouse(userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { device_id: DEVICE_ID, state: Released, button: Right }, + }); + result = ProcResult::Value(0); + }, + + WM_MBUTTONDOWN => { + use crate::event::ElementState::Pressed; + use crate::event::MouseButton::Middle; + use crate::event::WindowEvent::MouseInput; + + unsafe { capture_mouse(window, &mut userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { device_id: DEVICE_ID, state: Pressed, button: Middle }, + }); + result = ProcResult::Value(0); + }, + + WM_MBUTTONUP => { + use crate::event::ElementState::Released; + use crate::event::MouseButton::Middle; + use crate::event::WindowEvent::MouseInput; + + unsafe { release_mouse(userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { device_id: DEVICE_ID, state: Released, button: Middle }, + }); + result = ProcResult::Value(0); + }, + + WM_XBUTTONDOWN => { + use crate::event::ElementState::Pressed; + use crate::event::MouseButton::{Back, Forward, Other}; + use crate::event::WindowEvent::MouseInput; + let xbutton = super::get_xbutton_wparam(wparam as u32); + + unsafe { capture_mouse(window, &mut userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { + device_id: DEVICE_ID, + state: Pressed, + button: match xbutton { + 1 => Back, + 2 => Forward, + _ => Other(xbutton), + }, + }, + }); + result = ProcResult::Value(0); + }, + + WM_XBUTTONUP => { + use crate::event::ElementState::Released; + use crate::event::MouseButton::{Back, Forward, Other}; + use crate::event::WindowEvent::MouseInput; + let xbutton = super::get_xbutton_wparam(wparam as u32); + + unsafe { release_mouse(userdata.window_state_lock()) }; + + update_modifiers(window, userdata); + + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: MouseInput { + device_id: DEVICE_ID, + state: Released, + button: match xbutton { + 1 => Back, + 2 => Forward, + _ => Other(xbutton), + }, + }, + }); + result = ProcResult::Value(0); + }, + + WM_CAPTURECHANGED => { + // lparam here is a handle to the window which is gaining mouse capture. + // If it is the same as our window, then we're essentially retaining the capture. This + // can happen if `SetCapture` is called on our window when it already has the mouse + // capture. + if lparam != window { + userdata.window_state_lock().mouse.capture_count = 0; + } + result = ProcResult::Value(0); + }, + + WM_TOUCH => { + let pcount = super::loword(wparam as u32) as usize; + let mut inputs = Vec::with_capacity(pcount); + let htouch = lparam; + if unsafe { + GetTouchInputInfo( + htouch, + pcount as u32, + inputs.as_mut_ptr(), + mem::size_of::() as i32, + ) > 0 + } { + unsafe { inputs.set_len(pcount) }; + for input in &inputs { + let mut location = POINT { x: input.x / 100, y: input.y / 100 }; + + if unsafe { ScreenToClient(window, &mut location) } == false.into() { + continue; + } + + let x = location.x as f64 + (input.x % 100) as f64 / 100f64; + let y = location.y as f64 + (input.y % 100) as f64 / 100f64; + let location = PhysicalPosition::new(x, y); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Touch(Touch { + phase: if util::has_flag(input.dwFlags, TOUCHEVENTF_DOWN) { + TouchPhase::Started + } else if util::has_flag(input.dwFlags, TOUCHEVENTF_UP) { + TouchPhase::Ended + } else if util::has_flag(input.dwFlags, TOUCHEVENTF_MOVE) { + TouchPhase::Moved + } else { + continue; + }, + location, + force: None, // WM_TOUCH doesn't support pressure information + id: input.dwID as u64, + device_id: DEVICE_ID, + }), + }); + } + } + unsafe { CloseTouchInputHandle(htouch) }; + result = ProcResult::Value(0); + }, + + WM_POINTERDOWN | WM_POINTERUPDATE | WM_POINTERUP => { + if let ( + Some(GetPointerFrameInfoHistory), + Some(SkipPointerFrameMessages), + Some(GetPointerDeviceRects), + ) = ( + *util::GET_POINTER_FRAME_INFO_HISTORY, + *util::SKIP_POINTER_FRAME_MESSAGES, + *util::GET_POINTER_DEVICE_RECTS, + ) { + let pointer_id = super::loword(wparam as u32) as u32; + let mut entries_count = 0u32; + let mut pointers_count = 0u32; + if unsafe { + GetPointerFrameInfoHistory( + pointer_id, + &mut entries_count, + &mut pointers_count, + ptr::null_mut(), + ) + } == false.into() + { + result = ProcResult::Value(0); + return; + } + + let pointer_info_count = (entries_count * pointers_count) as usize; + let mut pointer_infos = Vec::with_capacity(pointer_info_count); + if unsafe { + GetPointerFrameInfoHistory( + pointer_id, + &mut entries_count, + &mut pointers_count, + pointer_infos.as_mut_ptr(), + ) + } == false.into() + { + result = ProcResult::Value(0); + return; + } + unsafe { pointer_infos.set_len(pointer_info_count) }; + + // https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getpointerframeinfohistory + // The information retrieved appears in reverse chronological order, with the most + // recent entry in the first row of the returned array + for pointer_info in pointer_infos.iter().rev() { + let mut device_rect = mem::MaybeUninit::uninit(); + let mut display_rect = mem::MaybeUninit::uninit(); + + if unsafe { + GetPointerDeviceRects( + pointer_info.sourceDevice, + device_rect.as_mut_ptr(), + display_rect.as_mut_ptr(), + ) + } == false.into() + { + continue; + } + + let device_rect = unsafe { device_rect.assume_init() }; + let display_rect = unsafe { display_rect.assume_init() }; + + // For the most precise himetric to pixel conversion we calculate the ratio + // between the resolution of the display device (pixel) and + // the touch device (himetric). + let himetric_to_pixel_ratio_x = (display_rect.right - display_rect.left) as f64 + / (device_rect.right - device_rect.left) as f64; + let himetric_to_pixel_ratio_y = (display_rect.bottom - display_rect.top) as f64 + / (device_rect.bottom - device_rect.top) as f64; + + // ptHimetricLocation's origin is 0,0 even on multi-monitor setups. + // On multi-monitor setups we need to translate the himetric location to the + // rect of the display device it's attached to. + let x = display_rect.left as f64 + + pointer_info.ptHimetricLocation.x as f64 * himetric_to_pixel_ratio_x; + let y = display_rect.top as f64 + + pointer_info.ptHimetricLocation.y as f64 * himetric_to_pixel_ratio_y; + + let mut location = POINT { x: x.floor() as i32, y: y.floor() as i32 }; + + if unsafe { ScreenToClient(window, &mut location) } == false.into() { + continue; + } + + let force = match pointer_info.pointerType { + PT_TOUCH => { + let mut touch_info = mem::MaybeUninit::uninit(); + util::GET_POINTER_TOUCH_INFO.and_then(|GetPointerTouchInfo| { + match unsafe { + GetPointerTouchInfo( + pointer_info.pointerId, + touch_info.as_mut_ptr(), + ) + } { + 0 => None, + _ => normalize_pointer_pressure(unsafe { + touch_info.assume_init().pressure + }), + } + }) + }, + PT_PEN => { + let mut pen_info = mem::MaybeUninit::uninit(); + util::GET_POINTER_PEN_INFO.and_then(|GetPointerPenInfo| { + match unsafe { + GetPointerPenInfo(pointer_info.pointerId, pen_info.as_mut_ptr()) + } { + 0 => None, + _ => normalize_pointer_pressure(unsafe { + pen_info.assume_init().pressure + }), + } + }) + }, + _ => None, + }; + + let x = location.x as f64 + x.fract(); + let y = location.y as f64 + y.fract(); + let location = PhysicalPosition::new(x, y); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: WindowEvent::Touch(Touch { + phase: if util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_DOWN) { + TouchPhase::Started + } else if util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_UP) { + TouchPhase::Ended + } else if util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_UPDATE) + { + TouchPhase::Moved + } else { + continue; + }, + location, + force, + id: pointer_info.pointerId as u64, + device_id: DEVICE_ID, + }), + }); + } + + unsafe { SkipPointerFrameMessages(pointer_id) }; + } + result = ProcResult::Value(0); + }, + + WM_NCACTIVATE => { + let is_active = wparam != false.into(); + let active_focus_changed = userdata.window_state_lock().set_active(is_active); + if active_focus_changed { + if is_active { + unsafe { gain_active_focus(window, userdata) }; + } else { + unsafe { lose_active_focus(window, userdata) }; + } + } + result = ProcResult::DefWindowProc(wparam); + }, + + WM_SETFOCUS => { + let active_focus_changed = userdata.window_state_lock().set_focused(true); + if active_focus_changed { + unsafe { gain_active_focus(window, userdata) }; + } + result = ProcResult::Value(0); + }, + + WM_KILLFOCUS => { + let active_focus_changed = userdata.window_state_lock().set_focused(false); + if active_focus_changed { + unsafe { lose_active_focus(window, userdata) }; + } + result = ProcResult::Value(0); + }, + + WM_SETCURSOR => { + let set_cursor_to = { + let window_state = userdata.window_state_lock(); + // The return value for the preceding `WM_NCHITTEST` message is conveniently + // provided through the low-order word of lParam. We use that here since + // `WM_MOUSEMOVE` seems to come after `WM_SETCURSOR` for a given cursor movement. + let in_client_area = super::loword(lparam as u32) as u32 == HTCLIENT; + if in_client_area { + Some(window_state.mouse.selected_cursor.clone()) + } else { + None + } + }; + + match set_cursor_to { + Some(selected_cursor) => { + let hcursor = match selected_cursor { + SelectedCursor::Named(cursor_icon) => unsafe { + LoadCursorW(0, util::to_windows_cursor(cursor_icon)) + }, + SelectedCursor::Custom(cursor) => cursor.as_raw_handle(), + }; + unsafe { SetCursor(hcursor) }; + result = ProcResult::Value(0); + }, + None => result = ProcResult::DefWindowProc(wparam), + } + }, + + WM_GETMINMAXINFO => { + let mmi = lparam as *mut MINMAXINFO; + + let window_state = userdata.window_state_lock(); + let window_flags = window_state.window_flags; + + if window_state.min_size.is_some() || window_state.max_size.is_some() { + if let Some(min_size) = window_state.min_size { + let min_size = min_size.to_physical(window_state.scale_factor); + let (width, height): (u32, u32) = + window_flags.adjust_size(window, min_size).into(); + unsafe { (*mmi).ptMinTrackSize = POINT { x: width as i32, y: height as i32 } }; + } + if let Some(max_size) = window_state.max_size { + let max_size = max_size.to_physical(window_state.scale_factor); + let (width, height): (u32, u32) = + window_flags.adjust_size(window, max_size).into(); + unsafe { (*mmi).ptMaxTrackSize = POINT { x: width as i32, y: height as i32 } }; + } + } + + result = ProcResult::Value(0); + }, + + // Only sent on Windows 8.1 or newer. On Windows 7 and older user has to log out to change + // DPI, therefore all applications are closed while DPI is changing. + WM_DPICHANGED => { + use crate::event::WindowEvent::ScaleFactorChanged; + + // This message actually provides two DPI values - x and y. However MSDN says that + // "you only need to use either the X-axis or the Y-axis value when scaling your + // application since they are the same". + // https://msdn.microsoft.com/en-us/library/windows/desktop/dn312083(v=vs.85).aspx + let new_dpi_x = super::loword(wparam as u32) as u32; + let new_scale_factor = dpi_to_scale_factor(new_dpi_x); + let old_scale_factor: f64; + + let (allow_resize, window_flags) = { + let mut window_state = userdata.window_state_lock(); + old_scale_factor = window_state.scale_factor; + window_state.scale_factor = new_scale_factor; + + if new_scale_factor == old_scale_factor { + result = ProcResult::Value(0); + return; + } + + let allow_resize = window_state.fullscreen.is_none() + && !window_state.window_flags().contains(WindowFlags::MAXIMIZED); + + (allow_resize, window_state.window_flags) + }; + + // New size as suggested by Windows. + let suggested_rect = unsafe { *(lparam as *const RECT) }; + + // The window rect provided is the window's outer size, not it's inner size. However, + // win32 doesn't provide an `UnadjustWindowRectEx` function to get the client rect from + // the outer rect, so we instead adjust the window rect to get the decoration margins + // and remove them from the outer size. + let margin_left: i32; + let margin_top: i32; + // let margin_right: i32; + // let margin_bottom: i32; + { + let adjusted_rect = + window_flags.adjust_rect(window, suggested_rect).unwrap_or(suggested_rect); + margin_left = suggested_rect.left - adjusted_rect.left; + margin_top = suggested_rect.top - adjusted_rect.top; + // margin_right = adjusted_rect.right - suggested_rect.right; + // margin_bottom = adjusted_rect.bottom - suggested_rect.bottom; + } + + let old_physical_inner_rect = util::WindowArea::Inner + .get_rect(window) + .expect("failed to query (old) inner window area"); + let old_physical_inner_size = PhysicalSize::new( + (old_physical_inner_rect.right - old_physical_inner_rect.left) as u32, + (old_physical_inner_rect.bottom - old_physical_inner_rect.top) as u32, + ); + + // `allow_resize` prevents us from re-applying DPI adjustment to the restored size after + // exiting fullscreen (the restored size is already DPI adjusted). + let new_physical_inner_size = match allow_resize { + // We calculate our own size because the default suggested rect doesn't do a great + // job of preserving the window's logical size. + true => old_physical_inner_size + .to_logical::(old_scale_factor) + .to_physical::(new_scale_factor), + false => old_physical_inner_size, + }; + + let new_inner_size = Arc::new(Mutex::new(new_physical_inner_size)); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ScaleFactorChanged { + scale_factor: new_scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&new_inner_size)), + }, + }); + + let new_physical_inner_size = *new_inner_size.lock().unwrap(); + drop(new_inner_size); + + let dragging_window: bool; + + { + let window_state = userdata.window_state_lock(); + dragging_window = + window_state.window_flags().contains(WindowFlags::MARKER_IN_SIZE_MOVE); + // Unset maximized if we're changing the window's size. + if new_physical_inner_size != old_physical_inner_size { + WindowState::set_window_flags(window_state, window, |f| { + f.set(WindowFlags::MAXIMIZED, false) + }); + } + } + + let new_outer_rect: RECT; + // PdfCraft patch: from Windows 11 (build 22000) on, apply the rect Windows suggests, as + // winit master does since 488c036a (rust-windowing/winit#4341). The adjustment below + // asks `MonitorFromWindow`, which during a drag still answers with the monitor being + // left: it nudges the window back onto that monitor, which sends another WM_DPICHANGED, + // and the window grows and stays on the wrong monitor at the wrong scale + // (rust-windowing/winit#4041, #4600; storytold/pdfcraft#324). + if !WIN10_BUILD_VERSION.is_some_and(|build| build < 22000) { + new_outer_rect = suggested_rect; + } else { + let suggested_ul = + (suggested_rect.left + margin_left, suggested_rect.top + margin_top); + + let mut conservative_rect = RECT { + left: suggested_ul.0, + top: suggested_ul.1, + right: suggested_ul.0 + new_physical_inner_size.width as i32, + bottom: suggested_ul.1 + new_physical_inner_size.height as i32, + }; + + conservative_rect = window_flags + .adjust_rect(window, conservative_rect) + .unwrap_or(conservative_rect); + + // If we're dragging the window, offset the window so that the cursor's + // relative horizontal position in the title bar is preserved. + if dragging_window { + let bias = { + let cursor_pos = { + let mut pos = unsafe { mem::zeroed() }; + unsafe { GetCursorPos(&mut pos) }; + pos + }; + let suggested_cursor_horizontal_ratio = (cursor_pos.x - suggested_rect.left) + as f64 + / (suggested_rect.right - suggested_rect.left) as f64; + + (cursor_pos.x + - (suggested_cursor_horizontal_ratio + * (conservative_rect.right - conservative_rect.left) as f64) + as i32) + - conservative_rect.left + }; + conservative_rect.left += bias; + conservative_rect.right += bias; + } + + // Check to see if the new window rect is on the monitor with the new DPI factor. + // If it isn't, offset the window so that it is. + let new_dpi_monitor = unsafe { MonitorFromWindow(window, MONITOR_DEFAULTTONULL) }; + let conservative_rect_monitor = + unsafe { MonitorFromRect(&conservative_rect, MONITOR_DEFAULTTONULL) }; + new_outer_rect = if conservative_rect_monitor == new_dpi_monitor { + conservative_rect + } else { + let get_monitor_rect = |monitor| { + let mut monitor_info = MONITORINFO { + cbSize: mem::size_of::() as _, + ..unsafe { mem::zeroed() } + }; + unsafe { GetMonitorInfoW(monitor, &mut monitor_info) }; + monitor_info.rcMonitor + }; + let wrong_monitor = conservative_rect_monitor; + let wrong_monitor_rect = get_monitor_rect(wrong_monitor); + let new_monitor_rect = get_monitor_rect(new_dpi_monitor); + + // The direction to nudge the window in to get the window onto the monitor with + // the new DPI factor. We calculate this by seeing which monitor edges are + // shared and nudging away from the wrong monitor based on those. + #[allow(clippy::bool_to_int_with_if)] + let delta_nudge_to_dpi_monitor = ( + if wrong_monitor_rect.left == new_monitor_rect.right { + -1 + } else if wrong_monitor_rect.right == new_monitor_rect.left { + 1 + } else { + 0 + }, + if wrong_monitor_rect.bottom == new_monitor_rect.top { + 1 + } else if wrong_monitor_rect.top == new_monitor_rect.bottom { + -1 + } else { + 0 + }, + ); + + let abort_after_iterations = new_monitor_rect.right - new_monitor_rect.left + + new_monitor_rect.bottom + - new_monitor_rect.top; + for _ in 0..abort_after_iterations { + conservative_rect.left += delta_nudge_to_dpi_monitor.0; + conservative_rect.right += delta_nudge_to_dpi_monitor.0; + conservative_rect.top += delta_nudge_to_dpi_monitor.1; + conservative_rect.bottom += delta_nudge_to_dpi_monitor.1; + + if unsafe { MonitorFromRect(&conservative_rect, MONITOR_DEFAULTTONULL) } + == new_dpi_monitor + { + break; + } + } + + conservative_rect + }; + } + + unsafe { + SetWindowPos( + window, + 0, + new_outer_rect.left, + new_outer_rect.top, + new_outer_rect.right - new_outer_rect.left, + new_outer_rect.bottom - new_outer_rect.top, + SWP_NOZORDER | SWP_NOACTIVATE, + ) + }; + + result = ProcResult::Value(0); + }, + + WM_SETTINGCHANGE => { + use crate::event::WindowEvent::ThemeChanged; + + let preferred_theme = userdata.window_state_lock().preferred_theme; + + if preferred_theme.is_none() { + let new_theme = try_theme(window, preferred_theme); + let mut window_state = userdata.window_state_lock(); + + if window_state.current_theme != new_theme { + window_state.current_theme = new_theme; + drop(window_state); + userdata.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ThemeChanged(new_theme), + }); + } + } + result = ProcResult::DefWindowProc(wparam); + }, + + _ => { + if msg == DESTROY_MSG_ID.get() { + unsafe { DestroyWindow(window) }; + result = ProcResult::Value(0); + } else if msg == SET_RETAIN_STATE_ON_SIZE_MSG_ID.get() { + let mut window_state = userdata.window_state_lock(); + window_state.set_window_flags_in_place(|f| { + f.set(WindowFlags::MARKER_RETAIN_STATE_ON_SIZE, wparam != 0) + }); + result = ProcResult::Value(0); + } else if msg == TASKBAR_CREATED.get() { + let window_state = userdata.window_state_lock(); + unsafe { set_skip_taskbar(window, window_state.skip_taskbar) }; + result = ProcResult::DefWindowProc(wparam); + } else { + result = ProcResult::DefWindowProc(wparam); + } + }, + }; + + userdata + .event_loop_runner + .catch_unwind(callback) + .unwrap_or_else(|| result = ProcResult::Value(-1)); + + match result { + ProcResult::DefWindowProc(wparam) => unsafe { DefWindowProcW(window, msg, wparam, lparam) }, + ProcResult::Value(val) => val, + } +} + +unsafe extern "system" fn thread_event_target_callback( + window: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + let userdata_ptr = + unsafe { super::get_window_long(window, GWL_USERDATA) } as *mut ThreadMsgTargetData; + if userdata_ptr.is_null() { + // `userdata_ptr` will always be null for the first `WM_GETMINMAXINFO`, as well as + // `WM_NCCREATE` and `WM_CREATE`. + return unsafe { DefWindowProcW(window, msg, wparam, lparam) }; + } + let userdata = unsafe { Box::from_raw(userdata_ptr) }; + + if msg != WM_PAINT { + unsafe { RedrawWindow(window, ptr::null(), 0, RDW_INTERNALPAINT) }; + } + + let mut userdata_removed = false; + + // I decided to bind the closure to `callback` and pass it to catch_unwind rather than passing + // the closure to catch_unwind directly so that the match body indentation wouldn't change and + // the git blame and history would be preserved. + let callback = || match msg { + WM_NCDESTROY => { + unsafe { super::set_window_long(window, GWL_USERDATA, 0) }; + userdata_removed = true; + 0 + }, + WM_PAINT => unsafe { + ValidateRect(window, ptr::null()); + // Default WM_PAINT behaviour. This makes sure modals and popups are shown immediately + // when opening them. + DefWindowProcW(window, msg, wparam, lparam) + }, + + WM_INPUT_DEVICE_CHANGE => { + let event = match wparam as u32 { + GIDC_ARRIVAL => DeviceEvent::Added, + GIDC_REMOVAL => DeviceEvent::Removed, + _ => unreachable!(), + }; + + userdata + .send_event(Event::DeviceEvent { device_id: wrap_device_id(lparam as u32), event }); + + 0 + }, + + WM_INPUT => { + if let Some(data) = raw_input::get_raw_input_data(lparam as _) { + unsafe { handle_raw_input(&userdata, data) }; + } + + unsafe { DefWindowProcW(window, msg, wparam, lparam) } + }, + + _ if msg == USER_EVENT_MSG_ID.get() => { + // synthesis a placeholder UserEvent, so that if the callback is + // re-entered it can be buffered for later delivery. the real + // user event is still in the mpsc channel and will be pulled + // once the placeholder event is delivered to the wrapper + // `event_handler` + userdata.send_event(Event::UserEvent(UserEventPlaceholder)); + 0 + }, + _ if msg == EXEC_MSG_ID.get() => { + let mut function: ThreadExecFn = unsafe { Box::from_raw(wparam as *mut _) }; + function(); + 0 + }, + _ => unsafe { DefWindowProcW(window, msg, wparam, lparam) }, + }; + + let result = userdata.event_loop_runner.catch_unwind(callback).unwrap_or(-1); + if userdata_removed { + drop(userdata); + } else { + Box::leak(userdata); + } + result +} + +unsafe fn handle_raw_input(userdata: &ThreadMsgTargetData, data: RAWINPUT) { + use crate::event::DeviceEvent::{Button, Key, Motion, MouseMotion, MouseWheel}; + use crate::event::ElementState::{Pressed, Released}; + use crate::event::MouseScrollDelta::LineDelta; + + let device_id = wrap_device_id(data.header.hDevice as _); + + if data.header.dwType == RIM_TYPEMOUSE { + let mouse = unsafe { data.data.mouse }; + + if util::has_flag(mouse.usFlags as u32, MOUSE_MOVE_RELATIVE) { + let x = mouse.lLastX as f64; + let y = mouse.lLastY as f64; + + if x != 0.0 { + userdata.send_event(Event::DeviceEvent { + device_id, + event: Motion { axis: 0, value: x }, + }); + } + + if y != 0.0 { + userdata.send_event(Event::DeviceEvent { + device_id, + event: Motion { axis: 1, value: y }, + }); + } + + if x != 0.0 || y != 0.0 { + userdata.send_event(Event::DeviceEvent { + device_id, + event: MouseMotion { delta: (x, y) }, + }); + } + } + + let button_flags = unsafe { mouse.Anonymous.Anonymous.usButtonFlags }; + if util::has_flag(button_flags as u32, RI_MOUSE_WHEEL) { + let button_data = unsafe { mouse.Anonymous.Anonymous.usButtonData } as i16; + let delta = button_data as f32 / WHEEL_DELTA as f32; + userdata.send_event(Event::DeviceEvent { + device_id, + event: MouseWheel { delta: LineDelta(0.0, delta) }, + }); + } + if util::has_flag(button_flags as u32, RI_MOUSE_HWHEEL) { + let button_data = unsafe { mouse.Anonymous.Anonymous.usButtonData } as i16; + let delta = -button_data as f32 / WHEEL_DELTA as f32; + userdata.send_event(Event::DeviceEvent { + device_id, + event: MouseWheel { delta: LineDelta(delta, 0.0) }, + }); + } + + let button_state = raw_input::get_raw_mouse_button_state(button_flags as u32); + for (button, state) in button_state.iter().enumerate() { + if let Some(state) = *state { + userdata.send_event(Event::DeviceEvent { + device_id, + event: Button { button: button as _, state }, + }); + } + } + } else if data.header.dwType == RIM_TYPEKEYBOARD { + let keyboard = unsafe { data.data.keyboard }; + + let pressed = keyboard.Message == WM_KEYDOWN || keyboard.Message == WM_SYSKEYDOWN; + let released = keyboard.Message == WM_KEYUP || keyboard.Message == WM_SYSKEYUP; + + if !pressed && !released { + return; + } + + if let Some(physical_key) = raw_input::get_keyboard_physical_key(keyboard) { + let state = if pressed { Pressed } else { Released }; + + userdata.send_event(Event::DeviceEvent { + device_id, + event: Key(RawKeyEvent { physical_key, state }), + }); + } + } +} + +enum PointerMoveKind { + /// Pointer entered to the window. + Enter, + /// Pointer leaved the window client area. + Leave, + /// Pointer is inside the window or `GetClientRect` failed. + None, +} + +fn get_pointer_move_kind( + window: HWND, + mouse_was_inside_window: bool, + x: i32, + y: i32, +) -> PointerMoveKind { + let rect: RECT = unsafe { + let mut rect: RECT = mem::zeroed(); + if GetClientRect(window, &mut rect) == false.into() { + return PointerMoveKind::None; // exit early if GetClientRect failed + } + rect + }; + + let x = (rect.left..rect.right).contains(&x); + let y = (rect.top..rect.bottom).contains(&y); + + if !mouse_was_inside_window && x && y { + PointerMoveKind::Enter + } else if mouse_was_inside_window && !(x && y) { + PointerMoveKind::Leave + } else { + PointerMoveKind::None + } +} diff --git a/vendor/winit/src/platform_impl/windows/event_loop/runner.rs b/vendor/winit/src/platform_impl/windows/event_loop/runner.rs new file mode 100644 index 00000000..3243ec47 --- /dev/null +++ b/vendor/winit/src/platform_impl/windows/event_loop/runner.rs @@ -0,0 +1,406 @@ +use std::any::Any; +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; +use std::time::Instant; +use std::{mem, panic}; + +use windows_sys::Win32::Foundation::HWND; + +use crate::dpi::PhysicalSize; +use crate::event::{Event, InnerSizeWriter, StartCause, WindowEvent}; +use crate::platform_impl::platform::event_loop::{WindowData, GWL_USERDATA}; +use crate::platform_impl::platform::get_window_long; +use crate::window::WindowId; + +use super::ControlFlow; + +pub(crate) type EventLoopRunnerShared = Rc>; + +type EventHandler = Cell)>>>; + +pub(crate) struct EventLoopRunner { + // The event loop's win32 handles + pub(super) thread_msg_target: HWND, + + // Setting this will ensure pump_events will return to the external + // loop asap. E.g. set after each RedrawRequested to ensure pump_events + // can't stall an external loop beyond a frame + pub(super) interrupt_msg_dispatch: Cell, + + control_flow: Cell, + exit: Cell>, + runner_state: Cell, + last_events_cleared: Cell, + event_handler: EventHandler, + event_buffer: RefCell>>, + + panic_error: Cell>, +} + +pub type PanicError = Box; + +/// See `move_state_to` function for details on how the state loop works. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum RunnerState { + /// The event loop has just been created, and an `Init` event must be sent. + Uninitialized, + /// The event loop is idling. + Idle, + /// The event loop is handling the OS's events and sending them to the user's callback. + /// `NewEvents` has been sent, and `AboutToWait` hasn't. + HandlingMainEvents, + /// The event loop has been destroyed. No other events will be emitted. + Destroyed, +} + +enum BufferedEvent { + Event(Event), + ScaleFactorChanged(WindowId, f64, PhysicalSize), +} + +impl EventLoopRunner { + pub(crate) fn new(thread_msg_target: HWND) -> EventLoopRunner { + EventLoopRunner { + thread_msg_target, + interrupt_msg_dispatch: Cell::new(false), + runner_state: Cell::new(RunnerState::Uninitialized), + control_flow: Cell::new(ControlFlow::default()), + exit: Cell::new(None), + panic_error: Cell::new(None), + last_events_cleared: Cell::new(Instant::now()), + event_handler: Cell::new(None), + event_buffer: RefCell::new(VecDeque::new()), + } + } + + /// Associate the application's event handler with the runner + /// + /// # Safety + /// This is ignoring the lifetime of the application handler (which may not + /// outlive the EventLoopRunner) and can lead to undefined behaviour if + /// the handler is not cleared before the end of real lifetime. + /// + /// All public APIs that take an event handler (`run`, `run_on_demand`, + /// `pump_events`) _must_ pair a call to `set_event_handler` with + /// a call to `clear_event_handler` before returning to avoid + /// undefined behaviour. + pub(crate) unsafe fn set_event_handler(&self, f: F) + where + F: FnMut(Event), + { + // Erase closure lifetime. + // SAFETY: Caller upholds that the lifetime of the closure is upheld. + let f = unsafe { + mem::transmute::)>, Box)>>(Box::new(f)) + }; + let old_event_handler = self.event_handler.replace(Some(f)); + assert!(old_event_handler.is_none()); + } + + pub(crate) fn clear_event_handler(&self) { + self.event_handler.set(None); + } + + pub(crate) fn reset_runner(&self) { + let EventLoopRunner { + thread_msg_target: _, + interrupt_msg_dispatch, + runner_state, + panic_error, + control_flow: _, + exit, + last_events_cleared: _, + event_handler, + event_buffer: _, + } = self; + interrupt_msg_dispatch.set(false); + runner_state.set(RunnerState::Uninitialized); + panic_error.set(None); + exit.set(None); + event_handler.set(None); + } +} + +/// State retrieval functions. +impl EventLoopRunner { + #[allow(unused)] + pub fn thread_msg_target(&self) -> HWND { + self.thread_msg_target + } + + pub fn take_panic_error(&self) -> Result<(), PanicError> { + match self.panic_error.take() { + Some(err) => Err(err), + None => Ok(()), + } + } + + pub fn set_control_flow(&self, control_flow: ControlFlow) { + self.control_flow.set(control_flow) + } + + pub fn control_flow(&self) -> ControlFlow { + self.control_flow.get() + } + + pub fn set_exit_code(&self, code: i32) { + self.exit.set(Some(code)) + } + + pub fn exit_code(&self) -> Option { + self.exit.get() + } + + pub fn clear_exit(&self) { + self.exit.set(None); + } + + pub fn should_buffer(&self) -> bool { + let handler = self.event_handler.take(); + let should_buffer = handler.is_none(); + self.event_handler.set(handler); + should_buffer + } +} + +/// Misc. functions +impl EventLoopRunner { + pub fn catch_unwind(&self, f: impl FnOnce() -> R) -> Option { + let panic_error = self.panic_error.take(); + if panic_error.is_none() { + let result = panic::catch_unwind(panic::AssertUnwindSafe(f)); + + // Check to see if the panic error was set in a re-entrant call to catch_unwind inside + // of `f`. If it was, that error takes priority. If it wasn't, check if our call to + // catch_unwind caught any panics and set panic_error appropriately. + match self.panic_error.take() { + None => match result { + Ok(r) => Some(r), + Err(e) => { + self.panic_error.set(Some(e)); + None + }, + }, + Some(e) => { + self.panic_error.set(Some(e)); + None + }, + } + } else { + self.panic_error.set(panic_error); + None + } + } +} + +/// Event dispatch functions. +impl EventLoopRunner { + pub(crate) fn prepare_wait(&self) { + self.move_state_to(RunnerState::Idle); + } + + pub(crate) fn wakeup(&self) { + self.move_state_to(RunnerState::HandlingMainEvents); + } + + pub(crate) fn send_event(&self, event: Event) { + if let Event::WindowEvent { event: WindowEvent::RedrawRequested, .. } = event { + self.call_event_handler(event); + // As a rule, to ensure that `pump_events` can't block an external event loop + // for too long, we always guarantee that `pump_events` will return control to + // the external loop asap after a `RedrawRequested` event is dispatched. + self.interrupt_msg_dispatch.set(true); + } else if self.should_buffer() { + // If the runner is already borrowed, we're in the middle of an event loop invocation. + // Add the event to a buffer to be processed later. + self.event_buffer.borrow_mut().push_back(BufferedEvent::from_event(event)) + } else { + self.call_event_handler(event); + self.dispatch_buffered_events(); + } + } + + pub(crate) fn loop_destroyed(&self) { + self.move_state_to(RunnerState::Destroyed); + } + + fn call_event_handler(&self, event: Event) { + self.catch_unwind(|| { + let mut event_handler = self.event_handler.take().expect( + "either event handler is re-entrant (likely), or no event handler is registered \ + (very unlikely)", + ); + + event_handler(event); + + assert!(self.event_handler.replace(Some(event_handler)).is_none()); + }); + } + + fn dispatch_buffered_events(&self) { + loop { + // We do this instead of using a `while let` loop because if we use a `while let` + // loop the reference returned `borrow_mut()` doesn't get dropped until the end + // of the loop's body and attempts to add events to the event buffer while in + // `process_event` will fail. + let buffered_event_opt = self.event_buffer.borrow_mut().pop_front(); + match buffered_event_opt { + Some(e) => e.dispatch_event(|e| self.call_event_handler(e)), + None => break, + } + } + } + + /// Dispatch control flow events (`NewEvents`, `AboutToWait`, and + /// `LoopExiting`) as necessary to bring the internal `RunnerState` to the + /// new runner state. + /// + /// The state transitions are defined as follows: + /// + /// ```text + /// Uninitialized + /// | + /// V + /// Idle + /// ^ | + /// | V + /// HandlingMainEvents + /// | + /// V + /// Destroyed + /// ``` + /// + /// Attempting to transition back to `Uninitialized` will result in a panic. Attempting to + /// transition *from* `Destroyed` will also result in a panic. Transitioning to the current + /// state is a no-op. Even if the `new_runner_state` isn't the immediate next state in the + /// runner state machine (e.g. `self.runner_state == HandlingMainEvents` and + /// `new_runner_state == Idle`), the intermediate state transitions will still be executed. + fn move_state_to(&self, new_runner_state: RunnerState) { + use RunnerState::{Destroyed, HandlingMainEvents, Idle, Uninitialized}; + + match (self.runner_state.replace(new_runner_state), new_runner_state) { + (Uninitialized, Uninitialized) + | (Idle, Idle) + | (HandlingMainEvents, HandlingMainEvents) + | (Destroyed, Destroyed) => (), + + // State transitions that initialize the event loop. + (Uninitialized, HandlingMainEvents) => { + self.call_new_events(true); + }, + (Uninitialized, Idle) => { + self.call_new_events(true); + self.call_event_handler(Event::AboutToWait); + self.last_events_cleared.set(Instant::now()); + }, + (Uninitialized, Destroyed) => { + self.call_new_events(true); + self.call_event_handler(Event::AboutToWait); + self.last_events_cleared.set(Instant::now()); + self.call_event_handler(Event::LoopExiting); + }, + (_, Uninitialized) => panic!("cannot move state to Uninitialized"), + + // State transitions that start the event handling process. + (Idle, HandlingMainEvents) => { + self.call_new_events(false); + }, + (Idle, Destroyed) => { + self.call_event_handler(Event::LoopExiting); + }, + + (HandlingMainEvents, Idle) => { + // This is always the last event we dispatch before waiting for new events + self.call_event_handler(Event::AboutToWait); + self.last_events_cleared.set(Instant::now()); + }, + (HandlingMainEvents, Destroyed) => { + self.call_event_handler(Event::AboutToWait); + self.last_events_cleared.set(Instant::now()); + self.call_event_handler(Event::LoopExiting); + }, + + (Destroyed, _) => panic!("cannot move state from Destroyed"), + } + } + + fn call_new_events(&self, init: bool) { + let start_cause = match (init, self.control_flow(), self.exit.get()) { + (true, ..) => StartCause::Init, + (false, ControlFlow::Poll, None) => StartCause::Poll, + (false, _, Some(_)) | (false, ControlFlow::Wait, None) => StartCause::WaitCancelled { + requested_resume: None, + start: self.last_events_cleared.get(), + }, + (false, ControlFlow::WaitUntil(requested_resume), None) => { + if Instant::now() < requested_resume { + StartCause::WaitCancelled { + requested_resume: Some(requested_resume), + start: self.last_events_cleared.get(), + } + } else { + StartCause::ResumeTimeReached { + requested_resume, + start: self.last_events_cleared.get(), + } + } + }, + }; + self.call_event_handler(Event::NewEvents(start_cause)); + // NB: For consistency all platforms must emit a 'resumed' event even though Windows + // applications don't themselves have a formal suspend/resume lifecycle. + if init { + self.call_event_handler(Event::Resumed); + } + self.dispatch_buffered_events(); + } +} + +impl BufferedEvent { + pub fn from_event(event: Event) -> BufferedEvent { + match event { + Event::WindowEvent { + event: WindowEvent::ScaleFactorChanged { scale_factor, inner_size_writer }, + window_id, + } => BufferedEvent::ScaleFactorChanged( + window_id, + scale_factor, + *inner_size_writer.new_inner_size.upgrade().unwrap().lock().unwrap(), + ), + event => BufferedEvent::Event(event), + } + } + + pub fn dispatch_event(self, dispatch: impl FnOnce(Event)) { + match self { + Self::Event(event) => dispatch(event), + Self::ScaleFactorChanged(window_id, scale_factor, new_inner_size) => { + let user_new_inner_size = Arc::new(Mutex::new(new_inner_size)); + dispatch(Event::WindowEvent { + window_id, + event: WindowEvent::ScaleFactorChanged { + scale_factor, + inner_size_writer: InnerSizeWriter::new(Arc::downgrade( + &user_new_inner_size, + )), + }, + }); + let inner_size = *user_new_inner_size.lock().unwrap(); + + drop(user_new_inner_size); + + if inner_size != new_inner_size { + let window_flags = unsafe { + let userdata = + get_window_long(window_id.0.into(), GWL_USERDATA) as *mut WindowData; + (*userdata).window_state_lock().window_flags + }; + + window_flags.set_size((window_id.0).0, inner_size); + } + }, + } + } +} diff --git a/vendor/winit/src/platform_impl/windows/icon.rs b/vendor/winit/src/platform_impl/windows/icon.rs new file mode 100644 index 00000000..4e5fe69e --- /dev/null +++ b/vendor/winit/src/platform_impl/windows/icon.rs @@ -0,0 +1,265 @@ +use std::ffi::c_void; +use std::path::Path; +use std::sync::Arc; +use std::{fmt, io, mem}; + +use cursor_icon::CursorIcon; +use windows_sys::core::PCWSTR; +use windows_sys::Win32::Foundation::HWND; +use windows_sys::Win32::Graphics::Gdi::{ + CreateBitmap, CreateCompatibleBitmap, DeleteObject, GetDC, ReleaseDC, SetBitmapBits, +}; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + CreateIcon, CreateIconIndirect, DestroyCursor, DestroyIcon, LoadImageW, SendMessageW, HCURSOR, + HICON, ICONINFO, ICON_BIG, ICON_SMALL, IMAGE_ICON, LR_DEFAULTSIZE, LR_LOADFROMFILE, WM_SETICON, +}; + +use crate::cursor::CursorImage; +use crate::dpi::PhysicalSize; +use crate::icon::*; + +use super::util; + +impl Pixel { + fn convert_to_bgra(&mut self) { + mem::swap(&mut self.r, &mut self.b); + } +} + +impl RgbaIcon { + fn into_windows_icon(self) -> Result { + let rgba = self.rgba; + let pixel_count = rgba.len() / PIXEL_SIZE; + let mut and_mask = Vec::with_capacity(pixel_count); + let pixels = + unsafe { std::slice::from_raw_parts_mut(rgba.as_ptr() as *mut Pixel, pixel_count) }; + for pixel in pixels { + and_mask.push(pixel.a.wrapping_sub(u8::MAX)); // invert alpha channel + pixel.convert_to_bgra(); + } + assert_eq!(and_mask.len(), pixel_count); + let handle = unsafe { + CreateIcon( + 0, + self.width as i32, + self.height as i32, + 1, + (PIXEL_SIZE * 8) as u8, + and_mask.as_ptr(), + rgba.as_ptr(), + ) + }; + if handle != 0 { + Ok(WinIcon::from_handle(handle)) + } else { + Err(BadIcon::OsError(io::Error::last_os_error())) + } + } +} + +#[derive(Debug)] +pub enum IconType { + Small = ICON_SMALL as isize, + Big = ICON_BIG as isize, +} + +#[derive(Debug)] +struct RaiiIcon { + handle: HICON, +} + +#[derive(Clone)] +pub struct WinIcon { + inner: Arc, +} + +unsafe impl Send for WinIcon {} + +impl WinIcon { + pub fn as_raw_handle(&self) -> HICON { + self.inner.handle + } + + pub fn from_path>( + path: P, + size: Option>, + ) -> Result { + // width / height of 0 along with LR_DEFAULTSIZE tells windows to load the default icon size + let (width, height) = size.map(Into::into).unwrap_or((0, 0)); + + let wide_path = util::encode_wide(path.as_ref()); + + let handle = unsafe { + LoadImageW( + 0, + wide_path.as_ptr(), + IMAGE_ICON, + width, + height, + LR_DEFAULTSIZE | LR_LOADFROMFILE, + ) + }; + if handle != 0 { + Ok(WinIcon::from_handle(handle as HICON)) + } else { + Err(BadIcon::OsError(io::Error::last_os_error())) + } + } + + pub fn from_resource( + resource_id: u16, + size: Option>, + ) -> Result { + Self::from_resource_ptr(resource_id as PCWSTR, size) + } + + pub fn from_resource_name( + resource_name: &str, + size: Option>, + ) -> Result { + let wide_name = util::encode_wide(resource_name); + Self::from_resource_ptr(wide_name.as_ptr(), size) + } + + fn from_resource_ptr( + resource: PCWSTR, + size: Option>, + ) -> Result { + // width / height of 0 along with LR_DEFAULTSIZE tells windows to load the default icon size + let (width, height) = size.map(Into::into).unwrap_or((0, 0)); + let handle = unsafe { + LoadImageW( + util::get_instance_handle(), + resource, + IMAGE_ICON, + width, + height, + LR_DEFAULTSIZE, + ) + }; + if handle != 0 { + Ok(WinIcon::from_handle(handle as HICON)) + } else { + Err(BadIcon::OsError(io::Error::last_os_error())) + } + } + + pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { + let rgba_icon = RgbaIcon::from_rgba(rgba, width, height)?; + rgba_icon.into_windows_icon() + } + + pub fn set_for_window(&self, hwnd: HWND, icon_type: IconType) { + unsafe { + SendMessageW(hwnd, WM_SETICON, icon_type as usize, self.as_raw_handle()); + } + } + + fn from_handle(handle: HICON) -> Self { + Self { inner: Arc::new(RaiiIcon { handle }) } + } +} + +impl Drop for RaiiIcon { + fn drop(&mut self) { + unsafe { DestroyIcon(self.handle) }; + } +} + +impl fmt::Debug for WinIcon { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + (*self.inner).fmt(formatter) + } +} + +pub fn unset_for_window(hwnd: HWND, icon_type: IconType) { + unsafe { + SendMessageW(hwnd, WM_SETICON, icon_type as usize, 0); + } +} + +#[derive(Debug, Clone)] +pub enum SelectedCursor { + Named(CursorIcon), + Custom(Arc), +} + +impl Default for SelectedCursor { + fn default() -> Self { + Self::Named(Default::default()) + } +} + +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub enum WinCursor { + Cursor(Arc), + Failed, +} + +impl WinCursor { + pub(crate) fn new(image: &CursorImage) -> Result { + let mut bgra = image.rgba.clone(); + bgra.chunks_exact_mut(4).for_each(|chunk| chunk.swap(0, 2)); + + let w = image.width as i32; + let h = image.height as i32; + + unsafe { + let hdc_screen = GetDC(0); + if hdc_screen == 0 { + return Err(io::Error::last_os_error()); + } + let hbm_color = CreateCompatibleBitmap(hdc_screen, w, h); + ReleaseDC(0, hdc_screen); + if hbm_color == 0 { + return Err(io::Error::last_os_error()); + } + if SetBitmapBits(hbm_color, bgra.len() as u32, bgra.as_ptr() as *const c_void) == 0 { + DeleteObject(hbm_color); + return Err(io::Error::last_os_error()); + }; + + // Mask created according to https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createbitmap#parameters + let mask_bits: Vec = vec![0xff; ((((w + 15) >> 4) << 1) * h) as usize]; + let hbm_mask = CreateBitmap(w, h, 1, 1, mask_bits.as_ptr() as *const _); + if hbm_mask == 0 { + DeleteObject(hbm_color); + return Err(io::Error::last_os_error()); + } + + let icon_info = ICONINFO { + fIcon: 0, + xHotspot: image.hotspot_x as u32, + yHotspot: image.hotspot_y as u32, + hbmMask: hbm_mask, + hbmColor: hbm_color, + }; + + let handle = CreateIconIndirect(&icon_info as *const _); + DeleteObject(hbm_color); + DeleteObject(hbm_mask); + if handle == 0 { + return Err(io::Error::last_os_error()); + } + + Ok(Self::Cursor(Arc::new(RaiiCursor { handle }))) + } + } +} + +#[derive(Debug, Hash, Eq, PartialEq)] +pub struct RaiiCursor { + handle: HCURSOR, +} + +impl Drop for RaiiCursor { + fn drop(&mut self) { + unsafe { DestroyCursor(self.handle) }; + } +} + +impl RaiiCursor { + pub fn as_raw_handle(&self) -> HICON { + self.handle + } +} diff --git a/vendor/winit/src/platform_impl/windows/ime.rs b/vendor/winit/src/platform_impl/windows/ime.rs new file mode 100644 index 00000000..eb65abf3 --- /dev/null +++ b/vendor/winit/src/platform_impl/windows/ime.rs @@ -0,0 +1,162 @@ +use std::ffi::{c_void, OsString}; +use std::os::windows::prelude::OsStringExt; +use std::ptr::null_mut; + +use windows_sys::Win32::Foundation::{POINT, RECT}; +use windows_sys::Win32::Globalization::HIMC; +use windows_sys::Win32::UI::Input::Ime::{ + ImmAssociateContextEx, ImmGetCompositionStringW, ImmGetContext, ImmReleaseContext, + ImmSetCandidateWindow, ImmSetCompositionWindow, ATTR_TARGET_CONVERTED, + ATTR_TARGET_NOTCONVERTED, CANDIDATEFORM, CFS_EXCLUDE, CFS_POINT, COMPOSITIONFORM, GCS_COMPATTR, + GCS_COMPSTR, GCS_CURSORPOS, GCS_RESULTSTR, IACE_CHILDREN, IACE_DEFAULT, +}; +use windows_sys::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_IMMENABLED}; + +use crate::dpi::{Position, Size}; +use crate::platform::windows::HWND; + +pub struct ImeContext { + hwnd: HWND, + himc: HIMC, +} + +impl ImeContext { + pub unsafe fn current(hwnd: HWND) -> Self { + let himc = unsafe { ImmGetContext(hwnd) }; + ImeContext { hwnd, himc } + } + + pub unsafe fn get_composing_text_and_cursor( + &self, + ) -> Option<(String, Option, Option)> { + let text = unsafe { self.get_composition_string(GCS_COMPSTR) }?; + let attrs = unsafe { self.get_composition_data(GCS_COMPATTR) }.unwrap_or_default(); + + let mut first = None; + let mut last = None; + let mut boundary_before_char = 0; + let mut attr_idx = 0; + + for chr in text.chars() { + let Some(attr) = attrs.get(attr_idx).copied() else { + break; + }; + + let char_is_targeted = + attr as u32 == ATTR_TARGET_CONVERTED || attr as u32 == ATTR_TARGET_NOTCONVERTED; + + if first.is_none() && char_is_targeted { + first = Some(boundary_before_char); + } else if first.is_some() && last.is_none() && !char_is_targeted { + last = Some(boundary_before_char); + } + + boundary_before_char += chr.len_utf8(); + attr_idx += chr.len_utf16(); + } + + if first.is_some() && last.is_none() { + last = Some(text.len()); + } else if first.is_none() { + // IME haven't split words and select any clause yet, so trying to retrieve normal + // cursor. + let cursor = unsafe { self.get_composition_cursor(&text) }; + first = cursor; + last = cursor; + } + + Some((text, first, last)) + } + + pub unsafe fn get_composed_text(&self) -> Option { + unsafe { self.get_composition_string(GCS_RESULTSTR) } + } + + unsafe fn get_composition_cursor(&self, text: &str) -> Option { + let cursor = unsafe { ImmGetCompositionStringW(self.himc, GCS_CURSORPOS, null_mut(), 0) }; + (cursor >= 0).then(|| text.chars().take(cursor as _).map(|c| c.len_utf8()).sum()) + } + + unsafe fn get_composition_string(&self, gcs_mode: u32) -> Option { + let data = unsafe { self.get_composition_data(gcs_mode) }?; + let (prefix, shorts, suffix) = unsafe { data.align_to::() }; + if prefix.is_empty() && suffix.is_empty() { + OsString::from_wide(shorts).into_string().ok() + } else { + None + } + } + + unsafe fn get_composition_data(&self, gcs_mode: u32) -> Option> { + let size = match unsafe { ImmGetCompositionStringW(self.himc, gcs_mode, null_mut(), 0) } { + 0 => return Some(Vec::new()), + size if size < 0 => return None, + size => size, + }; + + let mut buf = Vec::::with_capacity(size as _); + let size = unsafe { + ImmGetCompositionStringW( + self.himc, + gcs_mode, + buf.as_mut_ptr() as *mut c_void, + size as _, + ) + }; + + if size < 0 { + None + } else { + unsafe { buf.set_len(size as _) }; + Some(buf) + } + } + + pub unsafe fn set_ime_cursor_area(&self, spot: Position, size: Size, scale_factor: f64) { + if !unsafe { ImeContext::system_has_ime() } { + return; + } + + let (x, y) = spot.to_physical::(scale_factor).into(); + let (width, height): (i32, i32) = size.to_physical::(scale_factor).into(); + let rc_area = RECT { left: x, top: y, right: x + width, bottom: y + height }; + let candidate_form = CANDIDATEFORM { + dwIndex: 0, + dwStyle: CFS_EXCLUDE, + ptCurrentPos: POINT { x, y }, + rcArea: rc_area, + }; + let composition_form = COMPOSITIONFORM { + dwStyle: CFS_POINT, + ptCurrentPos: POINT { x, y: y + height }, + rcArea: rc_area, + }; + + unsafe { + ImmSetCompositionWindow(self.himc, &composition_form); + ImmSetCandidateWindow(self.himc, &candidate_form); + } + } + + pub unsafe fn set_ime_allowed(hwnd: HWND, allowed: bool) { + if !unsafe { ImeContext::system_has_ime() } { + return; + } + + if allowed { + unsafe { ImmAssociateContextEx(hwnd, 0, IACE_DEFAULT) }; + } else { + unsafe { ImmAssociateContextEx(hwnd, 0, IACE_CHILDREN) }; + } + } + + unsafe fn system_has_ime() -> bool { + unsafe { GetSystemMetrics(SM_IMMENABLED) != 0 } + } +} + +impl Drop for ImeContext { + fn drop(&mut self) { + unsafe { ImmReleaseContext(self.hwnd, self.himc) }; + } +} diff --git a/vendor/winit/src/platform_impl/windows/keyboard.rs b/vendor/winit/src/platform_impl/windows/keyboard.rs new file mode 100644 index 00000000..9c109490 --- /dev/null +++ b/vendor/winit/src/platform_impl/windows/keyboard.rs @@ -0,0 +1,1243 @@ +use std::char; +use std::ffi::OsString; +use std::mem::MaybeUninit; +use std::os::windows::ffi::OsStringExt; +use std::sync::atomic::AtomicU32; +use std::sync::atomic::Ordering::Relaxed; +use std::sync::{Mutex, MutexGuard}; + +use windows_sys::Win32::Foundation::{HWND, LPARAM, WPARAM}; +use windows_sys::Win32::System::SystemServices::LANG_KOREAN; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ + GetAsyncKeyState, GetKeyState, GetKeyboardLayout, GetKeyboardState, MapVirtualKeyExW, + MAPVK_VK_TO_VSC_EX, MAPVK_VSC_TO_VK_EX, VIRTUAL_KEY, VK_ABNT_C2, VK_ADD, VK_CAPITAL, VK_CLEAR, + VK_CONTROL, VK_DECIMAL, VK_DELETE, VK_DIVIDE, VK_DOWN, VK_END, VK_F4, VK_HOME, VK_INSERT, + VK_LCONTROL, VK_LEFT, VK_LMENU, VK_LSHIFT, VK_LWIN, VK_MENU, VK_MULTIPLY, VK_NEXT, VK_NUMLOCK, + VK_NUMPAD0, VK_NUMPAD1, VK_NUMPAD2, VK_NUMPAD3, VK_NUMPAD4, VK_NUMPAD5, VK_NUMPAD6, VK_NUMPAD7, + VK_NUMPAD8, VK_NUMPAD9, VK_PRIOR, VK_RCONTROL, VK_RETURN, VK_RIGHT, VK_RMENU, VK_RSHIFT, + VK_RWIN, VK_SCROLL, VK_SHIFT, VK_SUBTRACT, VK_UP, +}; +use windows_sys::Win32::UI::TextServices::HKL; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + PeekMessageW, MSG, PM_NOREMOVE, WM_CHAR, WM_DEADCHAR, WM_KEYDOWN, WM_KEYFIRST, WM_KEYLAST, + WM_KEYUP, WM_KILLFOCUS, WM_SETFOCUS, WM_SYSCHAR, WM_SYSDEADCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP, +}; + +use smol_str::SmolStr; +use tracing::{trace, warn}; +use unicode_segmentation::UnicodeSegmentation; + +use crate::event::{ElementState, KeyEvent}; +use crate::keyboard::{Key, KeyCode, KeyLocation, NamedKey, NativeKey, NativeKeyCode, PhysicalKey}; +use crate::platform_impl::platform::event_loop::ProcResult; +use crate::platform_impl::platform::keyboard_layout::{ + Layout, LayoutCache, WindowsModifiers, LAYOUT_CACHE, +}; +use crate::platform_impl::platform::{loword, primarylangid, KeyEventExtra}; + +pub type ExScancode = u16; + +pub struct MessageAsKeyEvent { + pub event: KeyEvent, + pub is_synthetic: bool, +} + +/// Stores information required to make `KeyEvent`s. +/// +/// A single Winit `KeyEvent` contains information which the Windows API passes to the application +/// in multiple window messages. In other words: a Winit `KeyEvent` cannot be built from a single +/// window message. Therefore, this type keeps track of certain information from previous events so +/// that a `KeyEvent` can be constructed when the last event related to a keypress is received. +/// +/// `PeekMessage` is sometimes used to determine whether the next window message still belongs to +/// the current keypress. If it doesn't and the current state represents a key event waiting to be +/// dispatched, then said event is considered complete and is dispatched. +/// +/// The sequence of window messages for a key press event is the following: +/// - Exactly one WM_KEYDOWN / WM_SYSKEYDOWN +/// - Zero or one WM_DEADCHAR / WM_SYSDEADCHAR +/// - Zero or more WM_CHAR / WM_SYSCHAR. These messages each come with a UTF-16 code unit which when +/// put together in the sequence they arrived in, forms the text which is the result of pressing +/// the key. +/// +/// Key release messages are a bit different due to the fact that they don't contribute to +/// text input. The "sequence" only consists of one WM_KEYUP / WM_SYSKEYUP event. +pub struct KeyEventBuilder { + event_info: Mutex>, + pending: PendingEventQueue, +} +impl Default for KeyEventBuilder { + fn default() -> Self { + KeyEventBuilder { event_info: Mutex::new(None), pending: Default::default() } + } +} +impl KeyEventBuilder { + /// Call this function for every window message. + /// Returns Some() if this window message completes a KeyEvent. + /// Returns None otherwise. + pub(crate) fn process_message( + &self, + hwnd: HWND, + msg_kind: u32, + wparam: WPARAM, + lparam: LPARAM, + result: &mut ProcResult, + ) -> Vec { + enum MatchResult { + Nothing, + TokenToRemove(PendingMessageToken), + MessagesToDispatch(Vec), + } + + let mut matcher = || -> MatchResult { + match msg_kind { + WM_SETFOCUS => { + // synthesize keydown events + let kbd_state = get_async_kbd_state(); + let key_events = Self::synthesize_kbd_state(ElementState::Pressed, &kbd_state); + MatchResult::MessagesToDispatch(self.pending.complete_multi(key_events)) + }, + WM_KILLFOCUS => { + // synthesize keyup events + let kbd_state = get_kbd_state(); + let key_events = Self::synthesize_kbd_state(ElementState::Released, &kbd_state); + MatchResult::MessagesToDispatch(self.pending.complete_multi(key_events)) + }, + WM_KEYDOWN | WM_SYSKEYDOWN => { + if msg_kind == WM_SYSKEYDOWN && wparam as VIRTUAL_KEY == VK_F4 { + // Don't dispatch Alt+F4 to the application. + // This is handled in `event_loop.rs` + return MatchResult::Nothing; + } + let pending_token = self.pending.add_pending(); + *result = ProcResult::Value(0); + + let next_msg = next_kbd_msg(hwnd); + + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + let mut finished_event_info = Some(PartialKeyEventInfo::from_message( + wparam, + lparam, + ElementState::Pressed, + &mut layouts, + )); + let mut event_info = self.event_info.lock().unwrap(); + *event_info = None; + if let Some(next_msg) = next_msg { + let next_msg_kind = next_msg.message; + let next_belongs_to_this = !matches!( + next_msg_kind, + WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP + ); + if next_belongs_to_this { + // The next OS event belongs to this Winit event, so let's just + // store the partial information, and add to it in the upcoming events + *event_info = finished_event_info.take(); + } else { + let (_, layout) = layouts.get_current_layout(); + let is_fake = { + let curr_event = finished_event_info.as_ref().unwrap(); + is_current_fake(curr_event, next_msg, layout) + }; + if is_fake { + finished_event_info = None; + } + } + } + if let Some(event_info) = finished_event_info { + let ev = event_info.finalize(); + return MatchResult::MessagesToDispatch(self.pending.complete_pending( + pending_token, + MessageAsKeyEvent { event: ev, is_synthetic: false }, + )); + } + MatchResult::TokenToRemove(pending_token) + }, + WM_DEADCHAR | WM_SYSDEADCHAR => { + let pending_token = self.pending.add_pending(); + *result = ProcResult::Value(0); + // At this point, we know that there isn't going to be any more events related + // to this key press + let event_info = self.event_info.lock().unwrap().take().unwrap(); + let ev = event_info.finalize(); + MatchResult::MessagesToDispatch(self.pending.complete_pending( + pending_token, + MessageAsKeyEvent { event: ev, is_synthetic: false }, + )) + }, + WM_CHAR | WM_SYSCHAR => { + let mut event_info = self.event_info.lock().unwrap(); + if event_info.is_none() { + trace!( + "Received a CHAR message but no `event_info` was available. The \ + message is probably IME, returning." + ); + return MatchResult::Nothing; + } + let pending_token = self.pending.add_pending(); + *result = ProcResult::Value(0); + let is_high_surrogate = (0xd800..=0xdbff).contains(&wparam); + let is_low_surrogate = (0xdc00..=0xdfff).contains(&wparam); + + let is_utf16 = is_high_surrogate || is_low_surrogate; + + if is_utf16 { + if let Some(ev_info) = event_info.as_mut() { + ev_info.utf16parts.push(wparam as u16); + } + } else { + // In this case, wparam holds a UTF-32 character. + // Let's encode it as UTF-16 and append it to the end of `utf16parts` + let utf16parts = match event_info.as_mut() { + Some(ev_info) => &mut ev_info.utf16parts, + None => { + warn!("The event_info was None when it was expected to be some"); + return MatchResult::TokenToRemove(pending_token); + }, + }; + let start_offset = utf16parts.len(); + let new_size = utf16parts.len() + 2; + utf16parts.resize(new_size, 0); + if let Some(ch) = char::from_u32(wparam as u32) { + let encode_len = ch.encode_utf16(&mut utf16parts[start_offset..]).len(); + let new_size = start_offset + encode_len; + utf16parts.resize(new_size, 0); + } + } + // It's important that we unlock the mutex, and create the pending event token + // before calling `next_msg` + std::mem::drop(event_info); + let next_msg = next_kbd_msg(hwnd); + let more_char_coming = next_msg + .map(|m| matches!(m.message, WM_CHAR | WM_SYSCHAR)) + .unwrap_or(false); + if more_char_coming { + // No need to produce an event just yet, because there are still more + // characters that need to be appended to this keyboard event + MatchResult::TokenToRemove(pending_token) + } else { + let mut event_info = self.event_info.lock().unwrap(); + let mut event_info = match event_info.take() { + Some(ev_info) => ev_info, + None => { + warn!("The event_info was None when it was expected to be some"); + return MatchResult::TokenToRemove(pending_token); + }, + }; + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + // It's okay to call `ToUnicode` here, because at this point the dead key + // is already consumed by the character. + let kbd_state = get_kbd_state(); + let mod_state = WindowsModifiers::active_modifiers(&kbd_state); + + let (_, layout) = layouts.get_current_layout(); + let ctrl_on = if layout.has_alt_graph { + let alt_on = mod_state.contains(WindowsModifiers::ALT); + !alt_on && mod_state.contains(WindowsModifiers::CONTROL) + } else { + mod_state.contains(WindowsModifiers::CONTROL) + }; + + // If Ctrl is not pressed, just use the text with all + // modifiers because that already consumed the dead key. Otherwise, + // we would interpret the character incorrectly, missing the dead key. + if !ctrl_on { + event_info.text = PartialText::System(event_info.utf16parts.clone()); + } else { + let mod_no_ctrl = mod_state.remove_only_ctrl(); + let num_lock_on = kbd_state[VK_NUMLOCK as usize] & 1 != 0; + let vkey = event_info.vkey; + let physical_key = &event_info.physical_key; + let key = layout.get_key(mod_no_ctrl, num_lock_on, vkey, physical_key); + event_info.text = PartialText::Text(key.to_text().map(SmolStr::new)); + } + let ev = event_info.finalize(); + MatchResult::MessagesToDispatch(self.pending.complete_pending( + pending_token, + MessageAsKeyEvent { event: ev, is_synthetic: false }, + )) + } + }, + WM_KEYUP | WM_SYSKEYUP => { + let pending_token = self.pending.add_pending(); + *result = ProcResult::Value(0); + + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + let event_info = PartialKeyEventInfo::from_message( + wparam, + lparam, + ElementState::Released, + &mut layouts, + ); + // We MUST release the layout lock before calling `next_kbd_msg`, otherwise it + // may deadlock + drop(layouts); + // It's important that we create the pending token before reading the next + // message. + let next_msg = next_kbd_msg(hwnd); + let mut valid_event_info = Some(event_info); + if let Some(next_msg) = next_msg { + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + let (_, layout) = layouts.get_current_layout(); + let is_fake = { + let event_info = valid_event_info.as_ref().unwrap(); + is_current_fake(event_info, next_msg, layout) + }; + if is_fake { + valid_event_info = None; + } + } + if let Some(event_info) = valid_event_info { + let event = event_info.finalize(); + return MatchResult::MessagesToDispatch(self.pending.complete_pending( + pending_token, + MessageAsKeyEvent { event, is_synthetic: false }, + )); + } + MatchResult::TokenToRemove(pending_token) + }, + _ => MatchResult::Nothing, + } + }; + let matcher_result = matcher(); + match matcher_result { + MatchResult::TokenToRemove(t) => self.pending.remove_pending(t), + MatchResult::MessagesToDispatch(m) => m, + MatchResult::Nothing => Vec::new(), + } + } + + // Allowing nominimal_bool lint because the `is_key_pressed` macro triggers this warning + // and I don't know of another way to resolve it and also keeping the macro + #[allow(clippy::nonminimal_bool)] + fn synthesize_kbd_state( + key_state: ElementState, + kbd_state: &[u8; 256], + ) -> Vec { + let mut key_events = Vec::new(); + + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + let (locale_id, _) = layouts.get_current_layout(); + + macro_rules! is_key_pressed { + ($vk:expr) => { + kbd_state[$vk as usize] & 0x80 != 0 + }; + } + + // Is caps-lock active? Note that this is different from caps-lock + // being held down. + let caps_lock_on = kbd_state[VK_CAPITAL as usize] & 1 != 0; + let num_lock_on = kbd_state[VK_NUMLOCK as usize] & 1 != 0; + + // We are synthesizing the press event for caps-lock first for the following reasons: + // 1. If caps-lock is *not* held down but *is* active, then we have to synthesize all + // printable keys, respecting the caps-lock state. + // 2. If caps-lock is held down, we could choose to synthesize its keypress after every + // other key, in which case all other keys *must* be synthesized as if the caps-lock + // state was be the opposite of what it currently is. + // -- + // For the sake of simplicity we are choosing to always synthesize + // caps-lock first, and always use the current caps-lock state + // to determine the produced text + if is_key_pressed!(VK_CAPITAL) { + let event = Self::create_synthetic( + VK_CAPITAL, + key_state, + caps_lock_on, + num_lock_on, + locale_id as HKL, + &mut layouts, + ); + if let Some(event) = event { + key_events.push(event); + } + } + let do_non_modifier = |key_events: &mut Vec<_>, layouts: &mut _| { + for vk in 0..256 { + match vk { + VK_CONTROL | VK_LCONTROL | VK_RCONTROL | VK_SHIFT | VK_LSHIFT | VK_RSHIFT + | VK_MENU | VK_LMENU | VK_RMENU | VK_CAPITAL => continue, + _ => (), + } + if !is_key_pressed!(vk) { + continue; + } + let event = Self::create_synthetic( + vk, + key_state, + caps_lock_on, + num_lock_on, + locale_id as HKL, + layouts, + ); + if let Some(event) = event { + key_events.push(event); + } + } + }; + let do_modifier = |key_events: &mut Vec<_>, layouts: &mut _| { + const CLEAR_MODIFIER_VKS: [VIRTUAL_KEY; 6] = + [VK_LCONTROL, VK_LSHIFT, VK_LMENU, VK_RCONTROL, VK_RSHIFT, VK_RMENU]; + for vk in CLEAR_MODIFIER_VKS.iter() { + if is_key_pressed!(*vk) { + let event = Self::create_synthetic( + *vk, + key_state, + caps_lock_on, + num_lock_on, + locale_id as HKL, + layouts, + ); + if let Some(event) = event { + key_events.push(event); + } + } + } + }; + + // Be cheeky and sequence modifier and non-modifier + // key events such that non-modifier keys are not affected + // by modifiers (except for caps-lock) + match key_state { + ElementState::Pressed => { + do_non_modifier(&mut key_events, &mut layouts); + do_modifier(&mut key_events, &mut layouts); + }, + ElementState::Released => { + do_modifier(&mut key_events, &mut layouts); + do_non_modifier(&mut key_events, &mut layouts); + }, + } + + key_events + } + + fn create_synthetic( + vk: VIRTUAL_KEY, + key_state: ElementState, + caps_lock_on: bool, + num_lock_on: bool, + locale_id: HKL, + layouts: &mut MutexGuard<'_, LayoutCache>, + ) -> Option { + let scancode = unsafe { MapVirtualKeyExW(vk as u32, MAPVK_VK_TO_VSC_EX, locale_id) }; + if scancode == 0 { + return None; + } + let scancode = scancode as ExScancode; + let physical_key = scancode_to_physicalkey(scancode as u32); + let mods = + if caps_lock_on { WindowsModifiers::CAPS_LOCK } else { WindowsModifiers::empty() }; + let layout = layouts.layouts.get(&(locale_id as u64)).unwrap(); + let logical_key = layout.get_key(mods, num_lock_on, vk, &physical_key); + let key_without_modifiers = + layout.get_key(WindowsModifiers::empty(), false, vk, &physical_key); + let text = if key_state == ElementState::Pressed { + logical_key.to_text().map(SmolStr::new) + } else { + None + }; + let event_info = PartialKeyEventInfo { + vkey: vk, + logical_key: PartialLogicalKey::This(logical_key.clone()), + key_without_modifiers, + key_state, + is_repeat: false, + physical_key, + location: get_location(scancode, locale_id), + utf16parts: Vec::with_capacity(8), + text: PartialText::Text(text.clone()), + }; + + let mut event = event_info.finalize(); + event.logical_key = logical_key; + event.platform_specific.text_with_all_modifiers = text; + Some(MessageAsKeyEvent { event, is_synthetic: true }) + } +} + +enum PartialText { + // Unicode + System(Vec), + Text(Option), +} + +enum PartialLogicalKey { + /// Use the text provided by the WM_CHAR messages and report that as a `Character` variant. If + /// the text consists of multiple grapheme clusters (user-perceived characters) that means that + /// dead key could not be combined with the second input, and in that case we should fall back + /// to using what would have without a dead-key input. + TextOr(Key), + + /// Use the value directly provided by this variant + This(Key), +} + +struct PartialKeyEventInfo { + vkey: VIRTUAL_KEY, + key_state: ElementState, + is_repeat: bool, + physical_key: PhysicalKey, + location: KeyLocation, + logical_key: PartialLogicalKey, + + key_without_modifiers: Key, + + /// The UTF-16 code units of the text that was produced by the keypress event. + /// This take all modifiers into account. Including CTRL + utf16parts: Vec, + + text: PartialText, +} + +impl PartialKeyEventInfo { + fn from_message( + wparam: WPARAM, + lparam: LPARAM, + state: ElementState, + layouts: &mut MutexGuard<'_, LayoutCache>, + ) -> Self { + const NO_MODS: WindowsModifiers = WindowsModifiers::empty(); + + let (_, layout) = layouts.get_current_layout(); + let lparam_struct = destructure_key_lparam(lparam); + let vkey = wparam as VIRTUAL_KEY; + let scancode = if lparam_struct.scancode == 0 { + // In some cases (often with media keys) the device reports a scancode of 0 but a + // valid virtual key. In these cases we obtain the scancode from the virtual key. + unsafe { MapVirtualKeyExW(vkey as u32, MAPVK_VK_TO_VSC_EX, layout.hkl as HKL) as u16 } + } else { + new_ex_scancode(lparam_struct.scancode, lparam_struct.extended) + }; + let physical_key = scancode_to_physicalkey(scancode as u32); + let location = get_location(scancode, layout.hkl as HKL); + + let kbd_state = get_kbd_state(); + let mods = WindowsModifiers::active_modifiers(&kbd_state); + let mods_without_ctrl = mods.remove_only_ctrl(); + let num_lock_on = kbd_state[VK_NUMLOCK as usize] & 1 != 0; + + // On Windows Ctrl+NumLock = Pause (and apparently Ctrl+Pause -> NumLock). In these cases + // the KeyCode still stores the real key, so in the name of consistency across platforms, we + // circumvent this mapping and force the key values to match the keycode. + // For more on this, read the article by Raymond Chen, titled: + // "Why does Ctrl+ScrollLock cancel dialogs?" + // https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503 + let code_as_key = if mods.contains(WindowsModifiers::CONTROL) { + match physical_key { + PhysicalKey::Code(KeyCode::NumLock) => Some(Key::Named(NamedKey::NumLock)), + PhysicalKey::Code(KeyCode::Pause) => Some(Key::Named(NamedKey::Pause)), + _ => None, + } + } else { + None + }; + + let preliminary_logical_key = + layout.get_key(mods_without_ctrl, num_lock_on, vkey, &physical_key); + let key_is_char = matches!(preliminary_logical_key, Key::Character(_)); + let is_pressed = state == ElementState::Pressed; + + let logical_key = if let Some(key) = code_as_key.clone() { + PartialLogicalKey::This(key) + } else if is_pressed && key_is_char && !mods.contains(WindowsModifiers::CONTROL) { + // In some cases we want to use the UNICHAR text for logical_key in order to allow + // dead keys to have an effect on the character reported by `logical_key`. + PartialLogicalKey::TextOr(preliminary_logical_key) + } else { + PartialLogicalKey::This(preliminary_logical_key) + }; + let key_without_modifiers = if let Some(key) = code_as_key { + key + } else { + match layout.get_key(NO_MODS, false, vkey, &physical_key) { + // We convert dead keys into their character. + // The reason for this is that `key_without_modifiers` is designed for key-bindings, + // but the US International layout treats `'` (apostrophe) as a dead key and the + // regular US layout treats it a character. In order for a single binding + // configuration to work with both layouts, we forward each dead key as a character. + Key::Dead(k) => { + if let Some(ch) = k { + // I'm avoiding the heap allocation. I don't want to talk about it :( + let mut utf8 = [0; 4]; + let s = ch.encode_utf8(&mut utf8); + Key::Character(SmolStr::new(s)) + } else { + Key::Unidentified(NativeKey::Unidentified) + } + }, + key => key, + } + }; + + PartialKeyEventInfo { + vkey, + key_state: state, + logical_key, + key_without_modifiers, + is_repeat: lparam_struct.is_repeat, + physical_key, + location, + utf16parts: Vec::with_capacity(8), + text: PartialText::System(Vec::new()), + } + } + + fn finalize(self) -> KeyEvent { + let mut char_with_all_modifiers = None; + if !self.utf16parts.is_empty() { + let os_string = OsString::from_wide(&self.utf16parts); + if let Ok(string) = os_string.into_string() { + char_with_all_modifiers = Some(SmolStr::new(string)); + } + } + + // The text without Ctrl + let mut text = None; + match self.text { + PartialText::System(wide) => { + if !wide.is_empty() { + let os_string = OsString::from_wide(&wide); + if let Ok(string) = os_string.into_string() { + text = Some(SmolStr::new(string)); + } + } + }, + PartialText::Text(s) => { + text = s.map(SmolStr::new); + }, + } + + let logical_key = match self.logical_key { + PartialLogicalKey::TextOr(fallback) => match text.as_ref() { + Some(s) => { + if s.grapheme_indices(true).count() > 1 { + fallback + } else { + Key::Character(s.clone()) + } + }, + None => Key::Unidentified(NativeKey::Windows(self.vkey)), + }, + PartialLogicalKey::This(v) => v, + }; + + KeyEvent { + physical_key: self.physical_key, + logical_key, + text, + location: self.location, + state: self.key_state, + repeat: self.is_repeat, + platform_specific: KeyEventExtra { + text_with_all_modifiers: char_with_all_modifiers, + key_without_modifiers: self.key_without_modifiers, + }, + } + } +} + +#[derive(Debug, Copy, Clone)] +struct KeyLParam { + pub scancode: u8, + pub extended: bool, + + /// This is `previous_state XOR transition_state`. See the lParam for WM_KEYDOWN and WM_KEYUP + /// for further details. + pub is_repeat: bool, +} + +fn destructure_key_lparam(lparam: LPARAM) -> KeyLParam { + let previous_state = (lparam >> 30) & 0x01; + let transition_state = (lparam >> 31) & 0x01; + KeyLParam { + scancode: ((lparam >> 16) & 0xff) as u8, + extended: ((lparam >> 24) & 0x01) != 0, + is_repeat: (previous_state ^ transition_state) != 0, + } +} + +#[inline] +fn new_ex_scancode(scancode: u8, extended: bool) -> ExScancode { + (scancode as u16) | (if extended { 0xe000 } else { 0 }) +} + +#[inline] +fn ex_scancode_from_lparam(lparam: LPARAM) -> ExScancode { + let lparam = destructure_key_lparam(lparam); + new_ex_scancode(lparam.scancode, lparam.extended) +} + +/// Gets the keyboard state as reported by messages that have been removed from the event queue. +/// See also: get_async_kbd_state +fn get_kbd_state() -> [u8; 256] { + unsafe { + let mut kbd_state: MaybeUninit<[u8; 256]> = MaybeUninit::uninit(); + GetKeyboardState(kbd_state.as_mut_ptr() as *mut u8); + kbd_state.assume_init() + } +} + +/// Gets the current keyboard state regardless of whether the corresponding keyboard events have +/// been removed from the event queue. See also: get_kbd_state +#[allow(clippy::uninit_assumed_init)] +fn get_async_kbd_state() -> [u8; 256] { + unsafe { + let mut kbd_state: [u8; 256] = [0; 256]; + for (vk, state) in kbd_state.iter_mut().enumerate() { + let vk = vk as VIRTUAL_KEY; + let async_state = GetAsyncKeyState(vk as i32); + let is_down = (async_state & (1 << 15)) != 0; + *state = if is_down { 0x80 } else { 0 }; + + if matches!(vk, VK_CAPITAL | VK_NUMLOCK | VK_SCROLL) { + // Toggle states aren't reported by `GetAsyncKeyState` + let toggle_state = GetKeyState(vk as i32); + let is_active = (toggle_state & 1) != 0; + *state |= u8::from(is_active); + } + } + kbd_state + } +} + +/// On windows, AltGr == Ctrl + Alt +/// +/// Due to this equivalence, the system generates a fake Ctrl key-press (and key-release) preceding +/// every AltGr key-press (and key-release). We check if the current event is a Ctrl event and if +/// the next event is a right Alt (AltGr) event. If this is the case, the current event must be the +/// fake Ctrl event. +fn is_current_fake(curr_info: &PartialKeyEventInfo, next_msg: MSG, layout: &Layout) -> bool { + let curr_is_ctrl = + matches!(curr_info.logical_key, PartialLogicalKey::This(Key::Named(NamedKey::Control))); + if layout.has_alt_graph { + let next_code = ex_scancode_from_lparam(next_msg.lParam); + let next_is_altgr = next_code == 0xe038; // 0xE038 is right alt + if curr_is_ctrl && next_is_altgr { + return true; + } + } + false +} + +enum PendingMessage { + Incomplete, + Complete(T), +} +struct IdentifiedPendingMessage { + token: PendingMessageToken, + msg: PendingMessage, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PendingMessageToken(u32); + +/// While processing keyboard events, we sometimes need +/// to call `PeekMessageW` (`next_msg`). But `PeekMessageW` +/// can also call the event handler, which means that the new event +/// gets processed before finishing to process the one that came before. +/// +/// This would mean that the application receives events in the wrong order. +/// To avoid this, we keep track whether we are in the middle of processing +/// an event. Such an event is an "incomplete pending event". A +/// "complete pending event" is one that has already finished processing, but +/// hasn't been dispatched to the application because there still are incomplete +/// pending events that came before it. +/// +/// When we finish processing an event, we call `complete_pending`, +/// which returns an empty array if there are incomplete pending events, but +/// if all pending events are complete, then it returns all pending events in +/// the order they were encountered. These can then be dispatched to the application +pub struct PendingEventQueue { + pending: Mutex>>, + next_id: AtomicU32, +} +impl PendingEventQueue { + /// Add a new pending event to the "pending queue" + pub fn add_pending(&self) -> PendingMessageToken { + let token = self.next_token(); + let mut pending = self.pending.lock().unwrap(); + pending.push(IdentifiedPendingMessage { token, msg: PendingMessage::Incomplete }); + token + } + + /// Returns all finished pending events + /// + /// If the return value is non empty, it's guaranteed to contain `msg` + /// + /// See also: `add_pending` + pub fn complete_pending(&self, token: PendingMessageToken, msg: T) -> Vec { + let mut pending = self.pending.lock().unwrap(); + let mut target_is_first = false; + for (i, pending_msg) in pending.iter_mut().enumerate() { + if pending_msg.token == token { + pending_msg.msg = PendingMessage::Complete(msg); + if i == 0 { + target_is_first = true; + } + break; + } + } + if target_is_first { + // If the message that we just finished was the first one in the pending queue, + // then we can empty the queue, and dispatch all of the messages. + Self::drain_pending(&mut *pending) + } else { + Vec::new() + } + } + + pub fn complete_multi(&self, msgs: Vec) -> Vec { + let mut pending = self.pending.lock().unwrap(); + if pending.is_empty() { + return msgs; + } + pending.reserve(msgs.len()); + for msg in msgs { + pending.push(IdentifiedPendingMessage { + token: self.next_token(), + msg: PendingMessage::Complete(msg), + }); + } + Vec::new() + } + + /// Returns all finished pending events + /// + /// It's safe to call this even if the element isn't in the list anymore + /// + /// See also: `add_pending` + pub fn remove_pending(&self, token: PendingMessageToken) -> Vec { + let mut pending = self.pending.lock().unwrap(); + let mut was_first = false; + if let Some(m) = pending.first() { + if m.token == token { + was_first = true; + } + } + pending.retain(|m| m.token != token); + if was_first { + Self::drain_pending(&mut *pending) + } else { + Vec::new() + } + } + + fn drain_pending(pending: &mut Vec>) -> Vec { + pending + .drain(..) + .map(|m| match m.msg { + PendingMessage::Complete(msg) => msg, + PendingMessage::Incomplete => { + panic!( + "Found an incomplete pending message when collecting messages. This \ + indicates a bug in winit." + ) + }, + }) + .collect() + } + + fn next_token(&self) -> PendingMessageToken { + // It's okay for the u32 to overflow here. Yes, that could mean + // that two different messages have the same token, + // but that would only happen after having about 4 billion + // messages sitting in the pending queue. + // + // In that case, having two identical tokens is the least of your concerns. + let id = self.next_id.fetch_add(1, Relaxed); + PendingMessageToken(id) + } +} +impl Default for PendingEventQueue { + fn default() -> Self { + PendingEventQueue { pending: Mutex::new(Vec::new()), next_id: AtomicU32::new(0) } + } +} + +/// WARNING: Due to using PeekMessage, the event handler +/// function may get called during this function. +/// (Re-entrance to the event handler) +/// +/// This can cause a deadlock if calling this function +/// while having a mutex locked. +/// +/// It can also cause code to get executed in a surprising order. +pub fn next_kbd_msg(hwnd: HWND) -> Option { + unsafe { + let mut next_msg = MaybeUninit::uninit(); + let peek_retval = + PeekMessageW(next_msg.as_mut_ptr(), hwnd, WM_KEYFIRST, WM_KEYLAST, PM_NOREMOVE); + (peek_retval != 0).then(|| next_msg.assume_init()) + } +} + +fn get_location(scancode: ExScancode, hkl: HKL) -> KeyLocation { + const ABNT_C2: VIRTUAL_KEY = VK_ABNT_C2 as VIRTUAL_KEY; + + let extension = 0xe000; + let extended = (scancode & extension) == extension; + let vkey = unsafe { MapVirtualKeyExW(scancode as u32, MAPVK_VSC_TO_VK_EX, hkl) as VIRTUAL_KEY }; + + // Use the native VKEY and the extended flag to cover most cases + // This is taken from the `druid` GUI library, specifically + // druid-shell/src/platform/windows/keyboard.rs + match vkey { + VK_LSHIFT | VK_LCONTROL | VK_LMENU | VK_LWIN => KeyLocation::Left, + VK_RSHIFT | VK_RCONTROL | VK_RMENU | VK_RWIN => KeyLocation::Right, + VK_RETURN if extended => KeyLocation::Numpad, + VK_INSERT | VK_DELETE | VK_END | VK_DOWN | VK_NEXT | VK_LEFT | VK_CLEAR | VK_RIGHT + | VK_HOME | VK_UP | VK_PRIOR => { + if extended { + KeyLocation::Standard + } else { + KeyLocation::Numpad + } + }, + VK_NUMPAD0 | VK_NUMPAD1 | VK_NUMPAD2 | VK_NUMPAD3 | VK_NUMPAD4 | VK_NUMPAD5 + | VK_NUMPAD6 | VK_NUMPAD7 | VK_NUMPAD8 | VK_NUMPAD9 | VK_DECIMAL | VK_DIVIDE + | VK_MULTIPLY | VK_SUBTRACT | VK_ADD | ABNT_C2 => KeyLocation::Numpad, + _ => KeyLocation::Standard, + } +} + +pub(crate) fn physicalkey_to_scancode(physical_key: PhysicalKey) -> Option { + // See `scancode_to_physicalkey` for more info + + let hkl = unsafe { GetKeyboardLayout(0) }; + + let primary_lang_id = primarylangid(loword(hkl as u32)); + let is_korean = primary_lang_id as u32 == LANG_KOREAN; + + let code = match physical_key { + PhysicalKey::Code(code) => code, + PhysicalKey::Unidentified(code) => { + return match code { + NativeKeyCode::Windows(scancode) => Some(scancode as u32), + _ => None, + }; + }, + }; + + match code { + KeyCode::Backquote => Some(0x0029), + KeyCode::Backslash => Some(0x002b), + KeyCode::Backspace => Some(0x000e), + KeyCode::BracketLeft => Some(0x001a), + KeyCode::BracketRight => Some(0x001b), + KeyCode::Comma => Some(0x0033), + KeyCode::Digit0 => Some(0x000b), + KeyCode::Digit1 => Some(0x0002), + KeyCode::Digit2 => Some(0x0003), + KeyCode::Digit3 => Some(0x0004), + KeyCode::Digit4 => Some(0x0005), + KeyCode::Digit5 => Some(0x0006), + KeyCode::Digit6 => Some(0x0007), + KeyCode::Digit7 => Some(0x0008), + KeyCode::Digit8 => Some(0x0009), + KeyCode::Digit9 => Some(0x000a), + KeyCode::Equal => Some(0x000d), + KeyCode::IntlBackslash => Some(0x0056), + KeyCode::IntlRo => Some(0x0073), + KeyCode::IntlYen => Some(0x007d), + KeyCode::KeyA => Some(0x001e), + KeyCode::KeyB => Some(0x0030), + KeyCode::KeyC => Some(0x002e), + KeyCode::KeyD => Some(0x0020), + KeyCode::KeyE => Some(0x0012), + KeyCode::KeyF => Some(0x0021), + KeyCode::KeyG => Some(0x0022), + KeyCode::KeyH => Some(0x0023), + KeyCode::KeyI => Some(0x0017), + KeyCode::KeyJ => Some(0x0024), + KeyCode::KeyK => Some(0x0025), + KeyCode::KeyL => Some(0x0026), + KeyCode::KeyM => Some(0x0032), + KeyCode::KeyN => Some(0x0031), + KeyCode::KeyO => Some(0x0018), + KeyCode::KeyP => Some(0x0019), + KeyCode::KeyQ => Some(0x0010), + KeyCode::KeyR => Some(0x0013), + KeyCode::KeyS => Some(0x001f), + KeyCode::KeyT => Some(0x0014), + KeyCode::KeyU => Some(0x0016), + KeyCode::KeyV => Some(0x002f), + KeyCode::KeyW => Some(0x0011), + KeyCode::KeyX => Some(0x002d), + KeyCode::KeyY => Some(0x0015), + KeyCode::KeyZ => Some(0x002c), + KeyCode::Minus => Some(0x000c), + KeyCode::Period => Some(0x0034), + KeyCode::Quote => Some(0x0028), + KeyCode::Semicolon => Some(0x0027), + KeyCode::Slash => Some(0x0035), + KeyCode::AltLeft => Some(0x0038), + KeyCode::AltRight => Some(0xe038), + KeyCode::CapsLock => Some(0x003a), + KeyCode::ContextMenu => Some(0xe05d), + KeyCode::ControlLeft => Some(0x001d), + KeyCode::ControlRight => Some(0xe01d), + KeyCode::Enter => Some(0x001c), + KeyCode::SuperLeft => Some(0xe05b), + KeyCode::SuperRight => Some(0xe05c), + KeyCode::ShiftLeft => Some(0x002a), + KeyCode::ShiftRight => Some(0x0036), + KeyCode::Space => Some(0x0039), + KeyCode::Tab => Some(0x000f), + KeyCode::Convert => Some(0x0079), + KeyCode::Lang1 => { + if is_korean { + Some(0xe0f2) + } else { + Some(0x0072) + } + }, + KeyCode::Lang2 => { + if is_korean { + Some(0xe0f1) + } else { + Some(0x0071) + } + }, + KeyCode::KanaMode => Some(0x0070), + KeyCode::NonConvert => Some(0x007b), + KeyCode::Delete => Some(0xe053), + KeyCode::End => Some(0xe04f), + KeyCode::Home => Some(0xe047), + KeyCode::Insert => Some(0xe052), + KeyCode::PageDown => Some(0xe051), + KeyCode::PageUp => Some(0xe049), + KeyCode::ArrowDown => Some(0xe050), + KeyCode::ArrowLeft => Some(0xe04b), + KeyCode::ArrowRight => Some(0xe04d), + KeyCode::ArrowUp => Some(0xe048), + KeyCode::NumLock => Some(0xe045), + KeyCode::Numpad0 => Some(0x0052), + KeyCode::Numpad1 => Some(0x004f), + KeyCode::Numpad2 => Some(0x0050), + KeyCode::Numpad3 => Some(0x0051), + KeyCode::Numpad4 => Some(0x004b), + KeyCode::Numpad5 => Some(0x004c), + KeyCode::Numpad6 => Some(0x004d), + KeyCode::Numpad7 => Some(0x0047), + KeyCode::Numpad8 => Some(0x0048), + KeyCode::Numpad9 => Some(0x0049), + KeyCode::NumpadAdd => Some(0x004e), + KeyCode::NumpadComma => Some(0x007e), + KeyCode::NumpadDecimal => Some(0x0053), + KeyCode::NumpadDivide => Some(0xe035), + KeyCode::NumpadEnter => Some(0xe01c), + KeyCode::NumpadEqual => Some(0x0059), + KeyCode::NumpadMultiply => Some(0x0037), + KeyCode::NumpadSubtract => Some(0x004a), + KeyCode::Escape => Some(0x0001), + KeyCode::F1 => Some(0x003b), + KeyCode::F2 => Some(0x003c), + KeyCode::F3 => Some(0x003d), + KeyCode::F4 => Some(0x003e), + KeyCode::F5 => Some(0x003f), + KeyCode::F6 => Some(0x0040), + KeyCode::F7 => Some(0x0041), + KeyCode::F8 => Some(0x0042), + KeyCode::F9 => Some(0x0043), + KeyCode::F10 => Some(0x0044), + KeyCode::F11 => Some(0x0057), + KeyCode::F12 => Some(0x0058), + KeyCode::F13 => Some(0x0064), + KeyCode::F14 => Some(0x0065), + KeyCode::F15 => Some(0x0066), + KeyCode::F16 => Some(0x0067), + KeyCode::F17 => Some(0x0068), + KeyCode::F18 => Some(0x0069), + KeyCode::F19 => Some(0x006a), + KeyCode::F20 => Some(0x006b), + KeyCode::F21 => Some(0x006c), + KeyCode::F22 => Some(0x006d), + KeyCode::F23 => Some(0x006e), + KeyCode::F24 => Some(0x0076), + KeyCode::PrintScreen => Some(0xe037), + // KeyCode::PrintScreen => Some(0x0054), // Alt + PrintScreen + KeyCode::ScrollLock => Some(0x0046), + KeyCode::Pause => Some(0x0045), + // KeyCode::Pause => Some(0xE046), // Ctrl + Pause + KeyCode::BrowserBack => Some(0xe06a), + KeyCode::BrowserFavorites => Some(0xe066), + KeyCode::BrowserForward => Some(0xe069), + KeyCode::BrowserHome => Some(0xe032), + KeyCode::BrowserRefresh => Some(0xe067), + KeyCode::BrowserSearch => Some(0xe065), + KeyCode::BrowserStop => Some(0xe068), + KeyCode::LaunchApp1 => Some(0xe06b), + KeyCode::LaunchApp2 => Some(0xe021), + KeyCode::LaunchMail => Some(0xe06c), + KeyCode::MediaPlayPause => Some(0xe022), + KeyCode::MediaSelect => Some(0xe06d), + KeyCode::MediaStop => Some(0xe024), + KeyCode::MediaTrackNext => Some(0xe019), + KeyCode::MediaTrackPrevious => Some(0xe010), + KeyCode::Power => Some(0xe05e), + KeyCode::AudioVolumeDown => Some(0xe02e), + KeyCode::AudioVolumeMute => Some(0xe020), + KeyCode::AudioVolumeUp => Some(0xe030), + _ => None, + } +} + +pub(crate) fn scancode_to_physicalkey(scancode: u32) -> PhysicalKey { + // See: https://www.win.tue.nl/~aeb/linux/kbd/scancodes-1.html + // and: https://www.w3.org/TR/uievents-code/ + // and: The widget/NativeKeyToDOMCodeName.h file in the firefox source + + PhysicalKey::Code(match scancode { + 0x0029 => KeyCode::Backquote, + 0x002b => KeyCode::Backslash, + 0x000e => KeyCode::Backspace, + 0x001a => KeyCode::BracketLeft, + 0x001b => KeyCode::BracketRight, + 0x0033 => KeyCode::Comma, + 0x000b => KeyCode::Digit0, + 0x0002 => KeyCode::Digit1, + 0x0003 => KeyCode::Digit2, + 0x0004 => KeyCode::Digit3, + 0x0005 => KeyCode::Digit4, + 0x0006 => KeyCode::Digit5, + 0x0007 => KeyCode::Digit6, + 0x0008 => KeyCode::Digit7, + 0x0009 => KeyCode::Digit8, + 0x000a => KeyCode::Digit9, + 0x000d => KeyCode::Equal, + 0x0056 => KeyCode::IntlBackslash, + 0x0073 => KeyCode::IntlRo, + 0x007d => KeyCode::IntlYen, + 0x001e => KeyCode::KeyA, + 0x0030 => KeyCode::KeyB, + 0x002e => KeyCode::KeyC, + 0x0020 => KeyCode::KeyD, + 0x0012 => KeyCode::KeyE, + 0x0021 => KeyCode::KeyF, + 0x0022 => KeyCode::KeyG, + 0x0023 => KeyCode::KeyH, + 0x0017 => KeyCode::KeyI, + 0x0024 => KeyCode::KeyJ, + 0x0025 => KeyCode::KeyK, + 0x0026 => KeyCode::KeyL, + 0x0032 => KeyCode::KeyM, + 0x0031 => KeyCode::KeyN, + 0x0018 => KeyCode::KeyO, + 0x0019 => KeyCode::KeyP, + 0x0010 => KeyCode::KeyQ, + 0x0013 => KeyCode::KeyR, + 0x001f => KeyCode::KeyS, + 0x0014 => KeyCode::KeyT, + 0x0016 => KeyCode::KeyU, + 0x002f => KeyCode::KeyV, + 0x0011 => KeyCode::KeyW, + 0x002d => KeyCode::KeyX, + 0x0015 => KeyCode::KeyY, + 0x002c => KeyCode::KeyZ, + 0x000c => KeyCode::Minus, + 0x0034 => KeyCode::Period, + 0x0028 => KeyCode::Quote, + 0x0027 => KeyCode::Semicolon, + 0x0035 => KeyCode::Slash, + 0x0038 => KeyCode::AltLeft, + 0xe038 => KeyCode::AltRight, + 0x003a => KeyCode::CapsLock, + 0xe05d => KeyCode::ContextMenu, + 0x001d => KeyCode::ControlLeft, + 0xe01d => KeyCode::ControlRight, + 0x001c => KeyCode::Enter, + 0xe05b => KeyCode::SuperLeft, + 0xe05c => KeyCode::SuperRight, + 0x002a => KeyCode::ShiftLeft, + 0x0036 => KeyCode::ShiftRight, + 0x0039 => KeyCode::Space, + 0x000f => KeyCode::Tab, + 0x0079 => KeyCode::Convert, + 0x0072 => KeyCode::Lang1, // for non-Korean layout + 0xe0f2 => KeyCode::Lang1, // for Korean layout + 0x0071 => KeyCode::Lang2, // for non-Korean layout + 0xe0f1 => KeyCode::Lang2, // for Korean layout + 0x0070 => KeyCode::KanaMode, + 0x007b => KeyCode::NonConvert, + 0xe053 => KeyCode::Delete, + 0xe04f => KeyCode::End, + 0xe047 => KeyCode::Home, + 0xe052 => KeyCode::Insert, + 0xe051 => KeyCode::PageDown, + 0xe049 => KeyCode::PageUp, + 0xe050 => KeyCode::ArrowDown, + 0xe04b => KeyCode::ArrowLeft, + 0xe04d => KeyCode::ArrowRight, + 0xe048 => KeyCode::ArrowUp, + 0xe045 => KeyCode::NumLock, + 0x0052 => KeyCode::Numpad0, + 0x004f => KeyCode::Numpad1, + 0x0050 => KeyCode::Numpad2, + 0x0051 => KeyCode::Numpad3, + 0x004b => KeyCode::Numpad4, + 0x004c => KeyCode::Numpad5, + 0x004d => KeyCode::Numpad6, + 0x0047 => KeyCode::Numpad7, + 0x0048 => KeyCode::Numpad8, + 0x0049 => KeyCode::Numpad9, + 0x004e => KeyCode::NumpadAdd, + 0x007e => KeyCode::NumpadComma, + 0x0053 => KeyCode::NumpadDecimal, + 0xe035 => KeyCode::NumpadDivide, + 0xe01c => KeyCode::NumpadEnter, + 0x0059 => KeyCode::NumpadEqual, + 0x0037 => KeyCode::NumpadMultiply, + 0x004a => KeyCode::NumpadSubtract, + 0x0001 => KeyCode::Escape, + 0x003b => KeyCode::F1, + 0x003c => KeyCode::F2, + 0x003d => KeyCode::F3, + 0x003e => KeyCode::F4, + 0x003f => KeyCode::F5, + 0x0040 => KeyCode::F6, + 0x0041 => KeyCode::F7, + 0x0042 => KeyCode::F8, + 0x0043 => KeyCode::F9, + 0x0044 => KeyCode::F10, + 0x0057 => KeyCode::F11, + 0x0058 => KeyCode::F12, + 0x0064 => KeyCode::F13, + 0x0065 => KeyCode::F14, + 0x0066 => KeyCode::F15, + 0x0067 => KeyCode::F16, + 0x0068 => KeyCode::F17, + 0x0069 => KeyCode::F18, + 0x006a => KeyCode::F19, + 0x006b => KeyCode::F20, + 0x006c => KeyCode::F21, + 0x006d => KeyCode::F22, + 0x006e => KeyCode::F23, + 0x0076 => KeyCode::F24, + 0xe037 => KeyCode::PrintScreen, + 0x0054 => KeyCode::PrintScreen, // Alt + PrintScreen + 0x0046 => KeyCode::ScrollLock, + 0x0045 => KeyCode::Pause, + 0xe046 => KeyCode::Pause, // Ctrl + Pause + 0xe06a => KeyCode::BrowserBack, + 0xe066 => KeyCode::BrowserFavorites, + 0xe069 => KeyCode::BrowserForward, + 0xe032 => KeyCode::BrowserHome, + 0xe067 => KeyCode::BrowserRefresh, + 0xe065 => KeyCode::BrowserSearch, + 0xe068 => KeyCode::BrowserStop, + 0xe06b => KeyCode::LaunchApp1, + 0xe021 => KeyCode::LaunchApp2, + 0xe06c => KeyCode::LaunchMail, + 0xe022 => KeyCode::MediaPlayPause, + 0xe06d => KeyCode::MediaSelect, + 0xe024 => KeyCode::MediaStop, + 0xe019 => KeyCode::MediaTrackNext, + 0xe010 => KeyCode::MediaTrackPrevious, + 0xe05e => KeyCode::Power, + 0xe02e => KeyCode::AudioVolumeDown, + 0xe020 => KeyCode::AudioVolumeMute, + 0xe030 => KeyCode::AudioVolumeUp, + _ => return PhysicalKey::Unidentified(NativeKeyCode::Windows(scancode as u16)), + }) +} diff --git a/vendor/winit/src/platform_impl/windows/keyboard_layout.rs b/vendor/winit/src/platform_impl/windows/keyboard_layout.rs new file mode 100644 index 00000000..5340e43d --- /dev/null +++ b/vendor/winit/src/platform_impl/windows/keyboard_layout.rs @@ -0,0 +1,986 @@ +use std::collections::hash_map::Entry; +use std::collections::{HashMap, HashSet}; +use std::ffi::OsString; +use std::os::windows::ffi::OsStringExt; +use std::sync::Mutex; + +use crate::utils::Lazy; +use smol_str::SmolStr; +use windows_sys::Win32::System::SystemServices::{LANG_JAPANESE, LANG_KOREAN}; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ + GetKeyState, GetKeyboardLayout, MapVirtualKeyExW, ToUnicodeEx, MAPVK_VK_TO_VSC_EX, VIRTUAL_KEY, + VK_ACCEPT, VK_ADD, VK_APPS, VK_ATTN, VK_BACK, VK_BROWSER_BACK, VK_BROWSER_FAVORITES, + VK_BROWSER_FORWARD, VK_BROWSER_HOME, VK_BROWSER_REFRESH, VK_BROWSER_SEARCH, VK_BROWSER_STOP, + VK_CANCEL, VK_CAPITAL, VK_CLEAR, VK_CONTROL, VK_CONVERT, VK_CRSEL, VK_DECIMAL, VK_DELETE, + VK_DIVIDE, VK_DOWN, VK_END, VK_EREOF, VK_ESCAPE, VK_EXECUTE, VK_EXSEL, VK_F1, VK_F10, VK_F11, + VK_F12, VK_F13, VK_F14, VK_F15, VK_F16, VK_F17, VK_F18, VK_F19, VK_F2, VK_F20, VK_F21, VK_F22, + VK_F23, VK_F24, VK_F3, VK_F4, VK_F5, VK_F6, VK_F7, VK_F8, VK_F9, VK_FINAL, VK_GAMEPAD_A, + VK_GAMEPAD_B, VK_GAMEPAD_DPAD_DOWN, VK_GAMEPAD_DPAD_LEFT, VK_GAMEPAD_DPAD_RIGHT, + VK_GAMEPAD_DPAD_UP, VK_GAMEPAD_LEFT_SHOULDER, VK_GAMEPAD_LEFT_THUMBSTICK_BUTTON, + VK_GAMEPAD_LEFT_THUMBSTICK_DOWN, VK_GAMEPAD_LEFT_THUMBSTICK_LEFT, + VK_GAMEPAD_LEFT_THUMBSTICK_RIGHT, VK_GAMEPAD_LEFT_THUMBSTICK_UP, VK_GAMEPAD_LEFT_TRIGGER, + VK_GAMEPAD_MENU, VK_GAMEPAD_RIGHT_SHOULDER, VK_GAMEPAD_RIGHT_THUMBSTICK_BUTTON, + VK_GAMEPAD_RIGHT_THUMBSTICK_DOWN, VK_GAMEPAD_RIGHT_THUMBSTICK_LEFT, + VK_GAMEPAD_RIGHT_THUMBSTICK_RIGHT, VK_GAMEPAD_RIGHT_THUMBSTICK_UP, VK_GAMEPAD_RIGHT_TRIGGER, + VK_GAMEPAD_VIEW, VK_GAMEPAD_X, VK_GAMEPAD_Y, VK_HANGUL, VK_HANJA, VK_HELP, VK_HOME, VK_ICO_00, + VK_ICO_CLEAR, VK_ICO_HELP, VK_INSERT, VK_JUNJA, VK_KANA, VK_KANJI, VK_LAUNCH_APP1, + VK_LAUNCH_APP2, VK_LAUNCH_MAIL, VK_LAUNCH_MEDIA_SELECT, VK_LBUTTON, VK_LCONTROL, VK_LEFT, + VK_LMENU, VK_LSHIFT, VK_LWIN, VK_MBUTTON, VK_MEDIA_NEXT_TRACK, VK_MEDIA_PLAY_PAUSE, + VK_MEDIA_PREV_TRACK, VK_MEDIA_STOP, VK_MENU, VK_MODECHANGE, VK_MULTIPLY, VK_NAVIGATION_ACCEPT, + VK_NAVIGATION_CANCEL, VK_NAVIGATION_DOWN, VK_NAVIGATION_LEFT, VK_NAVIGATION_MENU, + VK_NAVIGATION_RIGHT, VK_NAVIGATION_UP, VK_NAVIGATION_VIEW, VK_NEXT, VK_NONAME, VK_NONCONVERT, + VK_NUMLOCK, VK_NUMPAD0, VK_NUMPAD1, VK_NUMPAD2, VK_NUMPAD3, VK_NUMPAD4, VK_NUMPAD5, VK_NUMPAD6, + VK_NUMPAD7, VK_NUMPAD8, VK_NUMPAD9, VK_OEM_1, VK_OEM_102, VK_OEM_2, VK_OEM_3, VK_OEM_4, + VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_ATTN, VK_OEM_AUTO, VK_OEM_AX, VK_OEM_BACKTAB, + VK_OEM_CLEAR, VK_OEM_COMMA, VK_OEM_COPY, VK_OEM_CUSEL, VK_OEM_ENLW, VK_OEM_FINISH, + VK_OEM_FJ_LOYA, VK_OEM_FJ_MASSHOU, VK_OEM_FJ_ROYA, VK_OEM_FJ_TOUROKU, VK_OEM_JUMP, + VK_OEM_MINUS, VK_OEM_NEC_EQUAL, VK_OEM_PA1, VK_OEM_PA2, VK_OEM_PA3, VK_OEM_PERIOD, VK_OEM_PLUS, + VK_OEM_RESET, VK_OEM_WSCTRL, VK_PA1, VK_PACKET, VK_PAUSE, VK_PLAY, VK_PRINT, VK_PRIOR, + VK_PROCESSKEY, VK_RBUTTON, VK_RCONTROL, VK_RETURN, VK_RIGHT, VK_RMENU, VK_RSHIFT, VK_RWIN, + VK_SCROLL, VK_SELECT, VK_SEPARATOR, VK_SHIFT, VK_SLEEP, VK_SNAPSHOT, VK_SPACE, VK_SUBTRACT, + VK_TAB, VK_UP, VK_VOLUME_DOWN, VK_VOLUME_MUTE, VK_VOLUME_UP, VK_XBUTTON1, VK_XBUTTON2, VK_ZOOM, +}; +use windows_sys::Win32::UI::TextServices::HKL; + +use crate::keyboard::{Key, KeyCode, ModifiersState, NamedKey, NativeKey, PhysicalKey}; +use crate::platform_impl::{loword, primarylangid, scancode_to_physicalkey}; + +pub(crate) static LAYOUT_CACHE: Lazy> = + Lazy::new(|| Mutex::new(LayoutCache::default())); + +fn key_pressed(vkey: VIRTUAL_KEY) -> bool { + unsafe { (GetKeyState(vkey as i32) & (1 << 15)) == (1 << 15) } +} + +const NUMPAD_VKEYS: [VIRTUAL_KEY; 16] = [ + VK_NUMPAD0, + VK_NUMPAD1, + VK_NUMPAD2, + VK_NUMPAD3, + VK_NUMPAD4, + VK_NUMPAD5, + VK_NUMPAD6, + VK_NUMPAD7, + VK_NUMPAD8, + VK_NUMPAD9, + VK_MULTIPLY, + VK_ADD, + VK_SEPARATOR, + VK_SUBTRACT, + VK_DECIMAL, + VK_DIVIDE, +]; + +static NUMPAD_KEYCODES: Lazy> = Lazy::new(|| { + let mut keycodes = HashSet::new(); + keycodes.insert(KeyCode::Numpad0); + keycodes.insert(KeyCode::Numpad1); + keycodes.insert(KeyCode::Numpad2); + keycodes.insert(KeyCode::Numpad3); + keycodes.insert(KeyCode::Numpad4); + keycodes.insert(KeyCode::Numpad5); + keycodes.insert(KeyCode::Numpad6); + keycodes.insert(KeyCode::Numpad7); + keycodes.insert(KeyCode::Numpad8); + keycodes.insert(KeyCode::Numpad9); + keycodes.insert(KeyCode::NumpadMultiply); + keycodes.insert(KeyCode::NumpadAdd); + keycodes.insert(KeyCode::NumpadComma); + keycodes.insert(KeyCode::NumpadSubtract); + keycodes.insert(KeyCode::NumpadDecimal); + keycodes.insert(KeyCode::NumpadDivide); + keycodes +}); + +bitflags::bitflags! { + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct WindowsModifiers : u8 { + const SHIFT = 1 << 0; + const CONTROL = 1 << 1; + const ALT = 1 << 2; + const CAPS_LOCK = 1 << 3; + const FLAGS_END = 1 << 4; + } +} + +impl WindowsModifiers { + pub fn active_modifiers(key_state: &[u8; 256]) -> WindowsModifiers { + let shift = key_state[VK_SHIFT as usize] & 0x80 != 0; + let lshift = key_state[VK_LSHIFT as usize] & 0x80 != 0; + let rshift = key_state[VK_RSHIFT as usize] & 0x80 != 0; + + let control = key_state[VK_CONTROL as usize] & 0x80 != 0; + let lcontrol = key_state[VK_LCONTROL as usize] & 0x80 != 0; + let rcontrol = key_state[VK_RCONTROL as usize] & 0x80 != 0; + + let alt = key_state[VK_MENU as usize] & 0x80 != 0; + let lalt = key_state[VK_LMENU as usize] & 0x80 != 0; + let ralt = key_state[VK_RMENU as usize] & 0x80 != 0; + + let caps = key_state[VK_CAPITAL as usize] & 0x01 != 0; + + let mut result = WindowsModifiers::empty(); + if shift || lshift || rshift { + result.insert(WindowsModifiers::SHIFT); + } + if control || lcontrol || rcontrol { + result.insert(WindowsModifiers::CONTROL); + } + if alt || lalt || ralt { + result.insert(WindowsModifiers::ALT); + } + if caps { + result.insert(WindowsModifiers::CAPS_LOCK); + } + + result + } + + pub fn apply_to_kbd_state(self, key_state: &mut [u8; 256]) { + if self.intersects(Self::SHIFT) { + key_state[VK_SHIFT as usize] |= 0x80; + } else { + key_state[VK_SHIFT as usize] &= !0x80; + key_state[VK_LSHIFT as usize] &= !0x80; + key_state[VK_RSHIFT as usize] &= !0x80; + } + if self.intersects(Self::CONTROL) { + key_state[VK_CONTROL as usize] |= 0x80; + } else { + key_state[VK_CONTROL as usize] &= !0x80; + key_state[VK_LCONTROL as usize] &= !0x80; + key_state[VK_RCONTROL as usize] &= !0x80; + } + if self.intersects(Self::ALT) { + key_state[VK_MENU as usize] |= 0x80; + } else { + key_state[VK_MENU as usize] &= !0x80; + key_state[VK_LMENU as usize] &= !0x80; + key_state[VK_RMENU as usize] &= !0x80; + } + if self.intersects(Self::CAPS_LOCK) { + key_state[VK_CAPITAL as usize] |= 0x01; + } else { + key_state[VK_CAPITAL as usize] &= !0x01; + } + } + + /// Removes the control modifier if the alt modifier is not present. + /// This is useful because on Windows: (Control + Alt) == AltGr + /// but we don't want to interfere with the AltGr state. + pub fn remove_only_ctrl(mut self) -> WindowsModifiers { + if !self.contains(WindowsModifiers::ALT) { + self.remove(WindowsModifiers::CONTROL); + } + self + } +} + +pub(crate) struct Layout { + pub hkl: u64, + + /// Maps numpad keys from Windows virtual key to a `Key`. + /// + /// This is useful because some numpad keys generate different characters based on the locale. + /// For example `VK_DECIMAL` is sometimes "." and sometimes ",". Note: numpad-specific virtual + /// keys are only produced by Windows when the NumLock is active. + /// + /// Making this field separate from the `keys` field saves having to add NumLock as a modifier + /// to `WindowsModifiers`, which would double the number of items in keys. + pub numlock_on_keys: HashMap, + /// Like `numlock_on_keys` but this will map to the key that would be produced if numlock was + /// off. The keys of this map are identical to the keys of `numlock_on_keys`. + pub numlock_off_keys: HashMap, + + /// Maps a modifier state to group of key strings + /// We're not using `ModifiersState` here because that object cannot express caps lock, + /// but we need to handle caps lock too. + /// + /// This map shouldn't need to exist. + /// However currently this seems to be the only good way + /// of getting the label for the pressed key. Note that calling `ToUnicode` + /// just when the key is pressed/released would be enough if `ToUnicode` wouldn't + /// change the keyboard state (it clears the dead key). There is a flag to prevent + /// changing the state, but that flag requires Windows 10, version 1607 or newer) + pub keys: HashMap>, + pub has_alt_graph: bool, +} + +impl Layout { + pub fn get_key( + &self, + mods: WindowsModifiers, + num_lock_on: bool, + vkey: VIRTUAL_KEY, + physical_key: &PhysicalKey, + ) -> Key { + let native_code = NativeKey::Windows(vkey); + + let unknown_alt = vkey == VK_MENU; + if !unknown_alt { + // Here we try using the virtual key directly but if the virtual key doesn't distinguish + // between left and right alt, we can't report AltGr. Therefore, we only do this if the + // key is not the "unknown alt" key. + // + // The reason for using the virtual key directly is that `MapVirtualKeyExW` (used when + // building the keys map) sometimes maps virtual keys to odd scancodes that don't match + // the scancode coming from the KEYDOWN message for the same key. For example: `VK_LEFT` + // is mapped to `0x004B`, but the scancode for the left arrow is `0xE04B`. + let key_from_vkey = + vkey_to_non_char_key(vkey, native_code.clone(), self.hkl, self.has_alt_graph); + + if !matches!(key_from_vkey, Key::Unidentified(_)) { + return key_from_vkey; + } + } + if num_lock_on { + if let Some(key) = self.numlock_on_keys.get(&vkey) { + return key.clone(); + } + } else if let Some(key) = self.numlock_off_keys.get(&vkey) { + return key.clone(); + } + if let PhysicalKey::Code(code) = physical_key { + if let Some(keys) = self.keys.get(&mods) { + if let Some(key) = keys.get(code) { + return key.clone(); + } + } + } + Key::Unidentified(native_code) + } +} + +#[derive(Default)] +pub(crate) struct LayoutCache { + /// Maps locale identifiers (HKL) to layouts + pub layouts: HashMap, +} + +impl LayoutCache { + /// Checks whether the current layout is already known and + /// prepares the layout if it isn't known. + /// The current layout is then returned. + pub fn get_current_layout(&mut self) -> (u64, &Layout) { + let locale_id = unsafe { GetKeyboardLayout(0) } as u64; + match self.layouts.entry(locale_id) { + Entry::Occupied(entry) => (locale_id, entry.into_mut()), + Entry::Vacant(entry) => { + let layout = Self::prepare_layout(locale_id); + (locale_id, entry.insert(layout)) + }, + } + } + + pub fn get_agnostic_mods(&mut self) -> ModifiersState { + let (_, layout) = self.get_current_layout(); + let filter_out_altgr = layout.has_alt_graph && key_pressed(VK_RMENU); + let mut mods = ModifiersState::empty(); + mods.set(ModifiersState::SHIFT, key_pressed(VK_SHIFT)); + mods.set(ModifiersState::CONTROL, key_pressed(VK_CONTROL) && !filter_out_altgr); + mods.set(ModifiersState::ALT, key_pressed(VK_MENU) && !filter_out_altgr); + mods.set(ModifiersState::SUPER, key_pressed(VK_LWIN) || key_pressed(VK_RWIN)); + mods + } + + fn prepare_layout(locale_id: u64) -> Layout { + let mut layout = Layout { + hkl: locale_id, + numlock_on_keys: Default::default(), + numlock_off_keys: Default::default(), + keys: Default::default(), + has_alt_graph: false, + }; + + // We initialize the keyboard state with all zeros to + // simulate a scenario when no modifier is active. + let mut key_state = [0u8; 256]; + + // `MapVirtualKeyExW` maps (non-numpad-specific) virtual keys to scancodes as if numlock + // was off. We rely on this behavior to find all virtual keys which are not numpad-specific + // but map to the numpad. + // + // src_vkey: VK ==> scancode: u16 (on the numpad) + // + // Then we convert the source virtual key into a `Key` and the scancode into a virtual key + // to get the reverse mapping. + // + // src_vkey: VK ==> scancode: u16 (on the numpad) + // || || + // \/ \/ + // map_value: Key <- map_vkey: VK + layout.numlock_off_keys.reserve(NUMPAD_KEYCODES.len()); + for vk in 0..256 { + let scancode = unsafe { MapVirtualKeyExW(vk, MAPVK_VK_TO_VSC_EX, locale_id as HKL) }; + if scancode == 0 { + continue; + } + let keycode = match scancode_to_physicalkey(scancode) { + PhysicalKey::Code(code) => code, + // TODO: validate that we can skip on unidentified keys (probably never occurs?) + _ => continue, + }; + if !is_numpad_specific(vk as VIRTUAL_KEY) && NUMPAD_KEYCODES.contains(&keycode) { + let native_code = NativeKey::Windows(vk as VIRTUAL_KEY); + let map_vkey = keycode_to_vkey(keycode, locale_id); + if map_vkey == 0 { + continue; + } + let map_value = + vkey_to_non_char_key(vk as VIRTUAL_KEY, native_code, locale_id, false); + if matches!(map_value, Key::Unidentified(_)) { + continue; + } + layout.numlock_off_keys.insert(map_vkey, map_value); + } + } + + layout.numlock_on_keys.reserve(NUMPAD_VKEYS.len()); + for vk in NUMPAD_VKEYS.iter() { + let vk = (*vk) as u32; + let scancode = unsafe { MapVirtualKeyExW(vk, MAPVK_VK_TO_VSC_EX, locale_id as HKL) }; + let unicode = Self::to_unicode_string(&key_state, vk, scancode, locale_id); + if let ToUnicodeResult::Str(s) = unicode { + layout.numlock_on_keys.insert(vk as VIRTUAL_KEY, Key::Character(SmolStr::new(s))); + } + } + + // Iterate through every combination of modifiers + let mods_end = WindowsModifiers::FLAGS_END.bits(); + for mod_state in 0..mods_end { + let mut keys_for_this_mod = HashMap::with_capacity(256); + + let mod_state = WindowsModifiers::from_bits_retain(mod_state); + mod_state.apply_to_kbd_state(&mut key_state); + + // Virtual key values are in the domain [0, 255]. + // This is reinforced by the fact that the keyboard state array has 256 + // elements. This array is allowed to be indexed by virtual key values + // giving the key state for the virtual key used for indexing. + for vk in 0..256 { + let scancode = + unsafe { MapVirtualKeyExW(vk, MAPVK_VK_TO_VSC_EX, locale_id as HKL) }; + if scancode == 0 { + continue; + } + + let native_code = NativeKey::Windows(vk as VIRTUAL_KEY); + let key_code = match scancode_to_physicalkey(scancode) { + PhysicalKey::Code(code) => code, + // TODO: validate that we can skip on unidentified keys (probably never occurs?) + _ => continue, + }; + // Let's try to get the key from just the scancode and vk + // We don't necessarily know yet if AltGraph is present on this layout so we'll + // assume it isn't. Then we'll do a second pass where we set the "AltRight" keys to + // "AltGr" in case we find out that there's an AltGraph. + let preliminary_key = + vkey_to_non_char_key(vk as VIRTUAL_KEY, native_code, locale_id, false); + match preliminary_key { + Key::Unidentified(_) => (), + _ => { + keys_for_this_mod.insert(key_code, preliminary_key); + continue; + }, + } + + let unicode = Self::to_unicode_string(&key_state, vk, scancode, locale_id); + let key = match unicode { + ToUnicodeResult::Str(str) => Key::Character(SmolStr::new(str)), + ToUnicodeResult::Dead(dead_char) => { + // println!("{:?} - {:?} produced dead {:?}", key_code, mod_state, + // dead_char); + Key::Dead(dead_char) + }, + ToUnicodeResult::None => { + let has_alt = mod_state.contains(WindowsModifiers::ALT); + let has_ctrl = mod_state.contains(WindowsModifiers::CONTROL); + // HACK: `ToUnicodeEx` seems to fail getting the string for the numpad + // divide key, so we handle that explicitly here + if !has_alt && !has_ctrl && key_code == KeyCode::NumpadDivide { + Key::Character(SmolStr::new("/")) + } else { + // Just use the unidentified key, we got earlier + preliminary_key + } + }, + }; + + // Check for alt graph. + // The logic is that if a key pressed with no modifier produces + // a different `Character` from when it's pressed with CTRL+ALT then the layout + // has AltGr. + let ctrl_alt: WindowsModifiers = WindowsModifiers::CONTROL | WindowsModifiers::ALT; + let is_in_ctrl_alt = mod_state == ctrl_alt; + if !layout.has_alt_graph && is_in_ctrl_alt { + // Unwrapping here because if we are in the ctrl+alt modifier state + // then the alt modifier state must have come before. + let simple_keys = layout.keys.get(&WindowsModifiers::empty()).unwrap(); + if let Some(Key::Character(key_no_altgr)) = simple_keys.get(&key_code) { + if let Key::Character(key) = &key { + layout.has_alt_graph = key != key_no_altgr; + } + } + } + + keys_for_this_mod.insert(key_code, key); + } + layout.keys.insert(mod_state, keys_for_this_mod); + } + + // Second pass: replace right alt keys with AltGr if the layout has alt graph + if layout.has_alt_graph { + for mod_state in 0..mods_end { + let mod_state = WindowsModifiers::from_bits_retain(mod_state); + if let Some(keys) = layout.keys.get_mut(&mod_state) { + if let Some(key) = keys.get_mut(&KeyCode::AltRight) { + *key = Key::Named(NamedKey::AltGraph); + } + } + } + } + + layout + } + + fn to_unicode_string( + key_state: &[u8; 256], + vkey: u32, + scancode: u32, + locale_id: u64, + ) -> ToUnicodeResult { + unsafe { + let mut label_wide = [0u16; 8]; + let mut wide_len = ToUnicodeEx( + vkey, + scancode, + (&key_state[0]) as *const _, + (&mut label_wide[0]) as *mut _, + label_wide.len() as i32, + 0, + locale_id as HKL, + ); + if wide_len < 0 { + // If it's dead, we run `ToUnicode` again to consume the dead-key + wide_len = ToUnicodeEx( + vkey, + scancode, + (&key_state[0]) as *const _, + (&mut label_wide[0]) as *mut _, + label_wide.len() as i32, + 0, + locale_id as HKL, + ); + if wide_len > 0 { + let os_string = OsString::from_wide(&label_wide[0..wide_len as usize]); + if let Ok(label_str) = os_string.into_string() { + if let Some(ch) = label_str.chars().next() { + return ToUnicodeResult::Dead(Some(ch)); + } + } + } + return ToUnicodeResult::Dead(None); + } + if wide_len > 0 { + let os_string = OsString::from_wide(&label_wide[0..wide_len as usize]); + if let Ok(label_str) = os_string.into_string() { + return ToUnicodeResult::Str(label_str); + } + } + } + ToUnicodeResult::None + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +enum ToUnicodeResult { + Str(String), + Dead(Option), + None, +} + +fn is_numpad_specific(vk: VIRTUAL_KEY) -> bool { + matches!( + vk, + VK_NUMPAD0 + | VK_NUMPAD1 + | VK_NUMPAD2 + | VK_NUMPAD3 + | VK_NUMPAD4 + | VK_NUMPAD5 + | VK_NUMPAD6 + | VK_NUMPAD7 + | VK_NUMPAD8 + | VK_NUMPAD9 + | VK_ADD + | VK_SUBTRACT + | VK_DIVIDE + | VK_DECIMAL + | VK_SEPARATOR + ) +} + +fn keycode_to_vkey(keycode: KeyCode, hkl: u64) -> VIRTUAL_KEY { + let primary_lang_id = primarylangid(loword(hkl as u32)); + let is_korean = primary_lang_id as u32 == LANG_KOREAN; + let is_japanese = primary_lang_id as u32 == LANG_JAPANESE; + + match keycode { + KeyCode::Backquote => 0, + KeyCode::Backslash => 0, + KeyCode::BracketLeft => 0, + KeyCode::BracketRight => 0, + KeyCode::Comma => 0, + KeyCode::Digit0 => 0, + KeyCode::Digit1 => 0, + KeyCode::Digit2 => 0, + KeyCode::Digit3 => 0, + KeyCode::Digit4 => 0, + KeyCode::Digit5 => 0, + KeyCode::Digit6 => 0, + KeyCode::Digit7 => 0, + KeyCode::Digit8 => 0, + KeyCode::Digit9 => 0, + KeyCode::Equal => 0, + KeyCode::IntlBackslash => 0, + KeyCode::IntlRo => 0, + KeyCode::IntlYen => 0, + KeyCode::KeyA => 0, + KeyCode::KeyB => 0, + KeyCode::KeyC => 0, + KeyCode::KeyD => 0, + KeyCode::KeyE => 0, + KeyCode::KeyF => 0, + KeyCode::KeyG => 0, + KeyCode::KeyH => 0, + KeyCode::KeyI => 0, + KeyCode::KeyJ => 0, + KeyCode::KeyK => 0, + KeyCode::KeyL => 0, + KeyCode::KeyM => 0, + KeyCode::KeyN => 0, + KeyCode::KeyO => 0, + KeyCode::KeyP => 0, + KeyCode::KeyQ => 0, + KeyCode::KeyR => 0, + KeyCode::KeyS => 0, + KeyCode::KeyT => 0, + KeyCode::KeyU => 0, + KeyCode::KeyV => 0, + KeyCode::KeyW => 0, + KeyCode::KeyX => 0, + KeyCode::KeyY => 0, + KeyCode::KeyZ => 0, + KeyCode::Minus => 0, + KeyCode::Period => 0, + KeyCode::Quote => 0, + KeyCode::Semicolon => 0, + KeyCode::Slash => 0, + KeyCode::AltLeft => VK_LMENU, + KeyCode::AltRight => VK_RMENU, + KeyCode::Backspace => VK_BACK, + KeyCode::CapsLock => VK_CAPITAL, + KeyCode::ContextMenu => VK_APPS, + KeyCode::ControlLeft => VK_LCONTROL, + KeyCode::ControlRight => VK_RCONTROL, + KeyCode::Enter => VK_RETURN, + KeyCode::SuperLeft => VK_LWIN, + KeyCode::SuperRight => VK_RWIN, + KeyCode::ShiftLeft => VK_RSHIFT, + KeyCode::ShiftRight => VK_LSHIFT, + KeyCode::Space => VK_SPACE, + KeyCode::Tab => VK_TAB, + KeyCode::Convert => VK_CONVERT, + KeyCode::KanaMode => VK_KANA, + KeyCode::Lang1 if is_korean => VK_HANGUL, + KeyCode::Lang1 if is_japanese => VK_KANA, + KeyCode::Lang2 if is_korean => VK_HANJA, + KeyCode::Lang2 if is_japanese => 0, + KeyCode::Lang3 if is_japanese => VK_OEM_FINISH, + KeyCode::Lang4 if is_japanese => 0, + KeyCode::Lang5 if is_japanese => 0, + KeyCode::NonConvert => VK_NONCONVERT, + KeyCode::Delete => VK_DELETE, + KeyCode::End => VK_END, + KeyCode::Help => VK_HELP, + KeyCode::Home => VK_HOME, + KeyCode::Insert => VK_INSERT, + KeyCode::PageDown => VK_NEXT, + KeyCode::PageUp => VK_PRIOR, + KeyCode::ArrowDown => VK_DOWN, + KeyCode::ArrowLeft => VK_LEFT, + KeyCode::ArrowRight => VK_RIGHT, + KeyCode::ArrowUp => VK_UP, + KeyCode::NumLock => VK_NUMLOCK, + KeyCode::Numpad0 => VK_NUMPAD0, + KeyCode::Numpad1 => VK_NUMPAD1, + KeyCode::Numpad2 => VK_NUMPAD2, + KeyCode::Numpad3 => VK_NUMPAD3, + KeyCode::Numpad4 => VK_NUMPAD4, + KeyCode::Numpad5 => VK_NUMPAD5, + KeyCode::Numpad6 => VK_NUMPAD6, + KeyCode::Numpad7 => VK_NUMPAD7, + KeyCode::Numpad8 => VK_NUMPAD8, + KeyCode::Numpad9 => VK_NUMPAD9, + KeyCode::NumpadAdd => VK_ADD, + KeyCode::NumpadBackspace => VK_BACK, + KeyCode::NumpadClear => VK_CLEAR, + KeyCode::NumpadClearEntry => 0, + KeyCode::NumpadComma => VK_SEPARATOR, + KeyCode::NumpadDecimal => VK_DECIMAL, + KeyCode::NumpadDivide => VK_DIVIDE, + KeyCode::NumpadEnter => VK_RETURN, + KeyCode::NumpadEqual => 0, + KeyCode::NumpadHash => 0, + KeyCode::NumpadMemoryAdd => 0, + KeyCode::NumpadMemoryClear => 0, + KeyCode::NumpadMemoryRecall => 0, + KeyCode::NumpadMemoryStore => 0, + KeyCode::NumpadMemorySubtract => 0, + KeyCode::NumpadMultiply => VK_MULTIPLY, + KeyCode::NumpadParenLeft => 0, + KeyCode::NumpadParenRight => 0, + KeyCode::NumpadStar => 0, + KeyCode::NumpadSubtract => VK_SUBTRACT, + KeyCode::Escape => VK_ESCAPE, + KeyCode::Fn => 0, + KeyCode::FnLock => 0, + KeyCode::PrintScreen => VK_SNAPSHOT, + KeyCode::ScrollLock => VK_SCROLL, + KeyCode::Pause => VK_PAUSE, + KeyCode::BrowserBack => VK_BROWSER_BACK, + KeyCode::BrowserFavorites => VK_BROWSER_FAVORITES, + KeyCode::BrowserForward => VK_BROWSER_FORWARD, + KeyCode::BrowserHome => VK_BROWSER_HOME, + KeyCode::BrowserRefresh => VK_BROWSER_REFRESH, + KeyCode::BrowserSearch => VK_BROWSER_SEARCH, + KeyCode::BrowserStop => VK_BROWSER_STOP, + KeyCode::Eject => 0, + KeyCode::LaunchApp1 => VK_LAUNCH_APP1, + KeyCode::LaunchApp2 => VK_LAUNCH_APP2, + KeyCode::LaunchMail => VK_LAUNCH_MAIL, + KeyCode::MediaPlayPause => VK_MEDIA_PLAY_PAUSE, + KeyCode::MediaSelect => VK_LAUNCH_MEDIA_SELECT, + KeyCode::MediaStop => VK_MEDIA_STOP, + KeyCode::MediaTrackNext => VK_MEDIA_NEXT_TRACK, + KeyCode::MediaTrackPrevious => VK_MEDIA_PREV_TRACK, + KeyCode::Power => 0, + KeyCode::Sleep => 0, + KeyCode::AudioVolumeDown => VK_VOLUME_DOWN, + KeyCode::AudioVolumeMute => VK_VOLUME_MUTE, + KeyCode::AudioVolumeUp => VK_VOLUME_UP, + KeyCode::WakeUp => 0, + KeyCode::Hyper => 0, + KeyCode::Turbo => 0, + KeyCode::Abort => 0, + KeyCode::Resume => 0, + KeyCode::Suspend => 0, + KeyCode::Again => 0, + KeyCode::Copy => 0, + KeyCode::Cut => 0, + KeyCode::Find => 0, + KeyCode::Open => 0, + KeyCode::Paste => 0, + KeyCode::Props => 0, + KeyCode::Select => VK_SELECT, + KeyCode::Undo => 0, + KeyCode::Hiragana => 0, + KeyCode::Katakana => 0, + KeyCode::F1 => VK_F1, + KeyCode::F2 => VK_F2, + KeyCode::F3 => VK_F3, + KeyCode::F4 => VK_F4, + KeyCode::F5 => VK_F5, + KeyCode::F6 => VK_F6, + KeyCode::F7 => VK_F7, + KeyCode::F8 => VK_F8, + KeyCode::F9 => VK_F9, + KeyCode::F10 => VK_F10, + KeyCode::F11 => VK_F11, + KeyCode::F12 => VK_F12, + KeyCode::F13 => VK_F13, + KeyCode::F14 => VK_F14, + KeyCode::F15 => VK_F15, + KeyCode::F16 => VK_F16, + KeyCode::F17 => VK_F17, + KeyCode::F18 => VK_F18, + KeyCode::F19 => VK_F19, + KeyCode::F20 => VK_F20, + KeyCode::F21 => VK_F21, + KeyCode::F22 => VK_F22, + KeyCode::F23 => VK_F23, + KeyCode::F24 => VK_F24, + KeyCode::F25 => 0, + KeyCode::F26 => 0, + KeyCode::F27 => 0, + KeyCode::F28 => 0, + KeyCode::F29 => 0, + KeyCode::F30 => 0, + KeyCode::F31 => 0, + KeyCode::F32 => 0, + KeyCode::F33 => 0, + KeyCode::F34 => 0, + KeyCode::F35 => 0, + _ => 0, + } +} + +/// This converts virtual keys to `Key`s. Only virtual keys which can be unambiguously converted to +/// a `Key`, with only the information passed in as arguments, are converted. +/// +/// In other words: this function does not need to "prepare" the current layout in order to do +/// the conversion, but as such it cannot convert certain keys, like language-specific character +/// keys. +/// +/// The result includes all non-character keys defined within `Key` plus characters from numpad +/// keys. For example, backspace and tab are included. +fn vkey_to_non_char_key( + vkey: VIRTUAL_KEY, + native_code: NativeKey, + hkl: u64, + has_alt_graph: bool, +) -> Key { + // List of the Web key names and their corresponding platform-native key names: + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + + let primary_lang_id = primarylangid(loword(hkl as u32)); + let is_korean = primary_lang_id as u32 == LANG_KOREAN; + let is_japanese = primary_lang_id as u32 == LANG_JAPANESE; + + match vkey { + VK_LBUTTON => Key::Unidentified(NativeKey::Unidentified), // Mouse + VK_RBUTTON => Key::Unidentified(NativeKey::Unidentified), // Mouse + + // I don't think this can be represented with a Key + VK_CANCEL => Key::Unidentified(native_code), + + VK_MBUTTON => Key::Unidentified(NativeKey::Unidentified), // Mouse + VK_XBUTTON1 => Key::Unidentified(NativeKey::Unidentified), // Mouse + VK_XBUTTON2 => Key::Unidentified(NativeKey::Unidentified), // Mouse + VK_BACK => Key::Named(NamedKey::Backspace), + VK_TAB => Key::Named(NamedKey::Tab), + VK_CLEAR => Key::Named(NamedKey::Clear), + VK_RETURN => Key::Named(NamedKey::Enter), + VK_SHIFT => Key::Named(NamedKey::Shift), + VK_CONTROL => Key::Named(NamedKey::Control), + VK_MENU => Key::Named(NamedKey::Alt), + VK_PAUSE => Key::Named(NamedKey::Pause), + VK_CAPITAL => Key::Named(NamedKey::CapsLock), + + // VK_HANGEUL => Key::Named(NamedKey::HangulMode), // Deprecated in favour of VK_HANGUL + + // VK_HANGUL and VK_KANA are defined as the same constant, therefore + // we use appropriate conditions to differentiate between them + VK_HANGUL if is_korean => Key::Named(NamedKey::HangulMode), + VK_KANA if is_japanese => Key::Named(NamedKey::KanaMode), + + VK_JUNJA => Key::Named(NamedKey::JunjaMode), + VK_FINAL => Key::Named(NamedKey::FinalMode), + + // VK_HANJA and VK_KANJI are defined as the same constant, therefore + // we use appropriate conditions to differentiate between them + VK_HANJA if is_korean => Key::Named(NamedKey::HanjaMode), + VK_KANJI if is_japanese => Key::Named(NamedKey::KanjiMode), + + VK_ESCAPE => Key::Named(NamedKey::Escape), + VK_CONVERT => Key::Named(NamedKey::Convert), + VK_NONCONVERT => Key::Named(NamedKey::NonConvert), + VK_ACCEPT => Key::Named(NamedKey::Accept), + VK_MODECHANGE => Key::Named(NamedKey::ModeChange), + VK_SPACE => Key::Named(NamedKey::Space), + VK_PRIOR => Key::Named(NamedKey::PageUp), + VK_NEXT => Key::Named(NamedKey::PageDown), + VK_END => Key::Named(NamedKey::End), + VK_HOME => Key::Named(NamedKey::Home), + VK_LEFT => Key::Named(NamedKey::ArrowLeft), + VK_UP => Key::Named(NamedKey::ArrowUp), + VK_RIGHT => Key::Named(NamedKey::ArrowRight), + VK_DOWN => Key::Named(NamedKey::ArrowDown), + VK_SELECT => Key::Named(NamedKey::Select), + VK_PRINT => Key::Named(NamedKey::Print), + VK_EXECUTE => Key::Named(NamedKey::Execute), + VK_SNAPSHOT => Key::Named(NamedKey::PrintScreen), + VK_INSERT => Key::Named(NamedKey::Insert), + VK_DELETE => Key::Named(NamedKey::Delete), + VK_HELP => Key::Named(NamedKey::Help), + VK_LWIN => Key::Named(NamedKey::Super), + VK_RWIN => Key::Named(NamedKey::Super), + VK_APPS => Key::Named(NamedKey::ContextMenu), + VK_SLEEP => Key::Named(NamedKey::Standby), + + // Numpad keys produce characters + VK_NUMPAD0 => Key::Unidentified(native_code), + VK_NUMPAD1 => Key::Unidentified(native_code), + VK_NUMPAD2 => Key::Unidentified(native_code), + VK_NUMPAD3 => Key::Unidentified(native_code), + VK_NUMPAD4 => Key::Unidentified(native_code), + VK_NUMPAD5 => Key::Unidentified(native_code), + VK_NUMPAD6 => Key::Unidentified(native_code), + VK_NUMPAD7 => Key::Unidentified(native_code), + VK_NUMPAD8 => Key::Unidentified(native_code), + VK_NUMPAD9 => Key::Unidentified(native_code), + VK_MULTIPLY => Key::Unidentified(native_code), + VK_ADD => Key::Unidentified(native_code), + VK_SEPARATOR => Key::Unidentified(native_code), + VK_SUBTRACT => Key::Unidentified(native_code), + VK_DECIMAL => Key::Unidentified(native_code), + VK_DIVIDE => Key::Unidentified(native_code), + + VK_F1 => Key::Named(NamedKey::F1), + VK_F2 => Key::Named(NamedKey::F2), + VK_F3 => Key::Named(NamedKey::F3), + VK_F4 => Key::Named(NamedKey::F4), + VK_F5 => Key::Named(NamedKey::F5), + VK_F6 => Key::Named(NamedKey::F6), + VK_F7 => Key::Named(NamedKey::F7), + VK_F8 => Key::Named(NamedKey::F8), + VK_F9 => Key::Named(NamedKey::F9), + VK_F10 => Key::Named(NamedKey::F10), + VK_F11 => Key::Named(NamedKey::F11), + VK_F12 => Key::Named(NamedKey::F12), + VK_F13 => Key::Named(NamedKey::F13), + VK_F14 => Key::Named(NamedKey::F14), + VK_F15 => Key::Named(NamedKey::F15), + VK_F16 => Key::Named(NamedKey::F16), + VK_F17 => Key::Named(NamedKey::F17), + VK_F18 => Key::Named(NamedKey::F18), + VK_F19 => Key::Named(NamedKey::F19), + VK_F20 => Key::Named(NamedKey::F20), + VK_F21 => Key::Named(NamedKey::F21), + VK_F22 => Key::Named(NamedKey::F22), + VK_F23 => Key::Named(NamedKey::F23), + VK_F24 => Key::Named(NamedKey::F24), + VK_NAVIGATION_VIEW => Key::Unidentified(native_code), + VK_NAVIGATION_MENU => Key::Unidentified(native_code), + VK_NAVIGATION_UP => Key::Unidentified(native_code), + VK_NAVIGATION_DOWN => Key::Unidentified(native_code), + VK_NAVIGATION_LEFT => Key::Unidentified(native_code), + VK_NAVIGATION_RIGHT => Key::Unidentified(native_code), + VK_NAVIGATION_ACCEPT => Key::Unidentified(native_code), + VK_NAVIGATION_CANCEL => Key::Unidentified(native_code), + VK_NUMLOCK => Key::Named(NamedKey::NumLock), + VK_SCROLL => Key::Named(NamedKey::ScrollLock), + VK_OEM_NEC_EQUAL => Key::Unidentified(native_code), + // VK_OEM_FJ_JISHO => Key::Unidentified(native_code), // Conflicts with `VK_OEM_NEC_EQUAL` + VK_OEM_FJ_MASSHOU => Key::Unidentified(native_code), + VK_OEM_FJ_TOUROKU => Key::Unidentified(native_code), + VK_OEM_FJ_LOYA => Key::Unidentified(native_code), + VK_OEM_FJ_ROYA => Key::Unidentified(native_code), + VK_LSHIFT => Key::Named(NamedKey::Shift), + VK_RSHIFT => Key::Named(NamedKey::Shift), + VK_LCONTROL => Key::Named(NamedKey::Control), + VK_RCONTROL => Key::Named(NamedKey::Control), + VK_LMENU => Key::Named(NamedKey::Alt), + VK_RMENU => { + if has_alt_graph { + Key::Named(NamedKey::AltGraph) + } else { + Key::Named(NamedKey::Alt) + } + }, + VK_BROWSER_BACK => Key::Named(NamedKey::BrowserBack), + VK_BROWSER_FORWARD => Key::Named(NamedKey::BrowserForward), + VK_BROWSER_REFRESH => Key::Named(NamedKey::BrowserRefresh), + VK_BROWSER_STOP => Key::Named(NamedKey::BrowserStop), + VK_BROWSER_SEARCH => Key::Named(NamedKey::BrowserSearch), + VK_BROWSER_FAVORITES => Key::Named(NamedKey::BrowserFavorites), + VK_BROWSER_HOME => Key::Named(NamedKey::BrowserHome), + VK_VOLUME_MUTE => Key::Named(NamedKey::AudioVolumeMute), + VK_VOLUME_DOWN => Key::Named(NamedKey::AudioVolumeDown), + VK_VOLUME_UP => Key::Named(NamedKey::AudioVolumeUp), + VK_MEDIA_NEXT_TRACK => Key::Named(NamedKey::MediaTrackNext), + VK_MEDIA_PREV_TRACK => Key::Named(NamedKey::MediaTrackPrevious), + VK_MEDIA_STOP => Key::Named(NamedKey::MediaStop), + VK_MEDIA_PLAY_PAUSE => Key::Named(NamedKey::MediaPlayPause), + VK_LAUNCH_MAIL => Key::Named(NamedKey::LaunchMail), + VK_LAUNCH_MEDIA_SELECT => Key::Named(NamedKey::LaunchMediaPlayer), + VK_LAUNCH_APP1 => Key::Named(NamedKey::LaunchApplication1), + VK_LAUNCH_APP2 => Key::Named(NamedKey::LaunchApplication2), + + // This function only converts "non-printable" + VK_OEM_1 => Key::Unidentified(native_code), + VK_OEM_PLUS => Key::Unidentified(native_code), + VK_OEM_COMMA => Key::Unidentified(native_code), + VK_OEM_MINUS => Key::Unidentified(native_code), + VK_OEM_PERIOD => Key::Unidentified(native_code), + VK_OEM_2 => Key::Unidentified(native_code), + VK_OEM_3 => Key::Unidentified(native_code), + + VK_GAMEPAD_A => Key::Unidentified(native_code), + VK_GAMEPAD_B => Key::Unidentified(native_code), + VK_GAMEPAD_X => Key::Unidentified(native_code), + VK_GAMEPAD_Y => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_SHOULDER => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_SHOULDER => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_TRIGGER => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_TRIGGER => Key::Unidentified(native_code), + VK_GAMEPAD_DPAD_UP => Key::Unidentified(native_code), + VK_GAMEPAD_DPAD_DOWN => Key::Unidentified(native_code), + VK_GAMEPAD_DPAD_LEFT => Key::Unidentified(native_code), + VK_GAMEPAD_DPAD_RIGHT => Key::Unidentified(native_code), + VK_GAMEPAD_MENU => Key::Unidentified(native_code), + VK_GAMEPAD_VIEW => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_THUMBSTICK_BUTTON => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_THUMBSTICK_BUTTON => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_THUMBSTICK_UP => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_THUMBSTICK_DOWN => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_THUMBSTICK_RIGHT => Key::Unidentified(native_code), + VK_GAMEPAD_LEFT_THUMBSTICK_LEFT => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_THUMBSTICK_UP => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_THUMBSTICK_DOWN => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_THUMBSTICK_RIGHT => Key::Unidentified(native_code), + VK_GAMEPAD_RIGHT_THUMBSTICK_LEFT => Key::Unidentified(native_code), + + // This function only converts "non-printable" + VK_OEM_4 => Key::Unidentified(native_code), + VK_OEM_5 => Key::Unidentified(native_code), + VK_OEM_6 => Key::Unidentified(native_code), + VK_OEM_7 => Key::Unidentified(native_code), + VK_OEM_8 => Key::Unidentified(native_code), + VK_OEM_AX => Key::Unidentified(native_code), + VK_OEM_102 => Key::Unidentified(native_code), + + VK_ICO_HELP => Key::Unidentified(native_code), + VK_ICO_00 => Key::Unidentified(native_code), + + VK_PROCESSKEY => Key::Named(NamedKey::Process), + + VK_ICO_CLEAR => Key::Unidentified(native_code), + VK_PACKET => Key::Unidentified(native_code), + VK_OEM_RESET => Key::Unidentified(native_code), + VK_OEM_JUMP => Key::Unidentified(native_code), + VK_OEM_PA1 => Key::Unidentified(native_code), + VK_OEM_PA2 => Key::Unidentified(native_code), + VK_OEM_PA3 => Key::Unidentified(native_code), + VK_OEM_WSCTRL => Key::Unidentified(native_code), + VK_OEM_CUSEL => Key::Unidentified(native_code), + + VK_OEM_ATTN => Key::Named(NamedKey::Attn), + VK_OEM_FINISH => { + if is_japanese { + Key::Named(NamedKey::Katakana) + } else { + // This matches IE and Firefox behaviour according to + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + // At the time of writing, there is no `NamedKey::Finish` variant as + // Finish is not mentioned at https://w3c.github.io/uievents-key/ + // Also see: https://github.com/pyfisch/keyboard-types/issues/9 + Key::Unidentified(native_code) + } + }, + VK_OEM_COPY => Key::Named(NamedKey::Copy), + VK_OEM_AUTO => Key::Named(NamedKey::Hankaku), + VK_OEM_ENLW => Key::Named(NamedKey::Zenkaku), + VK_OEM_BACKTAB => Key::Named(NamedKey::Romaji), + VK_ATTN => Key::Named(NamedKey::KanaMode), + VK_CRSEL => Key::Named(NamedKey::CrSel), + VK_EXSEL => Key::Named(NamedKey::ExSel), + VK_EREOF => Key::Named(NamedKey::EraseEof), + VK_PLAY => Key::Named(NamedKey::Play), + VK_ZOOM => Key::Named(NamedKey::ZoomToggle), + VK_NONAME => Key::Unidentified(native_code), + VK_PA1 => Key::Unidentified(native_code), + VK_OEM_CLEAR => Key::Named(NamedKey::Clear), + _ => Key::Unidentified(native_code), + } +} diff --git a/vendor/winit/src/platform_impl/windows/minimal_ime.rs b/vendor/winit/src/platform_impl/windows/minimal_ime.rs new file mode 100644 index 00000000..71600abb --- /dev/null +++ b/vendor/winit/src/platform_impl/windows/minimal_ime.rs @@ -0,0 +1,67 @@ +use std::sync::{ + atomic::{AtomicBool, Ordering::Relaxed}, + Mutex, +}; + +use winapi::{ + shared::{ + minwindef::{LPARAM, WPARAM}, + windef::HWND, + }, + um::winuser, +}; + +use crate::platform_impl::platform::{event_loop::ProcResult, keyboard::next_kbd_msg}; + +pub struct MinimalIme { + // True if we're currently receiving messages belonging to a finished IME session. + getting_ime_text: AtomicBool, + + utf16parts: Mutex>, +} +impl Default for MinimalIme { + fn default() -> Self { + MinimalIme { + getting_ime_text: AtomicBool::new(false), + utf16parts: Mutex::new(Vec::with_capacity(16)), + } + } +} +impl MinimalIme { + pub(crate) fn process_message( + &self, + hwnd: HWND, + msg_kind: u32, + wparam: WPARAM, + _lparam: LPARAM, + result: &mut ProcResult, + ) -> Option { + match msg_kind { + winuser::WM_IME_ENDCOMPOSITION => { + self.getting_ime_text.store(true, Relaxed); + } + winuser::WM_CHAR | winuser::WM_SYSCHAR => { + if self.getting_ime_text.load(Relaxed) { + *result = ProcResult::Value(0); + self.utf16parts.lock().unwrap().push(wparam as u16); + // It's important that we push the new character and release the lock + // before getting the next message + let next_msg = next_kbd_msg(hwnd); + let more_char_coming = next_msg + .map(|m| matches!(m.message, winuser::WM_CHAR | winuser::WM_SYSCHAR)) + .unwrap_or(false); + if !more_char_coming { + let mut utf16parts = self.utf16parts.lock().unwrap(); + let result = String::from_utf16(&utf16parts).ok(); + utf16parts.clear(); + self.getting_ime_text.store(false, Relaxed); + return result; + } + } + } + _ => (), + } + + None + } +} diff --git a/vendor/winit/src/platform_impl/windows/mod.rs b/vendor/winit/src/platform_impl/windows/mod.rs new file mode 100644 index 00000000..9bb02fd4 --- /dev/null +++ b/vendor/winit/src/platform_impl/windows/mod.rs @@ -0,0 +1,196 @@ +use smol_str::SmolStr; +use windows_sys::Win32::Foundation::{HANDLE, HWND}; +use windows_sys::Win32::UI::WindowsAndMessaging::{HMENU, WINDOW_LONG_PTR_INDEX}; + +pub(crate) use self::event_loop::{ + ActiveEventLoop, EventLoop, EventLoopProxy, OwnedDisplayHandle, + PlatformSpecificEventLoopAttributes, +}; +pub(crate) use self::icon::{SelectedCursor, WinIcon}; +pub(crate) use self::keyboard::{physicalkey_to_scancode, scancode_to_physicalkey}; +pub(crate) use self::monitor::{MonitorHandle, VideoModeHandle}; +pub(crate) use self::window::Window; + +pub(crate) use self::icon::WinCursor as PlatformCustomCursor; +pub use self::icon::WinIcon as PlatformIcon; +pub(crate) use crate::cursor::OnlyCursorImageSource as PlatformCustomCursorSource; +use crate::platform_impl::Fullscreen; + +use crate::event::DeviceId as RootDeviceId; +use crate::icon::Icon; +use crate::keyboard::Key; +use crate::platform::windows::{BackdropType, Color, CornerPreference}; + +#[derive(Clone, Debug)] +pub struct PlatformSpecificWindowAttributes { + pub owner: Option, + pub menu: Option, + pub taskbar_icon: Option, + pub no_redirection_bitmap: bool, + pub drag_and_drop: bool, + pub skip_taskbar: bool, + pub class_name: String, + pub decoration_shadow: bool, + pub backdrop_type: BackdropType, + pub clip_children: bool, + pub border_color: Option, + pub title_background_color: Option, + pub title_text_color: Option, + pub corner_preference: Option, +} + +impl Default for PlatformSpecificWindowAttributes { + fn default() -> Self { + Self { + owner: None, + menu: None, + taskbar_icon: None, + no_redirection_bitmap: false, + drag_and_drop: true, + skip_taskbar: false, + class_name: "Window Class".to_string(), + decoration_shadow: false, + backdrop_type: BackdropType::default(), + clip_children: true, + border_color: None, + title_background_color: None, + title_text_color: None, + corner_preference: None, + } + } +} + +unsafe impl Send for PlatformSpecificWindowAttributes {} +unsafe impl Sync for PlatformSpecificWindowAttributes {} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId(u32); + +impl DeviceId { + pub const fn dummy() -> Self { + DeviceId(0) + } +} + +impl DeviceId { + pub fn persistent_identifier(&self) -> Option { + if self.0 != 0 { + raw_input::get_raw_input_device_name(self.0 as HANDLE) + } else { + None + } + } +} + +// Constant device ID, to be removed when this backend is updated to report real device IDs. +const DEVICE_ID: RootDeviceId = RootDeviceId(DeviceId(0)); + +fn wrap_device_id(id: u32) -> RootDeviceId { + RootDeviceId(DeviceId(id)) +} + +pub type OsError = std::io::Error; + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra { + pub text_with_all_modifiers: Option, + pub key_without_modifiers: Key, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId(HWND); +unsafe impl Send for WindowId {} +unsafe impl Sync for WindowId {} + +impl WindowId { + pub const fn dummy() -> Self { + WindowId(0) + } +} + +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0 as u64 + } +} + +impl From for HWND { + fn from(window_id: WindowId) -> Self { + window_id.0 + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id as HWND) + } +} + +#[inline(always)] +const fn get_xbutton_wparam(x: u32) -> u16 { + hiword(x) +} + +#[inline(always)] +const fn get_x_lparam(x: u32) -> i16 { + loword(x) as _ +} + +#[inline(always)] +const fn get_y_lparam(x: u32) -> i16 { + hiword(x) as _ +} + +#[inline(always)] +pub(crate) const fn primarylangid(lgid: u16) -> u16 { + lgid & 0x3ff +} + +#[inline(always)] +pub(crate) const fn loword(x: u32) -> u16 { + (x & 0xffff) as u16 +} + +#[inline(always)] +const fn hiword(x: u32) -> u16 { + ((x >> 16) & 0xffff) as u16 +} + +#[inline(always)] +unsafe fn get_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX) -> isize { + #[cfg(target_pointer_width = "64")] + return unsafe { windows_sys::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW(hwnd, nindex) }; + #[cfg(target_pointer_width = "32")] + return unsafe { + windows_sys::Win32::UI::WindowsAndMessaging::GetWindowLongW(hwnd, nindex) as isize + }; +} + +#[inline(always)] +unsafe fn set_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX, dwnewlong: isize) -> isize { + #[cfg(target_pointer_width = "64")] + return unsafe { + windows_sys::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW(hwnd, nindex, dwnewlong) + }; + #[cfg(target_pointer_width = "32")] + return unsafe { + windows_sys::Win32::UI::WindowsAndMessaging::SetWindowLongW(hwnd, nindex, dwnewlong as i32) + as isize + }; +} + +#[macro_use] +mod util; +mod dark_mode; +mod definitions; +mod dpi; +mod drop_handler; +mod event_loop; +mod icon; +mod ime; +mod keyboard; +mod keyboard_layout; +mod monitor; +mod raw_input; +mod window; +mod window_state; diff --git a/vendor/winit/src/platform_impl/windows/monitor.rs b/vendor/winit/src/platform_impl/windows/monitor.rs new file mode 100644 index 00000000..9a880db4 --- /dev/null +++ b/vendor/winit/src/platform_impl/windows/monitor.rs @@ -0,0 +1,256 @@ +use std::collections::{BTreeSet, VecDeque}; +use std::hash::Hash; +use std::{io, mem, ptr}; + +use windows_sys::Win32::Foundation::{BOOL, HWND, LPARAM, POINT, RECT}; +use windows_sys::Win32::Graphics::Gdi::{ + EnumDisplayMonitors, EnumDisplaySettingsExW, GetMonitorInfoW, MonitorFromPoint, + MonitorFromWindow, DEVMODEW, DM_BITSPERPEL, DM_DISPLAYFREQUENCY, DM_PELSHEIGHT, DM_PELSWIDTH, + ENUM_CURRENT_SETTINGS, HDC, HMONITOR, MONITORINFO, MONITORINFOEXW, MONITOR_DEFAULTTONEAREST, + MONITOR_DEFAULTTOPRIMARY, +}; + +use super::util::decode_wide; +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::monitor::VideoModeHandle as RootVideoModeHandle; +use crate::platform_impl::platform::dpi::{dpi_to_scale_factor, get_monitor_dpi}; +use crate::platform_impl::platform::util::has_flag; +use crate::platform_impl::platform::window::Window; + +#[derive(Clone)] +pub struct VideoModeHandle { + pub(crate) size: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate_millihertz: u32, + pub(crate) monitor: MonitorHandle, + // DEVMODEW is huge so we box it to avoid blowing up the size of winit::window::Fullscreen + pub(crate) native_video_mode: Box, +} + +impl PartialEq for VideoModeHandle { + fn eq(&self, other: &Self) -> bool { + self.size == other.size + && self.bit_depth == other.bit_depth + && self.refresh_rate_millihertz == other.refresh_rate_millihertz + && self.monitor == other.monitor + } +} + +impl Eq for VideoModeHandle {} + +impl std::hash::Hash for VideoModeHandle { + fn hash(&self, state: &mut H) { + self.size.hash(state); + self.bit_depth.hash(state); + self.refresh_rate_millihertz.hash(state); + self.monitor.hash(state); + } +} + +impl std::fmt::Debug for VideoModeHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VideoModeHandle") + .field("size", &self.size) + .field("bit_depth", &self.bit_depth) + .field("refresh_rate_millihertz", &self.refresh_rate_millihertz) + .field("monitor", &self.monitor) + .finish() + } +} + +impl VideoModeHandle { + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate_millihertz(&self) -> u32 { + self.refresh_rate_millihertz + } + + pub fn monitor(&self) -> MonitorHandle { + self.monitor.clone() + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)] +pub struct MonitorHandle(HMONITOR); + +// Send is not implemented for HMONITOR, we have to wrap it and implement it manually. +// For more info see: +// https://github.com/retep998/winapi-rs/issues/360 +// https://github.com/retep998/winapi-rs/issues/396 + +unsafe impl Send for MonitorHandle {} + +unsafe extern "system" fn monitor_enum_proc( + hmonitor: HMONITOR, + _hdc: HDC, + _place: *mut RECT, + data: LPARAM, +) -> BOOL { + let monitors = data as *mut VecDeque; + unsafe { (*monitors).push_back(MonitorHandle::new(hmonitor)) }; + true.into() // continue enumeration +} + +pub fn available_monitors() -> VecDeque { + let mut monitors: VecDeque = VecDeque::new(); + unsafe { + EnumDisplayMonitors( + 0, + ptr::null(), + Some(monitor_enum_proc), + &mut monitors as *mut _ as LPARAM, + ); + } + monitors +} + +pub fn primary_monitor() -> MonitorHandle { + const ORIGIN: POINT = POINT { x: 0, y: 0 }; + let hmonitor = unsafe { MonitorFromPoint(ORIGIN, MONITOR_DEFAULTTOPRIMARY) }; + MonitorHandle::new(hmonitor) +} + +pub fn current_monitor(hwnd: HWND) -> MonitorHandle { + let hmonitor = unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }; + MonitorHandle::new(hmonitor) +} + +impl Window { + pub fn available_monitors(&self) -> VecDeque { + available_monitors() + } + + pub fn primary_monitor(&self) -> Option { + let monitor = primary_monitor(); + Some(monitor) + } +} + +pub(crate) fn get_monitor_info(hmonitor: HMONITOR) -> Result { + let mut monitor_info: MONITORINFOEXW = unsafe { mem::zeroed() }; + monitor_info.monitorInfo.cbSize = mem::size_of::() as u32; + let status = unsafe { + GetMonitorInfoW(hmonitor, &mut monitor_info as *mut MONITORINFOEXW as *mut MONITORINFO) + }; + if status == false.into() { + Err(io::Error::last_os_error()) + } else { + Ok(monitor_info) + } +} + +impl MonitorHandle { + pub(crate) fn new(hmonitor: HMONITOR) -> Self { + MonitorHandle(hmonitor) + } + + #[inline] + pub fn name(&self) -> Option { + let monitor_info = get_monitor_info(self.0).unwrap(); + Some(decode_wide(&monitor_info.szDevice).to_string_lossy().to_string()) + } + + #[inline] + pub fn native_identifier(&self) -> String { + self.name().unwrap() + } + + #[inline] + pub fn hmonitor(&self) -> HMONITOR { + self.0 + } + + #[inline] + pub fn size(&self) -> PhysicalSize { + let rc_monitor = get_monitor_info(self.0).unwrap().monitorInfo.rcMonitor; + PhysicalSize { + width: (rc_monitor.right - rc_monitor.left) as u32, + height: (rc_monitor.bottom - rc_monitor.top) as u32, + } + } + + #[inline] + pub fn refresh_rate_millihertz(&self) -> Option { + let monitor_info = get_monitor_info(self.0).ok()?; + let device_name = monitor_info.szDevice.as_ptr(); + unsafe { + let mut mode: DEVMODEW = mem::zeroed(); + mode.dmSize = mem::size_of_val(&mode) as u16; + if EnumDisplaySettingsExW(device_name, ENUM_CURRENT_SETTINGS, &mut mode, 0) + == false.into() + { + None + } else { + Some(mode.dmDisplayFrequency * 1000) + } + } + } + + #[inline] + pub fn position(&self) -> PhysicalPosition { + get_monitor_info(self.0) + .map(|info| { + let rc_monitor = info.monitorInfo.rcMonitor; + PhysicalPosition { x: rc_monitor.left, y: rc_monitor.top } + }) + .unwrap_or(PhysicalPosition { x: 0, y: 0 }) + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + dpi_to_scale_factor(get_monitor_dpi(self.0).unwrap_or(96)) + } + + #[inline] + pub fn video_modes(&self) -> impl Iterator { + // EnumDisplaySettingsExW can return duplicate values (or some of the + // fields are probably changing, but we aren't looking at those fields + // anyway), so we're using a BTreeSet deduplicate + let mut modes = BTreeSet::::new(); + let mod_map = |mode: RootVideoModeHandle| mode.video_mode; + + let monitor_info = match get_monitor_info(self.0) { + Ok(monitor_info) => monitor_info, + Err(error) => { + tracing::warn!("Error from get_monitor_info: {error}"); + return modes.into_iter().map(mod_map); + }, + }; + + let device_name = monitor_info.szDevice.as_ptr(); + + let mut i = 0; + loop { + let mut mode: DEVMODEW = unsafe { mem::zeroed() }; + mode.dmSize = mem::size_of_val(&mode) as u16; + if unsafe { EnumDisplaySettingsExW(device_name, i, &mut mode, 0) } == false.into() { + break; + } + + const REQUIRED_FIELDS: u32 = + DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY; + assert!(has_flag(mode.dmFields, REQUIRED_FIELDS)); + + // Use Ord impl of RootVideoModeHandle + modes.insert(RootVideoModeHandle { + video_mode: VideoModeHandle { + size: (mode.dmPelsWidth, mode.dmPelsHeight), + bit_depth: mode.dmBitsPerPel as u16, + refresh_rate_millihertz: mode.dmDisplayFrequency * 1000, + monitor: self.clone(), + native_video_mode: Box::new(mode), + }, + }); + + i += 1; + } + + modes.into_iter().map(mod_map) + } +} diff --git a/vendor/winit/src/platform_impl/windows/raw_input.rs b/vendor/winit/src/platform_impl/windows/raw_input.rs new file mode 100644 index 00000000..f78a7bb0 --- /dev/null +++ b/vendor/winit/src/platform_impl/windows/raw_input.rs @@ -0,0 +1,301 @@ +use std::mem::{self, size_of}; +use std::ptr; + +use windows_sys::Win32::Devices::HumanInterfaceDevice::{ + HID_USAGE_GENERIC_KEYBOARD, HID_USAGE_GENERIC_MOUSE, HID_USAGE_PAGE_GENERIC, +}; +use windows_sys::Win32::Foundation::{HANDLE, HWND}; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ + MapVirtualKeyW, MAPVK_VK_TO_VSC_EX, VK_NUMLOCK, VK_SHIFT, +}; +use windows_sys::Win32::UI::Input::{ + GetRawInputData, GetRawInputDeviceInfoW, GetRawInputDeviceList, RegisterRawInputDevices, + HRAWINPUT, RAWINPUT, RAWINPUTDEVICE, RAWINPUTDEVICELIST, RAWINPUTHEADER, RAWKEYBOARD, + RIDEV_DEVNOTIFY, RIDEV_INPUTSINK, RIDEV_REMOVE, RIDI_DEVICEINFO, RIDI_DEVICENAME, + RID_DEVICE_INFO, RID_DEVICE_INFO_HID, RID_DEVICE_INFO_KEYBOARD, RID_DEVICE_INFO_MOUSE, + RID_INPUT, RIM_TYPEHID, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE, +}; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + RI_KEY_E0, RI_KEY_E1, RI_MOUSE_BUTTON_1_DOWN, RI_MOUSE_BUTTON_1_UP, RI_MOUSE_BUTTON_2_DOWN, + RI_MOUSE_BUTTON_2_UP, RI_MOUSE_BUTTON_3_DOWN, RI_MOUSE_BUTTON_3_UP, RI_MOUSE_BUTTON_4_DOWN, + RI_MOUSE_BUTTON_4_UP, RI_MOUSE_BUTTON_5_DOWN, RI_MOUSE_BUTTON_5_UP, +}; + +use super::scancode_to_physicalkey; +use crate::event::ElementState; +use crate::event_loop::DeviceEvents; +use crate::keyboard::{KeyCode, PhysicalKey}; +use crate::platform_impl::platform::util; + +#[allow(dead_code)] +pub fn get_raw_input_device_list() -> Option> { + let list_size = size_of::() as u32; + + let mut num_devices = 0; + let status = unsafe { GetRawInputDeviceList(ptr::null_mut(), &mut num_devices, list_size) }; + + if status == u32::MAX { + return None; + } + + let mut buffer = Vec::with_capacity(num_devices as _); + + let num_stored = + unsafe { GetRawInputDeviceList(buffer.as_mut_ptr(), &mut num_devices, list_size) }; + + if num_stored == u32::MAX { + return None; + } + + debug_assert_eq!(num_devices, num_stored); + + unsafe { buffer.set_len(num_devices as _) }; + + Some(buffer) +} + +#[allow(dead_code)] +pub enum RawDeviceInfo { + Mouse(RID_DEVICE_INFO_MOUSE), + Keyboard(RID_DEVICE_INFO_KEYBOARD), + Hid(RID_DEVICE_INFO_HID), +} + +impl From for RawDeviceInfo { + fn from(info: RID_DEVICE_INFO) -> Self { + unsafe { + match info.dwType { + RIM_TYPEMOUSE => RawDeviceInfo::Mouse(info.Anonymous.mouse), + RIM_TYPEKEYBOARD => RawDeviceInfo::Keyboard(info.Anonymous.keyboard), + RIM_TYPEHID => RawDeviceInfo::Hid(info.Anonymous.hid), + _ => unreachable!(), + } + } + } +} + +#[allow(dead_code)] +pub fn get_raw_input_device_info(handle: HANDLE) -> Option { + let mut info: RID_DEVICE_INFO = unsafe { mem::zeroed() }; + let info_size = size_of::() as u32; + + info.cbSize = info_size; + + let mut minimum_size = 0; + let status = unsafe { + GetRawInputDeviceInfoW(handle, RIDI_DEVICEINFO, &mut info as *mut _ as _, &mut minimum_size) + }; + + if status == u32::MAX || status == 0 { + return None; + } + + debug_assert_eq!(info_size, status); + + Some(info.into()) +} + +pub fn get_raw_input_device_name(handle: HANDLE) -> Option { + let mut minimum_size = 0; + let status = unsafe { + GetRawInputDeviceInfoW(handle, RIDI_DEVICENAME, ptr::null_mut(), &mut minimum_size) + }; + + if status != 0 { + return None; + } + + let mut name: Vec = Vec::with_capacity(minimum_size as _); + + let status = unsafe { + GetRawInputDeviceInfoW(handle, RIDI_DEVICENAME, name.as_ptr() as _, &mut minimum_size) + }; + + if status == u32::MAX || status == 0 { + return None; + } + + debug_assert_eq!(minimum_size, status); + + unsafe { name.set_len(minimum_size as _) }; + + util::decode_wide(&name).into_string().ok() +} + +pub fn register_raw_input_devices(devices: &[RAWINPUTDEVICE]) -> bool { + let device_size = size_of::() as u32; + + unsafe { + RegisterRawInputDevices(devices.as_ptr(), devices.len() as u32, device_size) == true.into() + } +} + +pub fn register_all_mice_and_keyboards_for_raw_input( + mut window_handle: HWND, + filter: DeviceEvents, +) -> bool { + // RIDEV_DEVNOTIFY: receive hotplug events + // RIDEV_INPUTSINK: receive events even if we're not in the foreground + // RIDEV_REMOVE: don't receive device events (requires NULL hwndTarget) + let flags = match filter { + DeviceEvents::Never => { + window_handle = 0; + RIDEV_REMOVE + }, + DeviceEvents::WhenFocused => RIDEV_DEVNOTIFY, + DeviceEvents::Always => RIDEV_DEVNOTIFY | RIDEV_INPUTSINK, + }; + + let devices: [RAWINPUTDEVICE; 2] = [ + RAWINPUTDEVICE { + usUsagePage: HID_USAGE_PAGE_GENERIC, + usUsage: HID_USAGE_GENERIC_MOUSE, + dwFlags: flags, + hwndTarget: window_handle, + }, + RAWINPUTDEVICE { + usUsagePage: HID_USAGE_PAGE_GENERIC, + usUsage: HID_USAGE_GENERIC_KEYBOARD, + dwFlags: flags, + hwndTarget: window_handle, + }, + ]; + + register_raw_input_devices(&devices) +} + +pub fn get_raw_input_data(handle: HRAWINPUT) -> Option { + let mut data: RAWINPUT = unsafe { mem::zeroed() }; + let mut data_size = size_of::() as u32; + let header_size = size_of::() as u32; + + let status = unsafe { + GetRawInputData(handle, RID_INPUT, &mut data as *mut _ as _, &mut data_size, header_size) + }; + + if status == u32::MAX || status == 0 { + return None; + } + + Some(data) +} + +fn button_flags_to_element_state( + button_flags: u32, + down_flag: u32, + up_flag: u32, +) -> Option { + // We assume the same button won't be simultaneously pressed and released. + if util::has_flag(button_flags, down_flag) { + Some(ElementState::Pressed) + } else if util::has_flag(button_flags, up_flag) { + Some(ElementState::Released) + } else { + None + } +} + +pub fn get_raw_mouse_button_state(button_flags: u32) -> [Option; 5] { + [ + button_flags_to_element_state(button_flags, RI_MOUSE_BUTTON_1_DOWN, RI_MOUSE_BUTTON_1_UP), + button_flags_to_element_state(button_flags, RI_MOUSE_BUTTON_2_DOWN, RI_MOUSE_BUTTON_2_UP), + button_flags_to_element_state(button_flags, RI_MOUSE_BUTTON_3_DOWN, RI_MOUSE_BUTTON_3_UP), + button_flags_to_element_state(button_flags, RI_MOUSE_BUTTON_4_DOWN, RI_MOUSE_BUTTON_4_UP), + button_flags_to_element_state(button_flags, RI_MOUSE_BUTTON_5_DOWN, RI_MOUSE_BUTTON_5_UP), + ] +} + +pub fn get_keyboard_physical_key(keyboard: RAWKEYBOARD) -> Option { + let extension = { + if util::has_flag(keyboard.Flags, RI_KEY_E0 as _) { + 0xe000 + } else if util::has_flag(keyboard.Flags, RI_KEY_E1 as _) { + 0xe100 + } else { + 0x0000 + } + }; + let scancode = if keyboard.MakeCode == 0 { + // In some cases (often with media keys) the device reports a scancode of 0 but a + // valid virtual key. In these cases we obtain the scancode from the virtual key. + unsafe { MapVirtualKeyW(keyboard.VKey as u32, MAPVK_VK_TO_VSC_EX) as u16 } + } else { + keyboard.MakeCode | extension + }; + if scancode == 0xe11d || scancode == 0xe02a { + // At the hardware (or driver?) level, pressing the Pause key is equivalent to pressing + // Ctrl+NumLock. + // This equivalence means that if the user presses Pause, the keyboard will emit two + // subsequent keypresses: + // 1, 0xE11D - Which is a left Ctrl (0x1D) with an extension flag (0xE100) + // 2, 0x0045 - Which on its own can be interpreted as Pause + // + // There's another combination which isn't quite an equivalence: + // PrtSc used to be Shift+Asterisk. This means that on some keyboards, pressing + // PrtSc (print screen) produces the following sequence: + // 1, 0xE02A - Which is a left shift (0x2A) with an extension flag (0xE000) + // 2, 0xE037 - Which is a numpad multiply (0x37) with an extension flag (0xE000). This on + // its own it can be interpreted as PrtSc + // + // For this reason, if we encounter the first keypress, we simply ignore it, trusting + // that there's going to be another event coming, from which we can extract the + // appropriate key. + // For more on this, read the article by Raymond Chen, titled: + // "Why does Ctrl+ScrollLock cancel dialogs?" + // https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503 + return None; + } + let physical_key = if keyboard.VKey == VK_NUMLOCK { + // Historically, the NumLock and the Pause key were one and the same physical key. + // The user could trigger Pause by pressing Ctrl+NumLock. + // Now these are often physically separate and the two keys can be differentiated by + // checking the extension flag of the scancode. NumLock is 0xE045, Pause is 0x0045. + // + // However in this event, both keys are reported as 0x0045 even on modern hardware. + // Therefore we use the virtual key instead to determine whether it's a NumLock and + // set the KeyCode accordingly. + // + // For more on this, read the article by Raymond Chen, titled: + // "Why does Ctrl+ScrollLock cancel dialogs?" + // https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503 + PhysicalKey::Code(KeyCode::NumLock) + } else { + scancode_to_physicalkey(scancode as u32) + }; + if keyboard.VKey == VK_SHIFT { + if let PhysicalKey::Code( + KeyCode::NumpadDecimal + | KeyCode::Numpad0 + | KeyCode::Numpad1 + | KeyCode::Numpad2 + | KeyCode::Numpad3 + | KeyCode::Numpad4 + | KeyCode::Numpad5 + | KeyCode::Numpad6 + | KeyCode::Numpad7 + | KeyCode::Numpad8 + | KeyCode::Numpad9, + ) = physical_key + { + // On Windows, holding the Shift key makes numpad keys behave as if NumLock + // wasn't active. The way this is exposed to applications by the system is that + // the application receives a fake key release event for the shift key at the + // moment when the numpad key is pressed, just before receiving the numpad key + // as well. + // + // The issue is that in the raw device event (here), the fake shift release + // event reports the numpad key as the scancode. Unfortunately, the event + // doesn't have any information to tell whether it's the + // left shift or the right shift that needs to get the fake + // release (or press) event so we don't forward this + // event to the application at all. + // + // For more on this, read the article by Raymond Chen, titled: + // "The shift key overrides NumLock" + // https://devblogs.microsoft.com/oldnewthing/20040906-00/?p=37953 + return None; + } + } + + Some(physical_key) +} diff --git a/vendor/winit/src/platform_impl/windows/util.rs b/vendor/winit/src/platform_impl/windows/util.rs new file mode 100644 index 00000000..a3a7705d --- /dev/null +++ b/vendor/winit/src/platform_impl/windows/util.rs @@ -0,0 +1,280 @@ +use std::ffi::{c_void, OsStr, OsString}; +use std::iter::once; +use std::ops::BitAnd; +use std::os::windows::prelude::{OsStrExt, OsStringExt}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::{io, mem, ptr}; + +use crate::utils::Lazy; +use windows_sys::core::{HRESULT, PCWSTR}; +use windows_sys::Win32::Foundation::{BOOL, HANDLE, HMODULE, HWND, POINT, RECT}; +use windows_sys::Win32::Graphics::Gdi::{ClientToScreen, HMONITOR}; +use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryA}; +use windows_sys::Win32::System::SystemServices::IMAGE_DOS_HEADER; +use windows_sys::Win32::UI::HiDpi::{ + DPI_AWARENESS_CONTEXT, MONITOR_DPI_TYPE, PROCESS_DPI_AWARENESS, +}; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetActiveWindow; +use windows_sys::Win32::UI::Input::Pointer::{POINTER_INFO, POINTER_PEN_INFO, POINTER_TOUCH_INFO}; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + ClipCursor, GetClientRect, GetClipCursor, GetCursorPos, GetSystemMetrics, GetWindowPlacement, + GetWindowRect, IsIconic, ShowCursor, IDC_APPSTARTING, IDC_ARROW, IDC_CROSS, IDC_HAND, IDC_HELP, + IDC_IBEAM, IDC_NO, IDC_SIZEALL, IDC_SIZENESW, IDC_SIZENS, IDC_SIZENWSE, IDC_SIZEWE, IDC_WAIT, + SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN, SW_MAXIMIZE, + WINDOWPLACEMENT, +}; + +use crate::window::CursorIcon; + +pub fn encode_wide(string: impl AsRef) -> Vec { + string.as_ref().encode_wide().chain(once(0)).collect() +} + +pub fn decode_wide(mut wide_c_string: &[u16]) -> OsString { + if let Some(null_pos) = wide_c_string.iter().position(|c| *c == 0) { + wide_c_string = &wide_c_string[..null_pos]; + } + + OsString::from_wide(wide_c_string) +} + +pub fn has_flag(bitset: T, flag: T) -> bool +where + T: Copy + PartialEq + BitAnd, +{ + bitset & flag == flag +} + +pub(crate) fn win_to_err(result: BOOL) -> Result<(), io::Error> { + if result != false.into() { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } +} + +pub enum WindowArea { + Outer, + Inner, +} + +impl WindowArea { + pub fn get_rect(self, hwnd: HWND) -> Result { + let mut rect = unsafe { mem::zeroed() }; + + match self { + WindowArea::Outer => { + win_to_err(unsafe { GetWindowRect(hwnd, &mut rect) })?; + }, + WindowArea::Inner => unsafe { + let mut top_left = mem::zeroed(); + + win_to_err(ClientToScreen(hwnd, &mut top_left))?; + win_to_err(GetClientRect(hwnd, &mut rect))?; + rect.left += top_left.x; + rect.top += top_left.y; + rect.right += top_left.x; + rect.bottom += top_left.y; + }, + } + + Ok(rect) + } +} + +pub fn is_maximized(window: HWND) -> bool { + unsafe { + let mut placement: WINDOWPLACEMENT = mem::zeroed(); + placement.length = mem::size_of::() as u32; + GetWindowPlacement(window, &mut placement); + placement.showCmd == SW_MAXIMIZE as u32 + } +} + +pub fn set_cursor_hidden(hidden: bool) { + static HIDDEN: AtomicBool = AtomicBool::new(false); + let changed = HIDDEN.swap(hidden, Ordering::SeqCst) ^ hidden; + if changed { + unsafe { ShowCursor(BOOL::from(!hidden)) }; + } +} + +pub fn get_cursor_position() -> Result { + unsafe { + let mut point: POINT = mem::zeroed(); + win_to_err(GetCursorPos(&mut point)).map(|_| point) + } +} + +pub fn get_cursor_clip() -> Result { + unsafe { + let mut rect: RECT = mem::zeroed(); + win_to_err(GetClipCursor(&mut rect)).map(|_| rect) + } +} + +/// Sets the cursor's clip rect. +/// +/// Note that calling this will automatically dispatch a `WM_MOUSEMOVE` event. +pub fn set_cursor_clip(rect: Option) -> Result<(), io::Error> { + unsafe { + let rect_ptr = rect.as_ref().map(|r| r as *const RECT).unwrap_or(ptr::null()); + win_to_err(ClipCursor(rect_ptr)) + } +} + +pub fn get_desktop_rect() -> RECT { + unsafe { + let left = GetSystemMetrics(SM_XVIRTUALSCREEN); + let top = GetSystemMetrics(SM_YVIRTUALSCREEN); + RECT { + left, + top, + right: left + GetSystemMetrics(SM_CXVIRTUALSCREEN), + bottom: top + GetSystemMetrics(SM_CYVIRTUALSCREEN), + } + } +} + +pub fn is_focused(window: HWND) -> bool { + window == unsafe { GetActiveWindow() } +} + +pub fn is_minimized(window: HWND) -> bool { + unsafe { IsIconic(window) != false.into() } +} + +pub fn get_instance_handle() -> HMODULE { + // Gets the instance handle by taking the address of the + // pseudo-variable created by the microsoft linker: + // https://devblogs.microsoft.com/oldnewthing/20041025-00/?p=37483 + + // This is preferred over GetModuleHandle(NULL) because it also works in DLLs: + // https://stackoverflow.com/questions/21718027/getmodulehandlenull-vs-hinstance + + extern "C" { + static __ImageBase: IMAGE_DOS_HEADER; + } + + unsafe { &__ImageBase as *const _ as _ } +} + +pub(crate) fn to_windows_cursor(cursor: CursorIcon) -> PCWSTR { + match cursor { + CursorIcon::Default => IDC_ARROW, + CursorIcon::Pointer => IDC_HAND, + CursorIcon::Crosshair => IDC_CROSS, + CursorIcon::Text | CursorIcon::VerticalText => IDC_IBEAM, + CursorIcon::NotAllowed | CursorIcon::NoDrop => IDC_NO, + CursorIcon::Grab | CursorIcon::Grabbing | CursorIcon::Move | CursorIcon::AllScroll => { + IDC_SIZEALL + }, + CursorIcon::EResize + | CursorIcon::WResize + | CursorIcon::EwResize + | CursorIcon::ColResize => IDC_SIZEWE, + CursorIcon::NResize + | CursorIcon::SResize + | CursorIcon::NsResize + | CursorIcon::RowResize => IDC_SIZENS, + CursorIcon::NeResize | CursorIcon::SwResize | CursorIcon::NeswResize => IDC_SIZENESW, + CursorIcon::NwResize | CursorIcon::SeResize | CursorIcon::NwseResize => IDC_SIZENWSE, + CursorIcon::Wait => IDC_WAIT, + CursorIcon::Progress => IDC_APPSTARTING, + CursorIcon::Help => IDC_HELP, + _ => IDC_ARROW, // use arrow for the missing cases. + } +} + +// Helper function to dynamically load function pointer as some functions +// may not be available on all Windows platforms supported by winit. +// +// `library` and `function` must be zero-terminated. +pub(super) fn get_function_impl(library: &str, function: &str) -> Option<*const c_void> { + assert_eq!(library.chars().last(), Some('\0')); + assert_eq!(function.chars().last(), Some('\0')); + + // Library names we will use are ASCII so we can use the A version to avoid string conversion. + let module = unsafe { LoadLibraryA(library.as_ptr()) }; + if module == 0 { + return None; + } + + unsafe { GetProcAddress(module, function.as_ptr()) }.map(|function_ptr| function_ptr as _) +} + +macro_rules! get_function { + ($lib:expr, $func:ident) => { + crate::platform_impl::platform::util::get_function_impl( + concat!($lib, '\0'), + concat!(stringify!($func), '\0'), + ) + .map(|f| unsafe { std::mem::transmute::<*const _, $func>(f) }) + }; +} + +pub type SetProcessDPIAware = unsafe extern "system" fn() -> BOOL; +pub type SetProcessDpiAwareness = + unsafe extern "system" fn(value: PROCESS_DPI_AWARENESS) -> HRESULT; +pub type SetProcessDpiAwarenessContext = + unsafe extern "system" fn(value: DPI_AWARENESS_CONTEXT) -> BOOL; +pub type GetDpiForWindow = unsafe extern "system" fn(hwnd: HWND) -> u32; +pub type GetDpiForMonitor = unsafe extern "system" fn( + hmonitor: HMONITOR, + dpi_type: MONITOR_DPI_TYPE, + dpi_x: *mut u32, + dpi_y: *mut u32, +) -> HRESULT; +pub type EnableNonClientDpiScaling = unsafe extern "system" fn(hwnd: HWND) -> BOOL; +pub type AdjustWindowRectExForDpi = unsafe extern "system" fn( + rect: *mut RECT, + dw_style: u32, + b_menu: BOOL, + dw_ex_style: u32, + dpi: u32, +) -> BOOL; + +pub type GetPointerFrameInfoHistory = unsafe extern "system" fn( + pointer_id: u32, + entries_count: *mut u32, + pointer_count: *mut u32, + pointer_info: *mut POINTER_INFO, +) -> BOOL; + +pub type SkipPointerFrameMessages = unsafe extern "system" fn(pointer_id: u32) -> BOOL; +pub type GetPointerDeviceRects = unsafe extern "system" fn( + device: HANDLE, + pointer_device_rect: *mut RECT, + display_rect: *mut RECT, +) -> BOOL; + +pub type GetPointerTouchInfo = + unsafe extern "system" fn(pointer_id: u32, touch_info: *mut POINTER_TOUCH_INFO) -> BOOL; + +pub type GetPointerPenInfo = + unsafe extern "system" fn(point_id: u32, pen_info: *mut POINTER_PEN_INFO) -> BOOL; + +pub(crate) static GET_DPI_FOR_WINDOW: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetDpiForWindow)); +pub(crate) static ADJUST_WINDOW_RECT_EX_FOR_DPI: Lazy> = + Lazy::new(|| get_function!("user32.dll", AdjustWindowRectExForDpi)); +pub(crate) static GET_DPI_FOR_MONITOR: Lazy> = + Lazy::new(|| get_function!("shcore.dll", GetDpiForMonitor)); +pub(crate) static ENABLE_NON_CLIENT_DPI_SCALING: Lazy> = + Lazy::new(|| get_function!("user32.dll", EnableNonClientDpiScaling)); +pub(crate) static SET_PROCESS_DPI_AWARENESS_CONTEXT: Lazy> = + Lazy::new(|| get_function!("user32.dll", SetProcessDpiAwarenessContext)); +pub(crate) static SET_PROCESS_DPI_AWARENESS: Lazy> = + Lazy::new(|| get_function!("shcore.dll", SetProcessDpiAwareness)); +pub(crate) static SET_PROCESS_DPI_AWARE: Lazy> = + Lazy::new(|| get_function!("user32.dll", SetProcessDPIAware)); +pub(crate) static GET_POINTER_FRAME_INFO_HISTORY: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetPointerFrameInfoHistory)); +pub(crate) static SKIP_POINTER_FRAME_MESSAGES: Lazy> = + Lazy::new(|| get_function!("user32.dll", SkipPointerFrameMessages)); +pub(crate) static GET_POINTER_DEVICE_RECTS: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetPointerDeviceRects)); +pub(crate) static GET_POINTER_TOUCH_INFO: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetPointerTouchInfo)); +pub(crate) static GET_POINTER_PEN_INFO: Lazy> = + Lazy::new(|| get_function!("user32.dll", GetPointerPenInfo)); diff --git a/vendor/winit/src/platform_impl/windows/window.rs b/vendor/winit/src/platform_impl/windows/window.rs new file mode 100644 index 00000000..228c6f03 --- /dev/null +++ b/vendor/winit/src/platform_impl/windows/window.rs @@ -0,0 +1,1575 @@ +#![cfg(windows_platform)] + +use std::cell::Cell; +use std::ffi::c_void; +use std::mem::{self, MaybeUninit}; +use std::sync::mpsc::channel; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::{io, panic, ptr}; + +use windows_sys::Win32::Foundation::{ + HWND, LPARAM, OLE_E_WRONGCOMPOBJ, POINT, POINTS, RECT, RPC_E_CHANGED_MODE, S_OK, WPARAM, +}; +use windows_sys::Win32::Graphics::Dwm::{ + DwmEnableBlurBehindWindow, DwmSetWindowAttribute, DWMWA_BORDER_COLOR, DWMWA_CAPTION_COLOR, + DWMWA_SYSTEMBACKDROP_TYPE, DWMWA_TEXT_COLOR, DWMWA_WINDOW_CORNER_PREFERENCE, DWM_BB_BLURREGION, + DWM_BB_ENABLE, DWM_BLURBEHIND, DWM_SYSTEMBACKDROP_TYPE, DWM_WINDOW_CORNER_PREFERENCE, +}; +use windows_sys::Win32::Graphics::Gdi::{ + ChangeDisplaySettingsExW, ClientToScreen, CreateRectRgn, DeleteObject, InvalidateRgn, + RedrawWindow, CDS_FULLSCREEN, DISP_CHANGE_BADFLAGS, DISP_CHANGE_BADMODE, DISP_CHANGE_BADPARAM, + DISP_CHANGE_FAILED, DISP_CHANGE_SUCCESSFUL, RDW_INTERNALPAINT, +}; +use windows_sys::Win32::System::Com::{ + CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_ALL, COINIT_APARTMENTTHREADED, +}; +use windows_sys::Win32::System::Ole::{OleInitialize, RegisterDragDrop}; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ + EnableWindow, GetActiveWindow, MapVirtualKeyW, ReleaseCapture, SendInput, ToUnicode, INPUT, + INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP, MAPVK_VK_TO_VSC, + VIRTUAL_KEY, VK_LMENU, VK_MENU, VK_SPACE, +}; +use windows_sys::Win32::UI::Input::Touch::{RegisterTouchWindow, TWF_WANTPALM}; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + CreateWindowExW, EnableMenuItem, FlashWindowEx, GetClientRect, GetCursorPos, + GetForegroundWindow, GetSystemMenu, GetSystemMetrics, GetWindowPlacement, GetWindowTextLengthW, + GetWindowTextW, IsWindowVisible, LoadCursorW, PeekMessageW, PostMessageW, RegisterClassExW, + SetCursor, SetCursorPos, SetForegroundWindow, SetMenuDefaultItem, SetWindowDisplayAffinity, + SetWindowPlacement, SetWindowPos, SetWindowTextW, TrackPopupMenu, CS_HREDRAW, CS_VREDRAW, + CW_USEDEFAULT, FLASHWINFO, FLASHW_ALL, FLASHW_STOP, FLASHW_TIMERNOFG, FLASHW_TRAY, + GWLP_HINSTANCE, HTBOTTOM, HTBOTTOMLEFT, HTBOTTOMRIGHT, HTCAPTION, HTLEFT, HTRIGHT, HTTOP, + HTTOPLEFT, HTTOPRIGHT, MENU_ITEM_STATE, MFS_DISABLED, MFS_ENABLED, MF_BYCOMMAND, NID_READY, + PM_NOREMOVE, SC_CLOSE, SC_MAXIMIZE, SC_MINIMIZE, SC_MOVE, SC_RESTORE, SC_SIZE, SM_DIGITIZER, + SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOSIZE, SWP_NOZORDER, TPM_LEFTALIGN, TPM_RETURNCMD, + WDA_EXCLUDEFROMCAPTURE, WDA_NONE, WM_NCLBUTTONDOWN, WM_SYSCOMMAND, WNDCLASSEXW, +}; + +use tracing::warn; + +use crate::cursor::Cursor; +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; +use crate::icon::Icon; +use crate::platform::windows::{BackdropType, Color, CornerPreference}; +use crate::platform_impl::platform::dark_mode::try_theme; +use crate::platform_impl::platform::definitions::{ + CLSID_TaskbarList, IID_ITaskbarList, IID_ITaskbarList2, ITaskbarList, ITaskbarList2, +}; +use crate::platform_impl::platform::dpi::{ + dpi_to_scale_factor, enable_non_client_dpi_scaling, hwnd_dpi, +}; +use crate::platform_impl::platform::drop_handler::FileDropHandler; +use crate::platform_impl::platform::event_loop::{self, ActiveEventLoop, DESTROY_MSG_ID}; +use crate::platform_impl::platform::icon::{self, IconType, WinCursor}; +use crate::platform_impl::platform::ime::ImeContext; +use crate::platform_impl::platform::keyboard::KeyEventBuilder; +use crate::platform_impl::platform::monitor::{self, MonitorHandle}; +use crate::platform_impl::platform::window_state::{ + CursorFlags, SavedWindow, WindowFlags, WindowState, +}; +use crate::platform_impl::platform::{util, Fullscreen, SelectedCursor, WindowId}; +use crate::window::{ + CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes, + WindowButtons, WindowLevel, +}; + +/// The Win32 implementation of the main `Window` object. +pub(crate) struct Window { + /// Main handle for the window. + window: HWND, + + /// The current window state. + window_state: Arc>, + + // The events loop proxy. + thread_executor: event_loop::EventLoopThreadExecutor, +} + +impl Window { + pub(crate) fn new( + event_loop: &ActiveEventLoop, + w_attr: WindowAttributes, + ) -> Result { + // We dispatch an `init` function because of code style. + // First person to remove the need for cloning here gets a cookie! + // + // done. you owe me -- ossi + unsafe { init(w_attr, event_loop) } + } + + pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) { + // TODO: Use `thread_executor` here + f(self) + } + + pub(crate) fn maybe_wait_on_main(&self, f: impl FnOnce(&Self) -> R + Send) -> R { + // TODO: Use `thread_executor` here + f(self) + } + + fn window_state_lock(&self) -> MutexGuard<'_, WindowState> { + self.window_state.lock().unwrap() + } + + pub fn set_title(&self, text: &str) { + let wide_text = util::encode_wide(text); + unsafe { + SetWindowTextW(self.hwnd(), wide_text.as_ptr()); + } + } + + pub fn set_transparent(&self, transparent: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::TRANSPARENT, transparent) + }); + }); + } + + pub fn set_blur(&self, _blur: bool) {} + + #[inline] + pub fn set_visible(&self, visible: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::VISIBLE, visible) + }); + }); + } + + #[inline] + pub fn is_visible(&self) -> Option { + Some(unsafe { IsWindowVisible(self.window) == 1 }) + } + + #[inline] + pub fn request_redraw(&self) { + // NOTE: mark that we requested a redraw to handle requests during `WM_PAINT` handling. + self.window_state.lock().unwrap().redraw_requested = true; + unsafe { + RedrawWindow(self.hwnd(), ptr::null(), 0, RDW_INTERNALPAINT); + } + } + + #[inline] + pub fn pre_present_notify(&self) {} + + #[inline] + pub fn outer_position(&self) -> Result, NotSupportedError> { + util::WindowArea::Outer + .get_rect(self.hwnd()) + .map(|rect| Ok(PhysicalPosition::new(rect.left, rect.top))) + .expect( + "Unexpected GetWindowRect failure; please report this error to \ + rust-windowing/winit", + ) + } + + #[inline] + pub fn inner_position(&self) -> Result, NotSupportedError> { + let mut position: POINT = unsafe { mem::zeroed() }; + if unsafe { ClientToScreen(self.hwnd(), &mut position) } == false.into() { + panic!( + "Unexpected ClientToScreen failure: please report this error to \ + rust-windowing/winit" + ) + } + Ok(PhysicalPosition::new(position.x, position.y)) + } + + #[inline] + pub fn set_outer_position(&self, position: Position) { + let (x, y): (i32, i32) = position.to_physical::(self.scale_factor()).into(); + + let window_state = Arc::clone(&self.window_state); + let window = self.window; + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::MAXIMIZED, false) + }); + }); + + unsafe { + SetWindowPos( + self.hwnd(), + 0, + x, + y, + 0, + 0, + SWP_ASYNCWINDOWPOS | SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE, + ); + InvalidateRgn(self.hwnd(), 0, false.into()); + } + } + + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + let mut rect: RECT = unsafe { mem::zeroed() }; + if unsafe { GetClientRect(self.hwnd(), &mut rect) } == false.into() { + panic!( + "Unexpected GetClientRect failure: please report this error to \ + rust-windowing/winit" + ) + } + PhysicalSize::new((rect.right - rect.left) as u32, (rect.bottom - rect.top) as u32) + } + + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + util::WindowArea::Outer + .get_rect(self.hwnd()) + .map(|rect| { + PhysicalSize::new((rect.right - rect.left) as u32, (rect.bottom - rect.top) as u32) + }) + .unwrap() + } + + #[inline] + pub fn request_inner_size(&self, size: Size) -> Option> { + let scale_factor = self.scale_factor(); + let physical_size = size.to_physical::(scale_factor); + + let window_flags = self.window_state_lock().window_flags; + window_flags.set_size(self.hwnd(), physical_size); + + if physical_size != self.inner_size() { + let window_state = Arc::clone(&self.window_state); + let window = self.window; + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::MAXIMIZED, false) + }); + }); + } + + None + } + + #[inline] + pub fn set_min_inner_size(&self, size: Option) { + self.window_state_lock().min_size = size; + // Make windows re-check the window size bounds. + let size = self.inner_size(); + self.request_inner_size(size.into()); + } + + #[inline] + pub fn set_max_inner_size(&self, size: Option) { + self.window_state_lock().max_size = size; + // Make windows re-check the window size bounds. + let size = self.inner_size(); + self.request_inner_size(size.into()); + } + + #[inline] + pub fn resize_increments(&self) -> Option> { + let w = self.window_state_lock(); + let scale_factor = w.scale_factor; + w.resize_increments.map(|size| size.to_physical(scale_factor)) + } + + #[inline] + pub fn set_resize_increments(&self, increments: Option) { + self.window_state_lock().resize_increments = increments; + } + + #[inline] + pub fn set_resizable(&self, resizable: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::RESIZABLE, resizable) + }); + }); + } + + #[inline] + pub fn is_resizable(&self) -> bool { + let window_state = self.window_state_lock(); + window_state.window_flags.contains(WindowFlags::RESIZABLE) + } + + #[inline] + pub fn set_enabled_buttons(&self, buttons: WindowButtons) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::MINIMIZABLE, buttons.contains(WindowButtons::MINIMIZE)); + f.set(WindowFlags::MAXIMIZABLE, buttons.contains(WindowButtons::MAXIMIZE)); + f.set(WindowFlags::CLOSABLE, buttons.contains(WindowButtons::CLOSE)) + }); + }); + } + + pub fn enabled_buttons(&self) -> WindowButtons { + let mut buttons = WindowButtons::empty(); + let window_state = self.window_state_lock(); + if window_state.window_flags.contains(WindowFlags::MINIMIZABLE) { + buttons |= WindowButtons::MINIMIZE; + } + if window_state.window_flags.contains(WindowFlags::MAXIMIZABLE) { + buttons |= WindowButtons::MAXIMIZE; + } + if window_state.window_flags.contains(WindowFlags::CLOSABLE) { + buttons |= WindowButtons::CLOSE; + } + buttons + } + + /// Returns the `hwnd` of this window. + #[inline] + pub fn hwnd(&self) -> HWND { + self.window + } + + #[cfg(feature = "rwh_04")] + #[inline] + pub fn raw_window_handle_rwh_04(&self) -> rwh_04::RawWindowHandle { + let mut window_handle = rwh_04::Win32Handle::empty(); + window_handle.hwnd = self.window as *mut _; + let hinstance = unsafe { super::get_window_long(self.hwnd(), GWLP_HINSTANCE) }; + window_handle.hinstance = hinstance as *mut _; + rwh_04::RawWindowHandle::Win32(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_window_handle_rwh_05(&self) -> rwh_05::RawWindowHandle { + let mut window_handle = rwh_05::Win32WindowHandle::empty(); + window_handle.hwnd = self.window as *mut _; + let hinstance = unsafe { super::get_window_long(self.hwnd(), GWLP_HINSTANCE) }; + window_handle.hinstance = hinstance as *mut _; + rwh_05::RawWindowHandle::Win32(window_handle) + } + + #[cfg(feature = "rwh_05")] + #[inline] + pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle { + rwh_05::RawDisplayHandle::Windows(rwh_05::WindowsDisplayHandle::empty()) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub unsafe fn rwh_06_no_thread_check( + &self, + ) -> Result { + let mut window_handle = rwh_06::Win32WindowHandle::new(unsafe { + // SAFETY: Handle will never be zero. + std::num::NonZeroIsize::new_unchecked(self.window) + }); + let hinstance = unsafe { super::get_window_long(self.hwnd(), GWLP_HINSTANCE) }; + window_handle.hinstance = std::num::NonZeroIsize::new(hinstance); + Ok(rwh_06::RawWindowHandle::Win32(window_handle)) + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_window_handle_rwh_06(&self) -> Result { + // TODO: Write a test once integration framework is ready to ensure that it holds. + // If we aren't in the GUI thread, we can't return the window. + if !self.thread_executor.in_event_loop_thread() { + tracing::error!("tried to access window handle outside of the main thread"); + return Err(rwh_06::HandleError::Unavailable); + } + + // SAFETY: We are on the correct thread. + unsafe { self.rwh_06_no_thread_check() } + } + + #[cfg(feature = "rwh_06")] + #[inline] + pub fn raw_display_handle_rwh_06( + &self, + ) -> Result { + Ok(rwh_06::RawDisplayHandle::Windows(rwh_06::WindowsDisplayHandle::new())) + } + + #[inline] + pub fn set_cursor(&self, cursor: Cursor) { + match cursor { + Cursor::Icon(icon) => { + self.window_state_lock().mouse.selected_cursor = SelectedCursor::Named(icon); + self.thread_executor.execute_in_thread(move || unsafe { + let cursor = LoadCursorW(0, util::to_windows_cursor(icon)); + SetCursor(cursor); + }); + }, + Cursor::Custom(cursor) => { + let new_cursor = match cursor.inner { + WinCursor::Cursor(cursor) => cursor, + WinCursor::Failed => { + warn!("Requested to apply failed cursor"); + return; + }, + }; + self.window_state_lock().mouse.selected_cursor = + SelectedCursor::Custom(new_cursor.clone()); + self.thread_executor.execute_in_thread(move || unsafe { + SetCursor(new_cursor.as_raw_handle()); + }); + }, + } + } + + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + let (tx, rx) = channel(); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + let result = window_state + .lock() + .unwrap() + .mouse + .set_cursor_flags(window, |f| { + f.set(CursorFlags::GRABBED, mode != CursorGrabMode::None); + f.set(CursorFlags::LOCKED, mode == CursorGrabMode::Locked); + }) + .map_err(|e| ExternalError::Os(os_error!(e))); + let _ = tx.send(result); + }); + rx.recv().unwrap() + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + let (tx, rx) = channel(); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + let result = window_state + .lock() + .unwrap() + .mouse + .set_cursor_flags(window, |f| f.set(CursorFlags::HIDDEN, !visible)) + .map_err(|e| e.to_string()); + let _ = tx.send(result); + }); + rx.recv().unwrap().ok(); + } + + #[inline] + pub fn scale_factor(&self) -> f64 { + self.window_state_lock().scale_factor + } + + #[inline] + pub fn set_cursor_position(&self, position: Position) -> Result<(), ExternalError> { + let scale_factor = self.scale_factor(); + let (x, y) = position.to_physical::(scale_factor).into(); + + let mut point = POINT { x, y }; + unsafe { + if ClientToScreen(self.hwnd(), &mut point) == false.into() { + return Err(ExternalError::Os(os_error!(io::Error::last_os_error()))); + } + if SetCursorPos(point.x, point.y) == false.into() { + return Err(ExternalError::Os(os_error!(io::Error::last_os_error()))); + } + } + Ok(()) + } + + unsafe fn handle_os_dragging(&self, wparam: WPARAM) { + let window = self.window; + let window_state = self.window_state.clone(); + + self.thread_executor.execute_in_thread(move || { + { + let mut guard = window_state.lock().unwrap(); + if !guard.dragging { + guard.dragging = true; + } else { + return; + } + } + + let points = { + let mut pos = unsafe { mem::zeroed() }; + unsafe { GetCursorPos(&mut pos) }; + pos + }; + let points = POINTS { x: points.x as i16, y: points.y as i16 }; + + // ReleaseCapture needs to execute on the main thread + unsafe { ReleaseCapture() }; + + unsafe { + PostMessageW(window, WM_NCLBUTTONDOWN, wparam, &points as *const _ as LPARAM) + }; + }); + } + + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + unsafe { + self.handle_os_dragging(HTCAPTION as WPARAM); + } + + Ok(()) + } + + #[inline] + pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { + unsafe { + self.handle_os_dragging(match direction { + ResizeDirection::East => HTRIGHT, + ResizeDirection::North => HTTOP, + ResizeDirection::NorthEast => HTTOPRIGHT, + ResizeDirection::NorthWest => HTTOPLEFT, + ResizeDirection::South => HTBOTTOM, + ResizeDirection::SouthEast => HTBOTTOMRIGHT, + ResizeDirection::SouthWest => HTBOTTOMLEFT, + ResizeDirection::West => HTLEFT, + } as WPARAM); + } + + Ok(()) + } + + unsafe fn handle_showing_window_menu(&self, position: Position) { + unsafe { + let point = { + let mut point = POINT { x: 0, y: 0 }; + let scale_factor = self.scale_factor(); + let (x, y) = position.to_physical::(scale_factor).into(); + point.x = x; + point.y = y; + if ClientToScreen(self.hwnd(), &mut point) == false.into() { + warn!( + "Can't convert client-area coordinates to screen coordinates when showing \ + window menu." + ); + return; + } + point + }; + + // get the current system menu + let h_menu = GetSystemMenu(self.hwnd(), 0); + if h_menu == 0 { + warn!("The corresponding window doesn't have a system menu"); + // This situation should not be treated as an error so just return without showing + // menu. + return; + } + + fn enable(b: bool) -> MENU_ITEM_STATE { + if b { + MFS_ENABLED + } else { + MFS_DISABLED + } + } + + // Change the menu items according to the current window status. + + let restore_btn = enable(self.is_maximized() && self.is_resizable()); + let size_btn = enable(!self.is_maximized() && self.is_resizable()); + let maximize_btn = enable(!self.is_maximized() && self.is_resizable()); + + EnableMenuItem(h_menu, SC_RESTORE, MF_BYCOMMAND | restore_btn); + EnableMenuItem(h_menu, SC_MOVE, MF_BYCOMMAND | enable(!self.is_maximized())); + EnableMenuItem(h_menu, SC_SIZE, MF_BYCOMMAND | size_btn); + EnableMenuItem(h_menu, SC_MINIMIZE, MF_BYCOMMAND | MFS_ENABLED); + EnableMenuItem(h_menu, SC_MAXIMIZE, MF_BYCOMMAND | maximize_btn); + EnableMenuItem(h_menu, SC_CLOSE, MF_BYCOMMAND | MFS_ENABLED); + + // Set the default menu item. + SetMenuDefaultItem(h_menu, SC_CLOSE, 0); + + // Popup the system menu at the position. + let result = TrackPopupMenu( + h_menu, + TPM_RETURNCMD | TPM_LEFTALIGN, /* for now im using LTR, but we have to use user + * layout direction */ + point.x, + point.y, + 0, + self.hwnd(), + std::ptr::null_mut(), + ); + + if result == 0 { + // User canceled the menu, no need to continue. + return; + } + + // Send the command that the user select to the corresponding window. + if PostMessageW(self.hwnd(), WM_SYSCOMMAND, result as _, 0) == 0 { + warn!("Can't post the system menu message to the window."); + } + } + } + + #[inline] + pub fn show_window_menu(&self, position: Position) { + unsafe { + self.handle_showing_window_menu(position); + } + } + + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::IGNORE_CURSOR_EVENT, !hittest) + }); + }); + + Ok(()) + } + + #[inline] + pub fn id(&self) -> WindowId { + WindowId(self.hwnd()) + } + + #[inline] + pub fn set_minimized(&self, minimized: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + + let is_minimized = util::is_minimized(self.hwnd()); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags_in_place(&mut window_state.lock().unwrap(), |f| { + f.set(WindowFlags::MINIMIZED, is_minimized) + }); + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::MINIMIZED, minimized) + }); + }); + } + + #[inline] + pub fn is_minimized(&self) -> Option { + Some(util::is_minimized(self.hwnd())) + } + + #[inline] + pub fn set_maximized(&self, maximized: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::MAXIMIZED, maximized) + }); + }); + } + + #[inline] + pub fn is_maximized(&self) -> bool { + let window_state = self.window_state_lock(); + window_state.window_flags.contains(WindowFlags::MAXIMIZED) + } + + #[inline] + pub fn fullscreen(&self) -> Option { + let window_state = self.window_state_lock(); + window_state.fullscreen.clone() + } + + #[inline] + pub fn set_fullscreen(&self, fullscreen: Option) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + + let mut window_state_lock = window_state.lock().unwrap(); + let old_fullscreen = window_state_lock.fullscreen.clone(); + + match (&old_fullscreen, &fullscreen) { + // Return if we already are in the same fullscreen mode + _ if old_fullscreen == fullscreen => return, + // Return if saved Borderless(monitor) is the same as current monitor when requested + // fullscreen is Borderless(None) + (Some(Fullscreen::Borderless(Some(monitor))), Some(Fullscreen::Borderless(None))) + if *monitor == monitor::current_monitor(window) => + { + return + }, + _ => {}, + } + + window_state_lock.fullscreen.clone_from(&fullscreen); + drop(window_state_lock); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + // Change video mode if we're transitioning to or from exclusive + // fullscreen + match (&old_fullscreen, &fullscreen) { + (_, Some(Fullscreen::Exclusive(video_mode))) => { + let monitor = video_mode.monitor(); + let monitor_info = monitor::get_monitor_info(monitor.hmonitor()).unwrap(); + + let res = unsafe { + ChangeDisplaySettingsExW( + monitor_info.szDevice.as_ptr(), + &*video_mode.native_video_mode, + 0, + CDS_FULLSCREEN, + ptr::null(), + ) + }; + + debug_assert!(res != DISP_CHANGE_BADFLAGS); + debug_assert!(res != DISP_CHANGE_BADMODE); + debug_assert!(res != DISP_CHANGE_BADPARAM); + debug_assert!(res != DISP_CHANGE_FAILED); + assert_eq!(res, DISP_CHANGE_SUCCESSFUL); + }, + (Some(Fullscreen::Exclusive(_)), _) => { + let res = unsafe { + ChangeDisplaySettingsExW( + ptr::null(), + ptr::null(), + 0, + CDS_FULLSCREEN, + ptr::null(), + ) + }; + + debug_assert!(res != DISP_CHANGE_BADFLAGS); + debug_assert!(res != DISP_CHANGE_BADMODE); + debug_assert!(res != DISP_CHANGE_BADPARAM); + debug_assert!(res != DISP_CHANGE_FAILED); + assert_eq!(res, DISP_CHANGE_SUCCESSFUL); + }, + _ => (), + } + + unsafe { + // There are some scenarios where calling `ChangeDisplaySettingsExW` takes long + // enough to execute that the DWM thinks our program has frozen and takes over + // our program's window. When that happens, the `SetWindowPos` call below gets + // eaten and the window doesn't get set to the proper fullscreen position. + // + // Calling `PeekMessageW` here notifies Windows that our process is still running + // fine, taking control back from the DWM and ensuring that the `SetWindowPos` call + // below goes through. + let mut msg = mem::zeroed(); + PeekMessageW(&mut msg, 0, 0, 0, PM_NOREMOVE); + } + + // Update window style + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set( + WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN, + matches!(fullscreen, Some(Fullscreen::Exclusive(_))), + ); + f.set( + WindowFlags::MARKER_BORDERLESS_FULLSCREEN, + matches!(fullscreen, Some(Fullscreen::Borderless(_))), + ); + }); + + // Mark as fullscreen window wrt to z-order + // + // this needs to be called before the below fullscreen SetWindowPos as this itself + // will generate WM_SIZE messages of the old window size that can race with what we set + // below + unsafe { + taskbar_mark_fullscreen(window, fullscreen.is_some()); + } + + // Update window bounds + match &fullscreen { + Some(fullscreen) => { + // Save window bounds before entering fullscreen + let placement = unsafe { + let mut placement = mem::zeroed(); + GetWindowPlacement(window, &mut placement); + placement + }; + + window_state.lock().unwrap().saved_window = Some(SavedWindow { placement }); + + let monitor = match &fullscreen { + Fullscreen::Exclusive(video_mode) => video_mode.monitor(), + Fullscreen::Borderless(Some(monitor)) => monitor.clone(), + Fullscreen::Borderless(None) => monitor::current_monitor(window), + }; + + let position: (i32, i32) = monitor.position().into(); + let size: (u32, u32) = monitor.size().into(); + + unsafe { + SetWindowPos( + window, + 0, + position.0, + position.1, + size.0 as i32, + size.1 as i32, + SWP_ASYNCWINDOWPOS | SWP_NOZORDER, + ); + InvalidateRgn(window, 0, false.into()); + } + }, + None => { + let mut window_state_lock = window_state.lock().unwrap(); + if let Some(SavedWindow { placement }) = window_state_lock.saved_window.take() { + drop(window_state_lock); + unsafe { + SetWindowPlacement(window, &placement); + InvalidateRgn(window, 0, false.into()); + } + } + }, + } + }); + } + + #[inline] + pub fn set_decorations(&self, decorations: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::MARKER_DECORATIONS, decorations) + }); + }); + } + + #[inline] + pub fn is_decorated(&self) -> bool { + let window_state = self.window_state_lock(); + window_state.window_flags.contains(WindowFlags::MARKER_DECORATIONS) + } + + #[inline] + pub fn set_window_level(&self, level: WindowLevel) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::ALWAYS_ON_TOP, level == WindowLevel::AlwaysOnTop); + f.set(WindowFlags::ALWAYS_ON_BOTTOM, level == WindowLevel::AlwaysOnBottom); + }); + }); + } + + #[inline] + pub fn current_monitor(&self) -> Option { + Some(monitor::current_monitor(self.hwnd())) + } + + #[inline] + pub fn set_window_icon(&self, window_icon: Option) { + if let Some(ref window_icon) = window_icon { + window_icon.inner.set_for_window(self.hwnd(), IconType::Small); + } else { + icon::unset_for_window(self.hwnd(), IconType::Small); + } + self.window_state_lock().window_icon = window_icon; + } + + #[inline] + pub fn set_enable(&self, enabled: bool) { + unsafe { EnableWindow(self.hwnd(), enabled.into()) }; + } + + #[inline] + pub fn set_taskbar_icon(&self, taskbar_icon: Option) { + if let Some(ref taskbar_icon) = taskbar_icon { + taskbar_icon.inner.set_for_window(self.hwnd(), IconType::Big); + } else { + icon::unset_for_window(self.hwnd(), IconType::Big); + } + self.window_state_lock().taskbar_icon = taskbar_icon; + } + + #[inline] + pub fn set_ime_cursor_area(&self, spot: Position, size: Size) { + let window = self.window; + let state = self.window_state.clone(); + self.thread_executor.execute_in_thread(move || unsafe { + let scale_factor = state.lock().unwrap().scale_factor; + ImeContext::current(window).set_ime_cursor_area(spot, size, scale_factor); + }); + } + + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + let window = self.window; + let state = self.window_state.clone(); + self.thread_executor.execute_in_thread(move || unsafe { + state.lock().unwrap().ime_allowed = allowed; + ImeContext::set_ime_allowed(window, allowed); + }) + } + + #[inline] + pub fn set_ime_purpose(&self, _purpose: ImePurpose) {} + + #[inline] + pub fn request_user_attention(&self, request_type: Option) { + let window = self.window; + let active_window_handle = unsafe { GetActiveWindow() }; + if window == active_window_handle { + return; + } + + self.thread_executor.execute_in_thread(move || unsafe { + let (flags, count) = request_type + .map(|ty| match ty { + UserAttentionType::Critical => (FLASHW_ALL | FLASHW_TIMERNOFG, u32::MAX), + UserAttentionType::Informational => (FLASHW_TRAY | FLASHW_TIMERNOFG, 0), + }) + .unwrap_or((FLASHW_STOP, 0)); + + let flash_info = FLASHWINFO { + cbSize: mem::size_of::() as u32, + hwnd: window, + dwFlags: flags, + uCount: count, + dwTimeout: 0, + }; + FlashWindowEx(&flash_info); + }); + } + + #[inline] + pub fn set_theme(&self, theme: Option) { + try_theme(self.window, theme); + } + + #[inline] + pub fn theme(&self) -> Option { + Some(self.window_state_lock().current_theme) + } + + #[inline] + pub fn has_focus(&self) -> bool { + let window_state = self.window_state.lock().unwrap(); + window_state.has_active_focus() + } + + pub fn title(&self) -> String { + let len = unsafe { GetWindowTextLengthW(self.window) } + 1; + let mut buf = vec![0; len as usize]; + unsafe { GetWindowTextW(self.window, buf.as_mut_ptr(), len) }; + util::decode_wide(&buf).to_string_lossy().to_string() + } + + #[inline] + pub fn set_skip_taskbar(&self, skip: bool) { + self.window_state_lock().skip_taskbar = skip; + unsafe { set_skip_taskbar(self.hwnd(), skip) }; + } + + #[inline] + pub fn set_undecorated_shadow(&self, shadow: bool) { + let window = self.window; + let window_state = Arc::clone(&self.window_state); + + self.thread_executor.execute_in_thread(move || { + let _ = &window; + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + f.set(WindowFlags::MARKER_UNDECORATED_SHADOW, shadow) + }); + }); + } + + #[inline] + pub fn set_system_backdrop(&self, backdrop_type: BackdropType) { + unsafe { + DwmSetWindowAttribute( + self.hwnd(), + DWMWA_SYSTEMBACKDROP_TYPE as u32, + &(backdrop_type as i32) as *const _ as _, + mem::size_of::() as _, + ); + } + } + + #[inline] + pub fn focus_window(&self) { + let window_flags = self.window_state_lock().window_flags(); + + let is_visible = window_flags.contains(WindowFlags::VISIBLE); + let is_minimized = util::is_minimized(self.hwnd()); + let is_foreground = self.window == unsafe { GetForegroundWindow() }; + + if is_visible && !is_minimized && !is_foreground { + unsafe { force_window_active(self.window) }; + } + } + + #[inline] + pub fn set_content_protected(&self, protected: bool) { + unsafe { + SetWindowDisplayAffinity( + self.hwnd(), + if protected { WDA_EXCLUDEFROMCAPTURE } else { WDA_NONE }, + ) + }; + } + + #[inline] + pub fn reset_dead_keys(&self) { + // `ToUnicode` consumes the dead-key by default, so we are constructing a fake (but valid) + // key input which we can call `ToUnicode` with. + unsafe { + let vk = VK_SPACE as VIRTUAL_KEY; + let scancode = MapVirtualKeyW(vk as u32, MAPVK_VK_TO_VSC); + let kbd_state = [0; 256]; + let mut char_buff = [MaybeUninit::uninit(); 8]; + ToUnicode( + vk as u32, + scancode, + kbd_state.as_ptr(), + char_buff[0].as_mut_ptr(), + char_buff.len() as i32, + 0, + ); + } + } + + #[inline] + pub fn set_border_color(&self, color: Color) { + unsafe { + DwmSetWindowAttribute( + self.hwnd(), + DWMWA_BORDER_COLOR as u32, + &color as *const _ as _, + mem::size_of::() as _, + ); + } + } + + #[inline] + pub fn set_title_background_color(&self, color: Color) { + unsafe { + DwmSetWindowAttribute( + self.hwnd(), + DWMWA_CAPTION_COLOR as u32, + &color as *const _ as _, + mem::size_of::() as _, + ); + } + } + + #[inline] + pub fn set_title_text_color(&self, color: Color) { + unsafe { + DwmSetWindowAttribute( + self.hwnd(), + DWMWA_TEXT_COLOR as u32, + &color as *const _ as _, + mem::size_of::() as _, + ); + } + } + + #[inline] + pub fn set_corner_preference(&self, preference: CornerPreference) { + unsafe { + DwmSetWindowAttribute( + self.hwnd(), + DWMWA_WINDOW_CORNER_PREFERENCE as u32, + &(preference as DWM_WINDOW_CORNER_PREFERENCE) as *const _ as _, + mem::size_of::() as _, + ); + } + } +} + +impl Drop for Window { + #[inline] + fn drop(&mut self) { + unsafe { + // The window must be destroyed from the same thread that created it, so we send a + // custom message to be handled by our callback to do the actual work. + PostMessageW(self.hwnd(), DESTROY_MSG_ID.get(), 0, 0); + } + } +} + +pub(super) struct InitData<'a> { + // inputs + pub event_loop: &'a ActiveEventLoop, + pub attributes: WindowAttributes, + pub window_flags: WindowFlags, + // outputs + pub window: Option, +} + +impl InitData<'_> { + unsafe fn create_window(&self, window: HWND) -> Window { + // Register for touch events if applicable + { + let digitizer = unsafe { GetSystemMetrics(SM_DIGITIZER) as u32 }; + if digitizer & NID_READY != 0 { + unsafe { RegisterTouchWindow(window, TWF_WANTPALM) }; + } + } + + let dpi = unsafe { hwnd_dpi(window) }; + let scale_factor = dpi_to_scale_factor(dpi); + + // If the system theme is dark, we need to set the window theme now + // before we update the window flags (and possibly show the + // window for the first time). + let current_theme = try_theme(window, self.attributes.preferred_theme); + + let window_state = { + let window_state = WindowState::new( + &self.attributes, + scale_factor, + current_theme, + self.attributes.preferred_theme, + ); + let window_state = Arc::new(Mutex::new(window_state)); + WindowState::set_window_flags(window_state.lock().unwrap(), window, |f| { + *f = self.window_flags + }); + window_state + }; + + enable_non_client_dpi_scaling(window); + + unsafe { ImeContext::set_ime_allowed(window, false) }; + + Window { window, window_state, thread_executor: self.event_loop.create_thread_executor() } + } + + unsafe fn create_window_data(&self, win: &Window) -> event_loop::WindowData { + let file_drop_handler = if self.attributes.platform_specific.drag_and_drop { + let ole_init_result = unsafe { OleInitialize(ptr::null_mut()) }; + // It is ok if the initialize result is `S_FALSE` because it might happen that + // multiple windows are created on the same thread. + if ole_init_result == OLE_E_WRONGCOMPOBJ { + panic!("OleInitialize failed! Result was: `OLE_E_WRONGCOMPOBJ`"); + } else if ole_init_result == RPC_E_CHANGED_MODE { + panic!( + "OleInitialize failed! Result was: `RPC_E_CHANGED_MODE`. Make sure other \ + crates are not using multithreaded COM library on the same thread or disable \ + drag and drop support." + ); + } + + let file_drop_runner = self.event_loop.runner_shared.clone(); + let file_drop_handler = FileDropHandler::new( + win.window, + Box::new(move |event| { + if let Ok(e) = event.map_nonuser_event() { + file_drop_runner.send_event(e) + } + }), + ); + + let handler_interface_ptr = + unsafe { &mut (*file_drop_handler.data).interface as *mut _ as *mut c_void }; + + assert_eq!(unsafe { RegisterDragDrop(win.window, handler_interface_ptr) }, S_OK); + Some(file_drop_handler) + } else { + None + }; + + event_loop::WindowData { + window_state: win.window_state.clone(), + event_loop_runner: self.event_loop.runner_shared.clone(), + key_event_builder: KeyEventBuilder::default(), + _file_drop_handler: file_drop_handler, + userdata_removed: Cell::new(false), + recurse_depth: Cell::new(0), + } + } + + // Returns a pointer to window user data on success. + // The user data will be registered for the window and can be accessed within the window event + // callback. + pub unsafe fn on_nccreate(&mut self, window: HWND) -> Option { + let runner = self.event_loop.runner_shared.clone(); + let result = runner.catch_unwind(|| { + let window = unsafe { self.create_window(window) }; + let window_data = unsafe { self.create_window_data(&window) }; + (window, window_data) + }); + + result.map(|(win, userdata)| { + self.window = Some(win); + let userdata = Box::into_raw(Box::new(userdata)); + userdata as _ + }) + } + + pub unsafe fn on_create(&mut self) { + let win = self.window.as_mut().expect("failed window creation"); + + // making the window transparent + if self.attributes.transparent && !self.attributes.platform_specific.no_redirection_bitmap { + // Empty region for the blur effect, so the window is fully transparent + let region = unsafe { CreateRectRgn(0, 0, -1, -1) }; + + let bb = DWM_BLURBEHIND { + dwFlags: DWM_BB_ENABLE | DWM_BB_BLURREGION, + fEnable: true.into(), + hRgnBlur: region, + fTransitionOnMaximized: false.into(), + }; + let hr = unsafe { DwmEnableBlurBehindWindow(win.hwnd(), &bb) }; + if hr < 0 { + warn!("Setting transparent window is failed. HRESULT Code: 0x{:X}", hr); + } + unsafe { DeleteObject(region) }; + } + + win.set_skip_taskbar(self.attributes.platform_specific.skip_taskbar); + win.set_window_icon(self.attributes.window_icon.clone()); + win.set_taskbar_icon(self.attributes.platform_specific.taskbar_icon.clone()); + + let attributes = self.attributes.clone(); + + if attributes.content_protected { + win.set_content_protected(true); + } + + win.set_cursor(attributes.cursor); + + // Set visible before setting the size to ensure the + // attribute is correctly applied. + win.set_visible(attributes.visible); + + win.set_enabled_buttons(attributes.enabled_buttons); + + let size = attributes.inner_size.unwrap_or_else(|| PhysicalSize::new(800, 600).into()); + let max_size = attributes + .max_inner_size + .unwrap_or_else(|| PhysicalSize::new(f64::MAX, f64::MAX).into()); + let min_size = attributes.min_inner_size.unwrap_or_else(|| PhysicalSize::new(0, 0).into()); + let clamped_size = Size::clamp(size, min_size, max_size, win.scale_factor()); + win.request_inner_size(clamped_size); + + // let margins = MARGINS { + // cxLeftWidth: 1, + // cxRightWidth: 1, + // cyTopHeight: 1, + // cyBottomHeight: 1, + // }; + // dbg!(DwmExtendFrameIntoClientArea(win.hwnd(), &margins as *const _)); + + if let Some(position) = attributes.position { + win.set_outer_position(position); + } + + win.set_system_backdrop(self.attributes.platform_specific.backdrop_type); + + if let Some(color) = self.attributes.platform_specific.border_color { + win.set_border_color(color); + } + if let Some(color) = self.attributes.platform_specific.title_background_color { + win.set_title_background_color(color); + } + if let Some(color) = self.attributes.platform_specific.title_text_color { + win.set_title_text_color(color); + } + if let Some(corner) = self.attributes.platform_specific.corner_preference { + win.set_corner_preference(corner); + } + } +} +unsafe fn init( + attributes: WindowAttributes, + event_loop: &ActiveEventLoop, +) -> Result { + let title = util::encode_wide(&attributes.title); + + let class_name = util::encode_wide(&attributes.platform_specific.class_name); + unsafe { register_window_class(&class_name) }; + + let mut window_flags = WindowFlags::empty(); + window_flags.set(WindowFlags::MARKER_DECORATIONS, attributes.decorations); + window_flags.set( + WindowFlags::MARKER_UNDECORATED_SHADOW, + attributes.platform_specific.decoration_shadow, + ); + window_flags + .set(WindowFlags::ALWAYS_ON_TOP, attributes.window_level == WindowLevel::AlwaysOnTop); + window_flags + .set(WindowFlags::ALWAYS_ON_BOTTOM, attributes.window_level == WindowLevel::AlwaysOnBottom); + window_flags + .set(WindowFlags::NO_BACK_BUFFER, attributes.platform_specific.no_redirection_bitmap); + window_flags.set(WindowFlags::MARKER_ACTIVATE, attributes.active); + window_flags.set(WindowFlags::TRANSPARENT, attributes.transparent); + // WindowFlags::VISIBLE and MAXIMIZED are set down below after the window has been configured. + window_flags.set(WindowFlags::RESIZABLE, attributes.resizable); + // Will be changed later using `window.set_enabled_buttons` but we need to set a default here + // so the diffing later can work. + window_flags.set(WindowFlags::CLOSABLE, true); + window_flags.set(WindowFlags::CLIP_CHILDREN, attributes.platform_specific.clip_children); + + let mut fallback_parent = || match attributes.platform_specific.owner { + Some(parent) => { + window_flags.set(WindowFlags::POPUP, true); + Some(parent) + }, + None => { + window_flags.set(WindowFlags::ON_TASKBAR, true); + None + }, + }; + + #[cfg(feature = "rwh_06")] + let parent = match attributes.parent_window.as_ref().map(|handle| handle.0) { + Some(rwh_06::RawWindowHandle::Win32(handle)) => { + window_flags.set(WindowFlags::CHILD, true); + if attributes.platform_specific.menu.is_some() { + warn!("Setting a menu on a child window is unsupported"); + } + Some(handle.hwnd.get() as HWND) + }, + Some(raw) => unreachable!("Invalid raw window handle {raw:?} on Windows"), + None => fallback_parent(), + }; + + #[cfg(not(feature = "rwh_06"))] + let parent = fallback_parent(); + + let menu = attributes.platform_specific.menu; + let fullscreen = attributes.fullscreen.clone(); + let maximized = attributes.maximized; + let mut initdata = InitData { event_loop, attributes, window_flags, window: None }; + + let (style, ex_style) = window_flags.to_window_styles(); + let handle = unsafe { + CreateWindowExW( + ex_style, + class_name.as_ptr(), + title.as_ptr(), + style, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + parent.unwrap_or(0), + menu.unwrap_or(0), + util::get_instance_handle(), + &mut initdata as *mut _ as *mut _, + ) + }; + + // If the window creation in `InitData` panicked, then should resume panicking here + if let Err(panic_error) = event_loop.runner_shared.take_panic_error() { + panic::resume_unwind(panic_error) + } + + if handle == 0 { + return Err(os_error!(io::Error::last_os_error())); + } + + // If the handle is non-null, then window creation must have succeeded, which means + // that we *must* have populated the `InitData.window` field. + let win = initdata.window.unwrap(); + + // Need to set FULLSCREEN or MAXIMIZED after CreateWindowEx + // This is because if the size is changed in WM_CREATE, the restored size will be stored in that + // size. + if fullscreen.is_some() { + win.set_fullscreen(fullscreen.map(Into::into)); + unsafe { force_window_active(win.window) }; + } else if maximized { + win.set_maximized(true); + } + + Ok(win) +} + +unsafe fn register_window_class(class_name: &[u16]) { + let class = WNDCLASSEXW { + cbSize: mem::size_of::() as u32, + style: CS_HREDRAW | CS_VREDRAW, + lpfnWndProc: Some(super::event_loop::public_window_callback), + cbClsExtra: 0, + cbWndExtra: 0, + hInstance: util::get_instance_handle(), + hIcon: 0, + hCursor: 0, // must be null in order for cursor state to work properly + hbrBackground: 0, + lpszMenuName: ptr::null(), + lpszClassName: class_name.as_ptr(), + hIconSm: 0, + }; + + // We ignore errors because registering the same window class twice would trigger + // an error, and because errors here are detected during CreateWindowEx anyway. + // Also since there is no weird element in the struct, there is no reason for this + // call to fail. + unsafe { RegisterClassExW(&class) }; +} + +struct ComInitialized(#[allow(dead_code)] *mut ()); +impl Drop for ComInitialized { + fn drop(&mut self) { + unsafe { CoUninitialize() }; + } +} + +thread_local! { + static COM_INITIALIZED: ComInitialized = { + unsafe { + CoInitializeEx(ptr::null(), COINIT_APARTMENTTHREADED as u32); + ComInitialized(ptr::null_mut()) + } + }; + + static TASKBAR_LIST: Cell<*mut ITaskbarList> = const { Cell::new(ptr::null_mut()) }; + static TASKBAR_LIST2: Cell<*mut ITaskbarList2> = const { Cell::new(ptr::null_mut()) }; +} + +pub fn com_initialized() { + COM_INITIALIZED.with(|_| {}); +} + +// Reference Implementation: +// https://github.com/chromium/chromium/blob/f18e79d901f56154f80eea1e2218544285e62623/ui/views/win/fullscreen_handler.cc +// +// As per MSDN marking the window as fullscreen should ensure that the +// taskbar is moved to the bottom of the Z-order when the fullscreen window +// is activated. If the window is not fullscreen, the Shell falls back to +// heuristics to determine how the window should be treated, which means +// that it could still consider the window as fullscreen. :( +unsafe fn taskbar_mark_fullscreen(handle: HWND, fullscreen: bool) { + com_initialized(); + + TASKBAR_LIST2.with(|task_bar_list2_ptr| { + let mut task_bar_list2 = task_bar_list2_ptr.get(); + + if task_bar_list2.is_null() { + let hr = unsafe { + CoCreateInstance( + &CLSID_TaskbarList, + ptr::null_mut(), + CLSCTX_ALL, + &IID_ITaskbarList2, + &mut task_bar_list2 as *mut _ as *mut _, + ) + }; + if hr != S_OK { + // In visual studio retrieving the taskbar list fails + return; + } + + let hr_init = unsafe { (*(*task_bar_list2).lpVtbl).parent.HrInit }; + if unsafe { hr_init(task_bar_list2.cast()) } != S_OK { + // In some old windows, the taskbar object could not be created, we just ignore it + return; + } + task_bar_list2_ptr.set(task_bar_list2) + } + + task_bar_list2 = task_bar_list2_ptr.get(); + let mark_fullscreen_window = unsafe { (*(*task_bar_list2).lpVtbl).MarkFullscreenWindow }; + unsafe { mark_fullscreen_window(task_bar_list2, handle, fullscreen.into()) }; + }) +} + +pub(crate) unsafe fn set_skip_taskbar(hwnd: HWND, skip: bool) { + com_initialized(); + TASKBAR_LIST.with(|task_bar_list_ptr| { + let mut task_bar_list = task_bar_list_ptr.get(); + + if task_bar_list.is_null() { + let hr = unsafe { + CoCreateInstance( + &CLSID_TaskbarList, + ptr::null_mut(), + CLSCTX_ALL, + &IID_ITaskbarList, + &mut task_bar_list as *mut _ as *mut _, + ) + }; + if hr != S_OK { + // In visual studio retrieving the taskbar list fails + return; + } + + let hr_init = unsafe { (*(*task_bar_list).lpVtbl).HrInit }; + if unsafe { hr_init(task_bar_list.cast()) } != S_OK { + // In some old windows, the taskbar object could not be created, we just ignore it + return; + } + task_bar_list_ptr.set(task_bar_list) + } + + task_bar_list = task_bar_list_ptr.get(); + if skip { + let delete_tab = unsafe { (*(*task_bar_list).lpVtbl).DeleteTab }; + unsafe { delete_tab(task_bar_list, hwnd) }; + } else { + let add_tab = unsafe { (*(*task_bar_list).lpVtbl).AddTab }; + unsafe { add_tab(task_bar_list, hwnd) }; + } + }); +} + +unsafe fn force_window_active(handle: HWND) { + // In some situation, calling SetForegroundWindow could not bring up the window, + // This is a little hack which can "steal" the foreground window permission + // We only call this function in the window creation, so it should be fine. + // See : https://stackoverflow.com/questions/10740346/setforegroundwindow-only-working-while-visual-studio-is-open + let alt_sc = unsafe { MapVirtualKeyW(VK_MENU as u32, MAPVK_VK_TO_VSC) }; + + let inputs = [ + INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: INPUT_0 { + ki: KEYBDINPUT { + wVk: VK_LMENU, + wScan: alt_sc as u16, + dwFlags: KEYEVENTF_EXTENDEDKEY, + dwExtraInfo: 0, + time: 0, + }, + }, + }, + INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: INPUT_0 { + ki: KEYBDINPUT { + wVk: VK_LMENU, + wScan: alt_sc as u16, + dwFlags: KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, + dwExtraInfo: 0, + time: 0, + }, + }, + }, + ]; + + // Simulate a key press and release + unsafe { SendInput(inputs.len() as u32, inputs.as_ptr(), mem::size_of::() as i32) }; + + unsafe { SetForegroundWindow(handle) }; +} diff --git a/vendor/winit/src/platform_impl/windows/window_state.rs b/vendor/winit/src/platform_impl/windows/window_state.rs new file mode 100644 index 00000000..8e24a4f2 --- /dev/null +++ b/vendor/winit/src/platform_impl/windows/window_state.rs @@ -0,0 +1,545 @@ +use crate::dpi::{PhysicalPosition, PhysicalSize, Size}; +use crate::icon::Icon; +use crate::keyboard::ModifiersState; +use crate::platform_impl::platform::{event_loop, util, Fullscreen, SelectedCursor}; +use crate::window::{Theme, WindowAttributes}; +use bitflags::bitflags; +use std::io; +use std::sync::MutexGuard; +use windows_sys::Win32::Foundation::{HWND, RECT}; +use windows_sys::Win32::Graphics::Gdi::InvalidateRgn; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + AdjustWindowRectEx, EnableMenuItem, GetMenu, GetSystemMenu, GetWindowLongW, SendMessageW, + SetWindowLongW, SetWindowPos, ShowWindow, GWL_EXSTYLE, GWL_STYLE, HWND_BOTTOM, HWND_NOTOPMOST, + HWND_TOPMOST, MF_BYCOMMAND, MF_DISABLED, MF_ENABLED, SC_CLOSE, SWP_ASYNCWINDOWPOS, + SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOREPOSITION, SWP_NOSIZE, SWP_NOZORDER, + SW_HIDE, SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, SW_SHOW, SW_SHOWNOACTIVATE, WINDOWPLACEMENT, + WINDOW_EX_STYLE, WINDOW_STYLE, WS_BORDER, WS_CAPTION, WS_CHILD, WS_CLIPCHILDREN, + WS_CLIPSIBLINGS, WS_EX_ACCEPTFILES, WS_EX_APPWINDOW, WS_EX_LAYERED, WS_EX_NOREDIRECTIONBITMAP, + WS_EX_TOPMOST, WS_EX_TRANSPARENT, WS_EX_WINDOWEDGE, WS_MAXIMIZE, WS_MAXIMIZEBOX, WS_MINIMIZE, + WS_MINIMIZEBOX, WS_OVERLAPPEDWINDOW, WS_POPUP, WS_SIZEBOX, WS_SYSMENU, WS_VISIBLE, +}; + +/// Contains information about states and the window that the callback is going to use. +pub(crate) struct WindowState { + pub mouse: MouseProperties, + + /// Used by `WM_GETMINMAXINFO`. + pub min_size: Option, + pub max_size: Option, + + pub resize_increments: Option, + + pub window_icon: Option, + pub taskbar_icon: Option, + + pub saved_window: Option, + pub scale_factor: f64, + + pub modifiers_state: ModifiersState, + pub fullscreen: Option, + pub current_theme: Theme, + pub preferred_theme: Option, + + pub window_flags: WindowFlags, + + pub ime_state: ImeState, + pub ime_allowed: bool, + + // Used by WM_NCACTIVATE, WM_SETFOCUS and WM_KILLFOCUS + pub is_active: bool, + pub is_focused: bool, + + // Flag whether redraw was requested. + pub redraw_requested: bool, + + pub dragging: bool, + + pub skip_taskbar: bool, +} + +#[derive(Clone)] +pub struct SavedWindow { + pub placement: WINDOWPLACEMENT, +} + +#[derive(Clone)] +pub struct MouseProperties { + pub(crate) selected_cursor: SelectedCursor, + pub capture_count: u32, + cursor_flags: CursorFlags, + pub last_position: Option>, +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct CursorFlags: u8 { + const GRABBED = 1 << 0; + const HIDDEN = 1 << 1; + const IN_WINDOW = 1 << 2; + const LOCKED = 1 << 3; + } +} +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct WindowFlags: u32 { + const RESIZABLE = 1 << 0; + const MINIMIZABLE = 1 << 1; + const MAXIMIZABLE = 1 << 2; + const CLOSABLE = 1 << 3; + const VISIBLE = 1 << 4; + const ON_TASKBAR = 1 << 5; + const ALWAYS_ON_TOP = 1 << 6; + const ALWAYS_ON_BOTTOM = 1 << 7; + const NO_BACK_BUFFER = 1 << 8; + const TRANSPARENT = 1 << 9; + const CHILD = 1 << 10; + const MAXIMIZED = 1 << 11; + const POPUP = 1 << 12; + + /// Marker flag for fullscreen. Should always match `WindowState::fullscreen`, but is + /// included here to make masking easier. + const MARKER_EXCLUSIVE_FULLSCREEN = 1 << 13; + const MARKER_BORDERLESS_FULLSCREEN = 1 << 14; + + /// The `WM_SIZE` event contains some parameters that can effect the state of `WindowFlags`. + /// In most cases, it's okay to let those parameters change the state. However, when we're + /// running the `WindowFlags::apply_diff` function, we *don't* want those parameters to + /// effect our stored state, because the purpose of `apply_diff` is to update the actual + /// window's state to match our stored state. This controls whether to accept those changes. + const MARKER_RETAIN_STATE_ON_SIZE = 1 << 15; + + const MARKER_IN_SIZE_MOVE = 1 << 16; + + const MINIMIZED = 1 << 17; + + const IGNORE_CURSOR_EVENT = 1 << 18; + + /// Fully decorated window (incl. caption, border and drop shadow). + const MARKER_DECORATIONS = 1 << 19; + /// Drop shadow for undecorated windows. + const MARKER_UNDECORATED_SHADOW = 1 << 20; + + const MARKER_ACTIVATE = 1 << 21; + + const CLIP_CHILDREN = 1 << 22; + + const EXCLUSIVE_FULLSCREEN_OR_MASK = WindowFlags::ALWAYS_ON_TOP.bits(); + } +} + +#[derive(Eq, PartialEq)] +pub enum ImeState { + Disabled, + Enabled, + Preedit, +} + +impl WindowState { + pub(crate) fn new( + attributes: &WindowAttributes, + scale_factor: f64, + current_theme: Theme, + preferred_theme: Option, + ) -> WindowState { + WindowState { + mouse: MouseProperties { + selected_cursor: SelectedCursor::default(), + capture_count: 0, + cursor_flags: CursorFlags::empty(), + last_position: None, + }, + + min_size: attributes.min_inner_size, + max_size: attributes.max_inner_size, + + resize_increments: attributes.resize_increments, + + window_icon: attributes.window_icon.clone(), + taskbar_icon: None, + + saved_window: None, + scale_factor, + + modifiers_state: ModifiersState::default(), + fullscreen: None, + current_theme, + preferred_theme, + window_flags: WindowFlags::empty(), + + ime_state: ImeState::Disabled, + ime_allowed: false, + + is_active: false, + is_focused: false, + redraw_requested: false, + + dragging: false, + + skip_taskbar: false, + } + } + + pub fn window_flags(&self) -> WindowFlags { + self.window_flags + } + + pub fn set_window_flags(mut this: MutexGuard<'_, Self>, window: HWND, f: F) + where + F: FnOnce(&mut WindowFlags), + { + let old_flags = this.window_flags; + f(&mut this.window_flags); + let new_flags = this.window_flags; + + drop(this); + old_flags.apply_diff(window, new_flags); + } + + pub fn set_window_flags_in_place(&mut self, f: F) + where + F: FnOnce(&mut WindowFlags), + { + f(&mut self.window_flags); + } + + pub fn has_active_focus(&self) -> bool { + self.is_active && self.is_focused + } + + // Updates is_active and returns whether active-focus state has changed + pub fn set_active(&mut self, is_active: bool) -> bool { + let old = self.has_active_focus(); + self.is_active = is_active; + old != self.has_active_focus() + } + + // Updates is_focused and returns whether active-focus state has changed + pub fn set_focused(&mut self, is_focused: bool) -> bool { + let old = self.has_active_focus(); + self.is_focused = is_focused; + old != self.has_active_focus() + } +} + +impl MouseProperties { + pub fn cursor_flags(&self) -> CursorFlags { + self.cursor_flags + } + + pub fn set_cursor_flags(&mut self, window: HWND, f: F) -> Result<(), io::Error> + where + F: FnOnce(&mut CursorFlags), + { + let old_flags = self.cursor_flags; + f(&mut self.cursor_flags); + match self.cursor_flags.refresh_os_cursor(window) { + Ok(()) => (), + Err(e) => { + self.cursor_flags = old_flags; + return Err(e); + }, + } + + Ok(()) + } +} + +impl WindowFlags { + fn mask(mut self) -> WindowFlags { + if self.contains(WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN) { + self |= WindowFlags::EXCLUSIVE_FULLSCREEN_OR_MASK; + } + self + } + + pub fn to_window_styles(self) -> (WINDOW_STYLE, WINDOW_EX_STYLE) { + // Required styles to properly support common window functionality like aero snap. + let mut style = WS_CAPTION | WS_BORDER | WS_CLIPSIBLINGS | WS_SYSMENU; + let mut style_ex = WS_EX_WINDOWEDGE | WS_EX_ACCEPTFILES; + + if self.contains(WindowFlags::RESIZABLE) { + style |= WS_SIZEBOX; + } + if self.contains(WindowFlags::MAXIMIZABLE) { + style |= WS_MAXIMIZEBOX; + } + if self.contains(WindowFlags::MINIMIZABLE) { + style |= WS_MINIMIZEBOX; + } + if self.contains(WindowFlags::VISIBLE) { + style |= WS_VISIBLE; + } + if self.contains(WindowFlags::ON_TASKBAR) { + style_ex |= WS_EX_APPWINDOW; + } + if self.contains(WindowFlags::ALWAYS_ON_TOP) { + style_ex |= WS_EX_TOPMOST; + } + if self.contains(WindowFlags::NO_BACK_BUFFER) { + style_ex |= WS_EX_NOREDIRECTIONBITMAP; + } + if self.contains(WindowFlags::CHILD) { + style |= WS_CHILD; // This is incompatible with WS_POPUP if that gets added eventually. + + // Remove decorations window styles for child + if !self.contains(WindowFlags::MARKER_DECORATIONS) { + style &= !(WS_CAPTION | WS_BORDER); + style_ex &= !WS_EX_WINDOWEDGE; + } + } + if self.contains(WindowFlags::POPUP) { + style |= WS_POPUP; + } + if self.contains(WindowFlags::MINIMIZED) { + style |= WS_MINIMIZE; + } + if self.contains(WindowFlags::MAXIMIZED) { + style |= WS_MAXIMIZE; + } + if self.contains(WindowFlags::IGNORE_CURSOR_EVENT) { + style_ex |= WS_EX_TRANSPARENT | WS_EX_LAYERED; + } + if self.contains(WindowFlags::CLIP_CHILDREN) { + style |= WS_CLIPCHILDREN; + } + + if self.intersects( + WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN | WindowFlags::MARKER_BORDERLESS_FULLSCREEN, + ) { + style &= !WS_OVERLAPPEDWINDOW; + } + + (style, style_ex) + } + + /// Adjust the window client rectangle to the return value, if present. + fn apply_diff(mut self, window: HWND, mut new: WindowFlags) { + self = self.mask(); + new = new.mask(); + + let mut diff = self ^ new; + + if diff == WindowFlags::empty() { + return; + } + + if new.contains(WindowFlags::VISIBLE) { + let flag = if !self.contains(WindowFlags::MARKER_ACTIVATE) { + self.set(WindowFlags::MARKER_ACTIVATE, true); + SW_SHOWNOACTIVATE + } else { + SW_SHOW + }; + unsafe { + ShowWindow(window, flag); + } + } + + if diff.intersects(WindowFlags::ALWAYS_ON_TOP | WindowFlags::ALWAYS_ON_BOTTOM) { + unsafe { + SetWindowPos( + window, + match ( + new.contains(WindowFlags::ALWAYS_ON_TOP), + new.contains(WindowFlags::ALWAYS_ON_BOTTOM), + ) { + (true, false) => HWND_TOPMOST, + (false, false) => HWND_NOTOPMOST, + (false, true) => HWND_BOTTOM, + (true, true) => unreachable!(), + }, + 0, + 0, + 0, + 0, + SWP_ASYNCWINDOWPOS | SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, + ); + InvalidateRgn(window, 0, false.into()); + } + } + + if diff.contains(WindowFlags::MAXIMIZED) || new.contains(WindowFlags::MAXIMIZED) { + unsafe { + ShowWindow(window, match new.contains(WindowFlags::MAXIMIZED) { + true => SW_MAXIMIZE, + false => SW_RESTORE, + }); + } + } + + // Minimize operations should execute after maximize for proper window animations + if diff.contains(WindowFlags::MINIMIZED) { + unsafe { + ShowWindow(window, match new.contains(WindowFlags::MINIMIZED) { + true => SW_MINIMIZE, + false => SW_RESTORE, + }); + } + + diff.remove(WindowFlags::MINIMIZED); + } + + if diff.contains(WindowFlags::CLOSABLE) || new.contains(WindowFlags::CLOSABLE) { + let flags = MF_BYCOMMAND + | if new.contains(WindowFlags::CLOSABLE) { MF_ENABLED } else { MF_DISABLED }; + + unsafe { + EnableMenuItem(GetSystemMenu(window, 0), SC_CLOSE, flags); + } + } + + if !new.contains(WindowFlags::VISIBLE) { + unsafe { + ShowWindow(window, SW_HIDE); + } + } + + if diff != WindowFlags::empty() { + let (style, style_ex) = new.to_window_styles(); + + unsafe { + SendMessageW(window, event_loop::SET_RETAIN_STATE_ON_SIZE_MSG_ID.get(), 1, 0); + + // This condition is necessary to avoid having an unrestorable window + if !new.contains(WindowFlags::MINIMIZED) { + SetWindowLongW(window, GWL_STYLE, style as i32); + SetWindowLongW(window, GWL_EXSTYLE, style_ex as i32); + } + + let mut flags = SWP_NOZORDER | SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED; + + // We generally don't want style changes here to affect window + // focus, but for fullscreen windows they must be activated + // (i.e. focused) so that they appear on top of the taskbar + if !new.contains(WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN) + && !new.contains(WindowFlags::MARKER_BORDERLESS_FULLSCREEN) + { + flags |= SWP_NOACTIVATE; + } + + // Refresh the window frame + SetWindowPos(window, 0, 0, 0, 0, 0, flags); + SendMessageW(window, event_loop::SET_RETAIN_STATE_ON_SIZE_MSG_ID.get(), 0, 0); + } + } + } + + pub fn adjust_rect(self, hwnd: HWND, mut rect: RECT) -> Result { + unsafe { + let mut style = GetWindowLongW(hwnd, GWL_STYLE) as u32; + let style_ex = GetWindowLongW(hwnd, GWL_EXSTYLE) as u32; + + // Frameless style implemented by manually overriding the non-client area in + // `WM_NCCALCSIZE`. + if !self.contains(WindowFlags::MARKER_DECORATIONS) { + style &= !(WS_CAPTION | WS_SIZEBOX); + } + + util::win_to_err({ + let b_menu = GetMenu(hwnd) != 0; + if let (Some(get_dpi_for_window), Some(adjust_window_rect_ex_for_dpi)) = + (*util::GET_DPI_FOR_WINDOW, *util::ADJUST_WINDOW_RECT_EX_FOR_DPI) + { + let dpi = get_dpi_for_window(hwnd); + adjust_window_rect_ex_for_dpi(&mut rect, style, b_menu.into(), style_ex, dpi) + } else { + AdjustWindowRectEx(&mut rect, style, b_menu.into(), style_ex) + } + })?; + Ok(rect) + } + } + + pub fn adjust_size(self, hwnd: HWND, size: PhysicalSize) -> PhysicalSize { + let (width, height): (u32, u32) = size.into(); + let rect = RECT { left: 0, right: width as i32, top: 0, bottom: height as i32 }; + let rect = self.adjust_rect(hwnd, rect).unwrap_or(rect); + + let outer_x = (rect.right - rect.left).abs(); + let outer_y = (rect.top - rect.bottom).abs(); + + PhysicalSize::new(outer_x as _, outer_y as _) + } + + pub fn set_size(self, hwnd: HWND, size: PhysicalSize) { + unsafe { + let (width, height): (u32, u32) = self.adjust_size(hwnd, size).into(); + SetWindowPos( + hwnd, + 0, + 0, + 0, + width as _, + height as _, + SWP_ASYNCWINDOWPOS | SWP_NOZORDER | SWP_NOREPOSITION | SWP_NOMOVE | SWP_NOACTIVATE, + ); + InvalidateRgn(hwnd, 0, false.into()); + } + } +} + +impl CursorFlags { + fn refresh_os_cursor(self, window: HWND) -> Result<(), io::Error> { + let client_rect = util::WindowArea::Inner.get_rect(window)?; + + if util::is_focused(window) { + let cursor_clip = match self.contains(CursorFlags::GRABBED) { + true => { + if self.contains(CursorFlags::LOCKED) { + if let Ok(pos) = util::get_cursor_position() { + Some(RECT { + left: pos.x, + right: pos.x + 1, + top: pos.y, + bottom: pos.y + 1, + }) + } else { + // If lock is applied while the cursor is not available, lock it to the + // middle of the window. + let cx = (client_rect.left + client_rect.right) / 2; + let cy = (client_rect.top + client_rect.bottom) / 2; + Some(RECT { left: cx, right: cx + 1, top: cy, bottom: cy + 1 }) + } + } else if self.contains(CursorFlags::HIDDEN) { + // Confine the cursor to the center of the window if the cursor is hidden. + // This avoids problems with the cursor activating + // the taskbar if the window borders or overlaps that. + let cx = (client_rect.left + client_rect.right) / 2; + let cy = (client_rect.top + client_rect.bottom) / 2; + Some(RECT { left: cx, right: cx + 1, top: cy, bottom: cy + 1 }) + } else { + Some(client_rect) + } + }, + false => None, + }; + + let rect_to_tuple = |rect: RECT| (rect.left, rect.top, rect.right, rect.bottom); + let active_cursor_clip = rect_to_tuple(util::get_cursor_clip()?); + let desktop_rect = rect_to_tuple(util::get_desktop_rect()); + + let active_cursor_clip = match desktop_rect == active_cursor_clip { + true => None, + false => Some(active_cursor_clip), + }; + + // We do this check because calling `set_cursor_clip` incessantly will flood the event + // loop with `WM_MOUSEMOVE` events, and `refresh_os_cursor` is called by + // `set_cursor_flags` which at times gets called once every iteration of the + // eventloop. + if active_cursor_clip != cursor_clip.map(rect_to_tuple) { + util::set_cursor_clip(cursor_clip)?; + } + } + + let cursor_in_client = self.contains(CursorFlags::IN_WINDOW); + if cursor_in_client { + util::set_cursor_hidden(self.contains(CursorFlags::HIDDEN)); + } else { + util::set_cursor_hidden(false); + } + + Ok(()) + } +} diff --git a/vendor/winit/src/utils.rs b/vendor/winit/src/utils.rs new file mode 100644 index 00000000..d9631edb --- /dev/null +++ b/vendor/winit/src/utils.rs @@ -0,0 +1,28 @@ +// A poly-fill for `lazy_cell` +// Replace with std::sync::LazyLock when https://github.com/rust-lang/rust/issues/109736 is stabilized. + +// This isn't used on every platform, which can come up as dead code warnings. +#![allow(dead_code)] + +use std::ops::Deref; +use std::sync::OnceLock; + +pub(crate) struct Lazy { + cell: OnceLock, + init: fn() -> T, +} + +impl Lazy { + pub const fn new(f: fn() -> T) -> Self { + Self { cell: OnceLock::new(), init: f } + } +} + +impl Deref for Lazy { + type Target = T; + + #[inline] + fn deref(&self) -> &'_ T { + self.cell.get_or_init(self.init) + } +} diff --git a/vendor/winit/src/window.rs b/vendor/winit/src/window.rs new file mode 100644 index 00000000..e0158eff --- /dev/null +++ b/vendor/winit/src/window.rs @@ -0,0 +1,1876 @@ +//! The [`Window`] struct and associated types. +use std::fmt; + +use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use crate::error::{ExternalError, NotSupportedError}; +use crate::monitor::{MonitorHandle, VideoModeHandle}; +use crate::platform_impl::{self, PlatformSpecificWindowAttributes}; + +pub use crate::cursor::{BadImage, Cursor, CustomCursor, CustomCursorSource, MAX_CURSOR_SIZE}; +pub use crate::icon::{BadIcon, Icon}; + +#[doc(inline)] +pub use cursor_icon::{CursorIcon, ParseError as CursorIconParseError}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Represents a window. +/// +/// The window is closed when dropped. +/// +/// ## Threading +/// +/// This is `Send + Sync`, meaning that it can be freely used from other +/// threads. +/// +/// However, some platforms (macOS, Web and iOS) only allow user interface +/// interactions on the main thread, so on those platforms, if you use the +/// window from a thread other than the main, the code is scheduled to run on +/// the main thread, and your thread may be blocked until that completes. +/// +/// ## Platform-specific +/// +/// **Web:** The [`Window`], which is represented by a `HTMLElementCanvas`, can +/// not be closed by dropping the [`Window`]. +pub struct Window { + pub(crate) window: platform_impl::Window, +} + +impl fmt::Debug for Window { + fn fmt(&self, fmtr: &mut fmt::Formatter<'_>) -> fmt::Result { + fmtr.pad("Window { .. }") + } +} + +impl Drop for Window { + /// This will close the [`Window`]. + /// + /// See [`Window`] for more details. + fn drop(&mut self) { + self.window.maybe_wait_on_main(|w| { + // If the window is in exclusive fullscreen, we must restore the desktop + // video mode (generally this would be done on application exit, but + // closing the window doesn't necessarily always mean application exit, + // such as when there are multiple windows) + if let Some(Fullscreen::Exclusive(_)) = w.fullscreen().map(|f| f.into()) { + w.set_fullscreen(None); + } + }) + } +} + +/// Identifier of a window. Unique for each window. +/// +/// Can be obtained with [`window.id()`][`Window::id`]. +/// +/// Whenever you receive an event specific to a window, this event contains a `WindowId` which you +/// can then compare to the ids of your windows. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId(pub(crate) platform_impl::WindowId); + +impl WindowId { + /// Returns a dummy id, useful for unit testing. + /// + /// # Notes + /// + /// The only guarantee made about the return value of this function is that + /// it will always be equal to itself and to future values returned by this function. + /// No other guarantees are made. This may be equal to a real [`WindowId`]. + pub const fn dummy() -> Self { + WindowId(platform_impl::WindowId::dummy()) + } +} + +impl fmt::Debug for WindowId { + fn fmt(&self, fmtr: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(fmtr) + } +} + +impl From for u64 { + fn from(window_id: WindowId) -> Self { + window_id.0.into() + } +} + +impl From for WindowId { + fn from(raw_id: u64) -> Self { + Self(raw_id.into()) + } +} + +/// Attributes used when creating a window. +#[derive(Debug, Clone)] +pub struct WindowAttributes { + pub inner_size: Option, + pub min_inner_size: Option, + pub max_inner_size: Option, + pub position: Option, + pub resizable: bool, + pub enabled_buttons: WindowButtons, + pub title: String, + pub maximized: bool, + pub visible: bool, + pub transparent: bool, + pub blur: bool, + pub decorations: bool, + pub window_icon: Option, + pub preferred_theme: Option, + pub resize_increments: Option, + pub content_protected: bool, + pub window_level: WindowLevel, + pub active: bool, + pub cursor: Cursor, + #[cfg(feature = "rwh_06")] + pub(crate) parent_window: Option, + pub fullscreen: Option, + // Platform-specific configuration. + #[allow(dead_code)] + pub(crate) platform_specific: PlatformSpecificWindowAttributes, +} + +impl Default for WindowAttributes { + #[inline] + fn default() -> WindowAttributes { + WindowAttributes { + inner_size: None, + min_inner_size: None, + max_inner_size: None, + position: None, + resizable: true, + enabled_buttons: WindowButtons::all(), + title: "winit window".to_owned(), + maximized: false, + fullscreen: None, + visible: true, + transparent: false, + blur: false, + decorations: true, + window_level: Default::default(), + window_icon: None, + preferred_theme: None, + resize_increments: None, + content_protected: false, + cursor: Cursor::default(), + #[cfg(feature = "rwh_06")] + parent_window: None, + active: true, + platform_specific: Default::default(), + } + } +} + +/// Wrapper for [`rwh_06::RawWindowHandle`] for [`WindowAttributes::parent_window`]. +/// +/// # Safety +/// +/// The user has to account for that when using [`WindowAttributes::with_parent_window()`], +/// which is `unsafe`. +#[derive(Debug, Clone)] +#[cfg(feature = "rwh_06")] +pub(crate) struct SendSyncRawWindowHandle(pub(crate) rwh_06::RawWindowHandle); + +#[cfg(feature = "rwh_06")] +unsafe impl Send for SendSyncRawWindowHandle {} +#[cfg(feature = "rwh_06")] +unsafe impl Sync for SendSyncRawWindowHandle {} + +impl WindowAttributes { + /// Initializes new attributes with default values. + #[inline] + #[deprecated = "use `Window::default_attributes` instead"] + pub fn new() -> Self { + Default::default() + } +} + +impl WindowAttributes { + /// Get the parent window stored on the attributes. + #[cfg(feature = "rwh_06")] + pub fn parent_window(&self) -> Option<&rwh_06::RawWindowHandle> { + self.parent_window.as_ref().map(|handle| &handle.0) + } + + /// Requests the window to be of specific dimensions. + /// + /// If this is not set, some platform-specific dimensions will be used. + /// + /// See [`Window::request_inner_size`] for details. + #[inline] + pub fn with_inner_size>(mut self, size: S) -> Self { + self.inner_size = Some(size.into()); + self + } + + /// Sets the minimum dimensions a window can have. + /// + /// If this is not set, the window will have no minimum dimensions (aside + /// from reserved). + /// + /// See [`Window::set_min_inner_size`] for details. + #[inline] + pub fn with_min_inner_size>(mut self, min_size: S) -> Self { + self.min_inner_size = Some(min_size.into()); + self + } + + /// Sets the maximum dimensions a window can have. + /// + /// If this is not set, the window will have no maximum or will be set to + /// the primary monitor's dimensions by the platform. + /// + /// See [`Window::set_max_inner_size`] for details. + #[inline] + pub fn with_max_inner_size>(mut self, max_size: S) -> Self { + self.max_inner_size = Some(max_size.into()); + self + } + + /// Sets a desired initial position for the window. + /// + /// If this is not set, some platform-specific position will be chosen. + /// + /// See [`Window::set_outer_position`] for details. + /// + /// ## Platform-specific + /// + /// - **macOS:** The top left corner position of the window content, the window's "inner" + /// position. The window title bar will be placed above it. The window will be positioned such + /// that it fits on screen, maintaining set `inner_size` if any. If you need to precisely + /// position the top left corner of the whole window you have to use + /// [`Window::set_outer_position`] after creating the window. + /// - **Windows:** The top left corner position of the window title bar, the window's "outer" + /// position. There may be a small gap between this position and the window due to the + /// specifics of the Window Manager. + /// - **X11:** The top left corner of the window, the window's "outer" position. + /// - **Others:** Ignored. + #[inline] + pub fn with_position>(mut self, position: P) -> Self { + self.position = Some(position.into()); + self + } + + /// Sets whether the window is resizable or not. + /// + /// The default is `true`. + /// + /// See [`Window::set_resizable`] for details. + #[inline] + pub fn with_resizable(mut self, resizable: bool) -> Self { + self.resizable = resizable; + self + } + + /// Sets the enabled window buttons. + /// + /// The default is [`WindowButtons::all`] + /// + /// See [`Window::set_enabled_buttons`] for details. + #[inline] + pub fn with_enabled_buttons(mut self, buttons: WindowButtons) -> Self { + self.enabled_buttons = buttons; + self + } + + /// Sets the initial title of the window in the title bar. + /// + /// The default is `"winit window"`. + /// + /// See [`Window::set_title`] for details. + #[inline] + pub fn with_title>(mut self, title: T) -> Self { + self.title = title.into(); + self + } + + /// Sets whether the window should be put into fullscreen upon creation. + /// + /// The default is `None`. + /// + /// See [`Window::set_fullscreen`] for details. + #[inline] + pub fn with_fullscreen(mut self, fullscreen: Option) -> Self { + self.fullscreen = fullscreen; + self + } + + /// Request that the window is maximized upon creation. + /// + /// The default is `false`. + /// + /// See [`Window::set_maximized`] for details. + #[inline] + pub fn with_maximized(mut self, maximized: bool) -> Self { + self.maximized = maximized; + self + } + + /// Sets whether the window will be initially visible or hidden. + /// + /// The default is to show the window. + /// + /// See [`Window::set_visible`] for details. + #[inline] + pub fn with_visible(mut self, visible: bool) -> Self { + self.visible = visible; + self + } + + /// Sets whether the background of the window should be transparent. + /// + /// If this is `true`, writing colors with alpha values different than + /// `1.0` will produce a transparent window. On some platforms this + /// is more of a hint for the system and you'd still have the alpha + /// buffer. To control it see [`Window::set_transparent`]. + /// + /// The default is `false`. + #[inline] + pub fn with_transparent(mut self, transparent: bool) -> Self { + self.transparent = transparent; + self + } + + /// Sets whether the background of the window should be blurred by the system. + /// + /// The default is `false`. + /// + /// See [`Window::set_blur`] for details. + #[inline] + pub fn with_blur(mut self, blur: bool) -> Self { + self.blur = blur; + self + } + + /// Get whether the window will support transparency. + #[inline] + pub fn transparent(&self) -> bool { + self.transparent + } + + /// Sets whether the window should have a border, a title bar, etc. + /// + /// The default is `true`. + /// + /// See [`Window::set_decorations`] for details. + #[inline] + pub fn with_decorations(mut self, decorations: bool) -> Self { + self.decorations = decorations; + self + } + + /// Sets the window level. + /// + /// This is just a hint to the OS, and the system could ignore it. + /// + /// The default is [`WindowLevel::Normal`]. + /// + /// See [`WindowLevel`] for details. + #[inline] + pub fn with_window_level(mut self, level: WindowLevel) -> Self { + self.window_level = level; + self + } + + /// Sets the window icon. + /// + /// The default is `None`. + /// + /// See [`Window::set_window_icon`] for details. + #[inline] + pub fn with_window_icon(mut self, window_icon: Option) -> Self { + self.window_icon = window_icon; + self + } + + /// Sets a specific theme for the window. + /// + /// If `None` is provided, the window will use the system theme. + /// + /// The default is `None`. + /// + /// ## Platform-specific + /// + /// - **Wayland:** This controls only CSD. When using `None` it'll try to use dbus to get the + /// system preference. When explicit theme is used, this will avoid dbus all together. + /// - **x11:** Build window with `_GTK_THEME_VARIANT` hint set to `dark` or `light`. + /// - **iOS / Android / Web / x11 / Orbital:** Ignored. + #[inline] + pub fn with_theme(mut self, theme: Option) -> Self { + self.preferred_theme = theme; + self + } + + /// Build window with resize increments hint. + /// + /// The default is `None`. + /// + /// See [`Window::set_resize_increments`] for details. + #[inline] + pub fn with_resize_increments>(mut self, resize_increments: S) -> Self { + self.resize_increments = Some(resize_increments.into()); + self + } + + /// Prevents the window contents from being captured by other apps. + /// + /// The default is `false`. + /// + /// ## Platform-specific + /// + /// - **macOS**: if `false`, [`NSWindowSharingNone`] is used but doesn't completely prevent all + /// apps from reading the window content, for instance, QuickTime. + /// - **iOS / Android / Web / x11 / Orbital:** Ignored. + /// + /// [`NSWindowSharingNone`]: https://developer.apple.com/documentation/appkit/nswindowsharingtype/nswindowsharingnone + #[inline] + pub fn with_content_protected(mut self, protected: bool) -> Self { + self.content_protected = protected; + self + } + + /// Whether the window will be initially focused or not. + /// + /// The window should be assumed as not focused by default + /// following by the [`WindowEvent::Focused`]. + /// + /// ## Platform-specific: + /// + /// **Android / iOS / X11 / Wayland / Orbital:** Unsupported. + /// + /// [`WindowEvent::Focused`]: crate::event::WindowEvent::Focused. + #[inline] + pub fn with_active(mut self, active: bool) -> Self { + self.active = active; + self + } + + /// Modifies the cursor icon of the window. + /// + /// The default is [`CursorIcon::Default`]. + /// + /// See [`Window::set_cursor()`] for more details. + #[inline] + pub fn with_cursor(mut self, cursor: impl Into) -> Self { + self.cursor = cursor.into(); + self + } + + /// Build window with parent window. + /// + /// The default is `None`. + /// + /// ## Safety + /// + /// `parent_window` must be a valid window handle. + /// + /// ## Platform-specific + /// + /// - **Windows** : A child window has the WS_CHILD style and is confined + /// to the client area of its parent window. For more information, see + /// + /// - **X11**: A child window is confined to the client area of its parent window. + /// - **Android / iOS / Wayland / Web:** Unsupported. + #[cfg(feature = "rwh_06")] + #[inline] + pub unsafe fn with_parent_window( + mut self, + parent_window: Option, + ) -> Self { + self.parent_window = parent_window.map(SendSyncRawWindowHandle); + self + } +} + +/// Base Window functions. +impl Window { + /// Create a new [`WindowAttributes`] which allows modifying the window's attributes before + /// creation. + #[inline] + pub fn default_attributes() -> WindowAttributes { + WindowAttributes::default() + } + + /// Returns an identifier unique to the window. + #[inline] + pub fn id(&self) -> WindowId { + let _span = tracing::debug_span!("winit::Window::id",).entered(); + + self.window.maybe_wait_on_main(|w| WindowId(w.id())) + } + + /// Returns the scale factor that can be used to map logical pixels to physical pixels, and + /// vice versa. + /// + /// Note that this value can change depending on user action (for example if the window is + /// moved to another screen); as such, tracking [`WindowEvent::ScaleFactorChanged`] events is + /// the most robust way to track the DPI you need to use to draw. + /// + /// This value may differ from [`MonitorHandle::scale_factor`]. + /// + /// See the [`dpi`] crate for more information. + /// + /// ## Platform-specific + /// + /// The scale factor is calculated differently on different platforms: + /// + /// - **Windows:** On Windows 8 and 10, per-monitor scaling is readily configured by users from + /// the display settings. While users are free to select any option they want, they're only + /// given a selection of "nice" scale factors, i.e. 1.0, 1.25, 1.5... on Windows 7. The scale + /// factor is global and changing it requires logging out. See [this article][windows_1] for + /// technical details. + /// - **macOS:** Recent macOS versions allow the user to change the scaling factor for specific + /// displays. When available, the user may pick a per-monitor scaling factor from a set of + /// pre-defined settings. All "retina displays" have a scaling factor above 1.0 by default, + /// but the specific value varies across devices. + /// - **X11:** Many man-hours have been spent trying to figure out how to handle DPI in X11. + /// Winit currently uses a three-pronged approach: + /// + Use the value in the `WINIT_X11_SCALE_FACTOR` environment variable if present. + /// + If not present, use the value set in `Xft.dpi` in Xresources. + /// + Otherwise, calculate the scale factor based on the millimeter monitor dimensions + /// provided by XRandR. + /// + /// If `WINIT_X11_SCALE_FACTOR` is set to `randr`, it'll ignore the `Xft.dpi` field and use + /// the XRandR scaling method. Generally speaking, you should try to configure the + /// standard system variables to do what you want before resorting to + /// `WINIT_X11_SCALE_FACTOR`. + /// - **Wayland:** The scale factor is suggested by the compositor for each window individually + /// by using the wp-fractional-scale protocol if available. Falls back to integer-scale + /// factors otherwise. + /// + /// The monitor scale factor may differ from the window scale factor. + /// - **iOS:** Scale factors are set by Apple to the value that best suits the device, and range + /// from `1.0` to `3.0`. See [this article][apple_1] and [this article][apple_2] for more + /// information. + /// + /// This uses the underlying `UIView`'s [`contentScaleFactor`]. + /// - **Android:** Scale factors are set by the manufacturer to the value that best suits the + /// device, and range from `1.0` to `4.0`. See [this article][android_1] for more information. + /// + /// This is currently unimplemented, and this function always returns 1.0. + /// - **Web:** The scale factor is the ratio between CSS pixels and the physical device pixels. + /// In other words, it is the value of [`window.devicePixelRatio`][web_1]. It is affected by + /// both the screen scaling and the browser zoom level and can go below `1.0`. + /// - **Orbital:** This is currently unimplemented, and this function always returns 1.0. + /// + /// [`WindowEvent::ScaleFactorChanged`]: crate::event::WindowEvent::ScaleFactorChanged + /// [windows_1]: https://docs.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows + /// [apple_1]: https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html + /// [apple_2]: https://developer.apple.com/design/human-interface-guidelines/macos/icons-and-images/image-size-and-resolution/ + /// [android_1]: https://developer.android.com/training/multiscreen/screendensities + /// [web_1]: https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio + /// [`contentScaleFactor`]: https://developer.apple.com/documentation/uikit/uiview/1622657-contentscalefactor?language=objc + #[inline] + pub fn scale_factor(&self) -> f64 { + let _span = tracing::debug_span!("winit::Window::scale_factor",).entered(); + + self.window.maybe_wait_on_main(|w| w.scale_factor()) + } + + /// Queues a [`WindowEvent::RedrawRequested`] event to be emitted that aligns with the windowing + /// system drawing loop. + /// + /// This is the **strongly encouraged** method of redrawing windows, as it can integrate with + /// OS-requested redraws (e.g. when a window gets resized). To improve the event delivery + /// consider using [`Window::pre_present_notify`] as described in docs. + /// + /// Applications should always aim to redraw whenever they receive a `RedrawRequested` event. + /// + /// There are no strong guarantees about when exactly a `RedrawRequest` event will be emitted + /// with respect to other events, since the requirements can vary significantly between + /// windowing systems. + /// + /// However as the event aligns with the windowing system drawing loop, it may not arrive in + /// same or even next event loop iteration. + /// + /// ## Platform-specific + /// + /// - **Windows** This API uses `RedrawWindow` to request a `WM_PAINT` message and + /// `RedrawRequested` is emitted in sync with any `WM_PAINT` messages. + /// - **iOS:** Can only be called on the main thread. + /// - **Wayland:** The events are aligned with the frame callbacks when + /// [`Window::pre_present_notify`] is used. + /// - **Web:** [`WindowEvent::RedrawRequested`] will be aligned with the + /// `requestAnimationFrame`. + /// + /// [`WindowEvent::RedrawRequested`]: crate::event::WindowEvent::RedrawRequested + #[inline] + pub fn request_redraw(&self) { + let _span = tracing::debug_span!("winit::Window::request_redraw",).entered(); + + self.window.maybe_queue_on_main(|w| w.request_redraw()) + } + + /// Notify the windowing system before presenting to the window. + /// + /// You should call this event after your drawing operations, but before you submit + /// the buffer to the display or commit your drawings. Doing so will help winit to properly + /// schedule and make assumptions about its internal state. For example, it could properly + /// throttle [`WindowEvent::RedrawRequested`]. + /// + /// ## Example + /// + /// This example illustrates how it looks with OpenGL, but it applies to other graphics + /// APIs and software rendering. + /// + /// ```no_run + /// # use winit::window::Window; + /// # fn swap_buffers() {} + /// # fn scope(window: &Window) { + /// // Do the actual drawing with OpenGL. + /// + /// // Notify winit that we're about to submit buffer to the windowing system. + /// window.pre_present_notify(); + /// + /// // Submit buffer to the windowing system. + /// swap_buffers(); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **Android / iOS / X11 / Web / Windows / macOS / Orbital:** Unsupported. + /// - **Wayland:** Schedules a frame callback to throttle [`WindowEvent::RedrawRequested`]. + /// + /// [`WindowEvent::RedrawRequested`]: crate::event::WindowEvent::RedrawRequested + #[inline] + pub fn pre_present_notify(&self) { + let _span = tracing::debug_span!("winit::Window::pre_present_notify",).entered(); + + self.window.maybe_queue_on_main(|w| w.pre_present_notify()); + } + + /// Reset the dead key state of the keyboard. + /// + /// This is useful when a dead key is bound to trigger an action. Then + /// this function can be called to reset the dead key state so that + /// follow-up text input won't be affected by the dead key. + /// + /// ## Platform-specific + /// - **Web, macOS:** Does nothing + // --------------------------- + // Developers' Note: If this cannot be implemented on every desktop platform + // at least, then this function should be provided through a platform specific + // extension trait + pub fn reset_dead_keys(&self) { + let _span = tracing::debug_span!("winit::Window::reset_dead_keys",).entered(); + + self.window.maybe_queue_on_main(|w| w.reset_dead_keys()) + } +} + +/// Position and size functions. +impl Window { + /// Returns the position of the top-left hand corner of the window's client area relative to the + /// top-left hand corner of the desktop. + /// + /// The same conditions that apply to [`Window::outer_position`] apply to this method. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Returns the top left coordinates of the + /// window's [safe area] in the screen space coordinate system. + /// - **Web:** Returns the top-left coordinates relative to the viewport. _Note: this returns + /// the same value as [`Window::outer_position`]._ + /// - **Android / Wayland:** Always returns [`NotSupportedError`]. + /// + /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc + #[inline] + pub fn inner_position(&self) -> Result, NotSupportedError> { + let _span = tracing::debug_span!("winit::Window::inner_position",).entered(); + + self.window.maybe_wait_on_main(|w| w.inner_position()) + } + + /// Returns the position of the top-left hand corner of the window relative to the + /// top-left hand corner of the desktop. + /// + /// Note that the top-left hand corner of the desktop is not necessarily the same as + /// the screen. If the user uses a desktop with multiple monitors, the top-left hand corner + /// of the desktop is the top-left hand corner of the monitor at the top-left of the desktop. + /// + /// The coordinates can be negative if the top-left hand corner of the window is outside + /// of the visible screen region. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Returns the top left coordinates of the + /// window in the screen space coordinate system. + /// - **Web:** Returns the top-left coordinates relative to the viewport. + /// - **Android / Wayland:** Always returns [`NotSupportedError`]. + #[inline] + pub fn outer_position(&self) -> Result, NotSupportedError> { + let _span = tracing::debug_span!("winit::Window::outer_position",).entered(); + + self.window.maybe_wait_on_main(|w| w.outer_position()) + } + + /// Modifies the position of the window. + /// + /// See [`Window::outer_position`] for more information about the coordinates. + /// This automatically un-maximizes the window if it's maximized. + /// + /// ```no_run + /// # use winit::dpi::{LogicalPosition, PhysicalPosition}; + /// # use winit::window::Window; + /// # fn scope(window: &Window) { + /// // Specify the position in logical dimensions like this: + /// window.set_outer_position(LogicalPosition::new(400.0, 200.0)); + /// + /// // Or specify the position in physical dimensions like this: + /// window.set_outer_position(PhysicalPosition::new(400, 200)); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Sets the top left coordinates of the + /// window in the screen space coordinate system. + /// - **Web:** Sets the top-left coordinates relative to the viewport. Doesn't account for CSS + /// [`transform`]. + /// - **Android / Wayland:** Unsupported. + /// + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + #[inline] + pub fn set_outer_position>(&self, position: P) { + let position = position.into(); + let _span = tracing::debug_span!( + "winit::Window::set_outer_position", + position = ?position + ) + .entered(); + + self.window.maybe_queue_on_main(move |w| w.set_outer_position(position)) + } + + /// Returns the physical size of the window's client area. + /// + /// The client area is the content of the window, excluding the title bar and borders. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Returns the `PhysicalSize` of the window's + /// [safe area] in screen space coordinates. + /// - **Web:** Returns the size of the canvas element. Doesn't account for CSS [`transform`]. + /// + /// [safe area]: https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + #[inline] + pub fn inner_size(&self) -> PhysicalSize { + let _span = tracing::debug_span!("winit::Window::inner_size",).entered(); + + self.window.maybe_wait_on_main(|w| w.inner_size()) + } + + /// Request the new size for the window. + /// + /// On platforms where the size is entirely controlled by the user the + /// applied size will be returned immediately, resize event in such case + /// may not be generated. + /// + /// On platforms where resizing is disallowed by the windowing system, the current + /// inner size is returned immediately, and the user one is ignored. + /// + /// When `None` is returned, it means that the request went to the display system, + /// and the actual size will be delivered later with the [`WindowEvent::Resized`]. + /// + /// See [`Window::inner_size`] for more information about the values. + /// + /// The request could automatically un-maximize the window if it's maximized. + /// + /// ```no_run + /// # use winit::dpi::{LogicalSize, PhysicalSize}; + /// # use winit::window::Window; + /// # fn scope(window: &Window) { + /// // Specify the size in logical dimensions like this: + /// let _ = window.request_inner_size(LogicalSize::new(400.0, 200.0)); + /// + /// // Or specify the size in physical dimensions like this: + /// let _ = window.request_inner_size(PhysicalSize::new(400, 200)); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **Web:** Sets the size of the canvas element. Doesn't account for CSS [`transform`]. + /// + /// [`WindowEvent::Resized`]: crate::event::WindowEvent::Resized + /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform + #[inline] + #[must_use] + pub fn request_inner_size>(&self, size: S) -> Option> { + let size = size.into(); + let _span = tracing::debug_span!( + "winit::Window::request_inner_size", + size = ?size + ) + .entered(); + self.window.maybe_wait_on_main(|w| w.request_inner_size(size)) + } + + /// Returns the physical size of the entire window. + /// + /// These dimensions include the title bar and borders. If you don't want that (and you usually + /// don't), use [`Window::inner_size`] instead. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. Returns the [`PhysicalSize`] of the window + /// in screen space coordinates. + /// - **Web:** Returns the size of the canvas element. _Note: this returns the same value as + /// [`Window::inner_size`]._ + #[inline] + pub fn outer_size(&self) -> PhysicalSize { + let _span = tracing::debug_span!("winit::Window::outer_size",).entered(); + self.window.maybe_wait_on_main(|w| w.outer_size()) + } + + /// Sets a minimum dimension size for the window. + /// + /// ```no_run + /// # use winit::dpi::{LogicalSize, PhysicalSize}; + /// # use winit::window::Window; + /// # fn scope(window: &Window) { + /// // Specify the size in logical dimensions like this: + /// window.set_min_inner_size(Some(LogicalSize::new(400.0, 200.0))); + /// + /// // Or specify the size in physical dimensions like this: + /// window.set_min_inner_size(Some(PhysicalSize::new(400, 200))); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Orbital:** Unsupported. + #[inline] + pub fn set_min_inner_size>(&self, min_size: Option) { + let min_size = min_size.map(|s| s.into()); + let _span = tracing::debug_span!( + "winit::Window::set_min_inner_size", + min_size = ?min_size + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.set_min_inner_size(min_size)) + } + + /// Sets a maximum dimension size for the window. + /// + /// ```no_run + /// # use winit::dpi::{LogicalSize, PhysicalSize}; + /// # use winit::window::Window; + /// # fn scope(window: &Window) { + /// // Specify the size in logical dimensions like this: + /// window.set_max_inner_size(Some(LogicalSize::new(400.0, 200.0))); + /// + /// // Or specify the size in physical dimensions like this: + /// window.set_max_inner_size(Some(PhysicalSize::new(400, 200))); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Orbital:** Unsupported. + #[inline] + pub fn set_max_inner_size>(&self, max_size: Option) { + let max_size = max_size.map(|s| s.into()); + let _span = tracing::debug_span!( + "winit::Window::max_size", + max_size = ?max_size + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.set_max_inner_size(max_size)) + } + + /// Returns window resize increments if any were set. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Orbital:** Always returns [`None`]. + #[inline] + pub fn resize_increments(&self) -> Option> { + let _span = tracing::debug_span!("winit::Window::resize_increments",).entered(); + self.window.maybe_wait_on_main(|w| w.resize_increments()) + } + + /// Sets window resize increments. + /// + /// This is a niche constraint hint usually employed by terminal emulators + /// and other apps that need "blocky" resizes. + /// + /// ## Platform-specific + /// + /// - **macOS:** Increments are converted to logical size and then macOS rounds them to whole + /// numbers. + /// - **iOS / Android / Web / Orbital:** Unsupported. + #[inline] + pub fn set_resize_increments>(&self, increments: Option) { + let increments = increments.map(Into::into); + let _span = tracing::debug_span!( + "winit::Window::set_resize_increments", + increments = ?increments + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.set_resize_increments(increments)) + } +} + +/// Misc. attribute functions. +impl Window { + /// Modifies the title of the window. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_title(&self, title: &str) { + let _span = tracing::debug_span!("winit::Window::set_title", title).entered(); + self.window.maybe_wait_on_main(|w| w.set_title(title)) + } + + /// Change the window transparency state. + /// + /// This is just a hint that may not change anything about + /// the window transparency, however doing a mismatch between + /// the content of your window and this hint may result in + /// visual artifacts. + /// + /// The default value follows the [`WindowAttributes::with_transparent`]. + /// + /// ## Platform-specific + /// + /// - **macOS:** This will reset the window's background color. + /// - **Web / iOS / Android:** Unsupported. + /// - **X11:** Can only be set while building the window, with + /// [`WindowAttributes::with_transparent`]. + #[inline] + pub fn set_transparent(&self, transparent: bool) { + let _span = tracing::debug_span!("winit::Window::set_transparent", transparent).entered(); + self.window.maybe_queue_on_main(move |w| w.set_transparent(transparent)) + } + + /// Change the window blur state. + /// + /// If `true`, this will make the transparent window background blurry. + /// + /// ## Platform-specific + /// + /// - **Android / iOS / X11 / Web / Windows:** Unsupported. + /// - **Wayland:** Only works with org_kde_kwin_blur_manager protocol. + #[inline] + pub fn set_blur(&self, blur: bool) { + let _span = tracing::debug_span!("winit::Window::set_blur", blur).entered(); + self.window.maybe_queue_on_main(move |w| w.set_blur(blur)) + } + + /// Modifies the window's visibility. + /// + /// If `false`, this will hide the window. If `true`, this will show the window. + /// + /// ## Platform-specific + /// + /// - **Android / Wayland / Web:** Unsupported. + /// - **iOS:** Can only be called on the main thread. + #[inline] + pub fn set_visible(&self, visible: bool) { + let _span = tracing::debug_span!("winit::Window::set_visible", visible).entered(); + self.window.maybe_queue_on_main(move |w| w.set_visible(visible)) + } + + /// Gets the window's current visibility state. + /// + /// `None` means it couldn't be determined, so it is not recommended to use this to drive your + /// rendering backend. + /// + /// ## Platform-specific + /// + /// - **X11:** Not implemented. + /// - **Wayland / iOS / Android / Web:** Unsupported. + #[inline] + pub fn is_visible(&self) -> Option { + let _span = tracing::debug_span!("winit::Window::is_visible",).entered(); + self.window.maybe_wait_on_main(|w| w.is_visible()) + } + + /// Sets whether the window is resizable or not. + /// + /// Note that making the window unresizable doesn't exempt you from handling + /// [`WindowEvent::Resized`], as that event can still be triggered by DPI scaling, entering + /// fullscreen mode, etc. Also, the window could still be resized by calling + /// [`Window::request_inner_size`]. + /// + /// ## Platform-specific + /// + /// This only has an effect on desktop platforms. + /// + /// - **X11:** Due to a bug in XFCE, this has no effect on Xfwm. + /// - **iOS / Android / Web:** Unsupported. + /// + /// [`WindowEvent::Resized`]: crate::event::WindowEvent::Resized + #[inline] + pub fn set_resizable(&self, resizable: bool) { + let _span = tracing::debug_span!("winit::Window::set_resizable", resizable).entered(); + self.window.maybe_queue_on_main(move |w| w.set_resizable(resizable)) + } + + /// Gets the window's current resizable state. + /// + /// ## Platform-specific + /// + /// - **X11:** Not implemented. + /// - **iOS / Android / Web:** Unsupported. + #[inline] + pub fn is_resizable(&self) -> bool { + let _span = tracing::debug_span!("winit::Window::is_resizable",).entered(); + self.window.maybe_wait_on_main(|w| w.is_resizable()) + } + + /// Sets the enabled window buttons. + /// + /// ## Platform-specific + /// + /// - **Wayland / X11 / Orbital:** Not implemented. + /// - **Web / iOS / Android:** Unsupported. + pub fn set_enabled_buttons(&self, buttons: WindowButtons) { + let _span = tracing::debug_span!( + "winit::Window::set_enabled_buttons", + buttons = ?buttons + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.set_enabled_buttons(buttons)) + } + + /// Gets the enabled window buttons. + /// + /// ## Platform-specific + /// + /// - **Wayland / X11 / Orbital:** Not implemented. Always returns [`WindowButtons::all`]. + /// - **Web / iOS / Android:** Unsupported. Always returns [`WindowButtons::all`]. + pub fn enabled_buttons(&self) -> WindowButtons { + let _span = tracing::debug_span!("winit::Window::enabled_buttons",).entered(); + self.window.maybe_wait_on_main(|w| w.enabled_buttons()) + } + + /// Sets the window to minimized or back + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Orbital:** Unsupported. + /// - **Wayland:** Un-minimize is unsupported. + #[inline] + pub fn set_minimized(&self, minimized: bool) { + let _span = tracing::debug_span!("winit::Window::set_minimized", minimized).entered(); + self.window.maybe_queue_on_main(move |w| w.set_minimized(minimized)) + } + + /// Gets the window's current minimized state. + /// + /// `None` will be returned, if the minimized state couldn't be determined. + /// + /// ## Note + /// + /// - You shouldn't stop rendering for minimized windows, however you could lower the fps. + /// + /// ## Platform-specific + /// + /// - **Wayland**: always `None`. + /// - **iOS / Android / Web / Orbital:** Unsupported. + #[inline] + pub fn is_minimized(&self) -> Option { + let _span = tracing::debug_span!("winit::Window::is_minimized",).entered(); + self.window.maybe_wait_on_main(|w| w.is_minimized()) + } + + /// Sets the window to maximized or back. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web:** Unsupported. + #[inline] + pub fn set_maximized(&self, maximized: bool) { + let _span = tracing::debug_span!("winit::Window::set_maximized", maximized).entered(); + self.window.maybe_queue_on_main(move |w| w.set_maximized(maximized)) + } + + /// Gets the window's current maximized state. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web:** Unsupported. + #[inline] + pub fn is_maximized(&self) -> bool { + let _span = tracing::debug_span!("winit::Window::is_maximized",).entered(); + self.window.maybe_wait_on_main(|w| w.is_maximized()) + } + + /// Sets the window to fullscreen or back. + /// + /// ## Platform-specific + /// + /// - **macOS:** [`Fullscreen::Exclusive`] provides true exclusive mode with a video mode + /// change. *Caveat!* macOS doesn't provide task switching (or spaces!) while in exclusive + /// fullscreen mode. This mode should be used when a video mode change is desired, but for a + /// better user experience, borderless fullscreen might be preferred. + /// + /// [`Fullscreen::Borderless`] provides a borderless fullscreen window on a + /// separate space. This is the idiomatic way for fullscreen games to work + /// on macOS. See `WindowExtMacOs::set_simple_fullscreen` if + /// separate spaces are not preferred. + /// + /// The dock and the menu bar are disabled in exclusive fullscreen mode. + /// - **iOS:** Can only be called on the main thread. + /// - **Wayland:** Does not support exclusive fullscreen mode and will no-op a request. + /// - **Windows:** Screen saver is disabled in fullscreen mode. + /// - **Android / Orbital:** Unsupported. + /// - **Web:** Does nothing without a [transient activation]. + /// + /// [transient activation]: https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation + #[inline] + pub fn set_fullscreen(&self, fullscreen: Option) { + let _span = tracing::debug_span!( + "winit::Window::set_fullscreen", + fullscreen = ?fullscreen + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.set_fullscreen(fullscreen.map(|f| f.into()))) + } + + /// Gets the window's current fullscreen state. + /// + /// ## Platform-specific + /// + /// - **iOS:** Can only be called on the main thread. + /// - **Android / Orbital:** Will always return `None`. + /// - **Wayland:** Can return `Borderless(None)` when there are no monitors. + /// - **Web:** Can only return `None` or `Borderless(None)`. + #[inline] + pub fn fullscreen(&self) -> Option { + let _span = tracing::debug_span!("winit::Window::fullscreen",).entered(); + self.window.maybe_wait_on_main(|w| w.fullscreen().map(|f| f.into())) + } + + /// Turn window decorations on or off. + /// + /// Enable/disable window decorations provided by the server or Winit. + /// By default this is enabled. Note that fullscreen windows and windows on + /// mobile and web platforms naturally do not have decorations. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web:** No effect. + #[inline] + pub fn set_decorations(&self, decorations: bool) { + let _span = tracing::debug_span!("winit::Window::set_decorations", decorations).entered(); + self.window.maybe_queue_on_main(move |w| w.set_decorations(decorations)) + } + + /// Gets the window's current decorations state. + /// + /// Returns `true` when windows are decorated (server-side or by Winit). + /// Also returns `true` when no decorations are required (mobile, web). + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web:** Always returns `true`. + #[inline] + pub fn is_decorated(&self) -> bool { + let _span = tracing::debug_span!("winit::Window::is_decorated",).entered(); + self.window.maybe_wait_on_main(|w| w.is_decorated()) + } + + /// Change the window level. + /// + /// This is just a hint to the OS, and the system could ignore it. + /// + /// See [`WindowLevel`] for details. + pub fn set_window_level(&self, level: WindowLevel) { + let _span = tracing::debug_span!( + "winit::Window::set_window_level", + level = ?level + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.set_window_level(level)) + } + + /// Sets the window icon. + /// + /// On Windows and X11, this is typically the small icon in the top-left + /// corner of the titlebar. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Wayland / macOS / Orbital:** Unsupported. + /// + /// - **Windows:** Sets `ICON_SMALL`. The base size for a window icon is 16x16, but it's + /// recommended to account for screen scaling and pick a multiple of that, i.e. 32x32. + /// + /// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. + /// That said, it's usually in the same ballpark as on Windows. + #[inline] + pub fn set_window_icon(&self, window_icon: Option) { + let _span = tracing::debug_span!("winit::Window::set_window_icon",).entered(); + self.window.maybe_queue_on_main(move |w| w.set_window_icon(window_icon)) + } + + /// Set the IME cursor editing area, where the `position` is the top left corner of that area + /// and `size` is the size of this area starting from the position. An example of such area + /// could be a input field in the UI or line in the editor. + /// + /// The windowing system could place a candidate box close to that area, but try to not obscure + /// the specified area, so the user input to it stays visible. + /// + /// The candidate box is the window / popup / overlay that allows you to select the desired + /// characters. The look of this box may differ between input devices, even on the same + /// platform. + /// + /// (Apple's official term is "candidate window", see their [chinese] and [japanese] guides). + /// + /// ## Example + /// + /// ```no_run + /// # use winit::dpi::{LogicalPosition, PhysicalPosition, LogicalSize, PhysicalSize}; + /// # use winit::window::Window; + /// # fn scope(window: &Window) { + /// // Specify the position in logical dimensions like this: + /// window.set_ime_cursor_area(LogicalPosition::new(400.0, 200.0), LogicalSize::new(100, 100)); + /// + /// // Or specify the position in physical dimensions like this: + /// window.set_ime_cursor_area(PhysicalPosition::new(400, 200), PhysicalSize::new(100, 100)); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **X11:** - area is not supported, only position. + /// - **iOS / Android / Web / Orbital:** Unsupported. + /// + /// [chinese]: https://support.apple.com/guide/chinese-input-method/use-the-candidate-window-cim12992/104/mac/12.0 + /// [japanese]: https://support.apple.com/guide/japanese-input-method/use-the-candidate-window-jpim10262/6.3/mac/12.0 + #[inline] + pub fn set_ime_cursor_area, S: Into>(&self, position: P, size: S) { + let position = position.into(); + let size = size.into(); + let _span = tracing::debug_span!( + "winit::Window::set_ime_cursor_area", + position = ?position, + size = ?size, + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.set_ime_cursor_area(position, size)) + } + + /// Sets whether the window should get IME events + /// + /// When IME is allowed, the window will receive [`Ime`] events, and during the + /// preedit phase the window will NOT get [`KeyboardInput`] events. The window + /// should allow IME while it is expecting text input. + /// + /// When IME is not allowed, the window won't receive [`Ime`] events, and will + /// receive [`KeyboardInput`] events for every keypress instead. Not allowing + /// IME is useful for games for example. + /// + /// IME is **not** allowed by default. + /// + /// ## Platform-specific + /// + /// - **macOS:** IME must be enabled to receive text-input where dead-key sequences are + /// combined. + /// - **iOS / Android:** This will show / hide the soft keyboard. + /// - **Web / Orbital:** Unsupported. + /// - **X11**: Enabling IME will disable dead keys reporting during compose. + /// + /// [`Ime`]: crate::event::WindowEvent::Ime + /// [`KeyboardInput`]: crate::event::WindowEvent::KeyboardInput + #[inline] + pub fn set_ime_allowed(&self, allowed: bool) { + let _span = tracing::debug_span!("winit::Window::set_ime_allowed", allowed).entered(); + self.window.maybe_queue_on_main(move |w| w.set_ime_allowed(allowed)) + } + + /// Sets the IME purpose for the window using [`ImePurpose`]. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Windows / X11 / macOS / Orbital:** Unsupported. + #[inline] + pub fn set_ime_purpose(&self, purpose: ImePurpose) { + let _span = tracing::debug_span!( + "winit::Window::set_ime_purpose", + purpose = ?purpose + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.set_ime_purpose(purpose)) + } + + /// Brings the window to the front and sets input focus. Has no effect if the window is + /// already in focus, minimized, or not visible. + /// + /// This method steals input focus from other applications. Do not use this method unless + /// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive + /// user experience. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Wayland / Orbital:** Unsupported. + #[inline] + pub fn focus_window(&self) { + let _span = tracing::debug_span!("winit::Window::focus_window",).entered(); + self.window.maybe_queue_on_main(|w| w.focus_window()) + } + + /// Gets whether the window has keyboard focus. + /// + /// This queries the same state information as [`WindowEvent::Focused`]. + /// + /// [`WindowEvent::Focused`]: crate::event::WindowEvent::Focused + #[inline] + pub fn has_focus(&self) -> bool { + let _span = tracing::debug_span!("winit::Window::has_focus",).entered(); + self.window.maybe_wait_on_main(|w| w.has_focus()) + } + + /// Requests user attention to the window, this has no effect if the application + /// is already focused. How requesting for user attention manifests is platform dependent, + /// see [`UserAttentionType`] for details. + /// + /// Providing `None` will unset the request for user attention. Unsetting the request for + /// user attention might not be done automatically by the WM when the window receives input. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Orbital:** Unsupported. + /// - **macOS:** `None` has no effect. + /// - **X11:** Requests for user attention must be manually cleared. + /// - **Wayland:** Requires `xdg_activation_v1` protocol, `None` has no effect. + #[inline] + pub fn request_user_attention(&self, request_type: Option) { + let _span = tracing::debug_span!( + "winit::Window::request_user_attention", + request_type = ?request_type + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.request_user_attention(request_type)) + } + + /// Set or override the window theme. + /// + /// Specify `None` to reset the theme to the system default. + /// + /// ## Platform-specific + /// + /// - **Wayland:** Sets the theme for the client side decorations. Using `None` will use dbus to + /// get the system preference. + /// - **X11:** Sets `_GTK_THEME_VARIANT` hint to `dark` or `light` and if `None` is used, it + /// will default to [`Theme::Dark`]. + /// - **iOS / Android / Web / Orbital:** Unsupported. + #[inline] + pub fn set_theme(&self, theme: Option) { + let _span = tracing::debug_span!( + "winit::Window::set_theme", + theme = ?theme + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.set_theme(theme)) + } + + /// Returns the current window theme. + /// + /// Returns `None` if it cannot be determined on the current platform. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / x11 / Orbital:** Unsupported. + /// - **Wayland:** Only returns theme overrides. + #[inline] + pub fn theme(&self) -> Option { + let _span = tracing::debug_span!("winit::Window::theme",).entered(); + self.window.maybe_wait_on_main(|w| w.theme()) + } + + /// Prevents the window contents from being captured by other apps. + /// + /// ## Platform-specific + /// + /// - **macOS**: if `false`, [`NSWindowSharingNone`] is used but doesn't completely prevent all + /// apps from reading the window content, for instance, QuickTime. + /// - **iOS / Android / x11 / Wayland / Web / Orbital:** Unsupported. + /// + /// [`NSWindowSharingNone`]: https://developer.apple.com/documentation/appkit/nswindowsharingtype/nswindowsharingnone + pub fn set_content_protected(&self, protected: bool) { + let _span = + tracing::debug_span!("winit::Window::set_content_protected", protected).entered(); + self.window.maybe_queue_on_main(move |w| w.set_content_protected(protected)) + } + + /// Gets the current title of the window. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / x11 / Wayland / Web:** Unsupported. Always returns an empty string. + #[inline] + pub fn title(&self) -> String { + let _span = tracing::debug_span!("winit::Window::title",).entered(); + self.window.maybe_wait_on_main(|w| w.title()) + } +} + +/// Cursor functions. +impl Window { + /// Modifies the cursor icon of the window. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Orbital:** Unsupported. + /// - **Web:** Custom cursors have to be loaded and decoded first, until then the previous + /// cursor is shown. + #[inline] + pub fn set_cursor(&self, cursor: impl Into) { + let cursor = cursor.into(); + let _span = tracing::debug_span!("winit::Window::set_cursor",).entered(); + self.window.maybe_queue_on_main(move |w| w.set_cursor(cursor)) + } + + /// Deprecated! Use [`Window::set_cursor()`] instead. + #[deprecated = "Renamed to `set_cursor`"] + #[inline] + pub fn set_cursor_icon(&self, icon: CursorIcon) { + self.set_cursor(icon) + } + + /// Changes the position of the cursor in window coordinates. + /// + /// ```no_run + /// # use winit::dpi::{LogicalPosition, PhysicalPosition}; + /// # use winit::window::Window; + /// # fn scope(window: &Window) { + /// // Specify the position in logical dimensions like this: + /// window.set_cursor_position(LogicalPosition::new(400.0, 200.0)); + /// + /// // Or specify the position in physical dimensions like this: + /// window.set_cursor_position(PhysicalPosition::new(400, 200)); + /// # } + /// ``` + /// + /// ## Platform-specific + /// + /// - **Wayland**: Cursor must be in [`CursorGrabMode::Locked`]. + /// - **iOS / Android / Web / Orbital:** Always returns an [`ExternalError::NotSupported`]. + #[inline] + pub fn set_cursor_position>(&self, position: P) -> Result<(), ExternalError> { + let position = position.into(); + let _span = tracing::debug_span!( + "winit::Window::set_cursor_position", + position = ?position + ) + .entered(); + self.window.maybe_wait_on_main(|w| w.set_cursor_position(position)) + } + + /// Set grabbing [mode][CursorGrabMode] on the cursor preventing it from leaving the window. + /// + /// # Example + /// + /// First try confining the cursor, and if that fails, try locking it instead. + /// + /// ```no_run + /// # use winit::window::{CursorGrabMode, Window}; + /// # fn scope(window: &Window) { + /// window + /// .set_cursor_grab(CursorGrabMode::Confined) + /// .or_else(|_e| window.set_cursor_grab(CursorGrabMode::Locked)) + /// .unwrap(); + /// # } + /// ``` + #[inline] + pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { + let _span = tracing::debug_span!( + "winit::Window::set_cursor_grab", + mode = ?mode + ) + .entered(); + self.window.maybe_wait_on_main(|w| w.set_cursor_grab(mode)) + } + + /// Modifies the cursor's visibility. + /// + /// If `false`, this will hide the cursor. If `true`, this will show the cursor. + /// + /// ## Platform-specific + /// + /// - **Windows:** The cursor is only hidden within the confines of the window. + /// - **X11:** The cursor is only hidden within the confines of the window. + /// - **Wayland:** The cursor is only hidden within the confines of the window. + /// - **macOS:** The cursor is hidden as long as the window has input focus, even if the cursor + /// is outside of the window. + /// - **iOS / Android:** Unsupported. + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + let _span = tracing::debug_span!("winit::Window::set_cursor_visible", visible).entered(); + self.window.maybe_queue_on_main(move |w| w.set_cursor_visible(visible)) + } + + /// Moves the window with the left mouse button until the button is released. + /// + /// There's no guarantee that this will work unless the left mouse button was pressed + /// immediately before this function is called. + /// + /// ## Platform-specific + /// + /// - **X11:** Un-grabs the cursor. + /// - **Wayland:** Requires the cursor to be inside the window to be dragged. + /// - **macOS:** May prevent the button release event to be triggered. + /// - **iOS / Android / Web:** Always returns an [`ExternalError::NotSupported`]. + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + let _span = tracing::debug_span!("winit::Window::drag_window",).entered(); + self.window.maybe_wait_on_main(|w| w.drag_window()) + } + + /// Resizes the window with the left mouse button until the button is released. + /// + /// There's no guarantee that this will work unless the left mouse button was pressed + /// immediately before this function is called. + /// + /// ## Platform-specific + /// + /// - **macOS:** Always returns an [`ExternalError::NotSupported`] + /// - **iOS / Android / Web:** Always returns an [`ExternalError::NotSupported`]. + #[inline] + pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), ExternalError> { + let _span = tracing::debug_span!( + "winit::Window::drag_resize_window", + direction = ?direction + ) + .entered(); + self.window.maybe_wait_on_main(|w| w.drag_resize_window(direction)) + } + + /// Show [window menu] at a specified position . + /// + /// This is the context menu that is normally shown when interacting with + /// the title bar. This is useful when implementing custom decorations. + /// + /// ## Platform-specific + /// **Android / iOS / macOS / Orbital / Wayland / Web / X11:** Unsupported. + /// + /// [window menu]: https://en.wikipedia.org/wiki/Common_menus_in_Microsoft_Windows#System_menu + pub fn show_window_menu(&self, position: impl Into) { + let position = position.into(); + let _span = tracing::debug_span!( + "winit::Window::show_window_menu", + position = ?position + ) + .entered(); + self.window.maybe_queue_on_main(move |w| w.show_window_menu(position)) + } + + /// Modifies whether the window catches cursor events. + /// + /// If `true`, the window will catch the cursor events. If `false`, events are passed through + /// the window such that any other window behind it receives them. By default hittest is + /// enabled. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Orbital:** Always returns an [`ExternalError::NotSupported`]. + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + let _span = tracing::debug_span!("winit::Window::set_cursor_hittest", hittest).entered(); + self.window.maybe_wait_on_main(|w| w.set_cursor_hittest(hittest)) + } +} + +/// Monitor info functions. +impl Window { + /// Returns the monitor on which the window currently resides. + /// + /// Returns `None` if current monitor can't be detected. + #[inline] + pub fn current_monitor(&self) -> Option { + let _span = tracing::debug_span!("winit::Window::current_monitor",).entered(); + self.window.maybe_wait_on_main(|w| w.current_monitor().map(|inner| MonitorHandle { inner })) + } + + /// Returns the list of all the monitors available on the system. + /// + /// This is the same as [`ActiveEventLoop::available_monitors`], and is provided for + /// convenience. + /// + /// [`ActiveEventLoop::available_monitors`]: crate::event_loop::ActiveEventLoop::available_monitors + #[inline] + pub fn available_monitors(&self) -> impl Iterator { + let _span = tracing::debug_span!("winit::Window::available_monitors",).entered(); + self.window.maybe_wait_on_main(|w| { + w.available_monitors().into_iter().map(|inner| MonitorHandle { inner }) + }) + } + + /// Returns the primary monitor of the system. + /// + /// Returns `None` if it can't identify any monitor as a primary one. + /// + /// This is the same as [`ActiveEventLoop::primary_monitor`], and is provided for convenience. + /// + /// ## Platform-specific + /// + /// **Wayland / Web:** Always returns `None`. + /// + /// [`ActiveEventLoop::primary_monitor`]: crate::event_loop::ActiveEventLoop::primary_monitor + #[inline] + pub fn primary_monitor(&self) -> Option { + let _span = tracing::debug_span!("winit::Window::primary_monitor",).entered(); + self.window.maybe_wait_on_main(|w| w.primary_monitor().map(|inner| MonitorHandle { inner })) + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasWindowHandle for Window { + fn window_handle(&self) -> Result, rwh_06::HandleError> { + let raw = self.window.raw_window_handle_rwh_06()?; + + // SAFETY: The window handle will never be deallocated while the window is alive, + // and the main thread safety requirements are upheld internally by each platform. + Ok(unsafe { rwh_06::WindowHandle::borrow_raw(raw) }) + } +} + +#[cfg(feature = "rwh_06")] +impl rwh_06::HasDisplayHandle for Window { + fn display_handle(&self) -> Result, rwh_06::HandleError> { + let raw = self.window.raw_display_handle_rwh_06()?; + + // SAFETY: The window handle will never be deallocated while the window is alive, + // and the main thread safety requirements are upheld internally by each platform. + Ok(unsafe { rwh_06::DisplayHandle::borrow_raw(raw) }) + } +} + +/// Wrapper to make objects `Send`. +/// +/// # Safety +/// +/// This is not safe! This is only used for `RawWindowHandle`, which only has unsafe getters. +#[cfg(any(feature = "rwh_05", feature = "rwh_04"))] +struct UnsafeSendWrapper(T); + +#[cfg(any(feature = "rwh_05", feature = "rwh_04"))] +unsafe impl Send for UnsafeSendWrapper {} + +#[cfg(feature = "rwh_05")] +unsafe impl rwh_05::HasRawWindowHandle for Window { + fn raw_window_handle(&self) -> rwh_05::RawWindowHandle { + self.window.maybe_wait_on_main(|w| UnsafeSendWrapper(w.raw_window_handle_rwh_05())).0 + } +} + +#[cfg(feature = "rwh_05")] +unsafe impl rwh_05::HasRawDisplayHandle for Window { + /// Returns a [`rwh_05::RawDisplayHandle`] used by the [`EventLoop`] that + /// created a window. + /// + /// [`EventLoop`]: crate::event_loop::EventLoop + fn raw_display_handle(&self) -> rwh_05::RawDisplayHandle { + self.window.maybe_wait_on_main(|w| UnsafeSendWrapper(w.raw_display_handle_rwh_05())).0 + } +} + +#[cfg(feature = "rwh_04")] +unsafe impl rwh_04::HasRawWindowHandle for Window { + fn raw_window_handle(&self) -> rwh_04::RawWindowHandle { + self.window.maybe_wait_on_main(|w| UnsafeSendWrapper(w.raw_window_handle_rwh_04())).0 + } +} + +/// The behavior of cursor grabbing. +/// +/// Use this enum with [`Window::set_cursor_grab`] to grab the cursor. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum CursorGrabMode { + /// No grabbing of the cursor is performed. + None, + + /// The cursor is confined to the window area. + /// + /// There's no guarantee that the cursor will be hidden. You should hide it by yourself if you + /// want to do so. + /// + /// ## Platform-specific + /// + /// - **macOS:** Not implemented. Always returns [`ExternalError::NotSupported`] for now. + /// - **iOS / Android / Web:** Always returns an [`ExternalError::NotSupported`]. + Confined, + + /// The cursor is locked inside the window area to the certain position. + /// + /// There's no guarantee that the cursor will be hidden. You should hide it by yourself if you + /// want to do so. + /// + /// ## Platform-specific + /// + /// - **X11:** Not implemented. Always returns [`ExternalError::NotSupported`] for now. + /// - **iOS / Android:** Always returns an [`ExternalError::NotSupported`]. + Locked, +} + +/// Defines the orientation that a window resize will be performed. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum ResizeDirection { + East, + North, + NorthEast, + NorthWest, + South, + SouthEast, + SouthWest, + West, +} + +impl From for CursorIcon { + fn from(direction: ResizeDirection) -> Self { + use ResizeDirection::*; + match direction { + East => CursorIcon::EResize, + North => CursorIcon::NResize, + NorthEast => CursorIcon::NeResize, + NorthWest => CursorIcon::NwResize, + South => CursorIcon::SResize, + SouthEast => CursorIcon::SeResize, + SouthWest => CursorIcon::SwResize, + West => CursorIcon::WResize, + } + } +} + +/// Fullscreen modes. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Fullscreen { + Exclusive(VideoModeHandle), + + /// Providing `None` to `Borderless` will fullscreen on the current monitor. + Borderless(Option), +} + +/// The theme variant to use. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Theme { + /// Use the light variant. + Light, + + /// Use the dark variant. + Dark, +} + +/// ## Platform-specific +/// +/// - **X11:** Sets the WM's `XUrgencyHint`. No distinction between [`Critical`] and +/// [`Informational`]. +/// +/// [`Critical`]: Self::Critical +/// [`Informational`]: Self::Informational +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum UserAttentionType { + /// ## Platform-specific + /// + /// - **macOS:** Bounces the dock icon until the application is in focus. + /// - **Windows:** Flashes both the window and the taskbar button until the application is in + /// focus. + Critical, + + /// ## Platform-specific + /// + /// - **macOS:** Bounces the dock icon once. + /// - **Windows:** Flashes the taskbar button until the application is in focus. + #[default] + Informational, +} + +bitflags::bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct WindowButtons: u32 { + const CLOSE = 1 << 0; + const MINIMIZE = 1 << 1; + const MAXIMIZE = 1 << 2; + } +} + +/// A window level groups windows with respect to their z-position. +/// +/// The relative ordering between windows in different window levels is fixed. +/// The z-order of a window within the same window level may change dynamically on user interaction. +/// +/// ## Platform-specific +/// +/// - **iOS / Android / Web / Wayland:** Unsupported. +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +pub enum WindowLevel { + /// The window will always be below normal windows. + /// + /// This is useful for a widget-based app. + AlwaysOnBottom, + + /// The default. + #[default] + Normal, + + /// The window will always be on top of normal windows. + AlwaysOnTop, +} + +/// Generic IME purposes for use in [`Window::set_ime_purpose`]. +/// +/// The purpose may improve UX by optimizing the IME for the specific use case, +/// if winit can express the purpose to the platform and the platform reacts accordingly. +/// +/// ## Platform-specific +/// +/// - **iOS / Android / Web / Windows / X11 / macOS / Orbital:** Unsupported. +#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] +#[non_exhaustive] +pub enum ImePurpose { + /// No special hints for the IME (default). + #[default] + Normal, + /// The IME is used for password input. + Password, + /// The IME is used to input into a terminal. + /// + /// For example, that could alter OSK on Wayland to show extra buttons. + Terminal, +} + +/// An opaque token used to activate the [`Window`]. +/// +/// [`Window`]: crate::window::Window +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct ActivationToken { + pub(crate) token: String, +} + +impl ActivationToken { + /// Make an [`ActivationToken`] from a string. + /// + /// This method should be used to wrap tokens passed by side channels to your application, like + /// dbus. + /// + /// The validity of the token is ensured by the windowing system. Using the invalid token will + /// only result in the side effect of the operation involving it being ignored (e.g. window + /// won't get focused automatically), but won't yield any errors. + /// + /// To obtain a valid token, use + #[cfg_attr( + any(x11_platform, wayland_platform, docsrs), + doc = " [`request_activation_token`](crate::platform::startup_notify::WindowExtStartupNotify::request_activation_token)." + )] + #[cfg_attr( + not(any(x11_platform, wayland_platform, docsrs)), + doc = " `request_activation_token`." + )] + pub fn from_raw(token: String) -> Self { + Self { token } + } + + /// Convert the token to its string representation to later pass via IPC. + pub fn into_raw(self) -> String { + self.token + } +} diff --git a/vendor/winit/tests/send_objects.rs b/vendor/winit/tests/send_objects.rs new file mode 100644 index 00000000..b8a5d3b7 --- /dev/null +++ b/vendor/winit/tests/send_objects.rs @@ -0,0 +1,36 @@ +#[allow(dead_code)] +fn needs_send() {} + +#[test] +fn event_loop_proxy_send() { + #[allow(dead_code)] + fn is_send() { + // ensures that `winit::EventLoopProxy` implements `Send` + needs_send::>(); + } +} + +#[test] +fn window_send() { + // ensures that `winit::Window` implements `Send` + needs_send::(); +} + +#[test] +fn window_builder_send() { + needs_send::(); +} + +#[test] +fn ids_send() { + // ensures that the various `..Id` types implement `Send` + needs_send::(); + needs_send::(); + needs_send::(); +} + +#[test] +fn custom_cursor_send() { + needs_send::(); + needs_send::(); +} diff --git a/vendor/winit/tests/serde_objects.rs b/vendor/winit/tests/serde_objects.rs new file mode 100644 index 00000000..c427373f --- /dev/null +++ b/vendor/winit/tests/serde_objects.rs @@ -0,0 +1,38 @@ +#![cfg(feature = "serde")] + +use serde::{Deserialize, Serialize}; +use winit::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; +use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase}; +use winit::keyboard::{Key, KeyCode, KeyLocation, ModifiersState, NamedKey, PhysicalKey}; +use winit::window::CursorIcon; + +#[allow(dead_code)] +fn needs_serde>() {} + +#[test] +fn window_serde() { + needs_serde::(); +} + +#[test] +fn events_serde() { + needs_serde::(); + needs_serde::(); + needs_serde::(); + needs_serde::(); + needs_serde::(); + needs_serde::(); + needs_serde::(); + needs_serde::(); + needs_serde::(); + needs_serde::(); +} + +#[test] +fn dpi_serde() { + needs_serde::>(); + needs_serde::>(); + needs_serde::>(); + needs_serde::>(); + needs_serde::>(); +} diff --git a/vendor/winit/tests/sync_object.rs b/vendor/winit/tests/sync_object.rs new file mode 100644 index 00000000..c7abbeb2 --- /dev/null +++ b/vendor/winit/tests/sync_object.rs @@ -0,0 +1,28 @@ +#[allow(dead_code)] +fn needs_sync() {} + +#[test] +fn event_loop_proxy_send() { + #[allow(dead_code)] + fn is_send() { + // ensures that `winit::EventLoopProxy` implements `Sync` + needs_sync::>(); + } +} + +#[test] +fn window_sync() { + // ensures that `winit::Window` implements `Sync` + needs_sync::(); +} + +#[test] +fn window_builder_sync() { + needs_sync::(); +} + +#[test] +fn custom_cursor_sync() { + needs_sync::(); + needs_sync::(); +} diff --git a/xtask/src/demo_pdf/pdf.rs b/xtask/src/demo_pdf/pdf.rs index 9f4b9cc5..297d4ce9 100644 --- a/xtask/src/demo_pdf/pdf.rs +++ b/xtask/src/demo_pdf/pdf.rs @@ -351,20 +351,41 @@ pub fn pdf_string(s: &str) -> String { } /// Map text to WinAnsiEncoding bytes (unmappable characters become `?`). +/// +/// Mirrors `pdfcraft_fonts::win_ansi` (ISO 32000-2 Annex D), which xtask doesn't depend on; +/// keep the 0x80–0x9F rows in step with `crates/fonts/src/encodings.rs`. pub fn winansi(s: &str) -> Vec { s.chars() .map(|c| match c { '\u{20}'..='\u{7e}' => c as u8, '\u{a0}'..='\u{ff}' => c as u32 as u8, - '\u{2022}' => 0x95, - '\u{2013}' => 0x96, - '\u{2014}' => 0x97, + '\u{20ac}' => 0x80, + '\u{201a}' => 0x82, + '\u{0192}' => 0x83, + '\u{201e}' => 0x84, + '\u{2026}' => 0x85, + '\u{2020}' => 0x86, + '\u{2021}' => 0x87, + '\u{02c6}' => 0x88, + '\u{2030}' => 0x89, + '\u{0160}' => 0x8a, + '\u{2039}' => 0x8b, + '\u{0152}' => 0x8c, + '\u{017d}' => 0x8e, '\u{2018}' => 0x91, '\u{2019}' => 0x92, '\u{201c}' => 0x93, '\u{201d}' => 0x94, - '\u{2026}' => 0x85, - '\u{20ac}' => 0x80, + '\u{2022}' => 0x95, + '\u{2013}' => 0x96, + '\u{2014}' => 0x97, + '\u{02dc}' => 0x98, + '\u{2122}' => 0x99, + '\u{0161}' => 0x9a, + '\u{203a}' => 0x9b, + '\u{0153}' => 0x9c, + '\u{017e}' => 0x9e, + '\u{0178}' => 0x9f, _ => b'?', }) .collect() @@ -415,4 +436,13 @@ mod tests { assert_eq!(pdf_string("a(b)\\"), "(a\\(b\\)\\\\)"); assert_eq!(pdf_string("—"), "(\\227)"); } + + #[test] + fn winansi_covers_the_0x80_to_0x9f_glyphs() { + // The 27 defined codes of ISO 32000-2 Annex D WinAnsiEncoding between 0x80 and 0x9F. + let high = "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ"; + let want: Vec = (0x80..=0x9f).filter(|b| ![0x81, 0x8d, 0x8f, 0x90, 0x9d].contains(b)).collect(); + assert_eq!(winansi(high), want); + assert_eq!(winansi("č"), b"?"); + } } diff --git a/xtask/src/layers.rs b/xtask/src/layers.rs index d66f08d4..5cb33439 100644 --- a/xtask/src/layers.rs +++ b/xtask/src/layers.rs @@ -202,11 +202,10 @@ pub fn check(crates: &[Crate]) -> Vec { } match classify(&d.name) { None => {} - Some(Class::Testkit) => { - if d.kind != DepKind::Dev && class != Class::Testkit { - out.push(Violation::TestkitAsNormalDep { krate: c.name.clone(), dep: d.name.clone() }); - } + Some(Class::Testkit) if d.kind != DepKind::Dev && class != Class::Testkit => { + out.push(Violation::TestkitAsNormalDep { krate: c.name.clone(), dep: d.name.clone() }); } + Some(Class::Testkit) => {} Some(dc) => { let to = dc.layer().unwrap_or(u8::MAX); let ok = to < layer || (to == layer && sideways_allowed(&c.name, &d.name));