From 19baad99492d7764cf7a146b2e929993aa28b970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Mon, 22 Jul 2024 19:11:47 -0400 Subject: [PATCH] Update egui to 0.28.1 (#143) * Merge from upstream egui 0.28.1 * Update everything else and fix compilation errors It seems we are now required to use a version of `wasm-bindgen-futures` that won't compile with Rust nightly versions older than 2024-02-06, so I upgraded the Rust toolchain to 2024-05-19, the latest version that works without encountering an unrelated compilation error in the `time` crate. * Fix another release-mode-only compilation error * Fix `BorrowMutError` in web builds * Make IME work again in web builds * Fix compiler warnings * Add spacing at the bottom of the map editor top bar This is to make it look consistent with how it was before the egui update. * Fix doc comment in crates/eframe/src/lib.rs * Update `image` from 0.24 to 0.25 This fixes the displaying of the icon in the "About" window. * Remove eframe text agent after panic in web builds * Focus the HTML canvas on startup in web builds The release notes for eframe 0.28.0 say you need to focus the canvas on startup if you're creating a fullscreen app. * Fix terminal scroll delta calculation when unit is pages * Fix compiler warning in steam.rs --- .github/workflows/build-steam.yml | 6 +- .github/workflows/build.yml | 8 +- .github/workflows/checks.yml | 10 +- Cargo.lock | 702 ++++++++--- Cargo.toml | 39 +- crates/components/src/database_view.rs | 2 +- crates/components/src/lib.rs | 12 +- crates/components/src/map_view.rs | 4 +- crates/components/src/syntax_highlighting.rs | 22 +- crates/core/Cargo.toml | 7 +- crates/eframe/CHANGELOG.md | 77 ++ crates/eframe/Cargo.toml | 52 +- crates/eframe/README.md | 2 +- crates/eframe/build.rs | 3 + crates/eframe/src/epi.rs | 40 +- crates/eframe/src/icon_data.rs | 2 +- crates/eframe/src/lib.rs | 123 +- crates/eframe/src/native/app_icon.rs | 44 +- crates/eframe/src/native/epi_integration.rs | 45 +- crates/eframe/src/native/file_storage.rs | 6 +- crates/eframe/src/native/glow_integration.rs | 237 ++-- crates/eframe/src/native/run.rs | 19 +- crates/eframe/src/native/wgpu_integration.rs | 238 ++-- crates/eframe/src/native/winit_integration.rs | 6 - crates/eframe/src/web/app_runner.rs | 77 +- crates/eframe/src/web/backend.rs | 18 +- crates/eframe/src/web/events.rs | 1058 ++++++++++------- crates/eframe/src/web/input.rs | 168 +-- crates/eframe/src/web/mod.rs | 178 ++- crates/eframe/src/web/text_agent.rs | 370 +++--- crates/eframe/src/web/web_logger.rs | 6 +- crates/eframe/src/web/web_runner.rs | 115 +- crates/egui-wgpu/CHANGELOG.md | 9 + crates/egui-wgpu/Cargo.toml | 6 +- crates/egui-wgpu/README.md | 2 +- crates/egui-wgpu/src/lib.rs | 2 +- crates/egui-wgpu/src/renderer.rs | 41 +- crates/egui-wgpu/src/winit.rs | 7 +- crates/filesystem/Cargo.toml | 8 +- crates/filesystem/src/path_cache.rs | 2 +- crates/graphics/Cargo.toml | 4 +- .../src/primitives/collision/shader.rs | 2 + crates/graphics/src/primitives/grid/shader.rs | 2 + .../graphics/src/primitives/sprite/shader.rs | 2 + .../graphics/src/primitives/tiles/shader.rs | 2 + crates/modals/src/database_modal/mod.rs | 2 +- crates/term/src/widget/mod.rs | 29 +- crates/ui/Cargo.toml | 2 +- crates/ui/src/tabs/map/brush.rs | 1 - crates/ui/src/tabs/map/mod.rs | 210 ++-- crates/ui/src/windows/actors.rs | 6 +- crates/ui/src/windows/armor.rs | 12 +- crates/ui/src/windows/common_event_edit.rs | 2 +- crates/ui/src/windows/enemies.rs | 42 +- crates/ui/src/windows/event_edit.rs | 2 +- crates/ui/src/windows/items.rs | 9 +- crates/ui/src/windows/misc.rs | 2 +- crates/ui/src/windows/preferences.rs | 6 +- crates/ui/src/windows/reporter.rs | 2 +- crates/ui/src/windows/script_edit.rs | 4 +- crates/ui/src/windows/skills.rs | 2 +- crates/ui/src/windows/states.rs | 4 +- crates/ui/src/windows/weapons.rs | 12 +- crates/web/Cargo.toml | 8 +- rust-toolchain.toml | 2 +- src/app/mod.rs | 13 + src/main.rs | 9 +- src/steam.rs | 5 +- 68 files changed, 2603 insertions(+), 1568 deletions(-) create mode 100644 crates/eframe/build.rs diff --git a/.github/workflows/build-steam.yml b/.github/workflows/build-steam.yml index 98417ddd..74798e26 100644 --- a/.github/workflows/build-steam.yml +++ b/.github/workflows/build-steam.yml @@ -23,7 +23,7 @@ jobs: sudo apt install libgtk-3-dev libatk1.0-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev libasound2-dev clang mold -y - uses: dtolnay/rust-toolchain@nightly with: - toolchain: nightly-2024-02-01 + toolchain: nightly-2024-05-19 - name: Rust Cache uses: Swatinem/rust-cache@v2 - name: Build luminol (Release) @@ -48,7 +48,7 @@ jobs: submodules: true - uses: dtolnay/rust-toolchain@nightly with: - toolchain: nightly-2024-02-01 + toolchain: nightly-2024-05-19 - name: Rust Cache uses: Swatinem/rust-cache@v2 - name: Build luminol (Release) @@ -73,7 +73,7 @@ jobs: submodules: true - uses: dtolnay/rust-toolchain@nightly with: - toolchain: nightly-2024-02-01 + toolchain: nightly-2024-05-19 - name: Rust Cache uses: Swatinem/rust-cache@v2 - name: Build luminol (Release) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2d42504d..86ce88f5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: sudo apt install libgtk-3-dev libatk1.0-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev libasound2-dev clang mold -y - uses: dtolnay/rust-toolchain@nightly with: - toolchain: nightly-2024-02-01 + toolchain: nightly-2024-05-19 - name: Rust Cache uses: Swatinem/rust-cache@v2 - name: Build luminol (Release) @@ -46,7 +46,7 @@ jobs: submodules: true - uses: dtolnay/rust-toolchain@nightly with: - toolchain: nightly-2024-02-01 + toolchain: nightly-2024-05-19 - name: Rust Cache uses: Swatinem/rust-cache@v2 - name: Build luminol (Release) @@ -70,7 +70,7 @@ jobs: submodules: true - uses: dtolnay/rust-toolchain@nightly with: - toolchain: nightly-2024-02-01 + toolchain: nightly-2024-05-19 - name: Rust Cache uses: Swatinem/rust-cache@v2 - name: Build luminol (Release) @@ -98,7 +98,7 @@ jobs: sudo apt install libgtk-3-dev libatk1.0-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev libasound2-dev clang mold -y - uses: dtolnay/rust-toolchain@nightly with: - toolchain: nightly-2024-02-01 + toolchain: nightly-2024-05-19 targets: wasm32-unknown-unknown components: rust-src - name: Download and install Trunk binary diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index ad638847..984a82aa 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -27,7 +27,7 @@ jobs: sudo apt install libgtk-3-dev libatk1.0-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev libasound2-dev clang mold -y - uses: dtolnay/rust-toolchain@nightly with: - toolchain: nightly-2024-02-01 + toolchain: nightly-2024-05-19 - name: Rust Cache uses: Swatinem/rust-cache@v2 - run: cargo check --all-features @@ -45,7 +45,7 @@ jobs: sudo apt install libgtk-3-dev libatk1.0-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev libasound2-dev clang mold -y - uses: dtolnay/rust-toolchain@nightly with: - toolchain: nightly-2024-02-01 + toolchain: nightly-2024-05-19 targets: wasm32-unknown-unknown components: rust-src - name: Rust Cache @@ -65,7 +65,7 @@ jobs: # sudo apt install libgtk-3-dev libatk1.0-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev libasound2-dev clang mold -y # - uses: dtolnay/rust-toolchain@nightly # with: - # toolchain: nightly-2024-02-01 + # toolchain: nightly-2024-05-19 # - name: Rust Cache # uses: Swatinem/rust-cache@v2 # - run: cargo test --workspace @@ -83,7 +83,7 @@ jobs: sudo apt install libgtk-3-dev libatk1.0-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev libasound2-dev clang mold -y - uses: dtolnay/rust-toolchain@nightly with: - toolchain: nightly-2024-02-01 + toolchain: nightly-2024-05-19 components: rustfmt - name: Rust Cache uses: Swatinem/rust-cache@v2 @@ -102,7 +102,7 @@ jobs: sudo apt install libgtk-3-dev libatk1.0-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev libasound2-dev clang mold -y - uses: dtolnay/rust-toolchain@nightly with: - toolchain: nightly-2024-02-01 + toolchain: nightly-2024-05-19 components: clippy - name: Rust Cache uses: Swatinem/rust-cache@v2 diff --git a/Cargo.lock b/Cargo.lock index 832c4774..b856984e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,9 +122,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b79b82693f705137f8fb9b37871d99e4f9a7df12b917eed79c3d3954830a60b" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "getrandom", @@ -150,7 +150,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc7ceabf6fc76511f616ca216b51398a2511f19ba9f71bcbd977999edff1b0d1" dependencies = [ "base64 0.21.7", - "bitflags 2.4.2", + "bitflags 2.6.0", "home", "libc", "log", @@ -173,6 +173,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + [[package]] name = "allocator-api2" version = "0.2.16" @@ -213,7 +219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37fe60779335388a88c01ac6c3be40304d1e349de3ada3b15f7808bb90fa9dce" dependencies = [ "alsa-sys", - "bitflags 2.4.2", + "bitflags 2.6.0", "libc", ] @@ -234,7 +240,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee91c0c2905bae44f84bfa4e044536541df26b7703fd0888deeb9060fcc44289" dependencies = [ "android-properties", - "bitflags 2.4.2", + "bitflags 2.6.0", "cc", "cesu8", "jni", @@ -275,6 +281,12 @@ version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" + [[package]] name = "arboard" version = "3.3.1" @@ -291,6 +303,17 @@ dependencies = [ "x11rb", ] +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.51", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -640,6 +663,29 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" +dependencies = [ + "arrayvec", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -682,7 +728,7 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -725,13 +771,19 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" dependencies = [ "serde", ] +[[package]] +name = "bitstream-io" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcde5f311c85b8ca30c2e4198d4326bc342c76541590106f5fa4a50946ea499" + [[package]] name = "block" version = "0.1.6" @@ -762,7 +814,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7" dependencies = [ - "objc-sys 0.3.2", + "objc-sys 0.3.5", ] [[package]] @@ -785,6 +837,15 @@ dependencies = [ "objc2 0.4.1", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + [[package]] name = "blocking" version = "1.5.1" @@ -801,6 +862,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "built" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4" + [[package]] name = "bumpalo" version = "3.15.3" @@ -833,6 +900,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[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.0" @@ -855,7 +928,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "log", "polling 3.5.0", "rustix 0.38.31", @@ -962,36 +1035,6 @@ dependencies = [ "error-code", ] -[[package]] -name = "cocoa" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation", - "core-foundation", - "core-graphics", - "foreign-types 0.5.0", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation", - "core-graphics-types", - "libc", - "objc", -] - [[package]] name = "codespan-reporting" version = "0.11.1" @@ -1286,11 +1329,11 @@ checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" [[package]] name = "d3d12" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307" +checksum = "b28bfe653d79bd16c77f659305b195b82bb5ce0c0eb2a4846b82ddbd77586813" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "libloading 0.8.1", "winapi", ] @@ -1404,24 +1447,24 @@ dependencies = [ ] [[package]] -name = "directories-next" -version = "2.0.0" +name = "directories" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" dependencies = [ - "cfg-if", - "dirs-sys-next", + "dirs-sys", ] [[package]] -name = "dirs-sys-next" -version = "0.1.2" +name = "dirs-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", + "option-ext", "redox_users", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -1475,22 +1518,24 @@ dependencies = [ [[package]] name = "ecolor" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20930a432bbd57a6d55e07976089708d4893f3d556cf42a0d79e9e321fa73b10" +checksum = "2e6b451ff1143f6de0f33fc7f1b68fecfd2c7de06e104de96c4514de3f5396f8" dependencies = [ "bytemuck", + "emath", "serde", ] [[package]] name = "egui" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "584c5d1bf9a67b25778a3323af222dbe1a1feb532190e103901187f92c7fe29a" +checksum = "20c97e70a2768de630f161bb5392cbd3874fcf72868f14df0e002e82e06cb798" dependencies = [ "accesskit", "ahash", + "emath", "epaint", "log", "nohash-hasher", @@ -1501,29 +1546,30 @@ dependencies = [ [[package]] name = "egui-modal" -version = "0.3.6" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "738cdffefd15dbb6a5fff75d118eee82a9e894bbfa45e41c24d7de42519fa673" +checksum = "4d3bc636d5ddc0c4a90d7a24c7d84db10fb2f789298e9c50cdddd28832e83f1e" dependencies = [ "egui", ] [[package]] name = "egui-notify" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "319327faee7bb116bcdbe43af1b8cbea06dc5d9ddbb23d35e012949afbd76cde" +checksum = "d0c6c49d9c02771e28f3b40289c16b4473308807b1ed09a878713d7f11e3dcad" dependencies = [ "egui", ] [[package]] name = "egui-winit" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e3da0cbe020f341450c599b35b92de4af7b00abde85624fd16f09c885573609" +checksum = "fac4e066af341bf92559f60dbdf2020b2a03c963415349af5f3f8d79ff7a4926" dependencies = [ "accesskit_winit", + "ahash", "arboard", "egui", "log", @@ -1538,9 +1584,9 @@ dependencies = [ [[package]] name = "egui_dock" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b8d9a54c0ed60f2670ad387c269663b4771431f090fa586906cf5f0bc586f4" +checksum = "629a8b0e440d69996795669ceacc0dd839a997843489273600d31d16c9cb3500" dependencies = [ "duplicate", "egui", @@ -1549,13 +1595,14 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b78779f35ded1a853786c9ce0b43fe1053e10a21ea3b23ebea411805ce41593" +checksum = "5bb783d9fa348f69ed5c340aa25af78b5472043090e8b809040e30960cc2a746" dependencies = [ + "ahash", "egui", "enum-map", - "image", + "image 0.25.2", "log", "mime_guess2", "resvg", @@ -1570,9 +1617,9 @@ checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "emath" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4c3a552cfca14630702449d35f41c84a0d15963273771c6059175a803620f3f" +checksum = "0a6a21708405ea88f63d8309650b4d77431f4bc28fb9d8e6f77d3963b51249e6" dependencies = [ "bytemuck", "serde", @@ -1654,9 +1701,9 @@ dependencies = [ [[package]] name = "epaint" -version = "0.27.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b381f8b149657a4acf837095351839f32cd5c4aec1817fc4df84e18d76334176" +checksum = "3f0dcc0a0771e7500e94cd1cb797bd13c9f23b9409bdc3c824e2cbc562b7fa01" dependencies = [ "ab_glyph", "ahash", @@ -2237,7 +2284,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18fcd4ae4e86d991ad1300b8f57166e5be0c95ef1f63f3f5b827f8a164548746" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "cfg_aliases", "cgl", "core-foundation", @@ -2313,7 +2360,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "gpu-alloc-types", ] @@ -2323,7 +2370,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", ] [[package]] @@ -2341,22 +2388,22 @@ dependencies = [ [[package]] name = "gpu-descriptor" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" +checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "gpu-descriptor-types", "hashbrown", ] [[package]] name = "gpu-descriptor-types" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", ] [[package]] @@ -2422,7 +2469,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "com", "libc", "libloading 0.8.1", @@ -2586,12 +2633,51 @@ dependencies = [ "tiff", ] +[[package]] +name = "image" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" +dependencies = [ + "byteorder-lite", + "quick-error", +] + [[package]] name = "imagesize" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" +[[package]] +name = "imgref" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" + [[package]] name = "indenter" version = "0.3.3" @@ -2641,6 +2727,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.51", +] + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -2730,9 +2827,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -2816,6 +2913,17 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libfuzzer-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + [[package]] name = "libloading" version = "0.7.4" @@ -2842,7 +2950,7 @@ version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "libc", "redox_syscall 0.4.1", ] @@ -2853,7 +2961,7 @@ version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "libc", "redox_syscall 0.4.1", ] @@ -2930,6 +3038,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "luminol" version = "0.4.0" @@ -2941,7 +3058,7 @@ dependencies = [ "egui_extras", "futures-lite 2.2.0", "git-version", - "image", + "image 0.25.2", "js-sys", "luminol-audio", "luminol-config", @@ -3006,7 +3123,7 @@ dependencies = [ "fragile", "fuzzy-matcher", "glam", - "image", + "image 0.25.2", "indextree", "itertools 0.11.0", "lexical-sort", @@ -3089,21 +3206,23 @@ dependencies = [ name = "luminol-eframe" version = "0.4.0" dependencies = [ + "ahash", "bytemuck", - "cocoa", - "directories-next", + "directories", "document-features", "egui", "egui-winit", "flume", "glutin", "glutin-winit", - "image", + "image 0.25.2", "js-sys", "log", "luminol-egui-wgpu", "luminol-web", - "objc", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation", "once_cell", "oneshot", "parking_lot", @@ -3116,7 +3235,6 @@ dependencies = [ "ron", "serde", "static_assertions", - "thiserror", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -3130,6 +3248,7 @@ dependencies = [ name = "luminol-egui-wgpu" version = "0.4.0" dependencies = [ + "ahash", "bytemuck", "document-features", "egui", @@ -3150,7 +3269,7 @@ dependencies = [ "async-fs 2.1.1", "async-std", "async_io_stream", - "bitflags 2.4.2", + "bitflags 2.6.0", "camino", "color-eyre", "dashmap", @@ -3193,7 +3312,7 @@ dependencies = [ "egui", "fragile", "glam", - "image", + "image 0.24.9", "itertools 0.11.0", "luminol-data", "luminol-egui-wgpu", @@ -3378,6 +3497,16 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.7.2" @@ -3413,11 +3542,11 @@ dependencies = [ [[package]] name = "metal" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" +checksum = "5637e166ea14be6063a3f8ba5ccb9a4159df7d8f6d61c02fc3d480b1f90dcfcb" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "block", "core-graphics-types", "foreign-types 0.5.0", @@ -3486,12 +3615,13 @@ checksum = "9252111cf132ba0929b6f8e030cac2a24b507f3a4d6db6fb2896f27b354c714b" [[package]] name = "naga" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8878eb410fc90853da3908aebfe61d73d26d4437ef850b70050461f939509899" +checksum = "e536ae46fcab0876853bd4a632ede5df4b1c2527a58f6c5a4150fe86be858231" dependencies = [ + "arrayvec", "bit-set", - "bitflags 2.4.2", + "bitflags 2.6.0", "codespan-reporting", "hexf-parse", "indexmap", @@ -3507,9 +3637,9 @@ dependencies = [ [[package]] name = "naga_oil" -version = "0.12.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86343242cc8fe7c38de0324f0c13a789729f3d360d98de12c464a815ad52feda" +checksum = "275d9720a7338eedac966141089232514c84d76a246a58ef501af88c5edf402f" dependencies = [ "bit-set", "codespan-reporting", @@ -3558,7 +3688,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "jni-sys", "log", "ndk-sys", @@ -3617,6 +3747,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3627,6 +3763,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -3644,6 +3790,26 @@ dependencies = [ "syn 2.0.51", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.18" @@ -3691,7 +3857,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", - "objc_exception", ] [[package]] @@ -3713,9 +3878,9 @@ checksum = "df3b9834c1e95694a05a828b59f55fa2afec6288359cda67146126b3f90a55d7" [[package]] name = "objc-sys" -version = "0.3.2" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c71324e4180d0899963fc83d9d241ac39e699609fc1025a850aadac8257459" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" [[package]] name = "objc2" @@ -3734,10 +3899,60 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d" dependencies = [ - "objc-sys 0.3.2", + "objc-sys 0.3.5", "objc2-encode 3.0.0", ] +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys 0.3.5", + "objc2-encode 4.0.3", +] + +[[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 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[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 0.5.1", + "objc2 0.5.2", + "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 0.5.1", + "objc2 0.5.2", + "objc2-foundation", + "objc2-metal", +] + [[package]] name = "objc2-encode" version = "2.0.0-pre.2" @@ -3754,12 +3969,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666" [[package]] -name = "objc_exception" -version = "0.1.2" +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "cc", + "bitflags 2.6.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[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 0.5.1", + "objc2 0.5.2", + "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 0.5.1", + "objc2 0.5.2", + "objc2-foundation", + "objc2-metal", ] [[package]] @@ -3833,7 +4082,7 @@ version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "cfg-if", "foreign-types 0.3.2", "libc", @@ -3871,6 +4120,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "orbclient" version = "0.3.47" @@ -3966,9 +4221,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -3976,18 +4231,18 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "backtrace", "cfg-if", "libc", "petgraph", - "redox_syscall 0.4.1", + "redox_syscall 0.5.3", "smallvec", "thread-id", - "windows-targets 0.48.5", + "windows-targets 0.52.3", ] [[package]] @@ -4246,6 +4501,19 @@ name = "profiling" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" +dependencies = [ + "quote", + "syn 2.0.51", +] [[package]] name = "puffin" @@ -4279,6 +4547,12 @@ dependencies = [ "unreachable", ] +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.31.0" @@ -4333,6 +4607,56 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.12.1", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "system-deps", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc13288f5ab39e6d7c9d501759712e6969fcc9734220846fc9ed26cae2cc4234" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "raw-window-handle" version = "0.5.2" @@ -4389,6 +4713,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "redox_users" version = "0.4.4" @@ -4446,9 +4779,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "renderdoc-sys" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216080ab382b992234dda86873c18d4c48358f5cfcb70fd693d7f6f2131b628b" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "reqwest" @@ -4557,7 +4890,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags 2.4.2", + "bitflags 2.6.0", "serde", "serde_derive", ] @@ -4619,7 +4952,7 @@ version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "errno", "itoa", "libc", @@ -4884,6 +5217,15 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "simplecss" version = "0.2.1" @@ -4930,7 +5272,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "calloop", "calloop-wayland-source", "cursor-icon", @@ -5004,7 +5346,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", ] [[package]] @@ -5255,18 +5597,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -5768,6 +6110,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.0" @@ -5820,7 +6173,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40eb22ae96f050e0c0d6f7ce43feeae26c348fc4dea56928ca81537cfaa6188b" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "cursor-icon", "log", "serde", @@ -5871,9 +6224,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -5881,9 +6234,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", @@ -5896,9 +6249,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.40" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -5908,9 +6261,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5918,9 +6271,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -5931,9 +6284,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wayland-backend" @@ -5955,7 +6308,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "rustix 0.38.31", "wayland-backend", "wayland-scanner", @@ -5967,7 +6320,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "cursor-icon", "wayland-backend", ] @@ -5989,7 +6342,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -6001,7 +6354,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -6014,7 +6367,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -6046,9 +6399,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.67" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -6066,17 +6419,18 @@ dependencies = [ [[package]] name = "webbrowser" -version = "0.8.12" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b2391658b02c27719fc5a0a73d6e696285138e8b12fba9d4baa70451023c71" +checksum = "425ba64c1e13b1c6e8c5d2541c8fac10022ca584f33da781db01b5756aef1f4e" dependencies = [ + "block2 0.5.1", "core-foundation", "home", "jni", "log", "ndk-context", - "objc", - "raw-window-handle 0.5.2", + "objc2 0.5.2", + "objc2-foundation", "url", "web-sys", ] @@ -6089,13 +6443,14 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "wgpu" -version = "0.19.1" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfe9a310dcf2e6b85f00c46059aaeaf4184caa8e29a1ecd4b7a704c3482332d" +checksum = "90e37c7b9921b75dfd26dd973fdcbce36f13dfa6e2dc82aece584e0ed48c355c" dependencies = [ "arrayvec", "cfg-if", "cfg_aliases", + "document-features", "js-sys", "log", "naga", @@ -6114,15 +6469,16 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "0.19.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b15e451d4060ada0d99a64df44e4d590213496da7c4f245572d51071e8e30ed" +checksum = "d50819ab545b867d8a454d1d756b90cd5f15da1f2943334ca314af10583c9d39" dependencies = [ "arrayvec", "bit-vec", - "bitflags 2.4.2", + "bitflags 2.6.0", "cfg_aliases", "codespan-reporting", + "document-features", "indexmap", "log", "naga", @@ -6140,15 +6496,15 @@ dependencies = [ [[package]] name = "wgpu-hal" -version = "0.19.1" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bb47856236bfafc0bc591a925eb036ac19cd987624a447ff353e7a7e7e6f72" +checksum = "172e490a87295564f3fcc0f165798d87386f6231b04d4548bca458cbbfd63222" dependencies = [ "android_system_properties", "arrayvec", "ash", "bit-set", - "bitflags 2.4.2", + "bitflags 2.6.0", "block", "cfg_aliases", "core-graphics-types", @@ -6166,6 +6522,7 @@ dependencies = [ "log", "metal", "naga", + "ndk-sys", "objc", "once_cell", "parking_lot", @@ -6184,11 +6541,11 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "895fcbeb772bfb049eb80b2d6e47f6c9af235284e9703c96fc0218a42ffd5af2" +checksum = "1353d9a46bff7f955a680577f34c69122628cc2076e1d6f3a9be6ef00ae793ef" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "js-sys", "web-sys", ] @@ -6538,7 +6895,7 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.4.2", + "bitflags 2.6.0", "bytemuck", "calloop", "cfg_aliases", @@ -6679,7 +7036,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "dlib", "log", "once_cell", @@ -6845,6 +7202,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + [[package]] name = "zune-inflate" version = "0.2.54" @@ -6854,6 +7217,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "zune-jpeg" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "3.15.2" diff --git a/Cargo.toml b/Cargo.toml index f74942f8..0429a678 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ authors = [ "Egor Poleshko ", ] edition = "2021" -rust-version = "1.74" +rust-version = "1.78" license = "GPL-3.0" readme = "README.md" repository = "https://github.com/Speak2Erase/Luminol" @@ -61,9 +61,9 @@ categories = ["games"] # Shared dependencies [workspace.dependencies] -egui = "0.27.2" -egui_extras = { version = "0.27.2", features = ["svg", "image"] } -epaint = "0.27.2" +egui = "0.28.1" +egui_extras = { version = "0.28.1", features = ["svg", "image"] } +epaint = "0.28.1" luminol-eframe = { version = "0.4.0", path = "crates/eframe/", features = [ "wgpu", @@ -74,11 +74,17 @@ luminol-eframe = { version = "0.4.0", path = "crates/eframe/", features = [ "wayland", ], default-features = false } luminol-egui-wgpu = { version = "0.4.0", path = "crates/egui-wgpu/" } -egui-winit = "0.27.2" +egui-winit = "0.28.1" -wgpu = { version = "0.19.1", features = ["naga-ir"] } +egui_dock = "0.13.0" +egui-notify = "0.15.0" +egui-modal = "0.4.0" + +wgpu = { version = "0.20.0", features = ["naga-ir"] } +naga = "0.20.0" +naga_oil = "0.14.0" glam = { version = "0.24.2", features = ["bytemuck"] } -image = "0.24.7" +image = { version = "0.25.0", features = ["png"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -107,11 +113,15 @@ winit = { version = "0.29.4", default-features = false } log = { version = "0.4", features = ["std"] } document-features = "0.2.8" web-time = "0.2" +ahash = "0.8.11" +glutin = "0.31" +glutin-winit = "0.4" -parking_lot = { version = "0.12.1", features = [ +parking_lot = { version = "0.12.3", features = [ "nightly", # This is required for parking_lot to work properly in WebAssembly builds with atomics support "deadlock_detection", ] } +parking_lot_core = "0.9.10" once_cell = "1.18.0" crossbeam = "0.8.2" dashmap = "5.5.3" @@ -139,6 +149,11 @@ murmur3 = "0.5.2" alacritty_terminal = "0.22.0" +wasm-bindgen = "0.2.91" +wasm-bindgen-futures = "0.4.42" +web-sys = "0.3.67" +js-sys = "0.3" + luminol-audio = { version = "0.4.0", path = "crates/audio/" } luminol-components = { version = "0.4.0", path = "crates/components/" } luminol-config = { version = "0.4.0", path = "crates/config/" } @@ -221,9 +236,9 @@ features = ["web"] # Web # Look into somehow pinning these as workspace dependencies [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2.87" -wasm-bindgen-futures = "=0.4.40" -js-sys = "0.3" +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true +js-sys.workspace = true oneshot.workspace = true @@ -233,7 +248,7 @@ tracing-wasm = "0.2" tracing-log = "0.1.3" tracing.workspace = true -web-sys = { version = "=0.3.67", features = [ +web-sys = { workspace = true, features = [ "BeforeUnloadEvent", "Window", "Worker", diff --git a/crates/components/src/database_view.rs b/crates/components/src/database_view.rs index 9ec9f7ee..011dacc6 100644 --- a/crates/components/src/database_view.rs +++ b/crates/components/src/database_view.rs @@ -81,7 +81,7 @@ impl DatabaseView { egui::Layout::bottom_up(ui.layout().horizontal_align()), |ui| { ui.horizontal(|ui| { - ui.style_mut().wrap = Some(true); + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap); ui.add(egui::DragValue::new(self.maximum.as_mut().unwrap())); diff --git a/crates/components/src/lib.rs b/crates/components/src/lib.rs index 3a4e9555..414eb99e 100644 --- a/crates/components/src/lib.rs +++ b/crates/components/src/lib.rs @@ -148,7 +148,7 @@ where .vertical(|ui| { let spacing = ui.spacing().item_spacing.y; ui.add_space(spacing); - ui.add(egui::Label::new(format!("{}:", self.name)).truncate(true)); + ui.add(egui::Label::new(format!("{}:", self.name)).truncate()); if ui.add(self.widget).changed() { changed = true; }; @@ -198,11 +198,11 @@ where let available_width = ui.available_width() - ui.spacing().item_spacing.x; let width = self.max_width.min(available_width); let mut response = egui::ComboBox::from_id_source(&self.id_source) - .wrap(true) + .wrap() .width(width) .selected_text(self.reference.to_string()) .show_ui(ui, |ui| { - ui.style_mut().wrap = Some(true); + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap); for (i, variant) in T::iter().enumerate() { ui.with_stripe(i % 2 != 0, |ui| { @@ -275,7 +275,7 @@ where let mut changed = false; let inner_response = egui::ComboBox::from_id_source(&self.id_source) - .wrap(true) + .wrap() .width(ui.available_width() - ui.spacing().item_spacing.x) .selected_text(formatter(&self)) .show_ui(ui, |ui| { @@ -409,7 +409,7 @@ where } }, |this, ui, ids, first_row_is_faint, show_none| { - ui.style_mut().wrap = Some(true); + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap); if show_none && ui @@ -465,7 +465,7 @@ where ui, |this| (this.formatter)(*this.reference), |this, ui, ids, first_row_is_faint, _| { - ui.style_mut().wrap = Some(true); + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap); let mut is_faint = first_row_is_faint; diff --git a/crates/components/src/map_view.rs b/crates/components/src/map_view.rs index bfd84280..6f1d8253 100644 --- a/crates/components/src/map_view.rs +++ b/crates/components/src/map_view.rs @@ -204,7 +204,7 @@ impl MapView { // Apply scroll and cap max zoom to 15% self.scale *= (delta / 9.0f32.exp2()).exp2(); - self.scale = self.scale.max(15.).min(300.); + self.scale = self.scale.clamp(15., 300.); // Get the normalized cursor position relative to pan let pos_norm = (pos - self.pan - canvas_center) / old_scale; @@ -901,7 +901,7 @@ impl MapView { screenshot .write_to( &mut std::io::BufWriter::new(&mut file), - image::ImageOutputFormat::Png, + image::ImageFormat::Png, ) .wrap_err(c)?; file.flush().wrap_err(c)?; diff --git a/crates/components/src/syntax_highlighting.rs b/crates/components/src/syntax_highlighting.rs index ccdc15ad..28c53a2a 100644 --- a/crates/components/src/syntax_highlighting.rs +++ b/crates/components/src/syntax_highlighting.rs @@ -26,6 +26,17 @@ use egui::text::LayoutJob; +impl egui::util::cache::ComputerMut<(luminol_config::CodeTheme, &str, &str), LayoutJob> + for Highlighter +{ + fn compute( + &mut self, + (theme, code, lang): (luminol_config::CodeTheme, &str, &str), + ) -> LayoutJob { + self.highlight(theme, code, lang) + } +} + /// View some code with syntax highlighting and selection. pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str, theme: luminol_config::CodeTheme) { let language = "rb"; @@ -54,17 +65,6 @@ pub fn highlight( code: &str, language: &str, ) -> LayoutJob { - impl egui::util::cache::ComputerMut<(luminol_config::CodeTheme, &str, &str), LayoutJob> - for Highlighter - { - fn compute( - &mut self, - (theme, code, lang): (luminol_config::CodeTheme, &str, &str), - ) -> LayoutJob { - self.highlight(theme, code, lang) - } - } - type HighlightCache = egui::util::cache::FrameCache; ctx.memory_mut(|m| { diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 4d5706d5..a025d8b7 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -18,10 +18,9 @@ workspace = true [dependencies] egui.workspace = true - -egui_dock = "0.12.0" -egui-notify = "0.14.0" -egui-modal = "0.3.6" +egui_dock.workspace = true +egui-notify.workspace = true +egui-modal.workspace = true poll-promise.workspace = true once_cell.workspace = true diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index dfea5671..f57b1f61 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -7,6 +7,83 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.28.1 - 2024-07-05 +* Web: only capture clicks/touches when actually over canvas [#4775](https://github.com/emilk/egui/pull/4775) by [@lucasmerlin](https://github.com/lucasmerlin) + + +## 0.28.0 - 2024-07-03 - Better integration of a eframe in a bigger website +### ✨ Highlights +The eframe web canvas now works properly when its a small part of a larger web page. +Previously this caused a lot of weird bugs, such as the eframe canvas stealing focus, and resizing the canvas in annoying ways. +Now it should all work seamlessly to have an eframe canvas as part of a web page, including having multiple different eframe apps next to each other. +As part of that the eframe canvas can now be focused (or not), just like an `` HTML element. + +We've also implemented a better method for sizing and positioning the canvas so that it yields pixel-perfect rendering on all known browsers except for Desktop Safari. +What this means is that text is much less likely to be blurry on web for users ([#4536](https://github.com/emilk/egui/pull/4536) by [@jprochazk](https://github.com/jprochazk)). + +### ⭐ Added +* Add `register_native_texture` in `eframe::Frame` [#4246](https://github.com/emilk/egui/pull/4246) by [@Chaojimengnan](https://github.com/Chaojimengnan) +* Add `NativeOptions::persistence_path` [#4423](https://github.com/emilk/egui/pull/4423) by [@lucasmerlin](https://github.com/lucasmerlin) +* Make sure to call `raw_input_hook` on web [#4646](https://github.com/emilk/egui/pull/4646) by [@owen-d](https://github.com/owen-d) + +### 🔧 Changed +* Early-out from context switching the `glow` backend [#4284](https://github.com/emilk/egui/pull/4284), [#4296](https://github.com/emilk/egui/pull/4296) by [@emilk](https://github.com/emilk) +* Allow users to create viewports larger than monitor on Windows & macOS [#4337](https://github.com/emilk/egui/pull/4337) by [@lopo12123](https://github.com/lopo12123) +* Use `objc2` and its framework crates [#4395](https://github.com/emilk/egui/pull/4395) by [@madsmtm](https://github.com/madsmtm) +* Emit physical key presses when a non-Latin layout is active [#4461](https://github.com/emilk/egui/pull/4461) by [@TicClick](https://github.com/TicClick) +* Clamp window size to monitor size by default on all platforms [#4410](https://github.com/emilk/egui/pull/4410) by [@rustbasic](https://github.com/rustbasic) +* Ignore synthetic key presses [#4514](https://github.com/emilk/egui/pull/4514) by [@hut](https://github.com/hut) +* Use `ResizeObserver` instead of `resize` event [#4536](https://github.com/emilk/egui/pull/4536) by [@jprochazk](https://github.com/jprochazk) +* Make pinch-to-zoom more responsive on web [#4621](https://github.com/emilk/egui/pull/4621) by [@emilk](https://github.com/emilk) +* Move first `request_animation_frame` into resize observer [#4628](https://github.com/emilk/egui/pull/4628) by [@jprochazk](https://github.com/jprochazk) +* Replace `directories-next` dependency with `directories` [#4661](https://github.com/emilk/egui/pull/4661) by [@crumblingstatue](https://github.com/crumblingstatue) +* `eframe::Result` is now short for `eframe::Result<()>` [#4706](https://github.com/emilk/egui/pull/4706) by [@emilk](https://github.com/emilk) +* Ignore keyboard events unless canvas has focus [#4718](https://github.com/emilk/egui/pull/4718) by [@emilk](https://github.com/emilk) + +### 🐛 Fixed +* Fix `ViewportCommand::InnerSize` not resizing viewport on Wayland (#4211) [#4211](https://github.com/emilk/egui/pull/4211) by [@rustbasic](https://github.com/rustbasic) +* Improve IME support with new `Event::Ime` [#4358](https://github.com/emilk/egui/pull/4358) by [@rustbasic](https://github.com/rustbasic) +* IME for chinese [#4436](https://github.com/emilk/egui/pull/4436) by [@rustbasic](https://github.com/rustbasic) +* Fix: Window position creeps between executions on scaled monitors [#4443](https://github.com/emilk/egui/pull/4443) by [@avery-radmacher](https://github.com/avery-radmacher) +* Fix: still track mouse when dragging outside web canvas [#4522](https://github.com/emilk/egui/pull/4522) by [@emilk](https://github.com/emilk) +* Fix: Don't `.forget()` RAF closure [#4551](https://github.com/emilk/egui/pull/4551) by [@jprochazk](https://github.com/jprochazk) +* Improve web text agent [#4561](https://github.com/emilk/egui/pull/4561) by [@jprochazk](https://github.com/jprochazk) +* Fix broken mouse coordinates when there's padding on the canvas element [#4729](https://github.com/emilk/egui/pull/4729) by [@emilk](https://github.com/emilk) +* Only repaint on cursor movements of area, or if dragging outside [#4730](https://github.com/emilk/egui/pull/4730) by [@emilk](https://github.com/emilk) +* Fix drag-and-drop file preview/hover [#4732](https://github.com/emilk/egui/pull/4732) by [@emilk](https://github.com/emilk) +* Fix stuck keys after pressing ctrl+C, cmd+A, etc [#4731](https://github.com/emilk/egui/pull/4731) by [@emilk](https://github.com/emilk) + +### 🧳 Migration +* Update MSRV to 1.76 [#4411](https://github.com/emilk/egui/pull/4411) by [@emilk](https://github.com/emilk) + +#### Wrap app creator in a `Result` +Applications can now return an error during the app creation ([#4565](https://github.com/emilk/egui/pull/4565) by [@emilk](https://github.com/emilk)), so you now need to wrap your `Box` in a `Result` like so: + + +``` diff +- eframe::run_native("My App", options, Box::new(|cc| Box::new(MyApp::new(cc)))); ++ eframe::run_native("My App", options, Box::new(|cc| Ok(Box::new(MyApp::new(cc))))); +``` + +#### Change web CSS +To make the eframe canvas fill the entire web browser, set its CSS to: + +``` css +top: 0; +left: 0; +width: 100%; +height: 100%; +``` + +See [`index.html`](https://github.com/emilk/egui/blob/a489374ca63f0d1ae983bb21d8bb766b2d68737b/web_demo/index.html#L30-L50) and [#4536](https://github.com/emilk/egui/pull/4536) for details. + +#### Web canvas focus +If you are using eframe for a fullscreen app, you should call `.focus()` on your canvas during startup: +```js +document.getElementById("the_canvas_id").focus(); +``` + + ## 0.27.2 - 2024-04-02 #### Desktop/Native * Fix continuous repaint on Wayland when TextEdit is focused or IME output is set [#4269](https://github.com/emilk/egui/pull/4269) (thanks [@white-axe](https://github.com/white-axe)!) diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 36b63952..0ba4c475 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -24,6 +24,9 @@ all-features = true rustc-args = ["--cfg=web_sys_unstable_apis"] targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] +[lints] +workspace = true + [lib] @@ -38,6 +41,9 @@ default = [ "x11", ] +# Dummy features so we don't run into compiler warnings +glow = [] + ## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/). accesskit = ["egui/accesskit", "egui-winit/accesskit"] @@ -67,7 +73,7 @@ default_fonts = ["egui/default_fonts"] ## Enable saving app state to disk. persistence = [ - "directories-next", + "directories", "egui-winit/serde", "egui/persistence", "ron", @@ -134,12 +140,12 @@ egui = { workspace = true, features = [ "log", ] } +ahash.workspace = true document-features.workspace = true log.workspace = true parking_lot.workspace = true raw-window-handle.workspace = true static_assertions = "1.1.0" -thiserror.workspace = true web-time.workspace = true # Optional dependencies @@ -151,7 +157,7 @@ rwh_05 = { package = "raw-window-handle", version = "0.5.2", optional = true, fe "std", ] } ron = { workspace = true, optional = true, features = ["integer128"] } -serde = { version = "1", optional = true, features = ["derive"] } +serde = { workspace = true, optional = true } # ------------------------------------------- # native: @@ -160,13 +166,11 @@ egui-winit = { workspace = true, features = [ "clipboard", "links", ] } -image = { version = "0.24", default-features = false, features = [ - "png", -] } # Needed for app icon +image = { workspace = true, features = ["png"] } # Needed for app icon winit = { workspace = true, default-features = false, features = ["rwh_06"] } # optional native: -directories-next = { version = "2", optional = true } +directories = { version = "5", optional = true } luminol-egui-wgpu = { workspace = true, optional = true, features = [ "winit", ] } # if wgpu is used, use it with winit @@ -174,8 +178,8 @@ pollster = { version = "0.3", optional = true } # needed for wgpu # we can expose these to user so that they can select which backends they want to enable to avoid compiling useless deps. # this can be done at the same time we expose x11/wayland features of winit crate. -glutin = { version = "0.31", optional = true } -glutin-winit = { version = "0.4", optional = true } +glutin = { workspace = true, optional = true } +glutin-winit = { workspace = true, optional = true } puffin = { workspace = true, optional = true } wgpu = { workspace = true, optional = true, features = [ # Let's enable some backends so that users can use `eframe` out-of-the-box @@ -186,8 +190,19 @@ wgpu = { workspace = true, optional = true, features = [ # mac: [target.'cfg(any(target_os = "macos"))'.dependencies] -cocoa = "0.25.0" -objc = "0.2.7" +objc2 = "0.5.1" +objc2-foundation = { version = "0.2.0", features = [ + "block2", + "NSData", + "NSString", +] } +objc2-app-kit = { version = "0.2.0", features = [ + "NSApplication", + "NSImage", + "NSMenu", + "NSMenuItem", + "NSResponder", +] } # windows: [target.'cfg(any(target_os = "windows"))'.dependencies] @@ -196,12 +211,12 @@ winapi = { version = "0.3.9", features = ["winuser"] } # ------------------------------------------- # web: [target.'cfg(target_arch = "wasm32")'.dependencies] -bytemuck = "1.7" +bytemuck.workspace = true js-sys = "0.3" percent-encoding = "2.1" -wasm-bindgen = "0.2" -wasm-bindgen-futures = "0.4" -web-sys = { version = "0.3.65", features = [ # Needs to be at least 0.3.65 for Luminol to work because that's the first version with a `WorkerGlobalScope.performance` binding +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true +web-sys = { workspace = true, features = [ # Needs to be at least 0.3.65 for Luminol to work because that's the first version with a `WorkerGlobalScope.performance` binding "BinaryType", "Blob", "Clipboard", @@ -233,7 +248,14 @@ web-sys = { version = "0.3.65", features = [ # Needs to be at least 0.3.65 for L "MediaQueryListEvent", "MouseEvent", "Navigator", + "Node", + "NodeList", "Performance", + "ResizeObserver", + "ResizeObserverBoxOptions", + "ResizeObserverEntry", + "ResizeObserverOptions", + "ResizeObserverSize", "Storage", "Touch", "TouchEvent", diff --git a/crates/eframe/README.md b/crates/eframe/README.md index 968a5e98..d185bace 100644 --- a/crates/eframe/README.md +++ b/crates/eframe/README.md @@ -1,5 +1,5 @@ > [!IMPORTANT] -> luminol-eframe is currently based on emilk/egui@0.27.2 +> luminol-eframe is currently based on emilk/egui@0.28.1 > [!NOTE] > This is Luminol's modified version of eframe. The original version is dual-licensed under MIT and Apache 2.0. diff --git a/crates/eframe/build.rs b/crates/eframe/build.rs new file mode 100644 index 00000000..5e6da5fb --- /dev/null +++ b/crates/eframe/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo::rustc-check-cfg=cfg(web_sys_unstable_apis)"); +} diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index a32ddc19..dec8fafa 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -41,10 +41,12 @@ pub type EventLoopBuilderHook = Box) #[cfg(any(feature = "glow", feature = "wgpu"))] pub type WindowBuilderHook = Box egui::ViewportBuilder>; +type DynError = Box; + /// This is how your app is created. /// /// You can use the [`CreationContext`] to setup egui, restore state, setup OpenGL things, etc. -pub type AppCreator = Box) -> Box>; +pub type AppCreator = Box) -> Result, DynError>>; /// Data that is passed to [`AppCreator`] that can be used to setup and initialize your app. pub struct CreationContext<'s> { @@ -150,6 +152,7 @@ pub trait App { /// On web the state is stored to "Local Storage". /// /// On native the path is picked using [`crate::storage_dir`]. + /// The path can be customized via [`NativeOptions::persistence_path`]. fn save(&mut self, _storage: &mut dyn Storage) {} /// Called once on shutdown, after [`Self::save`]. @@ -232,7 +235,7 @@ pub enum HardwareAcceleration { /// Do NOT use graphics acceleration. /// - /// On some platforms (MacOS) this is ignored and treated the same as [`Self::Preferred`]. + /// On some platforms (macOS) this is ignored and treated the same as [`Self::Preferred`]. Off, } @@ -362,6 +365,10 @@ pub struct NativeOptions { /// Controls whether or not the native window position and size will be /// persisted (only if the "persistence" feature is enabled). pub persist_window: bool, + + /// The folder where `eframe` will store the app state. If not set, eframe will get the paths + /// from [directories]. + pub persistence_path: Option, } #[cfg(not(target_arch = "wasm32"))] @@ -379,6 +386,8 @@ impl Clone for NativeOptions { #[cfg(feature = "wgpu")] wgpu_options: self.wgpu_options.clone(), + persistence_path: self.persistence_path.clone(), + ..*self } } @@ -418,6 +427,8 @@ impl Default for NativeOptions { wgpu_options: luminol_egui_wgpu::WgpuConfiguration::default(), persist_window: true, + + persistence_path: None, } } } @@ -455,11 +466,6 @@ pub struct WebOptions { /// Configures wgpu instance/device/adapter/surface creation and renderloop. #[cfg(feature = "wgpu")] pub wgpu_options: luminol_egui_wgpu::WgpuConfiguration, - - /// The size limit of the web app canvas. - /// - /// By default the max size is [`egui::Vec2::INFINITY`], i.e. unlimited. - pub max_size_points: egui::Vec2, } #[cfg(target_arch = "wasm32")] @@ -475,8 +481,6 @@ impl Default for WebOptions { #[cfg(feature = "wgpu")] wgpu_options: luminol_egui_wgpu::WgpuConfiguration::default(), - - max_size_points: egui::Vec2::INFINITY, } } } @@ -518,10 +522,10 @@ pub enum WebGlContextOption { /// Force use WebGL2. WebGl2, - /// Use WebGl2 first. + /// Use WebGL2 first. BestFirst, - /// Use WebGl1 first + /// Use WebGL1 first CompatibilityFirst, } @@ -614,6 +618,11 @@ pub struct Frame { #[cfg(feature = "glow")] pub(crate) gl: Option>, + /// Used to convert user custom [`glow::Texture`] to [`egui::TextureId`] + #[cfg(all(feature = "glow", not(target_arch = "wasm32")))] + pub(crate) glow_register_native_texture: + Option egui::TextureId>>, + /// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s. #[cfg(feature = "wgpu")] pub(crate) wgpu_render_state: Option, @@ -690,6 +699,15 @@ impl Frame { self.gl.as_ref() } + /// Register your own [`glow::Texture`], + /// and then you can use the returned [`egui::TextureId`] to render your texture with [`egui`]. + /// + /// This function will take the ownership of your [`glow::Texture`], so please do not delete your [`glow::Texture`] after registering. + #[cfg(all(feature = "glow", not(target_arch = "wasm32")))] + pub fn register_native_glow_texture(&mut self, native: glow::Texture) -> egui::TextureId { + self.glow_register_native_texture.as_mut().unwrap()(native) + } + /// The underlying WGPU render state. /// /// Only available when compiling with the `wgpu` feature and using [`Renderer::Wgpu`]. diff --git a/crates/eframe/src/icon_data.rs b/crates/eframe/src/icon_data.rs index 847228f9..ed514d00 100644 --- a/crates/eframe/src/icon_data.rs +++ b/crates/eframe/src/icon_data.rs @@ -54,7 +54,7 @@ impl IconDataExt for IconData { image .write_to( &mut std::io::Cursor::new(&mut png_bytes), - image::ImageOutputFormat::Png, + image::ImageFormat::Png, ) .map_err(|err| err.to_string())?; Ok(png_bytes) diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 05717d40..f95c8567 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -30,7 +30,7 @@ //! //! fn main() { //! let native_options = luminol_eframe::NativeOptions::default(); -//! luminol_eframe::run_native("My egui App", native_options, Box::new(|cc| Box::new(MyEguiApp::new(cc)))); +//! luminol_eframe::run_native("My egui App", native_options, Box::new(|cc| Ok(Box::new(MyEguiApp::new(cc))))); //! } //! //! #[derive(Default)] @@ -90,7 +90,7 @@ //! .start( //! canvas_id, //! luminol_eframe::WebOptions::default(), -//! Box::new(|cc| Box::new(MyEguiApp::new(cc))), +//! Box::new(|cc| Ok(Box::new(MyEguiApp::new(cc))),) //! ) //! .await //! } @@ -137,6 +137,7 @@ #![allow(clippy::needless_doctest_main)] // Luminol doesn't need everything from eframe, but we're leaving everything here to reduce merge conflicts #![allow(dead_code, unused_imports)] +#![allow(clippy::thread_local_initializer_can_be_made_const)] // Re-export all useful libraries: pub use {egui, egui::emath, egui::epaint}; @@ -199,9 +200,9 @@ pub mod icon_data; /// ``` no_run /// use luminol_eframe::egui; /// -/// fn main() -> luminol_eframe::Result<()> { +/// fn main() -> luminol_eframe::Result { /// let native_options = luminol_eframe::NativeOptions::default(); -/// luminol_eframe::run_native("MyApp", native_options, Box::new(|cc| Box::new(MyEguiApp::new(cc)))) +/// luminol_eframe::run_native("MyApp", native_options, Box::new(|cc| Ok(Box::new(MyEguiApp::new(cc))))) /// } /// /// #[derive(Default)] @@ -235,7 +236,7 @@ pub fn run_native( app_name: &str, mut native_options: NativeOptions, app_creator: AppCreator, -) -> Result<()> { +) -> Result { #[cfg(not(feature = "__screenshot"))] assert!( std::env::var("EFRAME_SCREENSHOT_TO").is_err(), @@ -280,7 +281,7 @@ pub fn run_native( /// /// # Example /// ``` no_run -/// fn main() -> luminol_eframe::Result<()> { +/// fn main() -> luminol_eframe::Result { /// // Our application state: /// let mut name = "Arthur".to_owned(); /// let mut age = 42; @@ -312,7 +313,7 @@ pub fn run_simple_native( app_name: &str, native_options: NativeOptions, update_fun: impl FnMut(&egui::Context, &mut Frame) + 'static, -) -> Result<()> { +) -> Result { struct SimpleApp { update_fun: U, } @@ -326,48 +327,128 @@ pub fn run_simple_native( run_native( app_name, native_options, - Box::new(|_cc| Box::new(SimpleApp { update_fun })), + Box::new(|_cc| Ok(Box::new(SimpleApp { update_fun }))), ) } // ---------------------------------------------------------------------------- /// The different problems that can occur when trying to run `eframe`. -#[derive(thiserror::Error, Debug)] +#[derive(Debug)] pub enum Error { + /// Something went wrong in user code when creating the app. + AppCreation(Box), + /// An error from [`winit`]. #[cfg(not(target_arch = "wasm32"))] - #[error("winit error: {0}")] - Winit(#[from] winit::error::OsError), + Winit(winit::error::OsError), /// An error from [`winit::event_loop::EventLoop`]. #[cfg(not(target_arch = "wasm32"))] - #[error("winit EventLoopError: {0}")] - WinitEventLoop(#[from] winit::error::EventLoopError), + WinitEventLoop(winit::error::EventLoopError), /// An error from [`glutin`] when using [`glow`]. #[cfg(all(feature = "glow", not(target_arch = "wasm32")))] - #[error("glutin error: {0}")] - Glutin(#[from] glutin::error::Error), + Glutin(glutin::error::Error), /// An error from [`glutin`] when using [`glow`]. #[cfg(all(feature = "glow", not(target_arch = "wasm32")))] - #[error("Found no glutin configs matching the template: {0:?}. Error: {1:?}")] NoGlutinConfigs(glutin::config::ConfigTemplate, Box), /// An error from [`glutin`] when using [`glow`]. #[cfg(feature = "glow")] - #[error("egui_glow: {0}")] - OpenGL(#[from] egui_glow::PainterError), + OpenGL(egui_glow::PainterError), /// An error from [`wgpu`]. #[cfg(feature = "wgpu")] - #[error("WGPU error: {0}")] - Wgpu(#[from] luminol_egui_wgpu::WgpuError), + Wgpu(luminol_egui_wgpu::WgpuError), +} + +impl std::error::Error for Error {} + +#[cfg(not(target_arch = "wasm32"))] +impl From for Error { + #[inline] + fn from(err: winit::error::OsError) -> Self { + Self::Winit(err) + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl From for Error { + #[inline] + fn from(err: winit::error::EventLoopError) -> Self { + Self::WinitEventLoop(err) + } +} + +#[cfg(all(feature = "glow", not(target_arch = "wasm32")))] +impl From for Error { + #[inline] + fn from(err: glutin::error::Error) -> Self { + Self::Glutin(err) + } +} + +#[cfg(feature = "glow")] +impl From for Error { + #[inline] + fn from(err: egui_glow::PainterError) -> Self { + Self::OpenGL(err) + } +} + +#[cfg(feature = "wgpu")] +impl From for Error { + #[inline] + fn from(err: luminol_egui_wgpu::WgpuError) -> Self { + Self::Wgpu(err) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::AppCreation(err) => write!(f, "app creation error: {err}"), + + #[cfg(not(target_arch = "wasm32"))] + Self::Winit(err) => { + write!(f, "winit error: {err}") + } + + #[cfg(not(target_arch = "wasm32"))] + Self::WinitEventLoop(err) => { + write!(f, "winit EventLoopError: {err}") + } + + #[cfg(all(feature = "glow", not(target_arch = "wasm32")))] + Self::Glutin(err) => { + write!(f, "glutin error: {err}") + } + + #[cfg(all(feature = "glow", not(target_arch = "wasm32")))] + Self::NoGlutinConfigs(template, err) => { + write!( + f, + "Found no glutin configs matching the template: {template:?}. Error: {err}" + ) + } + + #[cfg(feature = "glow")] + Self::OpenGL(err) => { + write!(f, "egui_glow: {err}") + } + + #[cfg(feature = "wgpu")] + Self::Wgpu(err) => { + write!(f, "WGPU error: {err}") + } + } + } } /// Short for `Result`. -pub type Result = std::result::Result; +pub type Result = std::result::Result; // --------------------------------------------------------------------------- diff --git a/crates/eframe/src/native/app_icon.rs b/crates/eframe/src/native/app_icon.rs index f1694208..840bf367 100644 --- a/crates/eframe/src/native/app_icon.rs +++ b/crates/eframe/src/native/app_icon.rs @@ -118,7 +118,7 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus { if image_scaled .write_to( &mut std::io::Cursor::new(&mut image_scaled_bytes), - image::ImageOutputFormat::Png, + image::ImageFormat::Png, ) .is_err() { @@ -203,12 +203,9 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS use crate::icon_data::IconDataExt as _; crate::profile_function!(); - use cocoa::{ - appkit::{NSApp, NSApplication, NSImage, NSMenu, NSWindow}, - base::{id, nil}, - foundation::{NSData, NSString}, - }; - use objc::{msg_send, sel, sel_impl}; + use objc2::ClassType; + use objc2_app_kit::{NSApplication, NSImage}; + use objc2_foundation::{NSData, NSString}; let png_bytes = if let Some(icon_data) = icon_data { match icon_data.to_png_bytes() { @@ -222,38 +219,35 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS None }; - // SAFETY: Accessing raw data from icon in a read-only manner. Icon data is static! + // TODO(madsmtm): Move this into `objc2-app-kit` + extern "C" { + static NSApp: Option<&'static NSApplication>; + } + + // SAFETY: we don't do anything dangerous here unsafe { - let app = NSApp(); - if app.is_null() { + let Some(app) = NSApp else { log::debug!("NSApp is null"); return AppIconStatus::NotSetIgnored; - } + }; if let Some(png_bytes) = png_bytes { - let data = NSData::dataWithBytes_length_( - nil, - png_bytes.as_ptr().cast::(), - png_bytes.len() as u64, - ); + let data = NSData::from_vec(png_bytes); log::trace!("NSImage::initWithData…"); - let app_icon = NSImage::initWithData_(NSImage::alloc(nil), data); + let app_icon = NSImage::initWithData(NSImage::alloc(), &data); crate::profile_scope!("setApplicationIconImage_"); log::trace!("setApplicationIconImage…"); - app.setApplicationIconImage_(app_icon); + app.setApplicationIconImage(app_icon.as_deref()); } // Change the title in the top bar - for python processes this would be again "python" otherwise. - let main_menu = app.mainMenu(); - if !main_menu.is_null() { - let item = main_menu.itemAtIndex_(0); - if !item.is_null() { - let app_menu: id = msg_send![item, submenu]; - if !app_menu.is_null() { + if let Some(main_menu) = app.mainMenu() { + if let Some(item) = main_menu.itemAtIndex(0) { + if let Some(app_menu) = item.submenu() { crate::profile_scope!("setTitle_"); - app_menu.setTitle_(NSString::alloc(nil).init_str(title)); + app_menu.setTitle(&NSString::from_str(title)); } } } diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index e98c8921..5ae99611 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -1,6 +1,8 @@ //! Common tools used by [`super::glow_integration`] and [`super::wgpu_integration`]. use web_time::Instant; + +use std::path::PathBuf; use winit::event_loop::EventLoopWindowTarget; use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _}; @@ -20,27 +22,41 @@ pub fn viewport_builder( let mut viewport_builder = native_options.viewport.clone(); + // On some Linux systems, a window size larger than the monitor causes crashes, + // and on Windows the window does not appear at all. + let clamp_size_to_monitor_size = viewport_builder.clamp_size_to_monitor_size.unwrap_or(true); + // Always use the default window size / position on iOS. Trying to restore the previous position // causes the window to be shown too small. #[cfg(not(target_os = "ios"))] let inner_size_points = if let Some(mut window_settings) = window_settings { // Restore pos/size from previous session - window_settings - .clamp_size_to_sane_values(largest_monitor_point_size(egui_zoom_factor, event_loop)); + if clamp_size_to_monitor_size { + window_settings.clamp_size_to_sane_values(largest_monitor_point_size( + egui_zoom_factor, + event_loop, + )); + } window_settings.clamp_position_to_monitors(egui_zoom_factor, event_loop); - viewport_builder = window_settings.initialize_viewport_builder(viewport_builder); + viewport_builder = window_settings.initialize_viewport_builder( + egui_zoom_factor, + event_loop, + viewport_builder, + ); window_settings.inner_size_points() } else { if let Some(pos) = viewport_builder.position { viewport_builder = viewport_builder.with_position(pos); } - if let Some(initial_window_size) = viewport_builder.inner_size { - let initial_window_size = initial_window_size - .at_most(largest_monitor_point_size(egui_zoom_factor, event_loop)); - viewport_builder = viewport_builder.with_inner_size(initial_window_size); + if clamp_size_to_monitor_size { + if let Some(initial_window_size) = viewport_builder.inner_size { + let initial_window_size = initial_window_size + .at_most(largest_monitor_point_size(egui_zoom_factor, event_loop)); + viewport_builder = viewport_builder.with_inner_size(initial_window_size); + } } viewport_builder.inner_size @@ -118,6 +134,16 @@ pub fn create_storage(_app_name: &str) -> Option> { None } +#[allow(clippy::unnecessary_wraps)] +pub fn create_storage_with_file(_file: impl Into) -> Option> { + #[cfg(feature = "persistence")] + return Some(Box::new( + super::file_storage::FileStorage::from_ron_filepath(_file), + )); + #[cfg(not(feature = "persistence"))] + None +} + // ---------------------------------------------------------------------------- /// Everything needed to make a winit-based integration for [`epi`]. @@ -152,6 +178,9 @@ impl EpiIntegration { native_options: &crate::NativeOptions, storage: Option>, #[cfg(feature = "glow")] gl: Option>, + #[cfg(feature = "glow")] glow_register_native_texture: Option< + Box egui::TextureId>, + >, #[cfg(feature = "wgpu")] wgpu_render_state: Option, ) -> Self { let frame = epi::Frame { @@ -162,6 +191,8 @@ impl EpiIntegration { storage, #[cfg(feature = "glow")] gl, + #[cfg(feature = "glow")] + glow_register_native_texture, #[cfg(feature = "wgpu")] wgpu_render_state, raw_display_handle: window.display_handle().map(|h| h.as_raw()), diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index 51c668ab..fb27642b 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -10,12 +10,12 @@ use std::{ /// [`egui::ViewportBuilder::app_id`] of [`crate::NativeOptions::viewport`] /// or the title argument to [`crate::run_native`]. /// -/// On native the path is picked using [`directories_next::ProjectDirs::data_dir`](https://docs.rs/directories-next/2.0.0/directories_next/struct.ProjectDirs.html#method.data_dir) which is: +/// On native the path is picked using [`directories::ProjectDirs::data_dir`](https://docs.rs/directories/5.0.1/directories/struct.ProjectDirs.html#method.data_dir) which is: /// * Linux: `/home/UserName/.local/share/APP_ID` /// * macOS: `/Users/UserName/Library/Application Support/APP_ID` /// * Windows: `C:\Users\UserName\AppData\Roaming\APP_ID` pub fn storage_dir(app_id: &str) -> Option { - directories_next::ProjectDirs::from("", "", app_id) + directories::ProjectDirs::from("", "", app_id) .map(|proj_dirs| proj_dirs.data_dir().to_path_buf()) } @@ -41,7 +41,7 @@ impl Drop for FileStorage { impl FileStorage { /// Store the state in this .ron file. - fn from_ron_filepath(ron_filepath: impl Into) -> Self { + pub(crate) fn from_ron_filepath(ron_filepath: impl Into) -> Self { crate::profile_function!(); let ron_filepath: PathBuf = ron_filepath.into(); log::debug!("Loading app state from {:?}…", ron_filepath); diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index ff45cb65..13576bdb 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -5,10 +5,15 @@ //! There is a bunch of improvements we could do, //! like removing a bunch of `unwraps`. -#![allow(clippy::arc_with_non_send_sync)] // glow::Context was accidentally non-Sync in glow 0.13, but that will be fixed in future releases of glow: https://github.com/grovesNL/glow/commit/c4a5f7151b9b4bbb380faa06ec27415235d1bf7e +// `clippy::arc_with_non_send_sync`: `glow::Context` was accidentally non-Sync in glow 0.13, +// but that will be fixed in future releases of glow. +// https://github.com/grovesNL/glow/commit/c4a5f7151b9b4bbb380faa06ec27415235d1bf7e +#![allow(clippy::arc_with_non_send_sync)] +#![allow(clippy::undocumented_unsafe_blocks)] use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant}; +use egui_winit::ActionRequested; use glutin::{ config::GlConfig, context::NotCurrentGlContext, @@ -21,10 +26,10 @@ use winit::{ window::{Window, WindowId}, }; +use ahash::{HashMap, HashSet}; use egui::{ - epaint::ahash::HashMap, DeferredViewportUiCallback, ImmediateViewport, ViewportBuilder, - ViewportClass, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, - ViewportOutput, + DeferredViewportUiCallback, ImmediateViewport, ViewportBuilder, ViewportClass, ViewportId, + ViewportIdMap, ViewportIdPair, ViewportInfo, ViewportOutput, }; #[cfg(feature = "accesskit")] use egui_winit::accesskit_winit; @@ -103,8 +108,9 @@ struct Viewport { ids: ViewportIdPair, class: ViewportClass, builder: ViewportBuilder, + deferred_commands: Vec, info: ViewportInfo, - screenshot_requested: bool, + actions_requested: HashSet, /// The user-callback that shows the ui. /// None for immediate viewports. @@ -189,13 +195,17 @@ impl GlowWinitApp { ) -> Result<&mut GlowWinitRunning> { crate::profile_function!(); - let storage = epi_integration::create_storage( - self.native_options - .viewport - .app_id - .as_ref() - .unwrap_or(&self.app_name), - ); + let storage = if let Some(file) = &self.native_options.persistence_path { + epi_integration::create_storage_with_file(file) + } else { + epi_integration::create_storage( + self.native_options + .viewport + .app_id + .as_ref() + .unwrap_or(&self.app_name), + ) + }; let egui_ctx = create_egui_context(storage.as_deref()); @@ -217,6 +227,7 @@ impl GlowWinitApp { let system_theme = winit_integration::system_theme(&glutin.window(ViewportId::ROOT), &self.native_options); + let painter = Rc::new(RefCell::new(painter)); let integration = EpiIntegration::new( egui_ctx, @@ -226,6 +237,10 @@ impl GlowWinitApp { &self.native_options, storage, Some(gl.clone()), + Some(Box::new({ + let painter = painter.clone(); + move |native| painter.borrow_mut().register_native_texture(native) + })), #[cfg(feature = "wgpu")] None, ); @@ -298,11 +313,10 @@ impl GlowWinitApp { raw_window_handle: window.window_handle().map(|h| h.as_raw()), }; crate::profile_scope!("app_creator"); - app_creator(&cc) + app_creator(&cc).map_err(crate::Error::AppCreation)? }; let glutin = Rc::new(RefCell::new(glutin)); - let painter = Rc::new(RefCell::new(painter)); { // Create weak pointers so that we don't keep @@ -350,21 +364,6 @@ impl WinitApp for GlowWinitApp { .map_or(0, |r| r.integration.egui_ctx.frame_nr_for(viewport_id)) } - fn is_focused(&self, window_id: WindowId) -> bool { - if let Some(running) = &self.running { - let glutin = running.glutin.borrow(); - if let Some(window_id) = glutin.viewport_from_window.get(&window_id) { - return glutin.focused_viewport == Some(*window_id); - } - } - - false - } - - fn integration(&self) -> Option<&EpiIntegration> { - self.running.as_ref().map(|r| &r.integration) - } - fn window(&self, window_id: WindowId) -> Option> { let running = self.running.as_ref()?; let glutin = running.glutin.borrow(); @@ -550,7 +549,7 @@ impl GlowWinitRunning { let Some(window) = viewport.window.as_ref() else { return EventResult::Wait; }; - egui_winit::update_viewport_info(&mut viewport.info, &egui_ctx, window); + egui_winit::update_viewport_info(&mut viewport.info, &egui_ctx, window, false); let Some(egui_winit) = viewport.egui_winit.as_mut() else { return EventResult::Wait; @@ -636,6 +635,8 @@ impl GlowWinitRunning { viewport_output, } = full_output; + glutin.remove_viewports_not_in(&viewport_output); + let GlutinWindowContext { viewports, current_gl_context, @@ -645,6 +646,7 @@ impl GlowWinitRunning { let Some(viewport) = viewports.get_mut(&viewport_id) else { return EventResult::Wait; }; + viewport.info.events.clear(); // they should have been processed let window = viewport.window.clone().unwrap(); let gl_surface = viewport.gl_surface.as_ref().unwrap(); @@ -675,17 +677,38 @@ impl GlowWinitRunning { ); { - let screenshot_requested = std::mem::take(&mut viewport.screenshot_requested); - if screenshot_requested { - let screenshot = painter.read_screen_rgba(screen_size_in_pixels); - egui_winit - .egui_input_mut() - .events - .push(egui::Event::Screenshot { - viewport_id, - image: screenshot.into(), - }); + for action in viewport.actions_requested.drain() { + match action { + ActionRequested::Screenshot => { + let screenshot = painter.read_screen_rgba(screen_size_in_pixels); + egui_winit + .egui_input_mut() + .events + .push(egui::Event::Screenshot { + viewport_id, + image: screenshot.into(), + }); + } + ActionRequested::Cut => { + egui_winit.egui_input_mut().events.push(egui::Event::Cut); + } + ActionRequested::Copy => { + egui_winit.egui_input_mut().events.push(egui::Event::Copy); + } + ActionRequested::Paste => { + if let Some(contents) = egui_winit.clipboard_text() { + let contents = contents.replace("\r\n", "\n"); + if !contents.is_empty() { + egui_winit + .egui_input_mut() + .events + .push(egui::Event::Paste(contents)); + } + } + } + } } + integration.post_rendering(&window); } @@ -711,7 +734,7 @@ impl GlowWinitRunning { } } - glutin.handle_viewport_output(event_loop, &integration.egui_ctx, viewport_output); + glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output); integration.report_frame_time(frame_timer.total_time_sec()); // don't count auto-save time as part of regular frame time @@ -842,6 +865,20 @@ fn change_gl_context( ) { crate::profile_function!(); + if !cfg!(target_os = "windows") { + // According to https://github.com/emilk/egui/issues/4289 + // we cannot do this early-out on Windows. + // TODO(emilk): optimize context switching on Windows too. + // See https://github.com/emilk/egui/issues/4173 + + if let Some(current_gl_context) = current_gl_context { + crate::profile_scope!("is_current"); + if gl_surface.is_current(current_gl_context) { + return; // Early-out to save a lot of time. + } + } + } + let not_current = { crate::profile_scope!("make_not_current"); current_gl_context @@ -850,6 +887,7 @@ fn change_gl_context( .make_not_current() .unwrap() }; + crate::profile_scope!("make_current"); *current_gl_context = Some(not_current.make_current(gl_surface).unwrap()); } @@ -986,8 +1024,7 @@ impl GlutinWindowContext { if let Some(window) = &window { viewport_from_window.insert(window.id(), ViewportId::ROOT); window_from_viewport.insert(ViewportId::ROOT, window.id()); - info.minimized = window.is_minimized(); - info.maximized = Some(window.is_maximized()); + egui_winit::update_viewport_info(&mut info, egui_ctx, window, true); } let mut viewports = ViewportIdMap::default(); @@ -997,8 +1034,9 @@ impl GlutinWindowContext { ids: ViewportIdPair::ROOT, class: ViewportClass::Root, builder: viewport_builder, + deferred_commands: vec![], info, - screenshot_requested: false, + actions_requested: Default::default(), viewport_ui_cb: None, gl_surface: None, window: window.map(Arc::new), @@ -1050,7 +1088,7 @@ impl GlutinWindowContext { &mut self, viewport_id: ViewportId, event_loop: &EventLoopWindowTarget, - ) -> Result<()> { + ) -> Result { crate::profile_function!(); let viewport = self @@ -1078,8 +1116,8 @@ impl GlutinWindowContext { &window, &viewport.builder, ); - viewport.info.minimized = window.is_minimized(); - viewport.info.maximized = Some(window.is_maximized()); + + egui_winit::update_viewport_info(&mut viewport.info, &self.egui_ctx, &window, true); viewport.window.insert(Arc::new(window)) }; @@ -1150,7 +1188,7 @@ impl GlutinWindowContext { } /// only applies for android. but we basically drop surface + window and make context not current - fn on_suspend(&mut self) -> Result<()> { + fn on_suspend(&mut self) -> Result { log::debug!("received suspend event. dropping window and surface"); for viewport in self.viewports.values_mut() { viewport.gl_surface = None; @@ -1184,15 +1222,7 @@ impl GlutinWindowContext { if let Some(viewport) = self.viewports.get(&viewport_id) { if let Some(gl_surface) = &viewport.gl_surface { - self.current_gl_context = Some( - self.current_gl_context - .take() - .unwrap() - .make_not_current() - .unwrap() - .make_current(gl_surface) - .unwrap(), - ); + change_gl_context(&mut self.current_gl_context, gl_surface); gl_surface.resize( self.current_gl_context .as_ref() @@ -1208,16 +1238,27 @@ impl GlutinWindowContext { self.gl_config.display().get_proc_address(addr) } + pub(crate) fn remove_viewports_not_in( + &mut self, + viewport_output: &ViewportIdMap, + ) { + // GC old viewports + self.viewports + .retain(|id, _| viewport_output.contains_key(id)); + self.viewport_from_window + .retain(|_, id| viewport_output.contains_key(id)); + self.window_from_viewport + .retain(|id, _| viewport_output.contains_key(id)); + } + fn handle_viewport_output( &mut self, event_loop: &EventLoopWindowTarget, egui_ctx: &egui::Context, - viewport_output: ViewportIdMap, + viewport_output: &ViewportIdMap, ) { crate::profile_function!(); - let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); - for ( viewport_id, ViewportOutput { @@ -1225,58 +1266,58 @@ impl GlutinWindowContext { class, builder, viewport_ui_cb, - commands, + mut commands, repaint_delay: _, // ignored - we listened to the repaint callback instead }, - ) in viewport_output + ) in viewport_output.clone() { let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent); let viewport = initialize_or_update_viewport( - egui_ctx, &mut self.viewports, ids, class, builder, viewport_ui_cb, - self.focused_viewport, ); if let Some(window) = &viewport.window { - let is_viewport_focused = self.focused_viewport == Some(viewport_id); + let old_inner_size = window.inner_size(); + + viewport.deferred_commands.append(&mut commands); + egui_winit::process_viewport_commands( egui_ctx, &mut viewport.info, - commands, + std::mem::take(&mut viewport.deferred_commands), window, - is_viewport_focused, - &mut viewport.screenshot_requested, + &mut viewport.actions_requested, ); + + // For Wayland : https://github.com/emilk/egui/issues/4196 + if cfg!(target_os = "linux") { + let new_inner_size = window.inner_size(); + if new_inner_size != old_inner_size { + self.resize(viewport_id, new_inner_size); + } + } } } // Create windows for any new viewports: self.initialize_all_windows(event_loop); - // GC old viewports - self.viewports - .retain(|id, _| active_viewports_ids.contains(id)); - self.viewport_from_window - .retain(|_, id| active_viewports_ids.contains(id)); - self.window_from_viewport - .retain(|id, _| active_viewports_ids.contains(id)); + self.remove_viewports_not_in(viewport_output); } } -fn initialize_or_update_viewport<'vp>( - egu_ctx: &egui::Context, - viewports: &'vp mut ViewportIdMap, +fn initialize_or_update_viewport( + viewports: &mut ViewportIdMap, ids: ViewportIdPair, class: ViewportClass, mut builder: ViewportBuilder, viewport_ui_cb: Option>, - focused_viewport: Option, -) -> &'vp mut Viewport { +) -> &mut Viewport { crate::profile_function!(); if builder.icon.is_none() { @@ -1294,8 +1335,9 @@ fn initialize_or_update_viewport<'vp>( ids, class, builder, + deferred_commands: vec![], info: Default::default(), - screenshot_requested: false, + actions_requested: Default::default(), viewport_ui_cb, window: None, egui_winit: None, @@ -1311,7 +1353,7 @@ fn initialize_or_update_viewport<'vp>( viewport.class = class; viewport.viewport_ui_cb = viewport_ui_cb; - let (delta_commands, recreate) = viewport.builder.patch(builder); + let (mut delta_commands, recreate) = viewport.builder.patch(builder); if recreate { log::debug!( @@ -1321,18 +1363,10 @@ fn initialize_or_update_viewport<'vp>( ); viewport.window = None; viewport.egui_winit = None; - } else if let Some(window) = &viewport.window { - let is_viewport_focused = focused_viewport == Some(ids.this); - egui_winit::process_viewport_commands( - egu_ctx, - &mut viewport.info, - delta_commands, - window, - is_viewport_focused, - &mut viewport.screenshot_requested, - ); } + viewport.deferred_commands.append(&mut delta_commands); + entry.into_mut() } } @@ -1362,13 +1396,11 @@ fn render_immediate_viewport( let mut glutin = glutin.borrow_mut(); initialize_or_update_viewport( - egui_ctx, &mut glutin.viewports, ids, ViewportClass::Immediate, builder, None, - None, ); if let Err(err) = glutin.initialize_window(viewport_id, event_loop) { @@ -1388,7 +1420,7 @@ fn render_immediate_viewport( let (Some(egui_winit), Some(window)) = (&mut viewport.egui_winit, &viewport.window) else { return; }; - egui_winit::update_viewport_info(&mut viewport.info, egui_ctx, window); + egui_winit::update_viewport_info(&mut viewport.info, egui_ctx, window, false); let mut raw_input = egui_winit.take_egui_input(window); raw_input.viewports = glutin @@ -1442,18 +1474,7 @@ fn render_immediate_viewport( let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); - { - crate::profile_function!("context-switch"); - *current_gl_context = Some( - current_gl_context - .take() - .unwrap() - .make_not_current() - .unwrap() - .make_current(gl_surface) - .unwrap(), - ); - } + change_gl_context(current_gl_context, gl_surface); let current_gl_context = current_gl_context.as_ref().unwrap(); @@ -1487,7 +1508,7 @@ fn render_immediate_viewport( egui_winit.handle_platform_output(window, platform_output); - glutin.handle_viewport_output(event_loop, egui_ctx, viewport_output); + glutin.handle_viewport_output(event_loop, egui_ctx, &viewport_output); } #[cfg(feature = "__screenshot")] diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 27da2713..50d60d40 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -2,7 +2,7 @@ use std::{cell::RefCell, time::Instant}; use winit::event_loop::{EventLoop, EventLoopBuilder}; -use egui::epaint::ahash::HashMap; +use ahash::HashMap; use crate::{ epi, @@ -43,7 +43,7 @@ fn with_event_loop( mut native_options: epi::NativeOptions, f: impl FnOnce(&mut EventLoop, epi::NativeOptions) -> R, ) -> Result { - thread_local!(static EVENT_LOOP: RefCell>> = const { RefCell::new(None) }); + thread_local!(static EVENT_LOOP: RefCell>> = RefCell::new(None)); EVENT_LOOP.with(|event_loop| { // Since we want to reference NativeOptions when creating the EventLoop we can't @@ -60,10 +60,7 @@ fn with_event_loop( } #[cfg(not(target_os = "ios"))] -fn run_and_return( - event_loop: &mut EventLoop, - mut winit_app: impl WinitApp, -) -> Result<()> { +fn run_and_return(event_loop: &mut EventLoop, mut winit_app: impl WinitApp) -> Result { use winit::{event_loop::ControlFlow, platform::run_on_demand::EventLoopExtRunOnDemand}; log::trace!("Entering the winit event loop (run_on_demand)…"); @@ -234,7 +231,7 @@ fn run_and_return( fn run_and_exit( event_loop: EventLoop, mut winit_app: impl WinitApp + 'static, -) -> Result<()> { +) -> Result { use winit::event_loop::ControlFlow; log::trace!("Entering the winit event loop (run)…"); @@ -390,7 +387,9 @@ pub fn run_glow( app_name: &str, mut native_options: epi::NativeOptions, app_creator: epi::AppCreator, -) -> Result<()> { +) -> Result { + #![allow(clippy::needless_return_with_question_mark)] // False positive + use super::glow_integration::GlowWinitApp; #[cfg(not(target_os = "ios"))] @@ -414,7 +413,9 @@ pub fn run_wgpu( app_name: &str, mut native_options: epi::NativeOptions, app_creator: epi::AppCreator, -) -> Result<()> { +) -> Result { + #![allow(clippy::needless_return_with_question_mark)] // False positive + use super::wgpu_integration::WgpuWinitApp; #[cfg(not(target_os = "ios"))] diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index ed02d416..657194f6 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -7,6 +7,7 @@ use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant}; +use egui_winit::ActionRequested; use parking_lot::Mutex; use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _}; use winit::{ @@ -14,10 +15,10 @@ use winit::{ window::{Window, WindowId}, }; +use ahash::{HashMap, HashSet, HashSetExt}; use egui::{ - ahash::HashMap, DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportBuilder, - ViewportClass, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, - ViewportOutput, + DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportBuilder, ViewportClass, + ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, ViewportOutput, }; #[cfg(feature = "accesskit")] use egui_winit::accesskit_winit; @@ -76,8 +77,9 @@ pub struct Viewport { ids: ViewportIdPair, class: ViewportClass, builder: ViewportBuilder, + deferred_commands: Vec, info: ViewportInfo, - screenshot_requested: bool, + actions_requested: HashSet, /// `None` for sync viewports. viewport_ui_cb: Option>, @@ -154,13 +156,11 @@ impl WgpuWinitApp { } = &mut *running.shared.borrow_mut(); initialize_or_update_viewport( - egui_ctx, viewports, ViewportIdPair::ROOT, ViewportClass::Root, self.native_options.viewport.clone(), None, - None, ) .initialize_window(event_loop, egui_ctx, viewport_from_window, painter); } @@ -182,7 +182,7 @@ impl WgpuWinitApp { storage: Option>, window: Window, builder: ViewportBuilder, - ) -> Result<&mut WgpuWinitRunning, luminol_egui_wgpu::WgpuError> { + ) -> crate::Result<&mut WgpuWinitRunning> { crate::profile_function!(); #[allow(unsafe_code, unused_mut, unused_unsafe)] @@ -215,6 +215,8 @@ impl WgpuWinitApp { storage, #[cfg(feature = "glow")] None, + #[cfg(feature = "glow")] + None, wgpu_render_state.clone(), ); @@ -270,12 +272,15 @@ impl WgpuWinitApp { }; let app = { crate::profile_scope!("user_app_creator"); - app_creator(&cc) + app_creator(&cc).map_err(crate::Error::AppCreation)? }; let mut viewport_from_window = HashMap::default(); viewport_from_window.insert(window.id(), ViewportId::ROOT); + let mut info = ViewportInfo::default(); + egui_winit::update_viewport_info(&mut info, &egui_ctx, &window, true); + let mut viewports = Viewports::default(); viewports.insert( ViewportId::ROOT, @@ -283,12 +288,9 @@ impl WgpuWinitApp { ids: ViewportIdPair::ROOT, class: ViewportClass::Root, builder, - info: ViewportInfo { - minimized: window.is_minimized(), - maximized: Some(window.is_maximized()), - ..Default::default() - }, - screenshot_requested: false, + deferred_commands: vec![], + info, + actions_requested: Default::default(), viewport_ui_cb: None, window: Some(window), egui_winit: Some(egui_winit), @@ -339,20 +341,6 @@ impl WinitApp for WgpuWinitApp { .map_or(0, |r| r.integration.egui_ctx.frame_nr_for(viewport_id)) } - fn is_focused(&self, window_id: WindowId) -> bool { - if let Some(running) = &self.running { - let shared = running.shared.borrow(); - let viewport_id = shared.viewport_from_window.get(&window_id).copied(); - shared.focused_viewport.is_some() && shared.focused_viewport == viewport_id - } else { - false - } - } - - fn integration(&self) -> Option<&EpiIntegration> { - self.running.as_ref().map(|r| &r.integration) - } - fn window(&self, window_id: WindowId) -> Option> { self.running .as_ref() @@ -418,13 +406,17 @@ impl WinitApp for WgpuWinitApp { self.recreate_window(event_loop, running); running } else { - let storage = epi_integration::create_storage( - self.native_options - .viewport - .app_id - .as_ref() - .unwrap_or(&self.app_name), - ); + let storage = if let Some(file) = &self.native_options.persistence_path { + epi_integration::create_storage_with_file(file) + } else { + epi_integration::create_storage( + self.native_options + .viewport + .app_id + .as_ref() + .unwrap_or(&self.app_name), + ) + }; let egui_ctx = winit_integration::create_egui_context(storage.as_deref()); let (window, builder) = create_window( &egui_ctx, @@ -601,7 +593,7 @@ impl WgpuWinitRunning { let Some(window) = window else { return EventResult::Wait; }; - egui_winit::update_viewport_info(info, &integration.egui_ctx, window); + egui_winit::update_viewport_info(info, &integration.egui_ctx, window, false); { crate::profile_scope!("set_window"); @@ -636,15 +628,25 @@ impl WgpuWinitRunning { // ------------------------------------------------------------ - let mut shared = shared.borrow_mut(); + let mut shared_mut = shared.borrow_mut(); let SharedState { egui_ctx, viewports, painter, viewport_from_window, - focused_viewport, - } = &mut *shared; + .. + } = &mut *shared_mut; + + let FullOutput { + platform_output, + textures_delta, + shapes, + pixels_per_point, + viewport_output, + } = full_output; + + remove_viewports_not_in(viewports, painter, viewport_from_window, &viewport_output); let Some(viewport) = viewports.get_mut(&viewport_id) else { return EventResult::Wait; @@ -661,19 +663,14 @@ impl WgpuWinitRunning { return EventResult::Wait; }; - let FullOutput { - platform_output, - textures_delta, - shapes, - pixels_per_point, - viewport_output, - } = full_output; - egui_winit.handle_platform_output(window, platform_output); let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); - let screenshot_requested = std::mem::take(&mut viewport.screenshot_requested); + let screenshot_requested = viewport + .actions_requested + .take(&ActionRequested::Screenshot) + .is_some(); let (vsync_secs, screenshot) = painter.paint_and_update_textures( viewport_id, pixels_per_point, @@ -692,15 +689,41 @@ impl WgpuWinitRunning { }); } + for action in viewport.actions_requested.drain() { + match action { + ActionRequested::Screenshot => { + // already handled above + } + ActionRequested::Cut => { + egui_winit.egui_input_mut().events.push(egui::Event::Cut); + } + ActionRequested::Copy => { + egui_winit.egui_input_mut().events.push(egui::Event::Copy); + } + ActionRequested::Paste => { + if let Some(contents) = egui_winit.clipboard_text() { + let contents = contents.replace("\r\n", "\n"); + if !contents.is_empty() { + egui_winit + .egui_input_mut() + .events + .push(egui::Event::Paste(contents)); + } + } + } + } + } + integration.post_rendering(window); let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); handle_viewport_output( &integration.egui_ctx, - viewport_output, + &viewport_output, viewports, - *focused_viewport, + painter, + viewport_from_window, ); // Prune dead viewports: @@ -874,9 +897,7 @@ impl Viewport { painter.max_texture_side(), )); - self.info.minimized = window.is_minimized(); - self.info.maximized = Some(window.is_maximized()); - + egui_winit::update_viewport_info(&mut self.info, egui_ctx, &window, true); self.window = Some(window); } Err(err) => { @@ -931,15 +952,8 @@ fn render_immediate_viewport( .. } = &mut *shared.borrow_mut(); - let viewport = initialize_or_update_viewport( - egui_ctx, - viewports, - ids, - ViewportClass::Immediate, - builder, - None, - None, - ); + let viewport = + initialize_or_update_viewport(viewports, ids, ViewportClass::Immediate, builder, None); if viewport.window.is_none() { viewport.initialize_window(event_loop, egui_ctx, viewport_from_window, painter); } @@ -947,7 +961,7 @@ fn render_immediate_viewport( let (Some(window), Some(egui_winit)) = (&viewport.window, &mut viewport.egui_winit) else { return; }; - egui_winit::update_viewport_info(&mut viewport.info, egui_ctx, window); + egui_winit::update_viewport_info(&mut viewport.info, egui_ctx, window, false); let mut input = egui_winit.take_egui_input(window); input.viewports = viewports @@ -976,13 +990,13 @@ fn render_immediate_viewport( // ------------------------------------------ - let mut shared = shared.borrow_mut(); + let mut shared_mut = shared.borrow_mut(); let SharedState { viewports, painter, - focused_viewport, + viewport_from_window, .. - } = &mut *shared; + } = &mut *shared_mut; let Some(viewport) = viewports.get_mut(&ids.this) else { return; @@ -1014,15 +1028,36 @@ fn render_immediate_viewport( egui_winit.handle_platform_output(window, platform_output); - handle_viewport_output(&egui_ctx, viewport_output, viewports, *focused_viewport); + handle_viewport_output( + &egui_ctx, + &viewport_output, + viewports, + painter, + viewport_from_window, + ); +} + +pub(crate) fn remove_viewports_not_in( + viewports: &mut ViewportIdMap, + painter: &mut luminol_egui_wgpu::winit::Painter, + viewport_from_window: &mut HashMap, + viewport_output: &ViewportIdMap, +) { + let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); + + // Prune dead viewports: + viewports.retain(|id, _| active_viewports_ids.contains(id)); + viewport_from_window.retain(|_, id| active_viewports_ids.contains(id)); + painter.gc_viewports(&active_viewports_ids); } /// Add new viewports, and update existing ones: fn handle_viewport_output( egui_ctx: &egui::Context, - viewport_output: ViewportIdMap, + viewport_output: &ViewportIdMap, viewports: &mut ViewportIdMap, - focused_viewport: Option, + painter: &mut luminol_egui_wgpu::winit::Painter, + viewport_from_window: &mut HashMap, ) { for ( viewport_id, @@ -1031,48 +1066,56 @@ fn handle_viewport_output( class, builder, viewport_ui_cb, - commands, + mut commands, repaint_delay: _, // ignored - we listened to the repaint callback instead }, - ) in viewport_output + ) in viewport_output.clone() { let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent); - let viewport = initialize_or_update_viewport( - egui_ctx, - viewports, - ids, - class, - builder, - viewport_ui_cb, - focused_viewport, - ); + let viewport = + initialize_or_update_viewport(viewports, ids, class, builder, viewport_ui_cb); if let Some(window) = viewport.window.as_ref() { - let is_viewport_focused = focused_viewport == Some(viewport_id); + let old_inner_size = window.inner_size(); + + viewport.deferred_commands.append(&mut commands); + egui_winit::process_viewport_commands( egui_ctx, &mut viewport.info, - commands, + std::mem::take(&mut viewport.deferred_commands), window, - is_viewport_focused, - &mut viewport.screenshot_requested, + &mut viewport.actions_requested, ); + + // For Wayland : https://github.com/emilk/egui/issues/4196 + if cfg!(target_os = "linux") { + let new_inner_size = window.inner_size(); + if new_inner_size != old_inner_size { + if let (Some(width), Some(height)) = ( + NonZeroU32::new(new_inner_size.width), + NonZeroU32::new(new_inner_size.height), + ) { + painter.on_window_resized(viewport_id, width, height); + } + } + } } } + + remove_viewports_not_in(viewports, painter, viewport_from_window, viewport_output); } type ViewportUiCallback = dyn Fn(&egui::Context) + Send + Sync; -fn initialize_or_update_viewport<'vp>( - egui_ctx: &egui::Context, - viewports: &'vp mut Viewports, +fn initialize_or_update_viewport( + viewports: &mut Viewports, ids: ViewportIdPair, class: ViewportClass, mut builder: ViewportBuilder, viewport_ui_cb: Option>, - focused_viewport: Option, -) -> &'vp mut Viewport { +) -> &mut Viewport { crate::profile_function!(); if builder.icon.is_none() { @@ -1090,8 +1133,9 @@ fn initialize_or_update_viewport<'vp>( ids, class, builder, + deferred_commands: vec![], info: Default::default(), - screenshot_requested: false, + actions_requested: HashSet::new(), viewport_ui_cb, window: None, egui_winit: None, @@ -1106,7 +1150,7 @@ fn initialize_or_update_viewport<'vp>( viewport.ids.parent = ids.parent; viewport.viewport_ui_cb = viewport_ui_cb; - let (delta_commands, recreate) = viewport.builder.patch(builder); + let (mut delta_commands, recreate) = viewport.builder.patch(builder); if recreate { log::debug!( @@ -1116,18 +1160,10 @@ fn initialize_or_update_viewport<'vp>( ); viewport.window = None; viewport.egui_winit = None; - } else if let Some(window) = &viewport.window { - let is_viewport_focused = focused_viewport == Some(ids.this); - egui_winit::process_viewport_commands( - egui_ctx, - &mut viewport.info, - delta_commands, - window, - is_viewport_focused, - &mut viewport.screenshot_requested, - ); } + viewport.deferred_commands.append(&mut delta_commands); + entry.into_mut() } } diff --git a/crates/eframe/src/native/winit_integration.rs b/crates/eframe/src/native/winit_integration.rs index 541253c0..fbbd7910 100644 --- a/crates/eframe/src/native/winit_integration.rs +++ b/crates/eframe/src/native/winit_integration.rs @@ -9,8 +9,6 @@ use egui::ViewportId; #[cfg(feature = "accesskit")] use egui_winit::accesskit_winit; -use super::epi_integration::EpiIntegration; - /// Create an egui context, restoring it from storage if possible. pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context { crate::profile_function!(); @@ -64,10 +62,6 @@ pub trait WinitApp { /// The current frame number, as reported by egui. fn frame_nr(&self, viewport_id: ViewportId) -> u64; - fn is_focused(&self, window_id: WindowId) -> bool; - - fn integration(&self) -> Option<&EpiIntegration>; - fn window(&self, window_id: WindowId) -> Option>; fn window_id_from_viewport_id(&self, id: ViewportId) -> Option; diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 206e5ce0..a18c11d0 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -2,9 +2,10 @@ use egui::TexturesDelta; use crate::{epi, App}; -use super::{now_sec, web_painter::WebPainter, NeedRepaint}; +use super::{now_sec, text_agent::TextAgent, web_painter::WebPainter, NeedRepaint}; pub struct AppRunner { + #[allow(dead_code)] web_options: crate::WebOptions, pub(crate) frame: epi::Frame, egui_ctx: egui::Context, @@ -13,8 +14,6 @@ pub struct AppRunner { app: Box, pub(crate) needs_repaint: std::sync::Arc, last_save_time: f64, - pub(crate) ime: Option, - pub(crate) mutable_text_under_cursor: bool, // Output for the last run: textures_delta: TexturesDelta, @@ -32,7 +31,7 @@ impl Drop for AppRunner { impl AppRunner { /// # Errors - /// Failure to initialize WebGL renderer. + /// Failure to initialize WebGL renderer, or failure to create app. pub async fn new( canvas: web_sys::OffscreenCanvas, web_options: crate::WebOptions, @@ -100,7 +99,7 @@ impl AppRunner { let theme = system_theme.unwrap_or(web_options.default_theme); egui_ctx.set_visuals(theme.egui_visuals()); - let app = app_creator(&epi::CreationContext { + let cc = epi::CreationContext { egui_ctx: egui_ctx.clone(), integration_info: info.clone(), storage: Some(&storage), @@ -115,7 +114,8 @@ impl AppRunner { wgpu_render_state: painter.render_state(), #[cfg(all(feature = "wgpu", feature = "glow"))] wgpu_render_state: None, - }); + }; + let app = app_creator(&cc).map_err(|err| err.to_string())?; let frame = epi::Frame { info, @@ -147,8 +147,6 @@ impl AppRunner { app, needs_repaint, last_save_time: now_sec(), - ime: None, - mutable_text_under_cursor: false, textures_delta: Default::default(), clipped_primitives: None, @@ -206,15 +204,33 @@ impl AppRunner { self.clipped_primitives.is_some() } + /* + pub fn update_focus(&mut self) { + let has_focus = self.has_focus(); + if self.input.raw.focused != has_focus { + log::trace!("{} Focus changed to {has_focus}", self.canvas().id()); + self.input.set_focus(has_focus); + + if !has_focus { + // We lost focus - good idea to save + self.save(); + } + self.egui_ctx().request_repaint(); + } + } + */ + /// Runs the logic, but doesn't paint the result. /// /// The result can be painted later with a call to [`Self::run_and_paint`] or [`Self::paint`]. pub fn logic(&mut self) { - let raw_input = self.input.new_frame( + let mut raw_input = self.input.new_frame( egui::vec2(self.painter.width as f32, self.painter.height as f32), self.painter.pixel_ratio, ); + self.app.raw_input_hook(&self.egui_ctx, &mut raw_input); + let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { self.app.update(egui_ctx, &mut self.frame); }); @@ -238,7 +254,6 @@ impl AppRunner { } } - self.mutable_text_under_cursor = platform_output.mutable_text_under_cursor; self.worker_options.channels.zoom_tx.store( self.egui_ctx.zoom_factor(), portable_atomic::Ordering::Relaxed, @@ -248,7 +263,6 @@ impl AppRunner { .send(super::WebRunnerOutput::PlatformOutput( platform_output, self.egui_ctx.options(|o| o.screen_reader), - self.egui_ctx.wants_keyboard_input(), )); self.textures_delta.append(textures_delta); self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point)); @@ -279,8 +293,10 @@ impl AppRunner { state: &super::MainState, platform_output: egui::PlatformOutput, screen_reader_enabled: bool, - wants_keyboard_input: bool, ) { + // We sometimes miss blur/focus events due to the text agent, so let's just poll each frame: + state.update_focus(); + #[cfg(feature = "web_screen_reader")] if screen_reader_enabled { super::screen_reader::speak(&platform_output.events_description()); @@ -292,8 +308,8 @@ impl AppRunner { cursor_icon, open_url, copied_text, - events: _, // already handled - mutable_text_under_cursor, + events: _, // already handled + mutable_text_under_cursor: _, // TODO(#4569): https://github.com/emilk/egui/issues/4569 ime, #[cfg(feature = "accesskit")] accesskit_update: _, // not currently implemented @@ -312,16 +328,33 @@ impl AppRunner { #[cfg(not(web_sys_unstable_apis))] let _ = copied_text; - { - let mut inner = state.inner.borrow_mut(); - inner.mutable_text_under_cursor = mutable_text_under_cursor; - inner.wants_keyboard_input = wants_keyboard_input; - - if inner.ime != ime { - super::text_agent::move_text_cursor(ime, &state.canvas); - inner.ime = ime; + // Can't have `inner` borrowed for the `text_agent` operations because apparently they + // yield to the asynchronous runtime + let has_focus = state.inner.borrow().has_focus; + + let text_agent = state + .text_agent + .get() + .expect("text agent should be initialized at this point"); + + if has_focus { + // The eframe app has focus. + if ime.is_some() { + // We are editing text: give the focus to the text agent. + text_agent.focus(); + } else { + // We are not editing text - give the focus to the canvas. + text_agent.blur(); + state.canvas.focus().ok(); } } + + if let Err(err) = text_agent.move_to(ime, &state.canvas) { + log::error!( + "failed to update text agent position: {}", + super::string_from_js_value(&err) + ); + } } } diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index 57e28cbb..bf52a6cd 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -12,10 +12,7 @@ use super::percent_decode; #[derive(Default)] pub(crate) struct WebInput { /// Required because we don't get a position on touched - pub latest_touch_pos: Option, - - /// Required to maintain a stable touch position for multi-touch gestures. - pub latest_touch_pos_id: Option, + pub primary_touch: Option, /// The raw input to `egui`. pub raw: egui::RawInput, @@ -36,14 +33,17 @@ impl WebInput { raw_input } - /// On alt-tab and similar. - pub fn on_web_page_focus_change(&mut self, focused: bool) { + /// On alt-tab, or user clicking another HTML element. + pub fn set_focus(&mut self, focused: bool) { + if self.raw.focused == focused { + return; + } + // log::debug!("on_web_page_focus_change: {focused}"); self.raw.modifiers = egui::Modifiers::default(); // Avoid sticky modifier keys on alt-tab: self.raw.focused = focused; self.raw.events.push(egui::Event::WindowFocused(focused)); - self.latest_touch_pos = None; - self.latest_touch_pos_id = None; + self.primary_touch = None; } } @@ -143,6 +143,7 @@ fn parse_query_map(query: &str) -> BTreeMap> { map } +/* // TODO(emilk): this test is never acgtually run, because this whole module is wasm32 only 🤦‍♂️ #[test] fn test_parse_query() { @@ -172,3 +173,4 @@ fn test_parse_query() { ]) ); } +*/ diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 1c07817c..5a7cbb27 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -1,4 +1,8 @@ use super::*; +use web_sys::EventTarget; + +// TODO(emilk): there are more calls to `prevent_default` and `stop_propagaton` +// than what is probably needed. // ------------------------------------------------------------------------ @@ -8,13 +12,17 @@ use super::*; pub(crate) fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> { // Only paint and schedule if there has been no panic if let Some(mut runner_lock) = runner_ref.try_lock() { + let ctx = runner_lock.egui_ctx().clone(); let mut width = runner_lock.painter.width; let mut height = runner_lock.painter.height; let mut pixel_ratio = runner_lock.painter.pixel_ratio; let mut modifiers = runner_lock.input.raw.modifiers; let mut should_save = false; + let mut should_repaint = false; let mut touch = None; let mut has_focus = None; + let mut hash = None; + let mut theme = None; runner_lock.input.raw.events = Vec::new(); let mut events = Vec::new(); @@ -24,6 +32,10 @@ pub(crate) fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> events.push(event); } + WebRunnerEvent::Repaint => { + should_repaint = true; + } + WebRunnerEvent::ScreenResize(new_width, new_height, new_pixel_ratio) => { width = new_width; height = new_height; @@ -38,8 +50,8 @@ pub(crate) fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> should_save = true; } - WebRunnerEvent::Touch(touch_id, touch_pos) => { - touch = Some((touch_id, touch_pos)); + WebRunnerEvent::Touch(touch_id) => { + touch = Some(touch_id); } WebRunnerEvent::Focus(new_has_focus) => { @@ -47,6 +59,51 @@ pub(crate) fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> events.push(egui::Event::WindowFocused(new_has_focus)); touch = None; } + + WebRunnerEvent::Wheel(wheel_unit, wheel_delta, wheel_modifiers) => { + if wheel_modifiers.ctrl && !modifiers.ctrl { + // The browser is saying the ctrl key is down, but it isn't _really_. + // This happens on pinch-to-zoom on a Mac trackpad. + // egui will treat ctrl+scroll as zoom, so it all works. + // However, we explicitly handle it here in order to better match the pinch-to-zoom + // speed of a native app, without being sensitive to egui's `scroll_zoom_speed` setting. + let pinch_to_zoom_sensitivity = 0.01; // Feels good on a Mac trackpad in 2024 + let zoom_factor = (pinch_to_zoom_sensitivity * wheel_delta.y).exp(); + events.push(egui::Event::Zoom(zoom_factor)); + } else { + events.push(egui::Event::MouseWheel { + unit: wheel_unit, + delta: wheel_delta, + modifiers: wheel_modifiers, + }); + } + } + + WebRunnerEvent::CommandKeyReleased => { + // When pressing Cmd+A (select all) or Ctrl+C (copy), + // chromium will not fire a `keyup` for the letter key. + // This leads to stuck keys, unless we do this hack. + // See https://github.com/emilk/egui/issues/4724 + + let keys_down = ctx.input(|i| i.keys_down.clone()); + for key in keys_down { + events.push(egui::Event::Key { + key, + physical_key: None, + pressed: false, + repeat: false, + modifiers, + }); + } + } + + WebRunnerEvent::Hash(new_hash) => { + hash = Some(new_hash); + } + + WebRunnerEvent::Theme(new_theme) => { + theme = Some(new_theme); + } } } @@ -54,15 +111,26 @@ pub(crate) fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> // touch state and trigger a rerender if let Some(has_focus) = has_focus { runner_lock.input.raw.focused = has_focus; - runner_lock.input.latest_touch_pos_id = None; - runner_lock.input.latest_touch_pos = None; + runner_lock.input.primary_touch = None; runner_lock.needs_repaint.repaint_asap(); } // If a touch event has been detected, put it into the input and trigger a rerender - if let Some((touch_id, touch_pos)) = touch { - runner_lock.input.latest_touch_pos_id = touch_id; - runner_lock.input.latest_touch_pos = Some(touch_pos); + if let Some(touch_id) = touch { + runner_lock.input.primary_touch = touch_id; + runner_lock.needs_repaint.repaint_asap(); + } + + // If the URL hash has changed, put it into the epi frame and trigger a rerender + if let Some(hash) = hash { + runner_lock.frame.info.web_info.location.hash = hash; + runner_lock.needs_repaint.repaint_asap(); + } + + // If the color scheme has changed, put it into the epi frame and trigger a rerender + if let Some(theme) = theme { + runner_lock.frame.info.system_theme = Some(theme); + runner_lock.egui_ctx().set_visuals(theme.egui_visuals()); runner_lock.needs_repaint.repaint_asap(); } @@ -84,6 +152,11 @@ pub(crate) fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> runner_lock.needs_repaint.repaint_asap(); } + // Rerender immediately if a repaint event was received + if should_repaint { + runner_lock.needs_repaint.repaint_asap(); + } + // Resize the canvas if the screen size has changed if runner_lock.painter.width != width || runner_lock.painter.height != height @@ -146,167 +219,266 @@ fn paint_if_needed(runner: &mut AppRunner) { // ------------------------------------------------------------------------ -pub(crate) fn install_document_events(state: &MainState) -> Result<(), JsValue> { - let document = web_sys::window().unwrap().document().unwrap(); +pub(crate) fn install_event_handlers(state: &MainState) -> Result<(), JsValue> { + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let canvas = state.canvas.clone(); + install_blur_focus(state, &canvas)?; + + prevent_default_and_stop_propagation( + state, + &canvas, + &[ + // Allow users to use ctrl-p for e.g. a command palette: + "afterprint", + // By default, right-clicks open a browser context menu. + // We don't want to do that (right clicks are handled by egui): + "contextmenu", + ], + )?; + + install_keydown(state, &canvas)?; + install_keyup(state, &canvas)?; + + // It seems copy/cut/paste events only work on the document, + // so we check if we have focus inside of the handler. + install_copy_cut_paste(state, &document)?; + + install_mousedown(state, &canvas)?; + // Use `document` here to notice if the user releases a drag outside of the canvas: + // See https://github.com/emilk/egui/issues/3157 + install_mousemove(state, &document)?; + install_mouseup(state, &document)?; + install_mouseleave(state, &canvas)?; + + install_touchstart(state, &canvas)?; + // Use `document` here to notice if the user drag outside of the canvas: + // See https://github.com/emilk/egui/issues/3157 + install_touchmove(state, &document)?; + install_touchend(state, &document)?; + install_touchcancel(state, &canvas)?; + + install_wheel(state, &canvas)?; + //install_drag_and_drop(runner_ref, &canvas)?; + install_window_events(state, &window)?; + Ok(()) +} + +fn install_blur_focus(state: &MainState, target: &EventTarget) -> Result<(), JsValue> { + // NOTE: because of the text agent we sometime miss 'blur' events, + // so we also poll the focus state each frame in `AppRunner::handle_platform_output`. for event_name in ["blur", "focus"] { let closure = move |_event: web_sys::MouseEvent, state: &MainState| { - // log::debug!("{event_name:?}"); - let has_focus = event_name == "focus"; + log::trace!("{} {event_name:?}", state.canvas.id()); + state.update_focus(); - if !has_focus { - // We lost focus - good idea to save + if event_name == "blur" { + // This might be a good time to save the state state.channels.send_custom(WebRunnerEvent::Save); } - - state.channels.send_custom(WebRunnerEvent::Focus(has_focus)); - //runner.egui_ctx().request_repaint(); }; - state.add_event_listener(&document, event_name, closure)?; + state.add_event_listener(target, event_name, closure)?; } + Ok(()) +} - state.add_event_listener( - &document, - "keydown", - |event: web_sys::KeyboardEvent, state| { - if event.is_composing() || event.key_code() == 229 { - // https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/ - return; - } +fn install_keydown(state: &MainState, target: &EventTarget) -> Result<(), JsValue> { + state.add_event_listener(target, "keydown", |event: web_sys::KeyboardEvent, state| { + if !state.inner.borrow().has_focus { + return; + } - let modifiers = modifiers_from_kb_event(&event); - state - .channels - .send_custom(WebRunnerEvent::Modifiers(modifiers)); + let modifiers = modifiers_from_kb_event(&event); + if !modifiers.ctrl + && !modifiers.command + // When text agent is focused, it is responsible for handling input events + && !state.text_agent.get().expect("text agent should be initialized at this point").has_focus() + { + if let Some(text) = text_from_keyboard_event(&event) { + state.channels.send(egui::Event::Text(text)); + //state.channels.send_custom(WebRunnerEvent::Repaint); - let key = event.key(); - let egui_key = translate_key(&key); + // If this is indeed text, then prevent any other action. + event.prevent_default(); - if let Some(key) = egui_key { - state.channels.send(egui::Event::Key { - key, - physical_key: None, // TODO(fornwall) - pressed: true, - repeat: false, // egui will fill this in for us! - modifiers, - }); - } - if !modifiers.ctrl - && !modifiers.command - && !should_ignore_key(&key) - // When text agent is shown, it sends text event instead. - && text_agent::text_agent().hidden() - { - state.channels.send(egui::Event::Text(key)); + // Assume egui uses all key events, and don't let them propagate to parent elements. + event.stop_propagation(); } - //runner.needs_repaint.repaint_asap(); - - let egui_wants_keyboard = state.inner.borrow().wants_keyboard_input; - - #[allow(clippy::if_same_then_else)] - let prevent_default = if egui_key == Some(egui::Key::Tab) { - // Always prevent moving cursor to url bar. - // egui wants to use tab to move to the next text field. - true - } else if matches!( - egui_key, - Some(egui::Key::P | egui::Key::S | egui::Key::O | egui::Key::F) - ) { - #[allow(clippy::needless_bool)] - if modifiers.ctrl || modifiers.command || modifiers.mac_cmd { - true // Prevent ctrl-P opening the print dialog. Users may want to use it for a command palette. - } else { - false // let normal P:s through - } - } else if egui_wants_keyboard { - matches!( - event.key().as_str(), - "Backspace" // so we don't go back to previous page when deleting text - | "ArrowDown" | "ArrowLeft" | "ArrowRight" | "ArrowUp" // cmd-left is "back" on Mac (https://github.com/emilk/egui/issues/58) - ) - } else { - // We never want to prevent: - // * F5 / cmd-R (refresh) - // * cmd-shift-C (debug tools) - // * cmd/ctrl-c/v/x (or we stop copy/past/cut events) - false - }; + } - // log::debug!( - // "On key-down {:?}, egui_wants_keyboard: {}, prevent_default: {}", - // event.key().as_str(), - // egui_wants_keyboard, - // prevent_default - // ); + on_keydown(event, state); + }) +} - if prevent_default { - event.prevent_default(); - // event.stop_propagation(); - } - }, - )?; +#[allow(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener` +pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, state: &MainState) { + let has_focus = state.inner.borrow().has_focus; + if !has_focus { + return; + } - state.add_event_listener( - &document, - "keyup", - |event: web_sys::KeyboardEvent, state| { - let modifiers = modifiers_from_kb_event(&event); - state - .channels - .send_custom(WebRunnerEvent::Modifiers(modifiers)); - if let Some(key) = translate_key(&event.key()) { - state.channels.send(egui::Event::Key { - key, - physical_key: None, // TODO(fornwall) - pressed: false, - repeat: false, - modifiers, - }); - } - //runner.needs_repaint.repaint_asap(); - }, - )?; + if event.is_composing() || event.key_code() == 229 { + // https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/ + return; + } + + let modifiers = modifiers_from_kb_event(&event); + state + .channels + .send_custom(WebRunnerEvent::Modifiers(modifiers)); + + let key = event.key(); + let egui_key = translate_key(&key); + + if let Some(egui_key) = egui_key { + state.channels.send(egui::Event::Key { + key: egui_key, + physical_key: None, // TODO(fornwall) + pressed: true, + repeat: false, // egui will fill this in for us! + modifiers, + }); + //state.channels.send_custom(WebRunnerEvent::Repaint); + + let prevent_default = should_prevent_default_for_key(state, &modifiers, egui_key); + + // log::debug!( + // "On keydown {:?} {egui_key:?}, has_focus: {has_focus}, egui_wants_keyboard: {}, prevent_default: {prevent_default}", + // event.key().as_str(), + // runner.egui_ctx().wants_keyboard_input() + // ); + + if prevent_default { + event.prevent_default(); + } + + // Assume egui uses all key events, and don't let them propagate to parent elements. + event.stop_propagation(); + } +} + +/// If the canvas (or text agent) has focus: +/// should we prevent the default browser event action when the user presses this key? +fn should_prevent_default_for_key( + state: &MainState, + modifiers: &egui::Modifiers, + egui_key: egui::Key, +) -> bool { + // NOTE: We never want to prevent: + // * F5 / cmd-R (refresh) + // * cmd-shift-C (debug tools) + // * cmd/ctrl-c/v/x (lest we prevent copy/paste/cut events) + + // Prevent ctrl-P from opening the print dialog. Users may want to use it for a command palette. + if matches!( + egui_key, + egui::Key::P | egui::Key::S | egui::Key::O | egui::Key::F, + ) && (modifiers.ctrl || modifiers.command || modifiers.mac_cmd) + { + return true; + } + + if egui_key == egui::Key::Space + && !state + .text_agent + .get() + .expect("text agent should be initialized at this point") + .has_focus() + { + // Space scrolls the web page, but we don't want that while canvas has focus + // However, don't prevent it if text agent has focus, or we can't type space! + return true; + } + + matches!( + egui_key, + // Prevent browser from focusing the next HTML element. + // egui uses Tab to move focus within the egui app. + egui::Key::Tab + + // So we don't go back to previous page while canvas has focus + | egui::Key::Backspace + + // Don't scroll web page while canvas has focus. + // Also, cmd-left is "back" on Mac (https://github.com/emilk/egui/issues/58) + | egui::Key::ArrowDown | egui::Key::ArrowLeft | egui::Key::ArrowRight | egui::Key::ArrowUp + ) +} + +fn install_keyup(state: &MainState, target: &EventTarget) -> Result<(), JsValue> { + state.add_event_listener(target, "keyup", on_keyup) +} +#[allow(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener` +pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, state: &MainState) { + let modifiers = modifiers_from_kb_event(&event); + state + .channels + .send_custom(WebRunnerEvent::Modifiers(modifiers)); + + if let Some(key) = translate_key(&event.key()) { + state.channels.send(egui::Event::Key { + key, + physical_key: None, // TODO(fornwall) + pressed: false, + repeat: false, + modifiers, + }); + } + + if event.key() == "Meta" || event.key() == "Control" { + state + .channels + .send_custom(WebRunnerEvent::CommandKeyReleased); + } + + //state.channels.send_custom(WebRunnerEvent::Repaint); + + let has_focus = state.inner.borrow().has_focus; + if has_focus { + // Assume egui uses all key events, and don't let them propagate to parent elements. + event.stop_propagation(); + } +} + +fn install_copy_cut_paste(state: &MainState, target: &EventTarget) -> Result<(), JsValue> { #[cfg(web_sys_unstable_apis)] - state.add_event_listener( - &document, - "paste", - |event: web_sys::ClipboardEvent, state| { - if let Some(data) = event.clipboard_data() { - if let Ok(text) = data.get_data("text") { - let text = text.replace("\r\n", "\n"); - if !text.is_empty() { - state.channels.send(egui::Event::Paste(text)); - //runner.needs_repaint.repaint_asap(); - } - event.stop_propagation(); - event.prevent_default(); + state.add_event_listener(target, "paste", |event: web_sys::ClipboardEvent, state| { + if let Some(data) = event.clipboard_data() { + if let Ok(text) = data.get_data("text") { + let text = text.replace("\r\n", "\n"); + if !text.is_empty() && state.inner.borrow().has_focus { + state.channels.send(egui::Event::Paste(text)); + //state.channels.send_custom(WebRunnerEvent::Repaint); } + event.stop_propagation(); + event.prevent_default(); } - }, - )?; + } + })?; #[cfg(web_sys_unstable_apis)] - state.add_event_listener(&document, "cut", |event: web_sys::ClipboardEvent, state| { - state.channels.send(egui::Event::Cut); + state.add_event_listener(target, "cut", |event: web_sys::ClipboardEvent, state| { + if state.inner.borrow().has_focus { + state.channels.send(egui::Event::Cut); - // In Safari we are only allowed to write to the clipboard during the - // event callback, which is why we run the app logic here and now: - //runner.logic(); + // In Safari we are only allowed to write to the clipboard during the + // event callback, which is why we run the app logic here and now: + //runner.logic(); - // Make sure we paint the output of the above logic call asap: - //runner.needs_repaint.repaint_asap(); + // Make sure we paint the output of the above logic call asap: + //state.channels.send_custom(WebRunnerEvent::Repaint); + } event.stop_propagation(); event.prevent_default(); })?; #[cfg(web_sys_unstable_apis)] - state.add_event_listener( - &document, - "copy", - |event: web_sys::ClipboardEvent, state| { + state.add_event_listener(target, "copy", |event: web_sys::ClipboardEvent, state| { + if state.inner.borrow().has_focus { state.channels.send(egui::Event::Copy); // In Safari we are only allowed to write to the clipboard during the @@ -314,58 +486,38 @@ pub(crate) fn install_document_events(state: &MainState) -> Result<(), JsValue> //runner.logic(); // Make sure we paint the output of the above logic call asap: - //runner.needs_repaint.repaint_asap(); + //state.channels.send_custom(WebRunnerEvent::Repaint); + } - event.stop_propagation(); - event.prevent_default(); - }, - )?; + event.stop_propagation(); + event.prevent_default(); + })?; Ok(()) } -pub(crate) fn install_window_events(state: &MainState) -> Result<(), JsValue> { - let window = web_sys::window().unwrap(); - - for event_name in ["blur", "focus"] { - let closure = move |_event: web_sys::MouseEvent, state: &MainState| { - // log::debug!("{event_name:?}"); - let has_focus = event_name == "focus"; - - if !has_focus { - // We lost focus - good idea to save - state.channels.send_custom(WebRunnerEvent::Save); - } - - state.channels.send_custom(WebRunnerEvent::Focus(has_focus)); - //runner.egui_ctx().request_repaint(); - }; - - state.add_event_listener(&window, event_name, closure)?; - } - - /* - +fn install_window_events(state: &MainState, window: &web_sys::Window) -> Result<(), JsValue> { // Save-on-close - runner_ref.add_event_listener(&window, "onbeforeunload", |_: web_sys::Event, runner| { - runner.save(); + state.add_event_listener(window, "onbeforeunload", |_: web_sys::Event, state| { + state.channels.send_custom(WebRunnerEvent::Save); })?; - for event_name in &["load", "pagehide", "pageshow", "resize"] { - runner_ref.add_event_listener(&window, event_name, move |_: web_sys::Event, runner| { + // NOTE: resize is handled by `ResizeObserver` below + for event_name in &["load", "pagehide", "pageshow"] { + state.add_event_listener(window, event_name, move |_: web_sys::Event, state| { // log::debug!("{event_name:?}"); - runner.needs_repaint.repaint_asap(); + state.channels.send_custom(WebRunnerEvent::Repaint); })?; } - runner_ref.add_event_listener(&window, "hashchange", |_: web_sys::Event, runner| { + state.add_event_listener(window, "hashchange", |_: web_sys::Event, state| { // `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here - runner.frame.info.web_info.location.hash = location_hash(); - runner.needs_repaint.repaint_asap(); // tell the user about the new hash + state + .channels + .send_custom(WebRunnerEvent::Hash(location_hash())); + //state.channels.send_custom(WebRunnerEvent::Repaint); // tell the user about the new hash })?; - */ - let closure = { let window = window.clone(); move |_event: web_sys::Event, state: &MainState| { @@ -389,23 +541,56 @@ pub(crate) fn install_window_events(state: &MainState) -> Result<(), JsValue> { } }; closure(web_sys::Event::new("")?, state); - state.add_event_listener(&window, "resize", closure)?; + state.add_event_listener(window, "resize", closure)?; + + { + let window = window.clone(); + // The canvas automatically resizes itself whenever a frame is drawn. + // The resizing does not take window.devicePixelRatio into account, + // so this mutation observer is to detect canvas resizes and correct them. + let callback: Closure = Closure::new(move |mutations: js_sys::Array| { + if PANIC_LOCK.get().is_some() { + return; + } + let width = window.inner_width().unwrap().as_f64().unwrap() as u32; + let height = window.inner_height().unwrap().as_f64().unwrap() as u32; + mutations.for_each(&mut |mutation, _, _| { + let mutation = mutation.unchecked_into::(); + if mutation.type_().as_str() == "attributes" { + let canvas = mutation + .target() + .unwrap() + .unchecked_into::(); + if canvas.width() != width || canvas.height() != height { + let _ = canvas.set_attribute("width", width.to_string().as_str()); + let _ = canvas.set_attribute("height", height.to_string().as_str()); + } + } + }); + }); + let observer = web_sys::MutationObserver::new(callback.as_ref().unchecked_ref())?; + let mut options = web_sys::MutationObserverInit::new(); + options.attributes(true); + observer.observe_with_options(&state.canvas, &options)?; + // We don't need to unregister this mutation observer on panic because it auto-deregisters + // when the target (the canvas) is removed from the DOM and garbage-collected + callback.forget(); + } Ok(()) } -pub(crate) fn install_color_scheme_change_event(runner_ref: &WebRunner) -> Result<(), JsValue> { +pub(crate) fn install_color_scheme_change_event(state: &MainState) -> Result<(), JsValue> { let window = web_sys::window().unwrap(); if let Some(media_query_list) = prefers_color_scheme_dark(&window)? { - runner_ref.add_event_listener::( + state.add_event_listener::( &media_query_list, "change", - |event, runner| { + |event, state| { let theme = theme_from_dark_mode(event.matches()); - runner.frame.info.system_theme = Some(theme); - runner.egui_ctx().set_visuals(theme.egui_visuals()); - runner.needs_repaint.repaint_asap(); + state.channels.send_custom(WebRunnerEvent::Theme(theme)); + //state.channels.send_custom(WebRunnerEvent::Repaint); }, )?; } @@ -413,85 +598,99 @@ pub(crate) fn install_color_scheme_change_event(runner_ref: &WebRunner) -> Resul Ok(()) } -pub(crate) fn install_canvas_events(state: &MainState) -> Result<(), JsValue> { - let window = web_sys::window().unwrap(); +fn prevent_default_and_stop_propagation( + state: &MainState, + target: &EventTarget, + event_names: &[&'static str], +) -> Result<(), JsValue> { + for event_name in event_names { + let closure = move |event: web_sys::MouseEvent, _state: &MainState| { + event.prevent_default(); + event.stop_propagation(); + // log::debug!("Preventing event {event_name:?}"); + }; - { - let prevent_default_events = [ - // By default, right-clicks open a context menu. - // We don't want to do that (right clicks is handled by egui): - "contextmenu", - // Allow users to use ctrl-p for e.g. a command palette: - "afterprint", - ]; + state.add_event_listener(target, event_name, closure)?; + } - for event_name in prevent_default_events { - let closure = move |event: web_sys::MouseEvent, _state: &_| { - event.prevent_default(); - // event.stop_propagation(); - // log::debug!("Preventing event {event_name:?}"); - }; + Ok(()) +} + +fn install_mousedown(state: &MainState, target: &EventTarget) -> Result<(), JsValue> { + state.add_event_listener(target, "mousedown", |event: web_sys::MouseEvent, state| { + let modifiers = modifiers_from_mouse_event(&event); + state + .channels + .send_custom(WebRunnerEvent::Modifiers(modifiers)); + if let Some(button) = button_from_mouse_event(&event) { + let pos = pos_from_mouse_event(&state.canvas, &event, state.channels.zoom_factor()); + let modifiers = modifiers_from_mouse_event(&event); + state.channels.send(egui::Event::PointerButton { + pos, + button, + pressed: true, + modifiers, + }); + + // In Safari we are only allowed to write to the clipboard during the + // event callback, which is why we run the app logic here and now: + //runner.logic(); - state.add_event_listener(&state.canvas, event_name, closure)?; + // Make sure we paint the output of the above logic call asap: + //state.channels.send_custom(WebRunnerEvent::Repaint); } - } + event.stop_propagation(); + // Note: prevent_default breaks VSCode tab focusing, hence why we don't call it here. + }) +} - state.add_event_listener( - &state.canvas, - "mousedown", - |event: web_sys::MouseEvent, state| { - let modifiers = modifiers_from_mouse_event(&event); - state - .channels - .send_custom(WebRunnerEvent::Modifiers(modifiers)); - if let Some(button) = button_from_mouse_event(&event) { - let pos = pos_from_mouse_event(&state.canvas, &event, state.channels.zoom_factor()); - let modifiers = modifiers_from_mouse_event(&event); - state.channels.send(egui::Event::PointerButton { - pos, - button, - pressed: true, - modifiers, - }); +/// Returns true if the cursor is above the canvas, or if we're dragging something. +/// Pass in the position in browser viewport coordinates (usually event.clientX/Y). +fn is_interested_in_pointer_event(state: &MainState, pos: egui::Pos2) -> bool { + let document = web_sys::window().unwrap().document().unwrap(); + let is_hovering_canvas = document + .element_from_point(pos.x, pos.y) + .is_some_and(|element| element.eq(&state.canvas)); + let is_pointer_down = state.channels.is_pointer_down(); - // In Safari we are only allowed to write to the clipboard during the - // event callback, which is why we run the app logic here and now: - //runner.logic(); + is_hovering_canvas || is_pointer_down +} - // Make sure we paint the output of the above logic call asap: - //runner.needs_repaint.repaint_asap(); - } - event.stop_propagation(); - // Note: prevent_default breaks VSCode tab focusing, hence why we don't call it here. - }, - )?; +fn install_mousemove(state: &MainState, target: &EventTarget) -> Result<(), JsValue> { + state.add_event_listener(target, "mousemove", |event: web_sys::MouseEvent, state| { + let modifiers = modifiers_from_mouse_event(&event); + state + .channels + .send_custom(WebRunnerEvent::Modifiers(modifiers)); - state.add_event_listener( - &state.canvas, - "mousemove", - |event: web_sys::MouseEvent, state| { - let modifiers = modifiers_from_mouse_event(&event); - state - .channels - .send_custom(WebRunnerEvent::Modifiers(modifiers)); - let pos = pos_from_mouse_event(&state.canvas, &event, state.channels.zoom_factor()); + let pos = pos_from_mouse_event(&state.canvas, &event, state.channels.zoom_factor()); + + if is_interested_in_pointer_event( + state, + egui::pos2(event.client_x() as f32, event.client_y() as f32), + ) { state.channels.send(egui::Event::PointerMoved(pos)); - //runner.needs_repaint.repaint_asap(); + //state.channels.send_custom(WebRunnerEvent::Repaint); event.stop_propagation(); event.prevent_default(); - }, - )?; + } + }) +} - state.add_event_listener( - &state.canvas, - "mouseup", - |event: web_sys::MouseEvent, state| { - let modifiers = modifiers_from_mouse_event(&event); - state - .channels - .send_custom(WebRunnerEvent::Modifiers(modifiers)); +fn install_mouseup(state: &MainState, target: &EventTarget) -> Result<(), JsValue> { + state.add_event_listener(target, "mouseup", |event: web_sys::MouseEvent, state| { + let modifiers = modifiers_from_mouse_event(&event); + state + .channels + .send_custom(WebRunnerEvent::Modifiers(modifiers)); + + let pos = pos_from_mouse_event(&state.canvas, &event, state.channels.zoom_factor()); + + if is_interested_in_pointer_event( + state, + egui::pos2(event.client_x() as f32, event.client_y() as f32), + ) { if let Some(button) = button_from_mouse_event(&event) { - let pos = pos_from_mouse_event(&state.canvas, &event, state.channels.zoom_factor()); state.channels.send(egui::Event::PointerButton { pos, button, @@ -499,120 +698,99 @@ pub(crate) fn install_canvas_events(state: &MainState) -> Result<(), JsValue> { modifiers, }); - // In Safari we are only allowed to write to the clipboard during the - // event callback, which is why we run the app logic here and now: + // In Safari we are only allowed to do certain things + // (like playing audio, start a download, etc) + // on user action, such as a click. + // So we need to run the app logic here and now: //runner.logic(); // Make sure we paint the output of the above logic call asap: - //runner.needs_repaint.repaint_asap(); + //state.channels.send_custom(WebRunnerEvent::Repaint); - text_agent::update_text_agent(state); + event.prevent_default(); + event.stop_propagation(); } - event.stop_propagation(); - event.prevent_default(); - }, - )?; + } + }) +} - state.add_event_listener( - &state.canvas, - "mouseleave", - |event: web_sys::MouseEvent, state| { - state.channels.send_custom(WebRunnerEvent::Save); +fn install_mouseleave(state: &MainState, target: &EventTarget) -> Result<(), JsValue> { + state.add_event_listener(target, "mouseleave", |event: web_sys::MouseEvent, state| { + state.channels.send_custom(WebRunnerEvent::Save); - state.channels.send(egui::Event::PointerGone); - //runner.needs_repaint.repaint_asap(); - event.stop_propagation(); - event.prevent_default(); - }, - )?; + state.channels.send(egui::Event::PointerGone); + //state.channels.send_custom(WebRunnerEvent::Repaint); + event.stop_propagation(); + event.prevent_default(); + }) +} - state.add_event_listener( - &state.canvas, - "touchstart", - |event: web_sys::TouchEvent, state| { - let mut inner = state.inner.borrow_mut(); - - inner.touch_pos = pos_from_touch_event( - &state.canvas, - &event, - &mut inner.touch_id, - state.channels.zoom_factor(), - ); - state - .channels - .send_custom(WebRunnerEvent::Touch(inner.touch_id, inner.touch_pos)); - let modifiers = modifiers_from_touch_event(&event); +fn install_touchstart(state: &MainState, target: &EventTarget) -> Result<(), JsValue> { + state.add_event_listener(target, "touchstart", |event: web_sys::TouchEvent, state| { + if let Some((pos, _)) = primary_touch_pos(state, &event, state.channels.zoom_factor()) { state.channels.send(egui::Event::PointerButton { - pos: inner.touch_pos, + pos, button: egui::PointerButton::Primary, pressed: true, - modifiers, + modifiers: modifiers_from_touch_event(&event), }); + } - push_touches(state, egui::TouchPhase::Start, &event); - //runner.needs_repaint.repaint_asap(); - event.stop_propagation(); - event.prevent_default(); - }, - )?; - - state.add_event_listener( - &state.canvas, - "touchmove", - |event: web_sys::TouchEvent, state| { - let mut inner = state.inner.borrow_mut(); - - inner.touch_pos = pos_from_touch_event( - &state.canvas, - &event, - &mut inner.touch_id, - state.channels.zoom_factor(), - ); - state - .channels - .send_custom(WebRunnerEvent::Touch(inner.touch_id, inner.touch_pos)); - state - .channels - .send(egui::Event::PointerMoved(inner.touch_pos)); + push_touches(state, egui::TouchPhase::Start, &event); + //state.channels.send_custom(WebRunnerEvent::Repaint); + event.stop_propagation(); + event.prevent_default(); + }) +} - push_touches(state, egui::TouchPhase::Move, &event); - //runner.needs_repaint.repaint_asap(); - event.stop_propagation(); - event.prevent_default(); - }, - )?; +fn install_touchmove(state: &MainState, target: &EventTarget) -> Result<(), JsValue> { + state.add_event_listener(target, "touchmove", |event: web_sys::TouchEvent, state| { + if let Some((pos, touch)) = primary_touch_pos(state, &event, state.channels.zoom_factor()) { + if is_interested_in_pointer_event( + state, + egui::pos2(touch.client_x() as f32, touch.client_y() as f32), + ) { + state.channels.send(egui::Event::PointerMoved(pos)); - state.add_event_listener( - &state.canvas, - "touchend", - |event: web_sys::TouchEvent, state| { - let inner = state.inner.borrow(); + push_touches(state, egui::TouchPhase::Move, &event); + //state.channels.send_custom(WebRunnerEvent::Repaint); + event.stop_propagation(); + event.prevent_default(); + } + } + }) +} - if inner.touch_id.is_some() { - let modifiers = modifiers_from_touch_event(&event); +fn install_touchend(state: &MainState, target: &EventTarget) -> Result<(), JsValue> { + state.add_event_listener(target, "touchend", |event: web_sys::TouchEvent, state| { + if let Some((pos, touch)) = primary_touch_pos(state, &event, state.channels.zoom_factor()) { + if is_interested_in_pointer_event( + state, + egui::pos2(touch.client_x() as f32, touch.client_y() as f32), + ) { // First release mouse to click: state.channels.send(egui::Event::PointerButton { - pos: inner.touch_pos, + pos, button: egui::PointerButton::Primary, pressed: false, - modifiers, + modifiers: modifiers_from_touch_event(&event), }); // Then remove hover effect: state.channels.send(egui::Event::PointerGone); push_touches(state, egui::TouchPhase::End, &event); - //runner.needs_repaint.repaint_asap(); - } - event.stop_propagation(); - event.prevent_default(); - // Finally, focus or blur text agent to toggle mobile keyboard: - text_agent::update_text_agent(state); - }, - )?; + //state.channels.send_custom(WebRunnerEvent::Repaint); + event.stop_propagation(); + event.prevent_default(); + } + } + }) +} +fn install_touchcancel(state: &MainState, target: &EventTarget) -> Result<(), JsValue> { state.add_event_listener( - &state.canvas, + target, "touchcancel", |event: web_sys::TouchEvent, state| { push_touches(state, egui::TouchPhase::Cancel, &event); @@ -621,89 +799,72 @@ pub(crate) fn install_canvas_events(state: &MainState) -> Result<(), JsValue> { }, )?; - state.add_event_listener( - &state.canvas, - "wheel", - |event: web_sys::WheelEvent, state| { - let unit = match event.delta_mode() { - web_sys::WheelEvent::DOM_DELTA_PIXEL => egui::MouseWheelUnit::Point, - web_sys::WheelEvent::DOM_DELTA_LINE => egui::MouseWheelUnit::Line, - web_sys::WheelEvent::DOM_DELTA_PAGE => egui::MouseWheelUnit::Page, - _ => return, - }; - // delta sign is flipped to match native (winit) convention. - let delta = -egui::vec2(event.delta_x() as f32, event.delta_y() as f32); - let modifiers = modifiers_from_wheel_event(&event); - - state.channels.send(egui::Event::MouseWheel { - unit, - delta, - modifiers, - }); + Ok(()) +} - let scroll_multiplier = match unit { - egui::MouseWheelUnit::Page => { - canvas_size_in_points(&state.canvas, state.channels.zoom_factor()).y - } - egui::MouseWheelUnit::Line => { - #[allow(clippy::let_and_return)] - let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in winit. - points_per_scroll_line - } - egui::MouseWheelUnit::Point => 1.0, - }; +fn install_wheel(state: &MainState, target: &EventTarget) -> Result<(), JsValue> { + state.add_event_listener(target, "wheel", |event: web_sys::WheelEvent, state| { + let unit = match event.delta_mode() { + web_sys::WheelEvent::DOM_DELTA_PIXEL => egui::MouseWheelUnit::Point, + web_sys::WheelEvent::DOM_DELTA_LINE => egui::MouseWheelUnit::Line, + web_sys::WheelEvent::DOM_DELTA_PAGE => egui::MouseWheelUnit::Page, + _ => return, + }; - let mut delta = scroll_multiplier * delta; + let delta = -egui::vec2(event.delta_x() as f32, event.delta_y() as f32); - // Report a zoom event in case CTRL (on Windows or Linux) or CMD (on Mac) is pressed. - // This if-statement is equivalent to how `Modifiers.command` is determined in - // `modifiers_from_event()`, but we cannot directly use that fn for a [`WheelEvent`]. - if event.ctrl_key() || event.meta_key() { - let factor = (delta.y / 200.0).exp(); - state.channels.send(egui::Event::Zoom(factor)); - } else { - if event.shift_key() { - // Treat as horizontal scrolling. - // Note: one Mac we already get horizontal scroll events when shift is down. - delta = egui::vec2(delta.x + delta.y, 0.0); - } + let modifiers = modifiers_from_wheel_event(&event); - state.channels.send(egui::Event::Scroll(delta)); - } + state + .channels + .send_custom(WebRunnerEvent::Wheel(unit, delta, modifiers)); - //runner.needs_repaint.repaint_asap(); - event.stop_propagation(); - event.prevent_default(); - }, - )?; - - /* Luminol's web filesystem can't read files from egui's file drag and drop system + //state.channels.send_custom(WebRunnerEvent::Repaint); + event.stop_propagation(); + event.prevent_default(); + }) +} - runner_ref.add_event_listener(&canvas, "dragover", |event: web_sys::DragEvent, runner| { +fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { + runner_ref.add_event_listener(target, "dragover", |event: web_sys::DragEvent, runner| { if let Some(data_transfer) = event.data_transfer() { runner.input.raw.hovered_files.clear(); - for i in 0..data_transfer.items().length() { - if let Some(item) = data_transfer.items().get(i) { + + // NOTE: data_transfer.files() is always empty in dragover + + let items = data_transfer.items(); + for i in 0..items.length() { + if let Some(item) = items.get(i) { runner.input.raw.hovered_files.push(egui::HoveredFile { mime: item.type_(), ..Default::default() }); } } + + if runner.input.raw.hovered_files.is_empty() { + // Fallback: just preview anything. Needed on Desktop Safari. + runner + .input + .raw + .hovered_files + .push(egui::HoveredFile::default()); + } + runner.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); } })?; - runner_ref.add_event_listener(&canvas, "dragleave", |event: web_sys::DragEvent, runner| { + runner_ref.add_event_listener(target, "dragleave", |event: web_sys::DragEvent, runner| { runner.input.raw.hovered_files.clear(); runner.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); })?; - runner_ref.add_event_listener(&canvas, "drop", { + runner_ref.add_event_listener(target, "drop", { let runner_ref = runner_ref.clone(); move |event: web_sys::DragEvent, runner| { @@ -759,41 +920,94 @@ pub(crate) fn install_canvas_events(state: &MainState) -> Result<(), JsValue> { } })?; - */ + Ok(()) +} - { - // The canvas automatically resizes itself whenever a frame is drawn. - // The resizing does not take window.devicePixelRatio into account, - // so this mutation observer is to detect canvas resizes and correct them. - let window = window.clone(); - let callback: Closure = Closure::new(move |mutations: js_sys::Array| { - if PANIC_LOCK.get().is_some() { - return; - } - let width = window.inner_width().unwrap().as_f64().unwrap() as u32; - let height = window.inner_height().unwrap().as_f64().unwrap() as u32; - mutations.for_each(&mut |mutation, _, _| { - let mutation = mutation.unchecked_into::(); - if mutation.type_().as_str() == "attributes" { - let canvas = mutation - .target() - .unwrap() - .unchecked_into::(); - if canvas.width() != width || canvas.height() != height { - let _ = canvas.set_attribute("width", width.to_string().as_str()); - let _ = canvas.set_attribute("height", height.to_string().as_str()); +/// Install a `ResizeObserver` to observe changes to the size of the canvas. +/// +/// This is the only way to ensure a canvas size change without an associated window `resize` event +/// actually results in a resize of the canvas. +/// +/// The resize observer is called the by the browser at `observe` time, instead of just on the first actual resize. +/// We use that to trigger the first `request_animation_frame` _after_ updating the size of the canvas to the correct dimensions, +/// to avoid [#4622](https://github.com/emilk/egui/issues/4622). +pub(crate) fn install_resize_observer(runner_ref: &WebRunner) -> Result<(), JsValue> { + let closure = Closure::wrap(Box::new({ + let runner_ref = runner_ref.clone(); + move |entries: js_sys::Array| { + // Only call the wrapped closure if the egui code has not panicked + if let Some(mut runner_lock) = runner_ref.try_lock() { + let canvas = runner_lock.canvas(); + let (width, height) = match get_display_size(&entries) { + Ok(v) => v, + Err(err) => { + log::error!("{}", super::string_from_js_value(&err)); + return; } - } - }); - }); - let observer = web_sys::MutationObserver::new(callback.as_ref().unchecked_ref())?; - let mut options = web_sys::MutationObserverInit::new(); - options.attributes(true); - observer.observe_with_options(&state.canvas, &options)?; - // We don't need to unregister this mutation observer on panic because it auto-deregisters - // when the target (the canvas) is removed from the DOM and garbage-collected - callback.forget(); + }; + canvas.set_width(width); + canvas.set_height(height); + + // force an immediate repaint + runner_lock.needs_repaint.repaint_asap(); + paint_if_needed(&mut runner_lock); + drop(runner_lock); + // we rely on the resize observer to trigger the first `request_animation_frame`: + if let Err(err) = runner_ref.request_animation_frame() { + log::error!("{}", super::string_from_js_value(&err)); + }; + } + } + }) as Box); + + let observer = web_sys::ResizeObserver::new(closure.as_ref().unchecked_ref())?; + let mut options = web_sys::ResizeObserverOptions::new(); + options.box_(web_sys::ResizeObserverBoxOptions::ContentBox); + if let Some(runner_lock) = runner_ref.try_lock() { + observer.observe_with_options(&runner_lock.canvas().clone().unchecked_into(), &options); + drop(runner_lock); + runner_ref.set_resize_observer(observer, closure); } Ok(()) } + +// Code ported to Rust from: +// https://webglfundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html +fn get_display_size(resize_observer_entries: &js_sys::Array) -> Result<(u32, u32), JsValue> { + let width; + let height; + let mut dpr = web_sys::window().unwrap().device_pixel_ratio(); + + let entry: web_sys::ResizeObserverEntry = resize_observer_entries.at(0).dyn_into()?; + if JsValue::from_str("devicePixelContentBoxSize").js_in(entry.as_ref()) { + // NOTE: Only this path gives the correct answer for most browsers. + // Unfortunately this doesn't work perfectly everywhere. + let size: web_sys::ResizeObserverSize = + entry.device_pixel_content_box_size().at(0).dyn_into()?; + width = size.inline_size(); + height = size.block_size(); + dpr = 1.0; // no need to apply + } else if JsValue::from_str("contentBoxSize").js_in(entry.as_ref()) { + let content_box_size = entry.content_box_size(); + let idx0 = content_box_size.at(0); + if !idx0.is_undefined() { + let size: web_sys::ResizeObserverSize = idx0.dyn_into()?; + width = size.inline_size(); + height = size.block_size(); + } else { + // legacy + let size = JsValue::clone(content_box_size.as_ref()); + let size: web_sys::ResizeObserverSize = size.dyn_into()?; + width = size.inline_size(); + height = size.block_size(); + } + } else { + // legacy + let content_rect = entry.content_rect(); + width = content_rect.width(); + height = content_rect.height(); + } + + Ok(((width.round() * dpr) as u32, (height.round() * dpr) as u32)) +} diff --git a/crates/eframe/src/web/input.rs b/crates/eframe/src/web/input.rs index f3bc9743..8a015c12 100644 --- a/crates/eframe/src/web/input.rs +++ b/crates/eframe/src/web/input.rs @@ -1,14 +1,14 @@ -use super::{canvas_origin, AppRunner}; +use super::{canvas_content_rect, AppRunner, WebRunnerEvent}; pub fn pos_from_mouse_event( canvas: &web_sys::HtmlCanvasElement, event: &web_sys::MouseEvent, zoom_factor: f32, ) -> egui::Pos2 { - let rect = canvas.get_bounding_client_rect(); + let rect = canvas_content_rect(canvas); egui::Pos2 { - x: (event.client_x() as f32 - rect.left() as f32) / zoom_factor, - y: (event.client_y() as f32 - rect.top() as f32) / zoom_factor, + x: (event.client_x() as f32 - rect.left()) / zoom_factor, + y: (event.client_y() as f32 - rect.top()) / zoom_factor, } } @@ -26,43 +26,59 @@ pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option, zoom_factor: f32, -) -> egui::Pos2 { - let touch_for_pos = if let Some(touch_id_for_pos) = touch_id_for_pos { - // search for the touch we previously used for the position - // (unfortunately, `event.touches()` is not a rust collection): - (0..event.touches().length()) - .map(|i| event.touches().get(i).unwrap()) - .find(|touch| egui::TouchId::from(touch.identifier()) == *touch_id_for_pos) - } else { - None - }; - // Use the touch found above or pick the first, or return a default position if there is no - // touch at all. (The latter is not expected as the current method is only called when there is - // at least one touch.) - touch_for_pos - .or_else(|| event.touches().get(0)) - .map_or(Default::default(), |touch| { - *touch_id_for_pos = Some(egui::TouchId::from(touch.identifier())); - pos_from_touch(canvas_origin(canvas), &touch, zoom_factor) - }) +) -> Option<(egui::Pos2, web_sys::Touch)> { + let all_touches: Vec<_> = (0..event.touches().length()) + .filter_map(|i| event.touches().get(i)) + // On touchend we don't get anything in `touches`, but we still get `changed_touches`, so include those: + .chain((0..event.changed_touches().length()).filter_map(|i| event.changed_touches().get(i))) + .collect(); + + let mut inner = state.inner.borrow_mut(); + + if let Some(primary_touch) = inner.touch_id { + // Is the primary touch is gone? + if !all_touches + .iter() + .any(|touch| primary_touch == egui::TouchId::from(touch.identifier())) + { + inner.touch_id = None; + state + .channels + .send_custom(WebRunnerEvent::Touch(inner.touch_id)); + } + } + + if inner.touch_id.is_none() { + inner.touch_id = all_touches + .first() + .map(|touch| egui::TouchId::from(touch.identifier())); + state + .channels + .send_custom(WebRunnerEvent::Touch(inner.touch_id)); + } + + let primary_touch = inner.touch_id; + + if let Some(primary_touch) = primary_touch { + for touch in all_touches { + if primary_touch == egui::TouchId::from(touch.identifier()) { + let canvas_rect = canvas_content_rect(&state.canvas); + return Some((pos_from_touch(canvas_rect, &touch, zoom_factor), touch)); + } + } + } + + None } -fn pos_from_touch( - canvas_origin: egui::Pos2, - touch: &web_sys::Touch, - zoom_factor: f32, -) -> egui::Pos2 { +fn pos_from_touch(canvas_rect: egui::Rect, touch: &web_sys::Touch, zoom_factor: f32) -> egui::Pos2 { egui::Pos2 { - x: (touch.page_x() as f32 - canvas_origin.x) / zoom_factor, - y: (touch.page_y() as f32 - canvas_origin.y) / zoom_factor, + x: (touch.client_x() as f32 - canvas_rect.left()) / zoom_factor, + y: (touch.client_y() as f32 - canvas_rect.top()) / zoom_factor, } } @@ -71,54 +87,64 @@ pub fn push_touches( phase: egui::TouchPhase, event: &web_sys::TouchEvent, ) { - let canvas_origin = canvas_origin(&state.canvas); + let canvas_rect = canvas_content_rect(&state.canvas); for touch_idx in 0..event.changed_touches().length() { if let Some(touch) = event.changed_touches().item(touch_idx) { state.channels.send(egui::Event::Touch { device_id: egui::TouchDeviceId(0), id: egui::TouchId::from(touch.identifier()), phase, - pos: pos_from_touch(canvas_origin, &touch, state.channels.zoom_factor()), + pos: pos_from_touch(canvas_rect, &touch, state.channels.zoom_factor()), force: Some(touch.force()), }); } } } -/// Web sends all keys as strings, so it is up to us to figure out if it is -/// a real text input or the name of a key. -pub fn should_ignore_key(key: &str) -> bool { +/// The text input from a keyboard event (e.g. `X` when pressing the `X` key). +pub fn text_from_keyboard_event(event: &web_sys::KeyboardEvent) -> Option { + let key = event.key(); + let is_function_key = key.starts_with('F') && key.len() > 1; - is_function_key - || matches!( - key, - "Alt" - | "ArrowDown" - | "ArrowLeft" - | "ArrowRight" - | "ArrowUp" - | "Backspace" - | "CapsLock" - | "ContextMenu" - | "Control" - | "Delete" - | "End" - | "Enter" - | "Esc" - | "Escape" - | "GroupNext" // https://github.com/emilk/egui/issues/510 - | "Help" - | "Home" - | "Insert" - | "Meta" - | "NumLock" - | "PageDown" - | "PageUp" - | "Pause" - | "ScrollLock" - | "Shift" - | "Tab" - ) + if is_function_key { + return None; + } + + let is_control_key = matches!( + key.as_str(), + "Alt" + | "ArrowDown" + | "ArrowLeft" + | "ArrowRight" + | "ArrowUp" + | "Backspace" + | "CapsLock" + | "ContextMenu" + | "Control" + | "Delete" + | "End" + | "Enter" + | "Esc" + | "Escape" + | "GroupNext" // https://github.com/emilk/egui/issues/510 + | "Help" + | "Home" + | "Insert" + | "Meta" + | "NumLock" + | "PageDown" + | "PageUp" + | "Pause" + | "ScrollLock" + | "Shift" + | "Tab" + ); + + if is_control_key { + return None; + } + + Some(key) } /// Web sends all keys as strings, so it is up to us to figure out if it is diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index 9946639a..86f7f8d6 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -40,7 +40,6 @@ pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu; pub use backend::*; -use egui::Vec2; use wasm_bindgen::prelude::*; use web_sys::MediaQueryList; @@ -54,6 +53,29 @@ pub(crate) fn string_from_js_value(value: &JsValue) -> String { value.as_string().unwrap_or_else(|| format!("{value:#?}")) } +/// Returns the `Element` with active focus. +/// +/// Elements can only be focused if they are: +/// - ``/`` with an `href` attribute +/// - ``/`