diff --git a/.github/workflows/cargo_machete.yml b/.github/workflows/cargo_machete.yml index 7e5747800..1dc162e56 100644 --- a/.github/workflows/cargo_machete.yml +++ b/.github/workflows/cargo_machete.yml @@ -9,7 +9,7 @@ jobs: steps: - uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.88 + toolchain: 1.92 - name: Machete install ## The official cargo-machete action uses: bnjbvr/cargo-machete@v0.9.1 diff --git a/.github/workflows/deploy_web_demo.yml b/.github/workflows/deploy_web_demo.yml index 21cfb159e..eb60d14fa 100644 --- a/.github/workflows/deploy_web_demo.yml +++ b/.github/workflows/deploy_web_demo.yml @@ -38,7 +38,7 @@ jobs: with: profile: minimal target: wasm32-unknown-unknown - toolchain: 1.88.0 + toolchain: 1.92.0 override: true - uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/spelling_and_links.yml b/.github/workflows/link_checker.yml similarity index 62% rename from .github/workflows/spelling_and_links.yml rename to .github/workflows/link_checker.yml index e09e01562..baf2aa402 100644 --- a/.github/workflows/spelling_and_links.yml +++ b/.github/workflows/link_checker.yml @@ -1,21 +1,8 @@ -name: Check spelling and links -on: [pull_request] +name: Link checker +# on: [pull_request] # Disabled because it is so broken +on: workflow_dispatch jobs: - typos: - # https://github.com/crate-ci/typos - # Add exceptions to .typos.toml - # install and run locally: cargo install typos-cli && typos - name: typos - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Checkout Actions Repository - uses: actions/checkout@v4 - - - name: Check spelling of entire workspace - uses: crate-ci/typos@v1.38.0 - lychee: name: lychee runs-on: ubuntu-latest @@ -33,4 +20,3 @@ jobs: uses: lycheeverse/lychee-action@v2 with: args: "'**/*.md' '**/*.toml' --exclude localhost --exclude reddit.com" # I guess reddit doesn't like github action IPs - diff --git a/.github/workflows/preview_build.yml b/.github/workflows/preview_build.yml index fe37eb8cb..22ea08cba 100644 --- a/.github/workflows/preview_build.yml +++ b/.github/workflows/preview_build.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.88.0 + toolchain: 1.92.0 targets: wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 with: diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f71588545..9018d251b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -19,7 +19,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.88.0 + toolchain: 1.92.0 - name: Install packages (Linux) if: runner.os == 'Linux' @@ -74,7 +74,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.88.0 + toolchain: 1.92.0 targets: wasm32-unknown-unknown - run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev @@ -148,7 +148,7 @@ jobs: - uses: actions/checkout@v4 - uses: EmbarkStudios/cargo-deny-action@v2 with: - rust-version: "1.88.0" + rust-version: "1.92.0" log-level: error command: check arguments: --target ${{ matrix.target }} @@ -164,7 +164,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.88.0 + toolchain: 1.92.0 targets: aarch64-linux-android - name: Set up cargo cache @@ -186,7 +186,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.88.0 + toolchain: 1.92.0 targets: aarch64-apple-ios - name: Set up cargo cache @@ -206,7 +206,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.88.0 + toolchain: 1.92.0 - name: Set up cargo cache uses: Swatinem/rust-cache@v2 @@ -228,7 +228,7 @@ jobs: lfs: true - uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.88.0 + toolchain: 1.92.0 - name: Set up cargo cache uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml new file mode 100644 index 000000000..19205d51e --- /dev/null +++ b/.github/workflows/typos.yml @@ -0,0 +1,17 @@ +name: Typos +on: [pull_request] + +jobs: + typos: + # https://github.com/crate-ci/typos + # Add exceptions to .typos.toml + # install and run locally: cargo install typos-cli && typos + name: typos + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout Actions Repository + uses: actions/checkout@v4 + + - name: Check spelling of entire workspace + uses: crate-ci/typos@v1.38.0 diff --git a/.typos.toml b/.typos.toml index 3ae860ad9..16659f4c7 100644 --- a/.typos.toml +++ b/.typos.toml @@ -5,8 +5,8 @@ [default.extend-words] ime = "ime" # Input Method Editor nknown = "nknown" # part of @55nknown username -isse = "isse" # part of @IsseW username -tye = "tye" # part of @tye-exe username +isse = "isse" # part of @IsseW username +tye = "tye" # part of @tye-exe username ro = "ro" # read-only, also part of the username @Phen-Ro typ = "typ" # Often used because `type` is a keyword in Rust diff --git a/CHANGELOG.md b/CHANGELOG.md index 856fe09da..12010e287 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ 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.33.3 - 2025-12-11 +* Treat `.` as a word-splitter in text navigation [#7741](https://github.com/emilk/egui/pull/7741) by [@emilk](https://github.com/emilk) +* Change text color of selected text [#7691](https://github.com/emilk/egui/pull/7691) by [@emilk](https://github.com/emilk) + + ## 0.33.2 - 2025-11-13 ### ⭐ Added * Add `Plugin::on_widget_under_pointer` to support widget inspector [#7652](https://github.com/emilk/egui/pull/7652) by [@juancampa](https://github.com/juancampa) diff --git a/Cargo.lock b/Cargo.lock index 61bf459b5..a99c0d281 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -863,6 +863,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "color" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18ef4657441fb193b65f34dc39b3781f0dfec23d3bd94d0eeb4e88cde421edb" +dependencies = [ + "bytemuck", +] + [[package]] name = "color-hex" version = "0.2.0" @@ -1248,7 +1257,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" [[package]] name = "ecolor" -version = "0.33.2" +version = "0.33.3" dependencies = [ "bytemuck", "cint", @@ -1260,7 +1269,7 @@ dependencies = [ [[package]] name = "eframe" -version = "0.33.2" +version = "0.33.3" dependencies = [ "ahash", "bytemuck", @@ -1299,7 +1308,7 @@ dependencies = [ [[package]] name = "egui" -version = "0.33.2" +version = "0.33.3" dependencies = [ "accesskit", "ahash", @@ -1319,7 +1328,7 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.33.2" +version = "0.33.3" dependencies = [ "ahash", "bytemuck", @@ -1337,7 +1346,7 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.33.2" +version = "0.33.3" dependencies = [ "accesskit_winit", "arboard", @@ -1360,7 +1369,7 @@ dependencies = [ [[package]] name = "egui_demo_app" -version = "0.33.2" +version = "0.33.3" dependencies = [ "accesskit", "accesskit_consumer", @@ -1390,7 +1399,7 @@ dependencies = [ [[package]] name = "egui_demo_lib" -version = "0.33.2" +version = "0.33.3" dependencies = [ "chrono", "criterion", @@ -1407,7 +1416,7 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.33.2" +version = "0.33.3" dependencies = [ "ahash", "chrono", @@ -1426,7 +1435,7 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.33.2" +version = "0.33.3" dependencies = [ "bytemuck", "document-features", @@ -1445,7 +1454,7 @@ dependencies = [ [[package]] name = "egui_kittest" -version = "0.33.2" +version = "0.33.3" dependencies = [ "dify", "document-features", @@ -1457,13 +1466,15 @@ dependencies = [ "kittest", "open", "pollster", + "serde", "tempfile", + "toml", "wgpu", ] [[package]] name = "egui_tests" -version = "0.33.2" +version = "0.33.3" dependencies = [ "egui", "egui_extras", @@ -1473,9 +1484,9 @@ dependencies = [ [[package]] name = "ehttp" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a81c221a1e4dad06cb9c9deb19aea1193a5eea084e8cd42d869068132bf876" +checksum = "04499d3c719edecfad5c9b46031726c8540905d73be6d7e4f9788c4a298da908" dependencies = [ "document-features", "js-sys", @@ -1493,7 +1504,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "emath" -version = "0.33.2" +version = "0.33.3" dependencies = [ "bytemuck", "document-features", @@ -1591,9 +1602,8 @@ dependencies = [ [[package]] name = "epaint" -version = "0.33.2" +version = "0.33.3" dependencies = [ - "ab_glyph", "ahash", "bytemuck", "criterion", @@ -1607,13 +1617,16 @@ dependencies = [ "parking_lot", "profiling", "rayon", + "self_cell", "serde", "similar-asserts", + "skrifa", + "vello_cpu", ] [[package]] name = "epaint_default_fonts" -version = "0.33.2" +version = "0.33.3" [[package]] name = "equivalent" @@ -1637,6 +1650,15 @@ version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" +[[package]] +name = "euclid" +version = "0.22.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +dependencies = [ + "num-traits", +] + [[package]] name = "event-listener" version = "5.3.1" @@ -1704,6 +1726,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fearless_simd" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb2907d1f08b2b316b9223ced5b0e89d87028ba8deae9764741dba8ff7f3903" +dependencies = [ + "bytemuck", +] + [[package]] name = "file_dialog" version = "0.1.0" @@ -1747,6 +1778,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "font-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5" +dependencies = [ + "bytemuck", +] + [[package]] name = "fontconfig-parser" version = "0.5.7" @@ -2526,6 +2566,17 @@ dependencies = [ "smallvec", ] +[[package]] +name = "kurbo" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce9729cc38c18d86123ab736fd2e7151763ba226ac2490ec092d1dd148825e32" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2575,6 +2626,12 @@ dependencies = [ "redox_syscall 0.5.7", ] +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -3251,6 +3308,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "peniko" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3c76095c9a636173600478e0373218c7b955335048c2bcd12dc6a79657649d8" +dependencies = [ + "bytemuck", + "color", + "kurbo 0.12.0", + "linebender_resource_handle", + "smallvec", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -3381,9 +3451,9 @@ dependencies = [ [[package]] name = "png" -version = "0.17.14" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -3425,7 +3495,7 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" [[package]] name = "popups" -version = "0.33.2" +version = "0.33.3" dependencies = [ "eframe", "env_logger", @@ -3690,6 +3760,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "read-fonts" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" +dependencies = [ + "bytemuck", + "font-types", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -3983,6 +4063,12 @@ dependencies = [ "tiny-skia", ] +[[package]] +name = "self_cell" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" + [[package]] name = "serde" version = "1.0.228" @@ -4036,6 +4122,15 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serial_windows" version = "0.1.0" @@ -4101,6 +4196,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "skrifa" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" +dependencies = [ + "bytemuck", + "read-fonts", +] + [[package]] name = "slab" version = "0.4.9" @@ -4222,7 +4327,7 @@ version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" dependencies = [ - "kurbo", + "kurbo 0.11.1", "siphasher", ] @@ -4491,11 +4596,26 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -4504,6 +4624,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] @@ -4707,7 +4829,7 @@ dependencies = [ "flate2", "fontdb", "imagesize", - "kurbo", + "kurbo 0.11.1", "log", "pico-args", "roxmltree", @@ -4735,6 +4857,30 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "vello_common" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a235ba928b3109ad9e7696270edb09445a52ae1c7c08e6d31a19b1cdd6cbc24a" +dependencies = [ + "bytemuck", + "fearless_simd", + "log", + "peniko", + "skrifa", + "smallvec", +] + +[[package]] +name = "vello_cpu" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0bd1fcf9c1814f17a491e07113623d44e3ec1125a9f3401f5e047d6d326da21" +dependencies = [ + "bytemuck", + "vello_common", +] + [[package]] name = "version_check" version = "0.9.5" @@ -5645,9 +5791,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.3" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] @@ -5748,7 +5894,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xtask" -version = "0.33.2" +version = "0.33.3" [[package]] name = "yaml-rust" diff --git a/Cargo.toml b/Cargo.toml index b33ca445c..470644bb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,8 +23,8 @@ members = [ [workspace.package] edition = "2024" license = "MIT OR Apache-2.0" -rust-version = "1.88" -version = "0.33.2" +rust-version = "1.92" +version = "0.33.3" [profile.release] @@ -55,23 +55,22 @@ opt-level = 2 [workspace.dependencies] -emath = { version = "0.33.2", path = "crates/emath", default-features = false } -ecolor = { version = "0.33.2", path = "crates/ecolor", default-features = false } -epaint = { version = "0.33.2", path = "crates/epaint", default-features = false } -epaint_default_fonts = { version = "0.33.2", path = "crates/epaint_default_fonts" } -egui = { version = "0.33.2", path = "crates/egui", default-features = false } -egui-winit = { version = "0.33.2", path = "crates/egui-winit", default-features = false } -egui_extras = { version = "0.33.2", path = "crates/egui_extras", default-features = false } -egui-wgpu = { version = "0.33.2", path = "crates/egui-wgpu", default-features = false } -egui_demo_lib = { version = "0.33.2", path = "crates/egui_demo_lib", default-features = false } -egui_glow = { version = "0.33.2", path = "crates/egui_glow", default-features = false } -egui_kittest = { version = "0.33.2", path = "crates/egui_kittest", default-features = false } -eframe = { version = "0.33.2", path = "crates/eframe", default-features = false } +emath = { version = "0.33.3", path = "crates/emath", default-features = false } +ecolor = { version = "0.33.3", path = "crates/ecolor", default-features = false } +epaint = { version = "0.33.3", path = "crates/epaint", default-features = false } +epaint_default_fonts = { version = "0.33.3", path = "crates/epaint_default_fonts" } +egui = { version = "0.33.3", path = "crates/egui", default-features = false } +egui-winit = { version = "0.33.3", path = "crates/egui-winit", default-features = false } +egui_extras = { version = "0.33.3", path = "crates/egui_extras", default-features = false } +egui-wgpu = { version = "0.33.3", path = "crates/egui-wgpu", default-features = false } +egui_demo_lib = { version = "0.33.3", path = "crates/egui_demo_lib", default-features = false } +egui_glow = { version = "0.33.3", path = "crates/egui_glow", default-features = false } +egui_kittest = { version = "0.33.3", path = "crates/egui_kittest", default-features = false } +eframe = { version = "0.33.3", path = "crates/eframe", default-features = false } accesskit = "0.21.1" accesskit_consumer = "0.30.1" accesskit_winit = "0.29.1" -ab_glyph = "0.2.32" ahash = { version = "0.8.12", default-features = false, features = [ "no-rng", # we don't need DOS-protection, so we let users opt-in to it instead "std", @@ -88,7 +87,7 @@ criterion = { version = "0.7.0", default-features = false } dify = { version = "0.7.4", default-features = false } directories = "6.0.0" document-features = "0.2.11" -ehttp = { version = "0.5.0", default-features = false } +ehttp = { version = "0.6.0", default-features = false } enum-map = "2.7.3" env_logger = { version = "0.11.8", default-features = false } glow = "0.16.0" @@ -122,8 +121,10 @@ rayon = "1.11.0" resvg = { version = "0.45.1", default-features = false } rfd = "0.15.4" ron = "0.11.0" +self_cell = "1.2.1" serde = { version = "1.0.228", features = ["derive"] } similar-asserts = "1.7.0" +skrifa = { version = "0.37.0", default-features = false, features = ["std", "autohint_shaping"] } smallvec = "1.15.1" smithay-clipboard = "0.7.2" static_assertions = "1.1.0" @@ -131,9 +132,11 @@ syntect = { version = "5.3.0", default-features = false } tempfile = "3.23.0" thiserror = "2.0.17" tokio = "1.47.1" +toml = "0.8" type-map = "0.5.1" unicode_names2 = { version = "2.0.0", default-features = false } unicode-segmentation = "1.12.0" +vello_cpu = { version = "0.0.4", default-features = false, features = ["std"] } wasm-bindgen = "0.2.100" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml wasm-bindgen-futures = "0.4.0" wayland-cursor = { version = "0.31.11", default-features = false } @@ -181,7 +184,9 @@ branches_sharing_code = "warn" char_lit_as_u8 = "warn" checked_conversions = "warn" clear_with_drain = "warn" +clone_on_ref_ptr = "warn" cloned_instead_of_copied = "warn" +coerce_container_to_any = "warn" dbg_macro = "warn" debug_assert_with_mut_call = "warn" default_union_representation = "warn" @@ -191,7 +196,9 @@ disallowed_methods = "warn" # See clippy.toml disallowed_names = "warn" # See clippy.toml disallowed_script_idents = "warn" # See clippy.toml disallowed_types = "warn" # See clippy.toml +doc_broken_link = "warn" doc_comment_double_space_linebreaks = "warn" +doc_include_without_cfg = "warn" doc_link_with_quotes = "warn" doc_markdown = "warn" elidable_lifetime_names = "warn" @@ -224,6 +231,7 @@ inefficient_to_string = "warn" infinite_loop = "warn" into_iter_without_iter = "warn" invalid_upcast_comparisons = "warn" +ip_constant = "warn" iter_filter_is_ok = "warn" iter_filter_is_some = "warn" iter_not_returning_iterator = "warn" @@ -277,6 +285,7 @@ non_zero_suggestions = "warn" nonstandard_macro_braces = "warn" option_as_ref_cloned = "warn" option_option = "warn" +or_fun_call = "warn" path_buf_push_overwrite = "warn" pathbuf_init_then_push = "warn" precedence_bits = "warn" @@ -295,6 +304,7 @@ ref_patterns = "warn" rest_pat_in_fully_bound_structs = "warn" return_and_then = "warn" same_functions_in_if_condition = "warn" +self_only_used_in_recursion = "warn" semicolon_if_nothing_returned = "warn" set_contains_or_insert = "warn" single_char_pattern = "warn" @@ -306,7 +316,6 @@ string_add = "warn" string_add_assign = "warn" string_lit_as_bytes = "warn" string_lit_chars_any = "warn" -string_to_string = "warn" suspicious_command_arg_space = "warn" suspicious_xor_used_as_pow = "warn" todo = "warn" @@ -315,7 +324,7 @@ trailing_empty_array = "warn" trait_duplication_in_bounds = "warn" transmute_ptr_to_ptr = "warn" tuple_array_conversions = "warn" -unchecked_duration_subtraction = "warn" +unchecked_time_subtraction = "warn" undocumented_unsafe_blocks = "warn" unimplemented = "warn" uninhabited_references = "warn" @@ -335,6 +344,7 @@ unused_peekable = "warn" unused_rounding = "warn" unused_self = "warn" unused_trait_names = "warn" +unwrap_used = "warn" use_self = "warn" useless_let_if_seq = "warn" useless_transmute = "warn" @@ -344,10 +354,10 @@ zero_sized_map_values = "warn" # TODO(emilk): maybe enable more of these lints? +cast_possible_wrap = "allow" comparison_chain = "allow" should_panic_without_expect = "allow" too_many_lines = "allow" -unwrap_used = "allow" # TODO(emilk): We really wanna warn on this one # These are meh: assigning_clones = "allow" # No please diff --git a/clippy.toml b/clippy.toml index 6602fbc6c..a57ed66fe 100644 --- a/clippy.toml +++ b/clippy.toml @@ -3,7 +3,7 @@ # ----------------------------------------------------------------------------- # Section identical to scripts/clippy_wasm/clippy.toml: -msrv = "1.88" +msrv = "1.92" allow-unwrap-in-tests = true @@ -39,9 +39,9 @@ disallowed-methods = [ # but we cannot disable them all here (because of e.g. https://github.com/rust-lang/rust-clippy/issues/10406) # so we do that in `clipppy_wasm.toml` instead. - { path = "std::env::temp_dir", readon = "Use the tempfile crate instead" }, + { path = "std::env::temp_dir", reason = "Use the tempfile crate instead" }, { path = "std::panic::catch_unwind", reason = "We compile with `panic = abort" }, - { path = "std::thread::spawn", readon = "Use `std::thread::Builder` and name the thread" }, + { path = "std::thread::spawn", reason = "Use `std::thread::Builder` and name the thread" }, ] # https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_names diff --git a/crates/ecolor/CHANGELOG.md b/crates/ecolor/CHANGELOG.md index 4dee81296..a4fe1f3de 100644 --- a/crates/ecolor/CHANGELOG.md +++ b/crates/ecolor/CHANGELOG.md @@ -6,6 +6,10 @@ 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.33.3 - 2025-12-11 +Nothing new + + ## 0.33.2 - 2025-11-13 Nothing new diff --git a/crates/ecolor/src/hsva.rs b/crates/ecolor/src/hsva.rs index 06adb121f..17008f5da 100644 --- a/crates/ecolor/src/hsva.rs +++ b/crates/ecolor/src/hsva.rs @@ -41,7 +41,7 @@ impl Hsva { /// From linear RGBA with premultiplied alpha #[inline] pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { - #![allow(clippy::many_single_char_names)] + #![expect(clippy::many_single_char_names)] if a <= 0.0 { if r == 0.0 && b == 0.0 && a == 0.0 { Self::default() @@ -57,7 +57,7 @@ impl Hsva { /// From linear RGBA without premultiplied alpha #[inline] pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { - #![allow(clippy::many_single_char_names)] + #![expect(clippy::many_single_char_names)] let (h, s, v) = hsv_from_rgb([r, g, b]); Self { h, s, v, a } } @@ -189,7 +189,7 @@ impl From for Hsva { /// All ranges in 0-1, rgb is linear. #[inline] pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) { - #![allow(clippy::many_single_char_names)] + #![expect(clippy::many_single_char_names)] let min = r.min(g.min(b)); let max = r.max(g.max(b)); // value @@ -213,7 +213,7 @@ pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) { /// All ranges in 0-1, rgb is linear. #[inline] pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] { - #![allow(clippy::many_single_char_names)] + #![expect(clippy::many_single_char_names)] let h = (h.fract() + 1.0).fract(); // wrap let s = s.clamp(0.0, 1.0); diff --git a/crates/ecolor/src/lib.rs b/crates/ecolor/src/lib.rs index f4cdf4d77..ea7cff6f7 100644 --- a/crates/ecolor/src/lib.rs +++ b/crates/ecolor/src/lib.rs @@ -19,7 +19,7 @@ #![cfg_attr(feature = "document-features", doc = document_features::document_features!())] //! -#![allow(clippy::wrong_self_convention)] +#![expect(clippy::wrong_self_convention)] #[cfg(feature = "cint")] mod cint_impl; diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index 11f1c8fc9..74a705251 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -7,6 +7,10 @@ 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.33.3 - 2025-12-11 +Nothing new + + ## 0.33.2 - 2025-11-13 * Fix jittering during window resize on MacOS for WGPU/Metal [#7641](https://github.com/emilk/egui/pull/7641) by [@aspcartman](https://github.com/aspcartman) * Make sure `native_pixels_per_point` is set during app creation [#7683](https://github.com/emilk/egui/pull/7683) by [@emilk](https://github.com/emilk) diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 6924633f1..86f63c50e 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -251,7 +251,10 @@ web-sys = { workspace = true, features = [ ] } # optional web: -egui-wgpu = { workspace = true, optional = true, features = ["capture"] } # if wgpu is used, use it without (!) winit +egui-wgpu = { workspace = true, optional = true, features = [ + # if wgpu is used, use it without (!) winit: + "capture", +] } wgpu = { workspace = true, optional = true } # Native dev dependencies for testing diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index a7bcfd6ef..c37dc1cf6 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -135,6 +135,31 @@ impl CreationContext<'_> { /// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe). pub trait App { + /// Called once before each call to [`Self::ui`], + /// and additionally also called when the UI is hidden, but [`egui::Context::request_repaint`] was called. + /// + /// You may NOT show any ui or do any painting during the call to [`Self::logic`]. + /// + /// The [`egui::Context`] can be cloned and saved if you like. + /// + /// To force another call to [`Self::logic`], call [`egui::Context::request_repaint`] at any time (e.g. from another thread). + fn logic(&mut self, ctx: &egui::Context, frame: &mut Frame) { + _ = (ctx, frame); + } + + /// Called each time the UI needs repainting, which may be many times per second. + /// + /// The given [`egui::Ui`] has no margin or background color. + /// You can wrap your UI code in [`egui::CentralPanel`] or a [`egui::Frame::central_panel`] to remedy this. + /// + /// The [`egui::Ui::ctx`] can be cloned and saved if you like. + /// To force a repaint, call [`egui::Context::request_repaint`] at any time (e.g. from another thread). + /// + /// This is called for the root viewport ([`egui::ViewportId::ROOT`]). + /// Use [`egui::Context::show_viewport_deferred`] to spawn additional viewports (windows). + /// (A "viewport" in egui means an native OS window). + fn ui(&mut self, ui: &mut egui::Ui, frame: &mut Frame); + /// Called each time the UI needs repainting, which may be many times per second. /// /// Put your widgets into a [`egui::Panel`], [`egui::CentralPanel`], [`egui::Window`] or [`egui::Area`]. @@ -146,7 +171,10 @@ pub trait App { /// This is called for the root viewport ([`egui::ViewportId::ROOT`]). /// Use [`egui::Context::show_viewport_deferred`] to spawn additional viewports (windows). /// (A "viewport" in egui means an native OS window). - fn update(&mut self, ctx: &egui::Context, frame: &mut Frame); + #[deprecated = "Use Self::ui instead"] + fn update(&mut self, ctx: &egui::Context, frame: &mut Frame) { + _ = (ctx, frame); + } /// Get a handle to the app. /// @@ -763,6 +791,7 @@ impl Frame { /// 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 { + #[expect(clippy::unwrap_used)] self.glow_register_native_texture.as_mut().unwrap()(native) } diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index d803a5249..151fb79ce 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -35,7 +35,7 @@ //! //! impl MyEguiApp { //! fn new(cc: &eframe::CreationContext<'_>) -> Self { -//! // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals. +//! // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_global_style. //! // Restore app state using cc.storage (requires the "persistence" feature). //! // Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use //! // for e.g. egui::PaintCallback. @@ -44,8 +44,8 @@ //! } //! //! impl eframe::App for MyEguiApp { -//! fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { -//! egui::CentralPanel::default().show(ctx, |ui| { +//! fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { +//! egui::CentralPanel::default().show_inside(ui, |ui| { //! ui.heading("Hello World!"); //! }); //! } @@ -142,7 +142,6 @@ //! #![warn(missing_docs)] // let's keep eframe well-documented -#![allow(clippy::needless_doctest_main)] // Limitation imposed by `accesskit_winit`: // https://github.com/AccessKit/accesskit/tree/accesskit-v0.18.0/platforms/winit#android-activity-compatibility` @@ -232,7 +231,7 @@ pub mod icon_data; /// /// impl MyEguiApp { /// fn new(cc: &eframe::CreationContext<'_>) -> Self { -/// // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals. +/// // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_global_style. /// // Restore app state using cc.storage (requires the "persistence" feature). /// // Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use /// // for e.g. egui::PaintCallback. @@ -241,8 +240,8 @@ pub mod icon_data; /// } /// /// impl eframe::App for MyEguiApp { -/// fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { -/// egui::CentralPanel::default().show(ctx, |ui| { +/// fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { +/// egui::CentralPanel::default().show_inside(ui, |ui| { /// ui.heading("Hello World!"); /// }); /// } @@ -253,7 +252,7 @@ pub mod icon_data; /// This function can fail if we fail to set up a graphics context. #[cfg(not(target_arch = "wasm32"))] #[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))] -#[allow(clippy::needless_pass_by_value, clippy::allow_attributes)] +#[allow(clippy::allow_attributes, clippy::needless_pass_by_value)] pub fn run_native( app_name: &str, mut native_options: NativeOptions, @@ -312,8 +311,8 @@ pub fn run_native( /// } /// /// impl eframe::App for MyEguiApp { -/// fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { -/// egui::CentralPanel::default().show(ctx, |ui| { +/// fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { +/// egui::CentralPanel::default().show_inside(ui, |ui| { /// ui.heading("Hello World!"); /// }); /// } @@ -399,8 +398,9 @@ fn init_native(app_name: &str, native_options: &mut NativeOptions) -> Renderer { /// let mut age = 42; /// /// let options = eframe::NativeOptions::default(); -/// eframe::run_simple_native("My egui App", options, move |ctx, _frame| { -/// egui::CentralPanel::default().show(ctx, |ui| { +/// eframe::run_ui_native("My egui App", options, move |ui, _frame| { +/// // Wrap everything in a CentralPanel so we get some margins and a background color: +/// egui::CentralPanel::default().show_inside(ui, |ui| { /// ui.heading("My egui Application"); /// ui.horizontal(|ui| { /// let name_label = ui.label("Your name: "); @@ -421,6 +421,65 @@ fn init_native(app_name: &str, native_options: &mut NativeOptions) -> Renderer { /// This function can fail if we fail to set up a graphics context. #[cfg(not(target_arch = "wasm32"))] #[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))] +pub fn run_ui_native( + app_name: &str, + native_options: NativeOptions, + ui_fun: impl FnMut(&mut egui::Ui, &mut Frame) + 'static, +) -> Result { + struct SimpleApp { + ui_fun: U, + } + + impl App for SimpleApp { + fn ui(&mut self, ui: &mut egui::Ui, frame: &mut Frame) { + (self.ui_fun)(ui, frame); + } + } + + run_native( + app_name, + native_options, + Box::new(|_cc| Ok(Box::new(SimpleApp { ui_fun }))), + ) +} + +/// The simplest way to get started when writing a native app. +/// +/// This does NOT support persistence of custom user data. For that you need to use [`run_native`]. +/// However, it DOES support persistence of egui data (window positions and sizes, how far the user has scrolled in a +/// [`ScrollArea`](egui::ScrollArea), etc.) if the persistence feature is enabled. +/// +/// # Example +/// ``` no_run +/// fn main() -> eframe::Result { +/// // Our application state: +/// let mut name = "Arthur".to_owned(); +/// let mut age = 42; +/// +/// let options = eframe::NativeOptions::default(); +/// eframe::run_simple_native("My egui App", options, move |ctx, _frame| { +/// egui::CentralPanel::default().show(ctx, |ui| { +/// ui.heading("My egui Application"); +/// ui.horizontal(|ui| { +/// let name_label = ui.label("Your name: "); +/// ui.text_edit_singleline(&mut name) +/// .labelled_by(name_label.id); +/// }); +/// ui.add(egui::Slider::new(&mut age, 0..=120).text("age")); +/// if ui.button("Increment").clicked() { +/// age += 1; +/// } +/// ui.label(format!("Hello '{name}', age {age}")); +/// }); +/// }) +/// } +/// ``` +/// +/// # Errors +/// This function can fail if we fail to set up a graphics context. +#[deprecated = "Use run_ui_native instead"] +#[cfg(not(target_arch = "wasm32"))] +#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))] pub fn run_simple_native( app_name: &str, native_options: NativeOptions, @@ -431,6 +490,8 @@ pub fn run_simple_native( } impl App for SimpleApp { + fn ui(&mut self, _ui: &mut egui::Ui, _frame: &mut Frame) {} + fn update(&mut self, ctx: &egui::Context, frame: &mut Frame) { (self.update_fun)(ctx, frame); } diff --git a/crates/eframe/src/native/app_icon.rs b/crates/eframe/src/native/app_icon.rs index 233bf4c9d..3ac61d8e6 100644 --- a/crates/eframe/src/native/app_icon.rs +++ b/crates/eframe/src/native/app_icon.rs @@ -47,7 +47,7 @@ enum AppIconStatus { NotSetTryAgain, /// We successfully set the icon and it should be visible now. - #[allow(dead_code, clippy::allow_attributes)] // Not used on Linux + #[allow(clippy::allow_attributes, dead_code)] // Not used on Linux Set, } @@ -71,7 +71,7 @@ fn set_title_and_icon(_title: &str, _icon_data: Option<&IconData>) -> AppIconSta #[cfg(target_os = "macos")] return set_title_and_icon_mac(_title, _icon_data); - #[allow(unreachable_code, clippy::allow_attributes)] + #[allow(clippy::allow_attributes, unreachable_code)] AppIconStatus::NotSetIgnored } diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 447b3ea9d..96a52db88 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -272,14 +272,27 @@ impl EpiIntegration { app.raw_input_hook(&self.egui_ctx, &mut raw_input); - let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { + let full_output = self.egui_ctx.run_ui(raw_input, |ui| { if let Some(viewport_ui_cb) = viewport_ui_cb { // Child viewport profiling::scope!("viewport_callback"); - viewport_ui_cb(egui_ctx); + viewport_ui_cb(ui); } else { - profiling::scope!("App::update"); - app.update(egui_ctx, &mut self.frame); + { + profiling::scope!("App::logic"); + app.logic(ui.ctx(), &mut self.frame); + } + + { + profiling::scope!("App::update"); + #[expect(deprecated)] + app.update(ui.ctx(), &mut self.frame); + } + + { + profiling::scope!("App::ui"); + app.ui(ui, &mut self.frame); + } } }); @@ -363,6 +376,7 @@ impl EpiIntegration { fn load_default_egui_icon() -> egui::IconData { profiling::function_scope!(); + #[expect(clippy::unwrap_used)] crate::icon_data::from_png_bytes(&include_bytes!("../../data/icon.png")[..]).unwrap() } diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index c5358527a..1cd49449f 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -5,7 +5,8 @@ //! There is a bunch of improvements we could do, //! like removing a bunch of `unwraps`. -#![allow(clippy::undocumented_unsafe_blocks)] +#![expect(clippy::undocumented_unsafe_blocks)] +#![expect(clippy::unwrap_used)] use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant}; @@ -216,7 +217,7 @@ impl<'app> GlowWinitApp<'app> { storage.as_deref(), &mut self.native_options, )?; - let gl = painter.gl().clone(); + let gl = Arc::clone(painter.gl()); let max_texture_side = painter.max_texture_side(); glutin.max_texture_side = Some(max_texture_side); @@ -234,9 +235,9 @@ impl<'app> GlowWinitApp<'app> { &self.app_name, &self.native_options, storage, - Some(gl.clone()), + Some(Arc::clone(&gl)), Some(Box::new({ - let painter = painter.clone(); + let painter = Rc::clone(&painter); move |native| painter.borrow_mut().register_native_texture(native) })), #[cfg(feature = "wgpu_no_default_features")] @@ -244,7 +245,7 @@ impl<'app> GlowWinitApp<'app> { ); { - let event_loop_proxy = self.repaint_proxy.clone(); + let event_loop_proxy = Arc::clone(&self.repaint_proxy); integration .egui_ctx .set_request_repaint_callback(move |info| { @@ -571,7 +572,7 @@ impl GlowWinitRunning<'_> { .options_mut(|opt| opt.begin_pass(&raw_input)); let clear_color = self .app - .clear_color(&self.integration.egui_ctx.style().visuals); + .clear_color(&self.integration.egui_ctx.global_style().visuals); let has_many_viewports = self.glutin.borrow().viewports.len() > 1; let clear_before_update = !has_many_viewports; // HACK: for some reason, an early clear doesn't "take" on Mac with multiple viewports. @@ -719,11 +720,11 @@ impl GlowWinitRunning<'_> { // vsync - don't count as frame-time: frame_timer.pause(); profiling::scope!("swap_buffers"); - let context = current_gl_context - .as_ref() - .ok_or(egui_glow::PainterError::from( + let context = current_gl_context.as_ref().ok_or_else(|| { + egui_glow::PainterError::from( "failed to get current context to swap buffers".to_owned(), - ))?; + ) + })?; gl_surface.swap_buffers(context)?; frame_timer.resume(); @@ -1362,7 +1363,7 @@ fn initialize_or_update_viewport( ids: ViewportIdPair, class: ViewportClass, mut builder: ViewportBuilder, - viewport_ui_cb: Option>, + viewport_ui_cb: Option>, ) -> &mut Viewport { profiling::function_scope!(); @@ -1494,8 +1495,8 @@ fn render_immediate_viewport( shapes, pixels_per_point, viewport_output, - } = egui_ctx.run(input, |ctx| { - viewport_ui_cb(ctx); + } = egui_ctx.run_ui(input, |ui| { + viewport_ui_cb(ui); }); // --------------------------------------------------- diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 7a3a8b4c4..0597d318c 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -349,8 +349,6 @@ pub fn run_glow( mut native_options: epi::NativeOptions, app_creator: epi::AppCreator<'_>, ) -> Result { - #![allow(clippy::needless_return_with_question_mark)] // False positive - use super::glow_integration::GlowWinitApp; #[cfg(not(target_os = "ios"))] @@ -387,8 +385,6 @@ pub fn run_wgpu( mut native_options: epi::NativeOptions, app_creator: epi::AppCreator<'_>, ) -> 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 c6c715c8c..cb634200a 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -219,7 +219,7 @@ impl<'app> WgpuWinitApp<'app> { { profiling::scope!("set_window"); - pollster::block_on(painter.set_window(ViewportId::ROOT, Some(window.clone())))?; + pollster::block_on(painter.set_window(ViewportId::ROOT, Some(Arc::clone(&window))))?; } let wgpu_render_state = painter.render_state(); @@ -238,7 +238,7 @@ impl<'app> WgpuWinitApp<'app> { ); { - let event_loop_proxy = self.repaint_proxy.clone(); + let event_loop_proxy = Arc::clone(&self.repaint_proxy); egui_ctx.set_request_repaint_callback(move |info| { log::trace!("request_repaint_callback: {info:?}"); @@ -256,7 +256,7 @@ impl<'app> WgpuWinitApp<'app> { }); } - #[allow(unused_mut, clippy::allow_attributes)] // used for accesskit + #[allow(clippy::allow_attributes, unused_mut)] // used for accesskit let mut egui_winit = egui_winit::State::new( egui_ctx.clone(), ViewportId::ROOT, @@ -610,7 +610,7 @@ impl WgpuWinitRunning<'_> { { profiling::scope!("set_window"); - pollster::block_on(painter.set_window(viewport_id, Some(window.clone())))?; + pollster::block_on(painter.set_window(viewport_id, Some(Arc::clone(window))))?; } let Some(egui_winit) = egui_winit.as_mut() else { @@ -690,7 +690,7 @@ impl WgpuWinitRunning<'_> { let vsync_secs = painter.paint_and_update_textures( viewport_id, pixels_per_point, - app.clear_color(&egui_ctx.style().visuals), + app.clear_color(&egui_ctx.global_style().visuals), &clipped_primitives, &textures_delta, screenshot_commands, @@ -919,7 +919,7 @@ impl Viewport { let window = Arc::new(window); if let Err(err) = - pollster::block_on(painter.set_window(viewport_id, Some(window.clone()))) + pollster::block_on(painter.set_window(viewport_id, Some(Arc::clone(&window)))) { log::error!("on set_window: viewport_id {viewport_id:?} {err}"); } @@ -1027,8 +1027,8 @@ fn render_immediate_viewport( shapes, pixels_per_point, viewport_output, - } = egui_ctx.run(input, |ctx| { - viewport_ui_cb(ctx); + } = egui_ctx.run_ui(input, |ui| { + viewport_ui_cb(ui); }); // ------------------------------------------ @@ -1051,7 +1051,8 @@ fn render_immediate_viewport( { profiling::scope!("set_window"); - if let Err(err) = pollster::block_on(painter.set_window(ids.this, Some(window.clone()))) { + if let Err(err) = pollster::block_on(painter.set_window(ids.this, Some(Arc::clone(window)))) + { log::error!( "when rendering viewport_id={:?}, set_window Error {err}", ids.this @@ -1155,7 +1156,7 @@ fn initialize_or_update_viewport<'a>( ids: ViewportIdPair, class: ViewportClass, mut builder: ViewportBuilder, - viewport_ui_cb: Option>, + viewport_ui_cb: Option>, painter: &mut egui_wgpu::winit::Painter, ) -> &'a mut Viewport { use std::collections::btree_map::Entry; diff --git a/crates/eframe/src/native/winit_integration.rs b/crates/eframe/src/native/winit_integration.rs index 2cf2e9019..012c22f8e 100644 --- a/crates/eframe/src/native/winit_integration.rs +++ b/crates/eframe/src/native/winit_integration.rs @@ -27,7 +27,10 @@ pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Contex egui_ctx.options_mut(|o| { // eframe supports multi-pass (Context::request_discard). - o.max_passes = 2.try_into().unwrap(); + #[expect(clippy::unwrap_used)] + { + o.max_passes = 2.try_into().unwrap(); + } }); let memory = crate::native::epi_integration::load_egui_memory(storage).unwrap_or_default(); diff --git a/crates/eframe/src/stopwatch.rs b/crates/eframe/src/stopwatch.rs index e6eabcbdd..b5f956c82 100644 --- a/crates/eframe/src/stopwatch.rs +++ b/crates/eframe/src/stopwatch.rs @@ -1,4 +1,4 @@ -#![allow(dead_code)] // not everything is used on wasm +#![allow(clippy::allow_attributes, dead_code)] // not used on all platforms use web_time::Instant; @@ -23,7 +23,7 @@ impl Stopwatch { } pub fn pause(&mut self) { - let start = self.start.take().unwrap(); + let start = self.start.take().expect("Stopwatch is not running"); let duration = start.elapsed(); self.total_time_ns += duration.as_nanos(); } diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 8a10a90ef..11654135d 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use egui::{TexturesDelta, UserData, ViewportCommand}; use crate::{App, epi, web::web_painter::WebPainter}; @@ -5,14 +7,14 @@ use crate::{App, epi, web::web_painter::WebPainter}; use super::{NeedRepaint, now_sec, text_agent::TextAgent}; pub struct AppRunner { - #[allow(dead_code, clippy::allow_attributes)] + #[allow(clippy::allow_attributes, dead_code)] pub(crate) web_options: crate::WebOptions, pub(crate) frame: epi::Frame, egui_ctx: egui::Context, painter: Box, pub(crate) input: super::WebInput, app: Box, - pub(crate) needs_repaint: std::sync::Arc, + pub(crate) needs_repaint: Arc, last_save_time: f64, pub(crate) text_agent: TextAgent, @@ -63,7 +65,7 @@ impl AppRunner { canvas, &web_options, )?; - gl = Some(painter.gl().clone()); + gl = Some(Arc::clone(painter.gl())); Box::new(painter) as Box } @@ -138,10 +140,9 @@ impl AppRunner { wgpu_render_state, }; - let needs_repaint: std::sync::Arc = - std::sync::Arc::new(NeedRepaint::new(web_options.max_fps)); + let needs_repaint: Arc = Arc::new(NeedRepaint::new(web_options.max_fps)); { - let needs_repaint = needs_repaint.clone(); + let needs_repaint = Arc::clone(&needs_repaint); egui_ctx.set_request_repaint_callback(move |info| { needs_repaint.repaint_after(info.delay.as_secs_f64()); }); @@ -273,8 +274,13 @@ impl AppRunner { 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); + let full_output = self.egui_ctx.run_ui(raw_input, |ui| { + self.app.logic(ui.ctx(), &mut self.frame); + + #[expect(deprecated)] + self.app.update(ui.ctx(), &mut self.frame); + + self.app.ui(ui, &mut self.frame); }); let egui::FullOutput { platform_output, @@ -331,7 +337,7 @@ impl AppRunner { } if let Err(err) = self.painter.paint_and_update_textures( - self.app.clear_color(&self.egui_ctx.style().visuals), + self.app.clear_color(&self.egui_ctx.global_style().visuals), &clipped_primitives, self.egui_ctx.pixels_per_point(), &textures_delta, diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index c61a80012..d77444563 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -137,25 +137,24 @@ fn install_keydown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), J && !modifiers.command // When text agent is focused, it is responsible for handling input events && !runner.text_agent.has_focus() + && let Some(text) = text_from_keyboard_event(&event) { - if let Some(text) = text_from_keyboard_event(&event) { - let egui_event = egui::Event::Text(text); - let should_stop_propagation = - (runner.web_options.should_stop_propagation)(&egui_event); - let should_prevent_default = - (runner.web_options.should_prevent_default)(&egui_event); - runner.input.raw.events.push(egui_event); - runner.needs_repaint.repaint_asap(); + let egui_event = egui::Event::Text(text); + let should_stop_propagation = + (runner.web_options.should_stop_propagation)(&egui_event); + let should_prevent_default = + (runner.web_options.should_prevent_default)(&egui_event); + runner.input.raw.events.push(egui_event); + runner.needs_repaint.repaint_asap(); - // If this is indeed text, then prevent any other action. - if should_prevent_default { - event.prevent_default(); - } + // If this is indeed text, then prevent any other action. + if should_prevent_default { + event.prevent_default(); + } - // Use web options to tell if the event should be propagated to parent elements. - if should_stop_propagation { - event.stop_propagation(); - } + // Use web options to tell if the event should be propagated to parent elements. + if should_stop_propagation { + event.stop_propagation(); } } @@ -196,11 +195,13 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner) let prevent_default = should_prevent_default_for_key(runner, &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 false { + log::debug!( + "On keydown {:?} {egui_key:?}, has_focus: {has_focus}, egui_wants_keyboard: {}, prevent_default: {prevent_default}", + event.key().as_str(), + runner.egui_ctx().egui_wants_keyboard_input() + ); + } if prevent_default { event.prevent_default(); @@ -319,30 +320,28 @@ fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Resul return; // The eframe app is not interested } - if let Some(data) = event.clipboard_data() { - if let Ok(text) = data.get_data("text") { - let text = text.replace("\r\n", "\n"); + if let Some(data) = event.clipboard_data() + && let Ok(text) = data.get_data("text") + { + let text = text.replace("\r\n", "\n"); - let mut should_stop_propagation = true; - let mut should_prevent_default = true; - if !text.is_empty() { - let egui_event = egui::Event::Paste(text); - should_stop_propagation = - (runner.web_options.should_stop_propagation)(&egui_event); - should_prevent_default = - (runner.web_options.should_prevent_default)(&egui_event); - runner.input.raw.events.push(egui_event); - runner.needs_repaint.repaint_asap(); - } + let mut should_stop_propagation = true; + let mut should_prevent_default = true; + if !text.is_empty() { + let egui_event = egui::Event::Paste(text); + should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event); + should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event); + runner.input.raw.events.push(egui_event); + runner.needs_repaint.repaint_asap(); + } - // Use web options to tell if the web event should be propagated to parent elements based on the egui event. - if should_stop_propagation { - event.stop_propagation(); - } + // Use web options to tell if the web event should be propagated to parent elements based on the egui event. + if should_stop_propagation { + event.stop_propagation(); + } - if should_prevent_default { - event.prevent_default(); - } + if should_prevent_default { + event.prevent_default(); } } })?; @@ -560,45 +559,44 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), if is_interested_in_pointer_event( runner, egui::pos2(event.client_x() as f32, event.client_y() as f32), - ) { - if let Some(button) = button_from_mouse_event(&event) { - let modifiers = runner.input.raw.modifiers; - let egui_event = egui::Event::PointerButton { - pos, - button, - pressed: false, - modifiers, - }; - let should_stop_propagation = - (runner.web_options.should_stop_propagation)(&egui_event); - let should_prevent_default = - (runner.web_options.should_prevent_default)(&egui_event); - runner.input.raw.events.push(egui_event); + ) && let Some(button) = button_from_mouse_event(&event) + { + let modifiers = runner.input.raw.modifiers; + let egui_event = egui::Event::PointerButton { + pos, + button, + pressed: false, + modifiers, + }; + let should_stop_propagation = + (runner.web_options.should_stop_propagation)(&egui_event); + let should_prevent_default = + (runner.web_options.should_prevent_default)(&egui_event); + runner.input.raw.events.push(egui_event); - // Previously on iOS, the canvas would not receive focus on - // any touch event, which resulted in the on-screen keyboard - // not working when focusing on a text field in an egui app. - // This attempts to fix that by forcing the focus on any - // click on the canvas. - runner.canvas().focus().ok(); + // Previously on iOS, the canvas would not receive focus on + // any touch event, which resulted in the on-screen keyboard + // not working when focusing on a text field in an egui app. + // This attempts to fix that by forcing the focus on any + // click on the canvas. + runner.canvas().focus().ok(); - // 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(); + // 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(); + // Make sure we paint the output of the above logic call asap: + runner.needs_repaint.repaint_asap(); - if should_prevent_default { - event.prevent_default(); - } + if should_prevent_default { + event.prevent_default(); + } - // Use web options to tell if the web event should be propagated to parent elements based on the egui event. - if should_stop_propagation { - event.stop_propagation(); - } + // Use web options to tell if the web event should be propagated to parent elements based on the egui event. + if should_stop_propagation { + event.stop_propagation(); } } }, @@ -711,29 +709,27 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<() fn install_touchmove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { runner_ref.add_event_listener(target, "touchmove", |event: web_sys::TouchEvent, runner| { - if let Some((pos, touch)) = primary_touch_pos(runner, &event) { - if is_interested_in_pointer_event( + if let Some((pos, touch)) = primary_touch_pos(runner, &event) + && is_interested_in_pointer_event( runner, egui::pos2(touch.client_x() as f32, touch.client_y() as f32), - ) { - let egui_event = egui::Event::PointerMoved(pos); - let should_stop_propagation = - (runner.web_options.should_stop_propagation)(&egui_event); - let should_prevent_default = - (runner.web_options.should_prevent_default)(&egui_event); - runner.input.raw.events.push(egui_event); + ) + { + let egui_event = egui::Event::PointerMoved(pos); + let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event); + let should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event); + runner.input.raw.events.push(egui_event); - push_touches(runner, egui::TouchPhase::Move, &event); - runner.needs_repaint.repaint(); + push_touches(runner, egui::TouchPhase::Move, &event); + runner.needs_repaint.repaint(); - // Use web options to tell if the web event should be propagated to parent elements based on the egui event. - if should_stop_propagation { - event.stop_propagation(); - } + // Use web options to tell if the web event should be propagated to parent elements based on the egui event. + if should_stop_propagation { + event.stop_propagation(); + } - if should_prevent_default { - event.prevent_default(); - } + if should_prevent_default { + event.prevent_default(); } } }) @@ -741,50 +737,49 @@ fn install_touchmove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { runner_ref.add_event_listener(target, "touchend", |event: web_sys::TouchEvent, runner| { - if let Some((pos, touch)) = primary_touch_pos(runner, &event) { - if is_interested_in_pointer_event( + if let Some((pos, touch)) = primary_touch_pos(runner, &event) + && is_interested_in_pointer_event( runner, egui::pos2(touch.client_x() as f32, touch.client_y() as f32), - ) { - // First release mouse to click: - let mut should_stop_propagation = true; - let mut should_prevent_default = true; - let egui_event = egui::Event::PointerButton { - pos, - button: egui::PointerButton::Primary, - pressed: false, - modifiers: runner.input.raw.modifiers, - }; - should_stop_propagation &= - (runner.web_options.should_stop_propagation)(&egui_event); - should_prevent_default &= (runner.web_options.should_prevent_default)(&egui_event); - runner.input.raw.events.push(egui_event); - // Then remove hover effect: - should_stop_propagation &= - (runner.web_options.should_stop_propagation)(&egui::Event::PointerGone); - should_prevent_default &= - (runner.web_options.should_prevent_default)(&egui::Event::PointerGone); - runner.input.raw.events.push(egui::Event::PointerGone); + ) + { + // First release mouse to click: + let mut should_stop_propagation = true; + let mut should_prevent_default = true; + let egui_event = egui::Event::PointerButton { + pos, + button: egui::PointerButton::Primary, + pressed: false, + modifiers: runner.input.raw.modifiers, + }; + should_stop_propagation &= (runner.web_options.should_stop_propagation)(&egui_event); + should_prevent_default &= (runner.web_options.should_prevent_default)(&egui_event); + runner.input.raw.events.push(egui_event); + // Then remove hover effect: + should_stop_propagation &= + (runner.web_options.should_stop_propagation)(&egui::Event::PointerGone); + should_prevent_default &= + (runner.web_options.should_prevent_default)(&egui::Event::PointerGone); + runner.input.raw.events.push(egui::Event::PointerGone); - push_touches(runner, egui::TouchPhase::End, &event); + push_touches(runner, egui::TouchPhase::End, &event); - runner.needs_repaint.repaint_asap(); + runner.needs_repaint.repaint_asap(); - // Use web options to tell if the web event should be propagated to parent elements based on the egui event. - if should_stop_propagation { - event.stop_propagation(); - } + // Use web options to tell if the web event should be propagated to parent elements based on the egui event. + if should_stop_propagation { + event.stop_propagation(); + } - if should_prevent_default { - event.prevent_default(); - } + if should_prevent_default { + event.prevent_default(); + } - // Fix virtual keyboard IOS - // Need call focus at the same time of event - if runner.text_agent.has_focus() { - runner.text_agent.set_focus(false); - runner.text_agent.set_focus(true); - } + // Fix virtual keyboard IOS + // Need call focus at the same time of event + if runner.text_agent.has_focus() { + runner.text_agent.set_focus(false); + runner.text_agent.set_focus(true); } } }) diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index ac4c637db..1e54d7a84 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -1,6 +1,7 @@ //! [`egui`] bindings for web apps (compiling to WASM). -#![allow(clippy::missing_errors_doc)] // So many `-> Result<_, JsValue>` +#![expect(clippy::missing_errors_doc)] // So many `-> Result<_, JsValue>` +#![expect(clippy::unwrap_used)] // TODO(emilk): remove unwraps mod app_runner; mod backend; @@ -145,18 +146,18 @@ fn canvas_content_rect(canvas: &web_sys::HtmlCanvasElement) -> egui::Rect { ); // We need to subtract padding and border: - if let Some(window) = web_sys::window() { - if let Ok(Some(style)) = window.get_computed_style(canvas) { - let get_property = |name: &str| -> Option { - let property = style.get_property_value(name).ok()?; - property.trim_end_matches("px").parse::().ok() - }; + if let Some(window) = web_sys::window() + && let Ok(Some(style)) = window.get_computed_style(canvas) + { + let get_property = |name: &str| -> Option { + let property = style.get_property_value(name).ok()?; + property.trim_end_matches("px").parse::().ok() + }; - rect.min.x += get_property("padding-left").unwrap_or_default(); - rect.min.y += get_property("padding-top").unwrap_or_default(); - rect.max.x -= get_property("padding-right").unwrap_or_default(); - rect.max.y -= get_property("padding-bottom").unwrap_or_default(); - } + rect.min.x += get_property("padding-left").unwrap_or_default(); + rect.min.y += get_property("padding-top").unwrap_or_default(); + rect.max.x -= get_property("padding-right").unwrap_or_default(); + rect.max.y -= get_property("padding-bottom").unwrap_or_default(); } rect @@ -284,8 +285,8 @@ fn create_clipboard_item(mime: &str, bytes: &[u8]) -> Result) { - #![allow(clippy::match_same_arms)] + #![expect(clippy::match_same_arms)] if !self.enabled(record.metadata()) { return; @@ -110,7 +110,7 @@ mod console { /// * `tokio-1.24.1/src/runtime/runtime.rs` /// * `rerun/src/main.rs` /// * `core/src/ops/function.rs` -#[allow(dead_code, clippy::allow_attributes)] // only used on web and in tests +#[allow(clippy::allow_attributes, dead_code)] // only used on web and in tests fn shorten_file_path(file_path: &str) -> &str { if let Some(i) = file_path.rfind("/src/") { if let Some(prev_slash) = file_path[..i].rfind('/') { diff --git a/crates/eframe/src/web/web_painter_glow.rs b/crates/eframe/src/web/web_painter_glow.rs index e2fc4a6f2..470fa40d3 100644 --- a/crates/eframe/src/web/web_painter_glow.rs +++ b/crates/eframe/src/web/web_painter_glow.rs @@ -28,7 +28,7 @@ impl WebPainterGlow { let (gl, shader_prefix) = init_glow_context_from_canvas(&canvas, options.webgl_context_option)?; - #[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm + #[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm let gl = std::sync::Arc::new(gl); let painter = egui_glow::Painter::new(gl, shader_prefix, None, options.dithering) @@ -91,7 +91,7 @@ impl WebPainter for WebPainterGlow { for data in data { events.push(Event::Screenshot { viewport_id: ViewportId::default(), - image: image.clone(), + image: Arc::clone(&image), user_data: data, }); } @@ -192,17 +192,13 @@ fn is_safari_and_webkit_gtk(gl: &web_sys::WebGlRenderingContext) -> bool { .get_extension("WEBGL_debug_renderer_info") .unwrap() .is_some() - { - if let Ok(renderer) = + && let Ok(renderer) = gl.get_parameter(web_sys::WebglDebugRendererInfo::UNMASKED_RENDERER_WEBGL) - { - if let Some(renderer) = renderer.as_string() { - if renderer.contains("Apple") { - return true; - } - } - } + && let Some(renderer) = renderer.as_string() + && renderer.contains("Apple") + { + true + } else { + false } - - false } diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index efecd12ee..264ce6adc 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -105,7 +105,7 @@ impl WebPainterWgpu { surface_configuration, depth_stencil_format, depth_texture_view: None, - on_surface_error: options.wgpu_options.on_surface_error.clone(), + on_surface_error: Arc::clone(&options.wgpu_options.on_surface_error) as _, screen_capture_state: None, capture_tx, capture_rx, @@ -243,13 +243,26 @@ impl WebPainter for WebPainterWgpu { depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| { wgpu::RenderPassDepthStencilAttachment { view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(1.0), - // It is very unlikely that the depth buffer is needed after egui finished rendering - // so no need to store it. (this can improve performance on tiling GPUs like mobile chips or Apple Silicon) - store: wgpu::StoreOp::Discard, - }), - stencil_ops: None, + depth_ops: self + .depth_stencil_format + .is_some_and(|depth_stencil_format| { + depth_stencil_format.has_depth_aspect() + }) + .then_some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + // It is very unlikely that the depth buffer is needed after egui finished rendering + // so no need to store it. (this can improve performance on tiling GPUs like mobile chips or Apple Silicon) + store: wgpu::StoreOp::Discard, + }), + stencil_ops: self + .depth_stencil_format + .is_some_and(|depth_stencil_format| { + depth_stencil_format.has_stencil_aspect() + }) + .then_some(wgpu::Operations { + load: wgpu::LoadOp::Clear(0), + store: wgpu::StoreOp::Discard, + }), } }), label: Some("egui_render"), @@ -269,14 +282,12 @@ impl WebPainter for WebPainterWgpu { let mut capture_buffer = None; - if capture { - if let Some(capture_state) = &mut self.screen_capture_state { - capture_buffer = Some(capture_state.copy_textures( - &render_state.device, - &output_frame, - &mut encoder, - )); - } + if capture && let Some(capture_state) = &mut self.screen_capture_state { + capture_buffer = Some(capture_state.copy_textures( + &render_state.device, + &output_frame, + &mut encoder, + )); } Some((output_frame, capture_buffer)) @@ -288,16 +299,16 @@ impl WebPainter for WebPainterWgpu { .submit(user_cmd_bufs.into_iter().chain([encoder.finish()])); if let Some((frame, capture_buffer)) = frame_and_capture_buffer { - if let Some(capture_buffer) = capture_buffer { - if let Some(capture_state) = &self.screen_capture_state { - capture_state.read_screen_rgba( - self.ctx.clone(), - capture_buffer, - capture_data, - self.capture_tx.clone(), - ViewportId::ROOT, - ); - } + if let Some(capture_buffer) = capture_buffer + && let Some(capture_state) = &self.screen_capture_state + { + capture_state.read_screen_rgba( + self.ctx.clone(), + capture_buffer, + capture_data, + self.capture_tx.clone(), + ViewportId::ROOT, + ); } frame.present(); @@ -323,7 +334,7 @@ impl WebPainter for WebPainterWgpu { events.push(Event::Screenshot { viewport_id, user_data: data, - image: screenshot.clone(), + image: Arc::clone(&screenshot), }); } } diff --git a/crates/egui-wgpu/CHANGELOG.md b/crates/egui-wgpu/CHANGELOG.md index be00dd049..cef640184 100644 --- a/crates/egui-wgpu/CHANGELOG.md +++ b/crates/egui-wgpu/CHANGELOG.md @@ -6,6 +6,10 @@ 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.33.3 - 2025-12-11 +Nothing new + + ## 0.33.2 - 2025-11-13 * Fix jittering during window resize on MacOS for WGPU/Metal [#7641](https://github.com/emilk/egui/pull/7641) by [@aspcartman](https://github.com/aspcartman) diff --git a/crates/egui-wgpu/src/capture.rs b/crates/egui-wgpu/src/capture.rs index cd42b838c..58407fdd6 100644 --- a/crates/egui-wgpu/src/capture.rs +++ b/crates/egui-wgpu/src/capture.rs @@ -126,7 +126,7 @@ impl CaptureState { // It would be more efficient to reuse the Buffer, e.g. via some kind of ring buffer, but // for most screenshot use cases this should be fine. When taking many screenshots (e.g. for a video) // it might make sense to revisit this and implement a more efficient solution. - #[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm + #[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("egui_screen_capture_buffer"), size: (self.padding.padded_bytes_per_row * self.texture.height()) as u64, @@ -186,9 +186,9 @@ impl CaptureState { tx: CaptureSender, viewport_id: ViewportId, ) { - #[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm + #[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm let buffer = Arc::new(buffer); - let buffer_clone = buffer.clone(); + let buffer_clone = Arc::clone(&buffer); let buffer_slice = buffer_clone.slice(..); let format = self.texture.format(); let tex_extent = self.texture.size(); diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index d340526af..880ab8f4a 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -16,8 +16,6 @@ #![doc = document_features::document_features!()] //! -#![allow(unsafe_code)] - pub use wgpu; /// Low-level painting of [`egui`](https://github.com/emilk/egui) on [`wgpu`]. @@ -247,7 +245,7 @@ impl RenderState { // On wasm, depending on feature flags, wgpu objects may or may not implement sync. // It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint. - #[allow(clippy::arc_with_non_send_sync, clippy::allow_attributes)] // For wasm + #[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm Ok(Self { adapter, #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index 108aa31c3..d3d21f19c 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -1,4 +1,4 @@ -#![allow(unsafe_code)] +#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps use std::{borrow::Cow, num::NonZeroU64, ops::Range}; diff --git a/crates/egui-wgpu/src/setup.rs b/crates/egui-wgpu/src/setup.rs index bd587350e..0c3cb8c39 100644 --- a/crates/egui-wgpu/src/setup.rs +++ b/crates/egui-wgpu/src/setup.rs @@ -48,7 +48,7 @@ impl WgpuSetup { pub async fn new_instance(&self) -> wgpu::Instance { match self { Self::CreateNew(create_new) => { - #[allow(unused_mut, clippy::allow_attributes)] + #[allow(clippy::allow_attributes, unused_mut)] let mut backends = create_new.instance_descriptor.backends; // Don't try WebGPU if we're not in a secure context. @@ -134,7 +134,7 @@ impl Clone for WgpuSetupCreateNew { instance_descriptor: self.instance_descriptor.clone(), power_preference: self.power_preference, native_adapter_selector: self.native_adapter_selector.clone(), - device_descriptor: self.device_descriptor.clone(), + device_descriptor: Arc::clone(&self.device_descriptor), } } } diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 3a286cc9e..167d10c79 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -1,5 +1,7 @@ -#![allow(clippy::missing_errors_doc)] -#![allow(clippy::undocumented_unsafe_blocks)] +#![expect(clippy::missing_errors_doc)] +#![expect(clippy::undocumented_unsafe_blocks)] +#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps +#![expect(unsafe_code)] use crate::{RenderState, SurfaceErrorAction, WgpuConfiguration, renderer}; use crate::{ @@ -526,13 +528,28 @@ impl Painter { depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map(|view| { wgpu::RenderPassDepthStencilAttachment { view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(1.0), - // It is very unlikely that the depth buffer is needed after egui finished rendering - // so no need to store it. (this can improve performance on tiling GPUs like mobile chips or Apple Silicon) - store: wgpu::StoreOp::Discard, - }), - stencil_ops: None, + depth_ops: self + .options + .depth_stencil_format + .is_some_and(|depth_stencil_format| { + depth_stencil_format.has_depth_aspect() + }) + .then_some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + // It is very unlikely that the depth buffer is needed after egui finished rendering + // so no need to store it. (this can improve performance on tiling GPUs like mobile chips or Apple Silicon) + store: wgpu::StoreOp::Discard, + }), + stencil_ops: self + .options + .depth_stencil_format + .is_some_and(|depth_stencil_format| { + depth_stencil_format.has_stencil_aspect() + }) + .then_some(wgpu::Operations { + load: wgpu::LoadOp::Clear(0), + store: wgpu::StoreOp::Discard, + }), } }), timestamp_writes: None, @@ -614,7 +631,7 @@ impl Painter { events.push(Event::Screenshot { viewport_id, user_data: data, - image: screenshot.clone(), + image: Arc::clone(&screenshot), }); } } diff --git a/crates/egui-winit/CHANGELOG.md b/crates/egui-winit/CHANGELOG.md index 8e555a7bc..f88a8c84a 100644 --- a/crates/egui-winit/CHANGELOG.md +++ b/crates/egui-winit/CHANGELOG.md @@ -5,6 +5,10 @@ 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.33.3 - 2025-12-11 +Nothing new + + ## 0.33.2 - 2025-11-13 * Don't enable `arboard` on iOS [#7663](https://github.com/emilk/egui/pull/7663) by [@irh](https://github.com/irh) diff --git a/crates/egui-winit/src/clipboard.rs b/crates/egui-winit/src/clipboard.rs index fc7334388..75d0469ec 100644 --- a/crates/egui-winit/src/clipboard.rs +++ b/crates/egui-winit/src/clipboard.rs @@ -175,7 +175,7 @@ fn init_arboard() -> Option { fn init_smithay_clipboard( raw_display_handle: Option, ) -> Option { - #![allow(clippy::undocumented_unsafe_blocks)] + #![expect(clippy::undocumented_unsafe_blocks)] profiling::function_scope!(); diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 7660e3cef..7cbaec624 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -7,7 +7,7 @@ #![cfg_attr(feature = "document-features", doc = document_features::document_features!())] //! -#![allow(clippy::manual_range_contains)] +#![expect(clippy::manual_range_contains)] #[cfg(feature = "accesskit")] pub use accesskit_winit; @@ -309,21 +309,21 @@ impl State { self.on_mouse_button_input(*state, *button); EventResponse { repaint: true, - consumed: self.egui_ctx.wants_pointer_input(), + consumed: self.egui_ctx.egui_wants_pointer_input(), } } WindowEvent::MouseWheel { delta, phase, .. } => { self.on_mouse_wheel(window, *delta, *phase); EventResponse { repaint: true, - consumed: self.egui_ctx.wants_pointer_input(), + consumed: self.egui_ctx.egui_wants_pointer_input(), } } WindowEvent::CursorMoved { position, .. } => { self.on_cursor_moved(window, *position); EventResponse { repaint: true, - consumed: self.egui_ctx.is_using_pointer(), + consumed: self.egui_ctx.egui_is_using_pointer(), } } WindowEvent::CursorLeft { .. } => { @@ -340,8 +340,10 @@ impl State { let consumed = match touch.phase { winit::event::TouchPhase::Started | winit::event::TouchPhase::Ended - | winit::event::TouchPhase::Cancelled => self.egui_ctx.wants_pointer_input(), - winit::event::TouchPhase::Moved => self.egui_ctx.is_using_pointer(), + | winit::event::TouchPhase::Cancelled => { + self.egui_ctx.egui_wants_pointer_input() + } + winit::event::TouchPhase::Moved => self.egui_ctx.egui_is_using_pointer(), }; EventResponse { repaint: true, @@ -392,7 +394,7 @@ impl State { EventResponse { repaint: true, - consumed: self.egui_ctx.wants_keyboard_input(), + consumed: self.egui_ctx.egui_wants_keyboard_input(), } } WindowEvent::KeyboardInput { @@ -413,7 +415,7 @@ impl State { self.on_keyboard_input(event); // When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes. - let consumed = self.egui_ctx.wants_keyboard_input() + let consumed = self.egui_ctx.egui_wants_keyboard_input() || event.logical_key == winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab); EventResponse { @@ -528,7 +530,7 @@ impl State { self.egui_input.events.push(egui::Event::Zoom(zoom_factor)); EventResponse { repaint: true, - consumed: self.egui_ctx.wants_pointer_input(), + consumed: self.egui_ctx.egui_wants_pointer_input(), } } @@ -541,7 +543,7 @@ impl State { .push(egui::Event::Rotate(-delta.to_radians())); EventResponse { repaint: true, - consumed: self.egui_ctx.wants_pointer_input(), + consumed: self.egui_ctx.egui_wants_pointer_input(), } } @@ -556,7 +558,7 @@ impl State { }); EventResponse { repaint: true, - consumed: self.egui_ctx.wants_pointer_input(), + consumed: self.egui_ctx.egui_wants_pointer_input(), } } } diff --git a/crates/egui-winit/src/safe_area.rs b/crates/egui-winit/src/safe_area.rs index d29d654a3..5f4a9f9cf 100644 --- a/crates/egui-winit/src/safe_area.rs +++ b/crates/egui-winit/src/safe_area.rs @@ -25,7 +25,7 @@ mod ios { let app = UIApplication::sharedApplication(main_thread_marker); - #[allow(unsafe_code)] + #[expect(unsafe_code)] unsafe { // Look for the first window scene that's in the foreground for scene in app.connectedScenes() { diff --git a/crates/egui/src/atomics/atom_kind.rs b/crates/egui/src/atomics/atom_kind.rs index 3c54c496b..10ca3353b 100644 --- a/crates/egui/src/atomics/atom_kind.rs +++ b/crates/egui/src/atomics/atom_kind.rs @@ -82,7 +82,7 @@ impl<'a> AtomKind<'a> { ) -> (Vec2, SizedAtomKind<'a>) { match self { AtomKind::Text(text) => { - let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode()); + let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode()); let galley = text.into_galley(ui, Some(wrap_mode), available_size.x, fallback_font); (galley.intrinsic_size(), SizedAtomKind::Text(galley)) } diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index 1df890250..8132a7dc9 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -168,7 +168,7 @@ impl<'a> AtomLayout<'a> { let fallback_font = fallback_font.unwrap_or_default(); - let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode()); + let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode()); // If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`. // If none is found, mark the first text item as `shrink`. @@ -188,7 +188,7 @@ impl<'a> AtomLayout<'a> { let fallback_text_color = fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color()); - let gap = gap.unwrap_or(ui.spacing().icon_spacing); + let gap = gap.unwrap_or_else(|| ui.spacing().icon_spacing); // The size available for the content let available_inner_size = ui.available_size() - frame.total_margin().sum(); diff --git a/crates/egui/src/atomics/atoms.rs b/crates/egui/src/atomics/atoms.rs index 4b19c9e26..1db7c63c6 100644 --- a/crates/egui/src/atomics/atoms.rs +++ b/crates/egui/src/atomics/atoms.rs @@ -198,8 +198,7 @@ macro_rules! all_the_atoms { $($T: IntoAtoms<'a>),* { fn collect(self, _atoms: &mut Atoms<'a>) { - #[allow(clippy::allow_attributes)] - #[allow(non_snake_case)] + #[allow(clippy::allow_attributes, non_snake_case)] let ($($T),*) = self; $($T.collect(_atoms);)* } diff --git a/crates/egui/src/cache/cache_storage.rs b/crates/egui/src/cache/cache_storage.rs index d4c3c9aef..255eca2d5 100644 --- a/crates/egui/src/cache/cache_storage.rs +++ b/crates/egui/src/cache/cache_storage.rs @@ -28,6 +28,7 @@ pub struct CacheStorage { impl CacheStorage { pub fn cache(&mut self) -> &mut Cache { + #[expect(clippy::unwrap_used)] self.caches .entry(std::any::TypeId::of::()) .or_insert_with(|| Box::::default()) diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 3ebac1d65..3b6d4006e 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -172,7 +172,7 @@ impl Area { /// Set the [`UiStackInfo`] of the area's [`Ui`]. /// - /// Default to [`UiStackInfo::new(UiKind::GenericArea)`]. + /// Default to [`UiStackInfo`] with kind [`UiKind::GenericArea`]. #[inline] pub fn info(mut self, info: UiStackInfo) -> Self { self.info = info; @@ -459,7 +459,7 @@ impl Area { state.pivot_pos = Some(new_pos); } state.pivot_pos.get_or_insert_with(|| { - default_pos.unwrap_or_else(|| automatic_area_position(ctx, layer_id)) + default_pos.unwrap_or_else(|| automatic_area_position(ctx, constrain_rect, layer_id)) }); state.interactable = interactable; @@ -469,7 +469,7 @@ impl Area { // during the sizing pass we will use this as the max size let mut size = default_size; - let default_area_size = ctx.style().spacing.default_area_size; + let default_area_size = ctx.global_style().spacing.default_area_size; if size.x.is_nan() { size.x = default_area_size.x; } @@ -523,6 +523,7 @@ impl Area { enabled, }, true, + Default::default(), ); // Used to prevent drift @@ -634,7 +635,8 @@ impl Prepared { { let age = ctx.input(|i| (i.time - last_became_visible_at) as f32 + i.predicted_dt / 2.0); - let opacity = crate::remap_clamp(age, 0.0..=ctx.style().animation_time, 0.0..=1.0); + let opacity = + crate::remap_clamp(age, 0.0..=ctx.global_style().animation_time, 0.0..=1.0); let opacity = emath::easing::quadratic_out(opacity); // slow fade-out = quick fade-in ui.multiply_opacity(opacity); if opacity < 1.0 { @@ -698,7 +700,7 @@ fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool { } } -fn automatic_area_position(ctx: &Context, layer_id: LayerId) -> Pos2 { +fn automatic_area_position(ctx: &Context, constrain_rect: Rect, layer_id: LayerId) -> Pos2 { let mut existing: Vec = ctx.memory(|mem| { mem.areas() .visible_windows() @@ -709,13 +711,9 @@ fn automatic_area_position(ctx: &Context, layer_id: LayerId) -> Pos2 { }); existing.sort_by_key(|r| r.left().round() as i32); - // NOTE: for the benefit of the egui demo, we position the windows so they don't - // cover the side panels, which means we use `available_rect` here instead of `constrain_rect` or `screen_rect`. - let available_rect = ctx.available_rect(); - let spacing = 16.0; - let left = available_rect.left() + spacing; - let top = available_rect.top() + spacing; + let left = constrain_rect.left() + spacing; + let top = constrain_rect.top() + spacing; if existing.is_empty() { return pos2(left, top); @@ -725,6 +723,7 @@ fn automatic_area_position(ctx: &Context, layer_id: LayerId) -> Pos2 { let mut column_bbs = vec![existing[0]]; for &rect in &existing { + #[expect(clippy::unwrap_used)] let current_column_bb = column_bbs.last_mut().unwrap(); if rect.left() < current_column_bb.right() { // same column @@ -749,14 +748,15 @@ fn automatic_area_position(ctx: &Context, layer_id: LayerId) -> Pos2 { // Find first column with some available space at the bottom of it: for col_bb in &column_bbs { - if col_bb.bottom() < available_rect.center().y { + if col_bb.bottom() < constrain_rect.center().y { return pos2(col_bb.left(), col_bb.bottom() + spacing); } } // Maybe we can fit a new column? + #[expect(clippy::unwrap_used)] let rightmost = column_bbs.last().unwrap().right(); - if rightmost + 200.0 < available_rect.right() { + if rightmost + 200.0 < constrain_rect.right() { return pos2(rightmost + spacing, top); } diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index 3afb77682..aca8ab138 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -69,7 +69,7 @@ impl CollapsingState { pub fn toggle(&mut self, ui: &Ui) { self.state.open = !self.state.open; - ui.ctx().request_repaint(); + ui.request_repaint(); } /// 0 for closed, 1 for open, with tweening diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index e4de2ca48..d556f827e 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -336,7 +336,7 @@ impl Frame { impl Frame { /// How much extra space the frame uses up compared to the content. /// - /// [`Self::inner_margin`] + [`Self.stroke`]`.width` + [`Self::outer_margin`]. + /// [`Self::inner_margin`] + [`Self::stroke`]`.width` + [`Self::outer_margin`]. #[inline] pub fn total_margin(&self) -> MarginF32 { MarginF32::from(self.inner_margin) diff --git a/crates/egui/src/containers/menu.rs b/crates/egui/src/containers/menu.rs index 756d68dd3..1bd5954c8 100644 --- a/crates/egui/src/containers/menu.rs +++ b/crates/egui/src/containers/menu.rs @@ -207,7 +207,7 @@ impl MenuState { /// egui::MenuBar::new().ui(ui, |ui| { /// ui.menu_button("File", |ui| { /// if ui.button("Quit").clicked() { -/// ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close); +/// ui.send_viewport_cmd(egui::ViewportCommand::Close); /// } /// }); /// }); @@ -444,11 +444,8 @@ impl SubMenu { let mut menu_config = self.config.unwrap_or_else(|| parent_config.clone()); menu_config.bar = false; - let menu_root_response = ui - .ctx() - .read_response(menu_id) - // Since we are a child of that ui, this should always exist - .unwrap(); + #[expect(clippy::unwrap_used)] // Since we are a child of that ui, this should always exist + let menu_root_response = ui.ctx().read_response(menu_id).unwrap(); let hover_pos = ui.ctx().pointer_hover_pos(); @@ -556,7 +553,7 @@ impl SubMenu { if is_moving_towards_rect { // We need to repaint while this is true, so we can detect when // the pointer is no longer moving towards the rect - ui.ctx().request_repaint(); + ui.request_repaint(); } let hovering_other_menu_entry = is_open && !is_hovered diff --git a/crates/egui/src/containers/old_popup.rs b/crates/egui/src/containers/old_popup.rs index 3ddf77bf6..f8e7cc900 100644 --- a/crates/egui/src/containers/old_popup.rs +++ b/crates/egui/src/containers/old_popup.rs @@ -1,5 +1,5 @@ //! Old and deprecated API for popups. Use [`Popup`] instead. -#![allow(deprecated)] +#![expect(deprecated)] use crate::containers::tooltip::Tooltip; use crate::{ diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index eb60f5f21..6281e6b41 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -255,9 +255,7 @@ impl<'a> PanelSizer<'a> { let side = self.panel.side; let size_range = self.panel.size_range; - if is_resizing && pointer.is_some() { - let pointer = pointer.unwrap(); - + if is_resizing && let Some(pointer) = pointer { match side { PanelSide::Vertical(side) => { self.size = (pointer.x - side.side_x(self.panel_rect)).abs(); @@ -291,13 +289,13 @@ impl<'a> PanelSizer<'a> { /// See the [module level docs](crate::containers::panel) for more details. /// /// ``` -/// # egui::__run_test_ctx(|ctx| { -/// egui::Panel::left("my_left_panel").show(ctx, |ui| { +/// # egui::__run_test_ui(|ui| { +/// egui::Panel::left("my_left_panel").show_inside(ui, |ui| { /// ui.label("Hello World!"); /// }); /// # }); /// ``` -#[must_use = "You should call .show()"] +#[must_use = "You should call .show_inside()"] pub struct Panel { side: PanelSide, id: Id, @@ -518,6 +516,7 @@ impl Panel { } /// Show the panel at the top level. + #[deprecated = "Use show_inside() instead"] pub fn show( self, ctx: &Context, @@ -528,12 +527,15 @@ impl Panel { /// Show the panel if `is_expanded` is `true`, /// otherwise don't show it, but with a nice animation between collapsed and expanded. + #[deprecated = "Use show_animated_inside() instead"] pub fn show_animated( self, ctx: &Context, is_expanded: bool, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option> { + #![expect(deprecated)] + let how_expanded = animate_expansion(ctx, self.id.with("animation"), is_expanded); let animated_panel = self.get_animated_panel(ctx, is_expanded)?; @@ -572,6 +574,7 @@ impl Panel { } /// Show either a collapsed or a expanded panel, with a nice animation between. + #[deprecated = "Use show_animated_between_inside() instead"] pub fn show_animated_between( ctx: &Context, is_expanded: bool, @@ -579,6 +582,8 @@ impl Panel { expanded_panel: Self, add_contents: impl FnOnce(&mut Ui, f32) -> R, ) -> Option> { + #![expect(deprecated)] + let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded); // Get either the fake or the real panel to animate @@ -713,7 +718,7 @@ impl Panel { } if resize_hover || is_resizing { - ui.ctx().set_cursor_icon(self.cursor_icon(&panel_sizer)); + ui.set_cursor_icon(self.cursor_icon(&panel_sizer)); } PanelState { rect }.store(ui.ctx(), id); @@ -753,6 +758,8 @@ impl Panel { ctx: &Context, add_contents: Box R + 'c>, ) -> InnerResponse { + #![expect(deprecated)] + let side = self.side; let available_rect = ctx.available_rect(); let mut panel_ui = Ui::new( @@ -802,9 +809,7 @@ impl Panel { let resize_id = self.id.with("__resize"); let resize_response = ui.ctx().read_response(resize_id); - if resize_response.is_some() { - let resize_response = resize_response.unwrap(); - + if let Some(resize_response) = resize_response { // NOTE(sharky98): The original code was initializing to // false first, but it doesn't seem necessary. let is_resizing = resize_response.dragged(); @@ -934,14 +939,14 @@ impl Panel { }; let get_spacing_size = || match panel.side { - PanelSide::Vertical(_) => ctx.style().spacing.interact_size.x, - PanelSide::Horizontal(_) => ctx.style().spacing.interact_size.y, + PanelSide::Vertical(_) => ctx.global_style().spacing.interact_size.x, + PanelSide::Horizontal(_) => ctx.global_style().spacing.interact_size.y, }; PanelState::load(ctx, panel.id) .map(get_rect_state_size) .or(panel.default_size) - .unwrap_or(get_spacing_size()) + .unwrap_or_else(get_spacing_size) } } @@ -950,6 +955,9 @@ impl Panel { /// A panel that covers the remainder of the screen, /// i.e. whatever area is left after adding other panels. /// +/// This acts very similar to [`Frame::central_panel`], but always expands +/// to use up all available space. +/// /// The order in which you add panels matter! /// The first panel you add will always be the outermost, and the last you add will always be the innermost. /// @@ -960,31 +968,41 @@ impl Panel { /// See the [module level docs](crate::containers::panel) for more details. /// /// ``` -/// # egui::__run_test_ctx(|ctx| { -/// egui::Panel::top("my_panel").show(ctx, |ui| { +/// # egui::__run_test_ui(|ui| { +/// egui::Panel::top("my_panel").show_inside(ui, |ui| { /// ui.label("Hello World! From `Panel`, that must be before `CentralPanel`!"); /// }); -/// egui::CentralPanel::default().show(ctx, |ui| { +/// egui::CentralPanel::default().show_inside(ui, |ui| { /// ui.label("Hello World!"); /// }); /// # }); /// ``` -#[must_use = "You should call .show()"] +#[must_use = "You should call .show_inside()"] #[derive(Default)] pub struct CentralPanel { frame: Option, } impl CentralPanel { + /// A central panel with no margin or background color + pub fn no_frame() -> Self { + Self { + frame: Some(Frame::NONE), + } + } + + /// A central panel with a background color and some inner margins + pub fn default_margins() -> Self { + Self { frame: None } + } + /// Change the background color, margins, etc. #[inline] pub fn frame(mut self, frame: Frame) -> Self { self.frame = Some(frame); self } -} -impl CentralPanel { /// Show the panel inside a [`Ui`]. pub fn show_inside( self, @@ -1012,13 +1030,19 @@ impl CentralPanel { panel_ui.set_clip_rect(panel_rect); // If we overflow, don't do so visibly (#4475) let frame = frame.unwrap_or_else(|| Frame::central_panel(ui.style())); - frame.show(&mut panel_ui, |ui| { + let response = frame.show(&mut panel_ui, |ui| { ui.expand_to_include_rect(ui.max_rect()); // Expand frame to include it all add_contents(ui) - }) + }); + + // Use up space in the parent: + ui.advance_cursor_after_rect(response.response.rect); + + response } /// Show the panel at the top level. + #[deprecated = "Use show_inside() instead"] pub fn show( self, ctx: &Context, @@ -1033,6 +1057,8 @@ impl CentralPanel { ctx: &Context, add_contents: Box R + 'c>, ) -> InnerResponse { + #![expect(deprecated)] + let id = Id::new((ctx.viewport_id(), "central_panel")); let mut panel_ui = Ui::new( diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index a9c00661d..0fb2a9f2a 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -465,7 +465,7 @@ impl<'a> Popup<'a> { pub fn get_best_align(&self) -> RectAlign { let expected_popup_size = self .get_expected_size() - .unwrap_or(vec2(self.width.unwrap_or(0.0), 0.0)); + .unwrap_or_else(|| vec2(self.width.unwrap_or(0.0), 0.0)); let Some(anchor_rect) = self.anchor.rect(self.id, &self.ctx) else { return self.rect_align; @@ -473,6 +473,7 @@ impl<'a> Popup<'a> { RectAlign::find_best_align( #[expect(clippy::iter_on_empty_collections)] + #[expect(clippy::or_fun_call)] once(self.rect_align).chain( self.alternative_aligns // Need the empty slice so the iters have the same type so we can unwrap_or diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 50cc28774..7ff943b3f 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -213,7 +213,7 @@ impl Resize { }); let mut state = State::load(ui.ctx(), id).unwrap_or_else(|| { - ui.ctx().request_repaint(); // counter frame delay + ui.request_repaint(); // counter frame delay let default_size = self .default_size @@ -362,20 +362,20 @@ impl Resize { paint_resize_corner(ui, &corner_response); if corner_response.hovered() || corner_response.dragged() { - ui.ctx().set_cursor_icon(CursorIcon::ResizeNwSe); + ui.set_cursor_icon(CursorIcon::ResizeNwSe); } } state.store(ui.ctx(), id); #[cfg(debug_assertions)] - if ui.ctx().style().debug.show_resize { - ui.ctx().debug_painter().debug_rect( + if ui.global_style().debug.show_resize { + ui.debug_painter().debug_rect( Rect::from_min_size(content_ui.min_rect().left_top(), state.desired_size), Color32::GREEN, "desired_size", ); - ui.ctx().debug_painter().debug_rect( + ui.debug_painter().debug_rect( Rect::from_min_size(content_ui.min_rect().left_top(), state.last_content_size), Color32::LIGHT_BLUE, "last_content_size", diff --git a/crates/egui/src/containers/scene.rs b/crates/egui/src/containers/scene.rs index 36222b138..093452289 100644 --- a/crates/egui/src/containers/scene.rs +++ b/crates/egui/src/containers/scene.rs @@ -244,8 +244,8 @@ impl Scene { && resp.contains_pointer() { let pointer_in_scene = to_global.inverse() * mouse_pos; - let zoom_delta = ui.ctx().input(|i| i.zoom_delta()); - let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta()); + let zoom_delta = ui.input(|i| i.zoom_delta()); + let pan_delta = ui.input(|i| i.smooth_scroll_delta()); // Most of the time we can return early. This is also important to // avoid `ui_from_scene` to change slightly due to floating point errors. diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 5ed4c31f3..afe1239c7 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -1,6 +1,6 @@ //! See [`ScrollArea`] for docs. -#![allow(clippy::needless_range_loop)] +#![expect(clippy::needless_range_loop)] use std::ops::{Add, AddAssign, BitOr, BitOrAssign}; @@ -9,7 +9,8 @@ use epaint::Margin; use crate::{ Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Response, Sense, Ui, UiBuilder, - UiKind, UiStackInfo, Vec2, Vec2b, emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, + UiKind, UiStackInfo, Vec2, Vec2b, WidgetInfo, emath, epaint, lerp, pass_state, pos2, remap, + remap_clamp, }; #[derive(Clone, Copy, Debug)] @@ -856,11 +857,11 @@ impl ScrollArea { if response.dragged() && let Some(cursor) = on_drag_cursor { - ui.ctx().set_cursor_icon(cursor); + ui.set_cursor_icon(cursor); } else if response.hovered() && let Some(cursor) = on_hover_cursor { - ui.ctx().set_cursor_icon(cursor); + ui.set_cursor_icon(cursor); } } @@ -1125,7 +1126,7 @@ impl Prepared { target_offset, }); } - ui.ctx().request_repaint(); + ui.request_repaint(); } } } @@ -1178,7 +1179,7 @@ impl Prepared { && direction_enabled[0] != direction_enabled[1]; for d in 0..2 { if direction_enabled[d] { - let scroll_delta = ui.ctx().input(|input| { + let scroll_delta = ui.input(|input| { if always_scroll_enabled_direction { // no bidirectional scrolling; allow horizontal scrolling without pressing shift input.smooth_scroll_delta()[0] + input.smooth_scroll_delta()[1] @@ -1195,7 +1196,7 @@ impl Prepared { state.offset[d] -= scroll_delta; // Clear scroll delta so no parent scroll will use it: - ui.ctx().input_mut(|input| { + ui.input_mut(|input| { if always_scroll_enabled_direction { input.smooth_scroll_delta()[0] = 0.0; input.smooth_scroll_delta()[1] = 0.0; @@ -1241,46 +1242,17 @@ impl Prepared { continue; } + let interact_id = id.with(d); + // Margin on either side of the scroll bar: let inner_margin = show_factor * scroll_style.bar_inner_margin; let outer_margin = show_factor * scroll_style.bar_outer_margin; - // top/bottom of a horizontal scroll (d==0). - // left/rigth of a vertical scroll (d==1). - let mut cross = if scroll_style.floating { - // The bounding rect of a fully visible bar. - // When we hover this area, we should show the full bar: - let max_bar_rect = if d == 0 { - outer_rect.with_min_y(outer_rect.max.y - outer_margin - scroll_style.bar_width) - } else { - outer_rect.with_min_x(outer_rect.max.x - outer_margin - scroll_style.bar_width) - }; + // bottom of a horizontal scroll (d==0). + // right of a vertical scroll (d==1). + let mut max_cross = outer_rect.max[1 - d] - outer_margin; - let is_hovering_bar_area = is_hovering_outer_rect - && ui.rect_contains_pointer(max_bar_rect) - && !is_dragging_background - || state.scroll_bar_interaction[d]; - - let is_hovering_bar_area_t = ui - .ctx() - .animate_bool_responsive(id.with((d, "bar_hover")), is_hovering_bar_area); - - let width = show_factor - * lerp( - scroll_style.floating_width..=scroll_style.bar_width, - is_hovering_bar_area_t, - ); - - let max_cross = outer_rect.max[1 - d] - outer_margin; - let min_cross = max_cross - width; - Rangef::new(min_cross, max_cross) - } else { - let min_cross = inner_rect.max[1 - d] + inner_margin; - let max_cross = outer_rect.max[1 - d] - outer_margin; - Rangef::new(min_cross, max_cross) - }; - - if ui.clip_rect().max[1 - d] < cross.max + outer_margin { + if ui.clip_rect().max[1 - d] - outer_margin < max_cross { // Move the scrollbar so it is visible. This is needed in some cases. // For instance: // * When we have a vertical-only scroll area in a top level panel, @@ -1290,21 +1262,59 @@ impl Prepared { // is outside the clip rectangle. // Really this should use the tighter clip_rect that ignores clip_rect_margin, but we don't store that. // clip_rect_margin is quite a hack. It would be nice to get rid of it. - let width = cross.max - cross.min; - cross.max = ui.clip_rect().max[1 - d] - outer_margin; - cross.min = cross.max - width; + max_cross = ui.clip_rect().max[1 - d] - outer_margin; } - let outer_scroll_bar_rect = if d == 0 { - Rect::from_min_max( - pos2(scroll_bar_rect.left(), cross.min), - pos2(scroll_bar_rect.right(), cross.max), - ) + let full_width = scroll_style.bar_width; + + // The bounding rect of a fully visible bar. + // When we hover this area, we should show the full bar: + let max_bar_rect = if d == 0 { + outer_rect.with_min_y(max_cross - full_width) } else { - Rect::from_min_max( - pos2(cross.min, scroll_bar_rect.top()), - pos2(cross.max, scroll_bar_rect.bottom()), - ) + outer_rect.with_min_x(max_cross - full_width) + }; + + let sense = if scroll_source.scroll_bar && ui.is_enabled() { + Sense::click_and_drag() + } else { + Sense::hover() + }; + + // We always sense interaction with the full width, even if we antimate it growing/shrinking. + // This is to present a more consistent target for our hit test code, + // and to avoid producing jitter in "thin widget" heuristics there. + // Also: it make sense to detect any hover where the scroll bar _will_ be. + let response = ui.interact(max_bar_rect, interact_id, sense); + + response.widget_info(|| WidgetInfo::new(crate::WidgetType::ScrollBar)); + + // top/bottom of a horizontal scroll (d==0). + // left/rigth of a vertical scroll (d==1). + let cross = if scroll_style.floating { + let is_hovering_bar_area = response.hovered() || state.scroll_bar_interaction[d]; + + let is_hovering_bar_area_t = ui + .ctx() + .animate_bool_responsive(id.with((d, "bar_hover")), is_hovering_bar_area); + + let width = show_factor + * lerp( + scroll_style.floating_width..=full_width, + is_hovering_bar_area_t, + ); + + let min_cross = max_cross - width; + Rangef::new(min_cross, max_cross) + } else { + let min_cross = inner_rect.max[1 - d] + inner_margin; + Rangef::new(min_cross, max_cross) + }; + + let outer_scroll_bar_rect = if d == 0 { + Rect::from_x_y_ranges(scroll_bar_rect.x_range(), cross) + } else { + Rect::from_x_y_ranges(cross, scroll_bar_rect.y_range()) }; let from_content = |content| { @@ -1344,14 +1354,6 @@ impl Prepared { let handle_rect = calculate_handle_rect(d, &state.offset); - let interact_id = id.with(d); - let sense = if scroll_source.scroll_bar && ui.is_enabled() { - Sense::click_and_drag() - } else { - Sense::hover() - }; - let response = ui.interact(outer_scroll_bar_rect, interact_id, sense); - state.scroll_bar_interaction[d] = response.hovered() || response.dragged(); if let Some(pointer_pos) = response.interact_pointer_pos() { @@ -1475,7 +1477,7 @@ impl Prepared { ui.advance_cursor_after_rect(outer_rect); if show_scroll_this_frame != state.show_scroll { - ui.ctx().request_repaint(); + ui.request_repaint(); } let available_offset = content_size - inner_rect.size(); diff --git a/crates/egui/src/containers/tooltip.rs b/crates/egui/src/containers/tooltip.rs index c46e21d57..78c5a726b 100644 --- a/crates/egui/src/containers/tooltip.rs +++ b/crates/egui/src/containers/tooltip.rs @@ -41,7 +41,7 @@ impl Tooltip<'_> { parent_widget: Id, anchor: impl Into, ) -> Self { - let width = ctx.style().spacing.tooltip_width; + let width = ctx.global_style().spacing.tooltip_width; Self { popup: Popup::new(parent_widget, ctx, anchor.into(), parent_layer) .kind(PopupKind::Tooltip) @@ -58,7 +58,7 @@ impl Tooltip<'_> { let popup = Popup::from_response(response) .kind(PopupKind::Tooltip) .gap(4.0) - .width(response.ctx.style().spacing.tooltip_width) + .width(response.ctx.global_style().spacing.tooltip_width) .sense(Sense::hover()); Self { popup, @@ -229,7 +229,7 @@ impl Tooltip<'_> { return false; } - let style = response.ctx.style(); + let style = response.ctx.global_style(); let tooltip_delay = style.interaction.tooltip_delay; let tooltip_grace_time = style.interaction.tooltip_grace_time; diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 8d7a95be7..6dc7927ae 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -70,6 +70,41 @@ impl<'open> Window<'open> { } } + /// Construct a [`Window`] that follows the given viewport. + pub fn from_viewport(id: ViewportId, viewport: ViewportBuilder) -> Self { + let ViewportBuilder { + title, + app_id, + inner_size, + min_inner_size, + max_inner_size, + resizable, + decorations, + title_shown, + minimize_button, + .. // A lot of things not implemented yet + } = viewport; + + let mut window = Self::new(title.or(app_id).unwrap_or_else(String::new)).id(Id::new(id)); + + if let Some(inner_size) = inner_size { + window = window.default_size(inner_size); + } + if let Some(min_inner_size) = min_inner_size { + window = window.min_size(min_inner_size); + } + if let Some(max_inner_size) = max_inner_size { + window = window.max_size(max_inner_size); + } + if let Some(resizable) = resizable { + window = window.resizable(resizable); + } + window = window.title_bar(decorations.unwrap_or(true) && title_shown.unwrap_or(true)); + window = window.collapsible(minimize_button.unwrap_or(true)); + + window + } + /// Assign a unique id to the Window. Required if the title changes, or is shared with another window. #[inline] pub fn id(mut self, id: Id) -> Self { @@ -440,9 +475,11 @@ impl Window<'_> { fade_out, } = self; + let style = ctx.global_style(); + let header_color = - frame.map_or_else(|| ctx.style().visuals.widgets.open.weak_bg_fill, |f| f.fill); - let mut window_frame = frame.unwrap_or_else(|| Frame::window(&ctx.style())); + frame.map_or_else(|| style.visuals.widgets.open.weak_bg_fill, |f| f.fill); + let mut window_frame = frame.unwrap_or_else(|| Frame::window(&style)); let is_explicitly_closed = matches!(open, Some(false)); let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible()); @@ -474,7 +511,6 @@ impl Window<'_> { // Calculate roughly how much larger the full window inner size is compared to the content rect let (title_bar_height_with_margin, title_content_spacing) = if with_title_bar { - let style = ctx.style(); let title_bar_inner_height = ctx .fonts_mut(|fonts| title.font_height(fonts, &style)) .at_least(style.spacing.interact_size.y); @@ -505,7 +541,7 @@ impl Window<'_> { // First check for resize to avoid frame delay: let last_frame_outer_rect = area.state().rect(); - let resize_interaction = resize_interaction( + let resize_interaction = do_resize_interaction( ctx, possible, area.id(), @@ -586,6 +622,17 @@ impl Window<'_> { .map_or((None, None), |ir| (Some(ir.inner), Some(ir.response))); let outer_rect = frame.end(&mut area_content_ui).rect; + + // Do resize interaction _again_, to move their widget rectangles on TOP of the rest of the window. + let resize_interaction = do_resize_interaction( + ctx, + possible, + area.id(), + area_layer_id, + last_frame_outer_rect, + window_frame, + ); + paint_resize_corner( &area_content_ui, &possible, @@ -887,7 +934,7 @@ fn move_and_resize_window(ctx: &Context, id: Id, interaction: &ResizeInteraction Some(rect.round_ui()) } -fn resize_interaction( +fn do_resize_interaction( ctx: &Context, possible: PossibleInteractions, _accessibility_parent: Id, @@ -921,7 +968,16 @@ fn resize_interaction( enabled: true, }, true, + InteractOptions { + // We call this multiple times. + // First to read the result (to avoid frame delay) + // and the second time to move it to the top, above the window contents. + move_to_top: true, + }, ); + + response.widget_info(|| WidgetInfo::new(crate::WidgetType::ResizeHandle)); + SideResponse { hover: response.hovered(), drag: response.dragged(), @@ -930,8 +986,10 @@ fn resize_interaction( let id = Id::new(layer_id).with("edge_drag"); - let side_grab_radius = ctx.style().interaction.resize_grab_radius_side; - let corner_grab_radius = ctx.style().interaction.resize_grab_radius_corner; + let style = ctx.global_style(); + + let side_grab_radius = style.interaction.resize_grab_radius_side; + let corner_grab_radius = style.interaction.resize_grab_radius_corner; let vetrtical_rect = |a: Pos2, b: Pos2| { Rect::from_min_max(a, b).expand2(vec2(side_grab_radius, -corner_grab_radius)) @@ -1141,8 +1199,7 @@ impl TitleBar { title_bar_height_with_margin: f32, ) -> Self { if false { - ui.ctx() - .debug_painter() + ui.debug_painter() .debug_rect(ui.min_rect(), Color32::GREEN, "outer_min_rect"); } @@ -1170,8 +1227,7 @@ impl TitleBar { let min_rect = Rect::from_min_size(ui.min_rect().min, min_inner_size); if false { - ui.ctx() - .debug_painter() + ui.debug_painter() .debug_rect(min_rect, Color32::LIGHT_BLUE, "min_rect"); } @@ -1208,8 +1264,7 @@ impl TitleBar { let title_inner_rect = self.inner_rect; if false { - ui.ctx() - .debug_painter() + ui.debug_painter() .debug_rect(self.inner_rect, Color32::RED, "TitleBar"); } @@ -1240,7 +1295,7 @@ impl TitleBar { let text_pos = text_pos - self.title_galley.rect.min.to_vec2(); ui.painter().galley( text_pos, - self.title_galley.clone(), + Arc::clone(&self.title_galley), ui.visuals().text_color(), ); @@ -1248,8 +1303,7 @@ impl TitleBar { // Paint separator between title and content: let content_rect = content_response.rect; if false { - ui.ctx() - .debug_painter() + ui.debug_painter() .debug_rect(content_rect, Color32::RED, "content_rect"); } let y = title_inner_rect.bottom() + window_frame.stroke.width / 2.0; @@ -1263,11 +1317,8 @@ impl TitleBar { let double_click_rect = title_inner_rect.shrink2(vec2(32.0, 0.0)); if false { - ui.ctx().debug_painter().debug_rect( - double_click_rect, - Color32::GREEN, - "double_click_rect", - ); + ui.debug_painter() + .debug_rect(double_click_rect, Color32::GREEN, "double_click_rect"); } let id = ui.unique_id().with("__window_title_bar"); diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 2ca7af4fc..f8996b8a3 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -19,8 +19,8 @@ use crate::{ ImmediateViewportRendererCallback, Key, KeyboardShortcut, Label, LayerId, Memory, ModifierNames, Modifiers, NumExt as _, Order, Painter, RawInput, Response, RichText, SafeAreaInsets, ScrollArea, Sense, Style, TextStyle, TextureHandle, TextureOptions, Ui, - ViewportBuilder, ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, - ViewportOutput, Widget as _, WidgetRect, WidgetText, + UiBuilder, ViewportBuilder, ViewportCommand, ViewportId, ViewportIdMap, ViewportIdPair, + ViewportIdSet, ViewportOutput, Visuals, Widget as _, WidgetRect, WidgetText, animation_manager::AnimationManager, containers::{self, area::AreaState}, data::output::PlatformOutput, @@ -34,8 +34,7 @@ use crate::{ os::OperatingSystem, output::FullOutput, pass_state::PassState, - plugin, - plugin::TypedPluginHandle, + plugin::{self, TypedPluginHandle}, resize, response, scroll_area, util::IdTypeMap, viewport::ViewportClass, @@ -194,7 +193,7 @@ impl ContextImpl { pub struct ViewportState { /// The type of viewport. /// - /// This will never be [`ViewportClass::Embedded`], + /// This will never be [`ViewportClass::EmbeddedWindow`], /// since those don't result in real viewports. pub class: ViewportClass, @@ -564,7 +563,10 @@ impl ContextImpl { log::trace!("Adding new fonts"); } - let text_alpha_from_coverage = self.memory.options.style().visuals.text_alpha_from_coverage; + let Visuals { + mut text_options, .. + } = self.memory.options.style().visuals; + text_options.max_texture_side = max_texture_side; let mut is_new = false; @@ -573,21 +575,17 @@ impl ContextImpl { is_new = true; profiling::scope!("Fonts::new"); - Fonts::new( - max_texture_side, - text_alpha_from_coverage, - self.font_definitions.clone(), - ) + Fonts::new(text_options, self.font_definitions.clone()) }); { profiling::scope!("Fonts::begin_pass"); - fonts.begin_pass(max_texture_side, text_alpha_from_coverage); + fonts.begin_pass(text_options); } } - fn accesskit_node_builder(&mut self, id: Id) -> &mut accesskit::Node { - let state = self.viewport().this_pass.accesskit_state.as_mut().unwrap(); + fn accesskit_node_builder(&mut self, id: Id) -> Option<&mut accesskit::Node> { + let state = self.viewport().this_pass.accesskit_state.as_mut()?; let builders = &mut state.nodes; if let std::collections::hash_map::Entry::Vacant(entry) = builders.entry(id) { @@ -611,13 +609,13 @@ impl ContextImpl { } let parent_id = find_accesskit_parent(&state.parent_map, builders, id) - .unwrap_or(crate::accesskit_root_id()); + .unwrap_or_else(crate::accesskit_root_id); - let parent_builder = builders.get_mut(&parent_id).unwrap(); + let parent_builder = builders.get_mut(&parent_id)?; parent_builder.push_child(id.accesskit_id()); } - builders.get_mut(&id).unwrap() + builders.get_mut(&id) } fn pixels_per_point(&mut self) -> f32 { @@ -758,6 +756,63 @@ impl Context { writer(&mut self.0.write()) } + /// Run the ui code for one frame. + /// + /// At most [`Options::max_passes`] calls will be issued to `run_ui`, + /// and only on the rare occasion that [`Context::request_discard`] is called. + /// Usually, it `run_ui` will only be called once. + /// + /// The [`Ui`] given to the callback will cover the entire [`Self::content_rect`], + /// with no margin or background color. Use [`crate::Frame`] to add that. + /// + /// You can organize your GUI using [`crate::Panel`]. + /// + /// Instead of calling `run_ui`, you can alternatively use [`Self::begin_pass`] and [`Context::end_pass`]. + /// + /// ``` + /// // One egui context that you keep reusing: + /// let mut ctx = egui::Context::default(); + /// + /// // Each frame: + /// let input = egui::RawInput::default(); + /// let full_output = ctx.run_ui(input, |ui| { + /// ui.label("Hello egui!"); + /// }); + /// // handle full_output + /// ``` + /// + /// ## See also + /// * [`Self::run`] + #[must_use] + pub fn run_ui(&self, new_input: RawInput, mut run_ui: impl FnMut(&mut Ui)) -> FullOutput { + self.run_ui_dyn(new_input, &mut run_ui) + } + + #[must_use] + fn run_ui_dyn(&self, new_input: RawInput, run_ui: &mut dyn FnMut(&mut Ui)) -> FullOutput { + let plugins = self.read(|ctx| ctx.plugins.ordered_plugins()); + #[expect(deprecated)] + self.run(new_input, |ctx| { + let mut top_ui = Ui::new( + ctx.clone(), + Id::new((ctx.viewport_id(), "__top_ui")), + UiBuilder::new() + .layer_id(LayerId::background()) + .max_rect(ctx.available_rect().round_ui()), + ); + + { + plugins.on_begin_pass(&mut top_ui); + run_ui(&mut top_ui); + plugins.on_end_pass(&mut top_ui); + } + + // Inform ctx about what we actually used, so we can shrink the native window to fit. + // TODO(emilk): make better use of this somehow + ctx.pass_state_mut(|state| state.allocate_central_panel(top_ui.min_rect())); + }) + } + /// Run the ui code for one frame. /// /// At most [`Options::max_passes`] calls will be issued to `run_ui`, @@ -781,8 +836,17 @@ impl Context { /// }); /// // handle full_output /// ``` + /// + /// ## See also + /// * [`Self::run_ui`] #[must_use] - pub fn run(&self, mut new_input: RawInput, mut run_ui: impl FnMut(&Self)) -> FullOutput { + #[deprecated = "Call run_ui instead"] + pub fn run(&self, new_input: RawInput, mut run_ui: impl FnMut(&Self)) -> FullOutput { + self.run_dyn(new_input, &mut run_ui) + } + + #[must_use] + fn run_dyn(&self, mut new_input: RawInput, run_ui: &mut dyn FnMut(&Self)) -> FullOutput { profiling::function_scope!(); let viewport_id = new_input.viewport_id; let max_passes = self.write(|ctx| ctx.memory.options.max_passes.get()); @@ -877,9 +941,6 @@ impl Context { plugins.on_input(&mut new_input); self.write(|ctx| ctx.begin_pass(new_input)); - - // Plugins run just after the pass starts: - plugins.on_begin_pass(self); } /// See [`Self::begin_pass`]. @@ -1101,7 +1162,7 @@ impl Context { let content_rect = self.content_rect(); let text = format!("🔥 {text}"); - let color = self.style().visuals.error_fg_color; + let color = self.global_style().visuals.error_fg_color; let painter = self.debug_painter(); painter.rect_stroke(widget_rect, 0.0, (1.0, color), StrokeKind::Outside); @@ -1165,7 +1226,12 @@ impl Context { /// /// `allow_focus` should usually be true, unless you call this function multiple times with the /// same widget, then `allow_focus` should only be true once (like in [`Ui::new`] (true) and [`Ui::remember_min_rect`] (false)). - pub(crate) fn create_widget(&self, w: WidgetRect, allow_focus: bool) -> Response { + pub(crate) fn create_widget( + &self, + w: WidgetRect, + allow_focus: bool, + options: crate::InteractOptions, + ) -> Response { let interested_in_focus = w.enabled && w.sense.is_focusable() && self.memory(|mem| mem.allows_interaction(w.layer_id)); @@ -1177,7 +1243,7 @@ impl Context { // We add all widgets here, even non-interactive ones, // because we need this list not only for checking for blocking widgets, // but also to know when we have reached the widget we are checking for cover. - viewport.this_pass.widgets.insert(w.layer_id, w); + viewport.this_pass.widgets.insert(w.layer_id, w, options); if allow_focus && interested_in_focus { ctx.memory.interested_in_focus(w.id, w.layer_id); @@ -1193,7 +1259,7 @@ impl Context { self.check_for_id_clash(w.id, w.rect, "widget"); } - #[allow(clippy::let_and_return, clippy::allow_attributes)] + #[allow(clippy::allow_attributes, clippy::let_and_return)] let res = self.get_response(w); #[cfg(debug_assertions)] @@ -1458,7 +1524,7 @@ impl Context { Painter::new(self.clone(), layer_id, content_rect) } - /// Paint on top of everything else + /// Paint on top of _everything_ else (even on top of tooltips and popups). pub fn debug_painter(&self) -> Painter { Self::layer_painter(self, LayerId::debug()) } @@ -1556,7 +1622,7 @@ impl Context { .. } = ModifierNames::SYMBOLS; - let font_id = TextStyle::Body.resolve(&self.style()); + let font_id = TextStyle::Body.resolve(&self.global_style()); self.fonts_mut(|f| { let mut font = f.fonts.font(&font_id.family); font.has_glyphs(alt) @@ -1898,7 +1964,7 @@ impl Context { pub fn add_plugin(&self, plugin: impl plugin::Plugin + 'static) { let handle = plugin::PluginHandle::new(plugin); - let added = self.write(|ctx| ctx.plugins.add(handle.clone())); + let added = self.write(|ctx| ctx.plugins.add(Arc::clone(&handle))); if added { handle.lock().dyn_plugin_mut().setup(self); @@ -1957,15 +2023,12 @@ impl Context { pub fn set_fonts(&self, font_definitions: FontDefinitions) { profiling::function_scope!(); - let mut update_fonts = true; - - self.read(|ctx| { - if let Some(current_fonts) = ctx.fonts.as_ref() { - // NOTE: this comparison is expensive since it checks TTF data for equality - if current_fonts.definitions() == &font_definitions { - update_fonts = false; // no need to update - } - } + let update_fonts = self.read(|ctx| { + // NOTE: this comparison is expensive since it checks TTF data for equality + // TODO(valadaptive): add_font only checks the *names* for equality. Change this? + ctx.fonts + .as_ref() + .is_none_or(|fonts| fonts.definitions() != &font_definitions) }); if update_fonts { @@ -2008,13 +2071,13 @@ impl Context { } /// The [`Theme`] used to select the appropriate [`Style`] (dark or light) - /// used by all subsequent windows, panels etc. + /// used by all subsequent popups, menus, etc. pub fn theme(&self) -> Theme { self.options(|opt| opt.theme()) } /// The [`Theme`] used to select between dark and light [`Self::style`] - /// as the active style used by all subsequent windows, panels etc. + /// as the active style used by all subsequent popups, menus, etc. /// /// Example: /// ``` @@ -2025,37 +2088,70 @@ impl Context { self.options_mut(|opt| opt.theme_preference = theme_preference.into()); } - /// The currently active [`Style`] used by all subsequent windows, panels etc. - pub fn style(&self) -> Arc