diff --git a/.gitattributes b/.gitattributes index fee89fd7ee6..b5348bf23b4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,8 @@ * text=auto eol=lf Cargo.lock linguist-generated=false *.png filter=lfs diff=lfs merge=lfs -text -# The icon.png is needed when including eframe via git, so it may not be in lfs + +# Exclude some small files from LFS: crates/eframe/data/* !filter !diff !merge text=auto eol=lf -examples/images/src/cat.webp filter=lfs diff=lfs merge=lfs -text -examples/images/screenshot.png filter=lfs diff=lfs merge=lfs -text +crates/egui_demo_lib/data/* !filter !diff !merge text=auto eol=lf +crates/egui/assets/* !filter !diff !merge text=auto eol=lf diff --git a/.github/workflows/deploy_web_demo.yml b/.github/workflows/deploy_web_demo.yml index c5924a3b0f0..6eb54696139 100644 --- a/.github/workflows/deploy_web_demo.yml +++ b/.github/workflows/deploy_web_demo.yml @@ -39,7 +39,7 @@ jobs: with: profile: minimal target: wasm32-unknown-unknown - toolchain: 1.79.0 + toolchain: 1.80.0 override: true - uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/png_only_on_lfs.yml b/.github/workflows/png_only_on_lfs.yml index e3d68ea3abf..624a7f4502d 100644 --- a/.github/workflows/png_only_on_lfs.yml +++ b/.github/workflows/png_only_on_lfs.yml @@ -13,11 +13,18 @@ jobs: - name: Check that png files are on git LFS run: | binary_extensions="png" - exclude="crates/eframe/data" + exclude_paths=( + "crates/eframe/data" + "crates/egui_demo_lib/data/" + "crates/egui/assets/" + ) # Find binary files that are not tracked by Git LFS for ext in $binary_extensions; do - if comm -23 <(git ls-files | grep -v "^$exclude" | sort) <(git lfs ls-files -n | sort) | grep "\.${ext}$"; then + # Create grep pattern to exclude multiple paths + exclude_pattern=$(printf "|^%s" "${exclude_paths[@]}" | sed 's/^|//') + + if comm -23 <(git ls-files | grep -Ev "$exclude_pattern" | sort) <(git lfs ls-files -n | sort) | grep "\.${ext}$"; then echo "Error: Found binary file with extension .$ext not tracked by git LFS. See CONTRIBUTING.md" exit 1 fi diff --git a/.github/workflows/preview_build.yml b/.github/workflows/preview_build.yml index 70cd5ce2aae..c44c10f0c83 100644 --- a/.github/workflows/preview_build.yml +++ b/.github/workflows/preview_build.yml @@ -42,9 +42,11 @@ jobs: - name: Generate meta.json env: PR_NUMBER: ${{ github.event.number }} - PR_BRANCH: ${{ github.head_ref }} + URL_SLUG: ${{ github.event.number }}-${{ github.head_ref }} run: | - echo "{\"pr_number\": \"$PR_NUMBER\", \"pr_branch\": \"$PR_BRANCH\"}" > meta.json + # Sanitize the URL_SLUG to only contain alphanumeric characters and dashes + URL_SLUG=$(echo $URL_SLUG | tr -cd '[:alnum:]-') + echo "{\"pr_number\": \"$PR_NUMBER\", \"url_slug\": \"$URL_SLUG\"}" > meta.json - uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/preview_cleanup.yml b/.github/workflows/preview_cleanup.yml index 3aba668a2b3..6e2b94b83aa 100644 --- a/.github/workflows/preview_cleanup.yml +++ b/.github/workflows/preview_cleanup.yml @@ -15,9 +15,14 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - run: mkdir -p empty_dir - - name: Url slug variable + - name: Generate URL_SLUG + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + URL_SLUG: ${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.ref }} run: | - echo "URL_SLUG=${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.ref }}" >> $GITHUB_ENV + # Sanitize the URL_SLUG to only contain alphanumeric characters and dashes + URL_SLUG=$(echo $URL_SLUG | tr -cd '[:alnum:]-') + echo "URL_SLUG=$URL_SLUG" >> $GITHUB_ENV - name: Deploy uses: JamesIves/github-pages-deploy-action@v4 with: diff --git a/.github/workflows/preview_deploy.yml b/.github/workflows/preview_deploy.yml index 9fdcfaf755f..f98f96da162 100644 --- a/.github/workflows/preview_deploy.yml +++ b/.github/workflows/preview_deploy.yml @@ -40,11 +40,7 @@ jobs: - name: Parse meta.json run: | echo "PR_NUMBER=$(jq -r .pr_number meta.json)" >> $GITHUB_ENV - echo "PR_BRANCH=$(jq -r .pr_branch meta.json)" >> $GITHUB_ENV - - - name: Url slug variable - run: | - echo "URL_SLUG=${{ env.PR_NUMBER }}-${{ env.PR_BRANCH }}" >> $GITHUB_ENV + echo "URL_SLUG=$(jq -r .url_slug meta.json)" >> $GITHUB_ENV - name: Deploy uses: JamesIves/github-pages-deploy-action@v4 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e669fe4c543..e2a5896980e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -18,7 +18,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.79.0 + toolchain: 1.80.0 - name: Install packages (Linux) if: runner.os == 'Linux' @@ -83,7 +83,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.79.0 + toolchain: 1.80.0 targets: wasm32-unknown-unknown - run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev @@ -155,7 +155,7 @@ jobs: - uses: actions/checkout@v4 - uses: EmbarkStudios/cargo-deny-action@v1 with: - rust-version: "1.79.0" + rust-version: "1.80.0" log-level: error command: check arguments: --target ${{ matrix.target }} @@ -170,7 +170,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.79.0 + toolchain: 1.80.0 targets: aarch64-linux-android - name: Set up cargo cache @@ -189,7 +189,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.79.0 + toolchain: 1.80.0 targets: aarch64-apple-ios - name: Set up cargo cache @@ -208,7 +208,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.79.0 + toolchain: 1.80.0 - name: Set up cargo cache uses: Swatinem/rust-cache@v2 @@ -232,7 +232,7 @@ jobs: lfs: true - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.79.0 + toolchain: 1.80.0 - name: Set up cargo cache uses: Swatinem/rust-cache@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eeeb1e0b87..4642da637f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,107 @@ # egui changelog All notable changes to the `egui` crate will be documented in this file. -NOTE: this is just the changelog for the core `egui` crate. [`eframe`](crates/eframe/CHANGELOG.md), [`ecolor`](crates/ecolor/CHANGELOG.md), [`epaint`](crates/epaint/CHANGELOG.md), [`egui-winit`](crates/egui-winit/CHANGELOG.md), [`egui_glow`](crates/egui_glow/CHANGELOG.md) and [`egui-wgpu`](crates/egui-wgpu/CHANGELOG.md) have their own changelogs! +This is just the changelog for the core `egui` crate. Every crate in this repository has their own changelog: +* [`epaint` changelog](crates/epaint/CHANGELOG.md) +* [`egui-winit` changelog](crates/egui-winit/CHANGELOG.md) +* [`egui-wgpu` changelog](crates/egui-wgpu/CHANGELOG.md) +* [`egui_kittest` changelog](crates/egui_kittest/CHANGELOG.md) +* [`egui_glow` changelog](crates/egui_glow/CHANGELOG.md) +* [`ecolor` changelog](crates/ecolor/CHANGELOG.md) +* [`eframe` changelog](crates/eframe/CHANGELOG.md) 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.30.0 - 2024-12-16 - Modals and better layer support + +### ✨ Highlights +* Add `Modal`, a popup that blocks input to the rest of the application ([#5358](https://github.com/emilk/egui/pull/5358) by [@lucasmerlin](https://github.com/lucasmerlin)) +* Improved support for transform layers ([#5465](https://github.com/emilk/egui/pull/5465), [#5468](https://github.com/emilk/egui/pull/5468), [#5429](https://github.com/emilk/egui/pull/5429)) + +#### `egui_kittest` +This release welcomes a new crate to the family: [egui_kittest](https://github.com/emilk/egui/tree/master/crates/egui_kittest). +`egui_kittest` is a testing framework for egui, allowing you to test both automation (simulated clicks and other events), +and also do screenshot testing (useful for regression tests). +`egui_kittest` is built using [`kittest`](https://github.com/rerun-io/kittest), which is a general GUI testing framework that aims to work with any Rust GUI (not just egui!). +`kittest` uses the accessibility library [`AccessKit`](https://github.com/AccessKit/accesskit/) for automatation and to query the widget tree. + +`kittest` and `egui_kittest` are written by [@lucasmerlin](https://github.com/lucasmerlin). + +Here's a quick example of how to use `egui_kittest` to test a checkbox: + +```rust +use egui::accesskit::Toggled; +use egui_kittest::{Harness, kittest::Queryable}; + +fn main() { + let mut checked = false; + let app = |ui: &mut egui::Ui| { + ui.checkbox(&mut checked, "Check me!"); + }; + + let mut harness = egui_kittest::Harness::new_ui(app); + + let checkbox = harness.get_by_label("Check me!"); + assert_eq!(checkbox.toggled(), Some(Toggled::False)); + checkbox.click(); + + harness.run(); + + let checkbox = harness.get_by_label("Check me!"); + assert_eq!(checkbox.toggled(), Some(Toggled::True)); + + // You can even render the ui and do image snapshot tests + #[cfg(all(feature = "wgpu", feature = "snapshot"))] + harness.wgpu_snapshot("readme_example"); +} +``` + +### ⭐ Added +* Add `Modal` and `Memory::set_modal_layer` [#5358](https://github.com/emilk/egui/pull/5358) by [@lucasmerlin](https://github.com/lucasmerlin) +* Add `UiBuilder::layer_id` and remove `layer_id` from `Ui::new` [#5195](https://github.com/emilk/egui/pull/5195) by [@emilk](https://github.com/emilk) +* Allow easier setting of background color for `TextEdit` [#5203](https://github.com/emilk/egui/pull/5203) by [@bircni](https://github.com/bircni) +* Set `Response::intrinsic_size` for `TextEdit` [#5266](https://github.com/emilk/egui/pull/5266) by [@lucasmerlin](https://github.com/lucasmerlin) +* Expose center position in `MultiTouchInfo` [#5247](https://github.com/emilk/egui/pull/5247) by [@lucasmerlin](https://github.com/lucasmerlin) +* `Context::add_font` [#5228](https://github.com/emilk/egui/pull/5228) by [@frederik-uni](https://github.com/frederik-uni) +* Impl from `Box` for `WidgetText`, `RichText` [#5309](https://github.com/emilk/egui/pull/5309) by [@dimtpap](https://github.com/dimtpap) +* Add `Window::scroll_bar_visibility` [#5231](https://github.com/emilk/egui/pull/5231) by [@Zeenobit](https://github.com/Zeenobit) +* Add `ComboBox::close_behavior` [#5305](https://github.com/emilk/egui/pull/5305) by [@avalsch](https://github.com/avalsch) +* Add `painter.line()` [#5291](https://github.com/emilk/egui/pull/5291) by [@bircni](https://github.com/bircni) +* Allow attaching custom user data to a screenshot command [#5416](https://github.com/emilk/egui/pull/5416) by [@emilk](https://github.com/emilk) +* Add `Button::image_tint_follows_text_color` [#5430](https://github.com/emilk/egui/pull/5430) by [@emilk](https://github.com/emilk) +* Consume escape keystroke when bailing out from a drag operation [#5433](https://github.com/emilk/egui/pull/5433) by [@abey79](https://github.com/abey79) +* Add `Context::layer_transform_to_global` & `layer_transform_from_global` [#5465](https://github.com/emilk/egui/pull/5465) by [@emilk](https://github.com/emilk) + +### πŸ”§ Changed +* Update MSRV to Rust 1.80 [#5421](https://github.com/emilk/egui/pull/5421), [#5457](https://github.com/emilk/egui/pull/5457) by [@emilk](https://github.com/emilk) +* Expand max font atlas size from 8k to 16k [#5257](https://github.com/emilk/egui/pull/5257) by [@rustbasic](https://github.com/rustbasic) +* Put font data into `Arc` to reduce memory consumption [#5276](https://github.com/emilk/egui/pull/5276) by [@StarStarJ](https://github.com/StarStarJ) +* Move `egui::util::cache` to `egui::cache`; add `FramePublisher` [#5426](https://github.com/emilk/egui/pull/5426) by [@emilk](https://github.com/emilk) +* Remove `Order::PanelResizeLine` [#5455](https://github.com/emilk/egui/pull/5455) by [@emilk](https://github.com/emilk) +* Drag-and-drop: keep cursor set by user, if any [#5467](https://github.com/emilk/egui/pull/5467) by [@abey79](https://github.com/abey79) +* Use `profiling` crate to support more profiler backends [#5150](https://github.com/emilk/egui/pull/5150) by [@teddemunnik](https://github.com/teddemunnik) +* Improve hit-test of thin widgets, and widgets across layers [#5468](https://github.com/emilk/egui/pull/5468) by [@emilk](https://github.com/emilk) + +### πŸ› Fixed +* Update `ScrollArea` drag velocity when drag stopped [#5175](https://github.com/emilk/egui/pull/5175) by [@valadaptive](https://github.com/valadaptive) +* Fix bug causing wrong-fire of `ViewportCommand::Visible` [#5244](https://github.com/emilk/egui/pull/5244) by [@rustbasic](https://github.com/rustbasic) +* Fix: `Ui::new_child` does not consider the `sizing_pass` field of `UiBuilder` [#5262](https://github.com/emilk/egui/pull/5262) by [@zhatuokun](https://github.com/zhatuokun) +* Fix Ctrl+Shift+Z redo shortcut [#5258](https://github.com/emilk/egui/pull/5258) by [@YgorSouza](https://github.com/YgorSouza) +* Fix: `Window::default_pos` does not work [#5315](https://github.com/emilk/egui/pull/5315) by [@rustbasic](https://github.com/rustbasic) +* Fix: `Sides` did not apply the layout position correctly [#5303](https://github.com/emilk/egui/pull/5303) by [@zhatuokun](https://github.com/zhatuokun) +* Respect `Style::override_font_id` in `RichText` [#5310](https://github.com/emilk/egui/pull/5310) by [@MStarha](https://github.com/MStarha) +* Fix disabled widgets "eating" focus [#5370](https://github.com/emilk/egui/pull/5370) by [@lucasmerlin](https://github.com/lucasmerlin) +* Fix cursor clipping in `TextEdit` inside a `ScrollArea` [#3660](https://github.com/emilk/egui/pull/3660) by [@juancampa](https://github.com/juancampa) +* Make text cursor always appear on click [#5420](https://github.com/emilk/egui/pull/5420) by [@juancampa](https://github.com/juancampa) +* Fix `on_hover_text_at_pointer` for transformed layers [#5429](https://github.com/emilk/egui/pull/5429) by [@emilk](https://github.com/emilk) +* Fix: don't interact with `Area` outside its `constrain_rect` [#5459](https://github.com/emilk/egui/pull/5459) by [@MScottMcBee](https://github.com/MScottMcBee) +* Fix broken images on egui.rs (move from git lfs to normal git) [#5480](https://github.com/emilk/egui/pull/5480) by [@emilk](https://github.com/emilk) +* Fix: `ui.new_child` should now respect `disabled` [#5483](https://github.com/emilk/egui/pull/5483) by [@emilk](https://github.com/emilk) +* Fix zero-width strokes still affecting the feathering color of boxes [#5485](https://github.com/emilk/egui/pull/5485) by [@emilk](https://github.com/emilk) + + ## 0.29.1 - 2024-10-01 - Bug fixes * Remove debug-assert triggered by `with_layer_id/dnd_drag_source` [#5191](https://github.com/emilk/egui/pull/5191) by [@emilk](https://github.com/emilk) * Fix id clash in `Ui::response` [#5192](https://github.com/emilk/egui/pull/5192) by [@emilk](https://github.com/emilk) diff --git a/Cargo.lock b/Cargo.lock index f0e1440bfcb..c119354f16d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,7 +39,7 @@ dependencies = [ "atspi-common", "serde", "thiserror", - "zvariant", + "zvariant 4.2.0", ] [[package]] @@ -81,7 +81,7 @@ dependencies = [ "futures-lite", "futures-util", "serde", - "zbus", + "zbus 4.4.0", ] [[package]] @@ -189,6 +189,23 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +[[package]] +name = "android_log-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937" + +[[package]] +name = "android_logger" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -258,6 +275,28 @@ dependencies = [ "libloading", ] +[[package]] +name = "ashpd" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c39d707614dbcc6bed00015539f488d8e3fe3e66ed60961efc0c90f4b380b3" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand", + "raw-window-handle 0.6.2", + "serde", + "serde_repr", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus 5.1.1", +] + [[package]] name = "async-broadcast" version = "0.7.1" @@ -336,6 +375,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-process" version = "2.3.0" @@ -401,18 +451,6 @@ dependencies = [ "syn", ] -[[package]] -name = "atk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -439,11 +477,11 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "zbus", + "zbus 4.4.0", "zbus-lockstep", "zbus-lockstep-macros", - "zbus_names", - "zvariant", + "zbus_names 3.0.0", + "zvariant 4.2.0", ] [[package]] @@ -455,7 +493,7 @@ dependencies = [ "atspi-common", "atspi-proxies", "futures-lite", - "zbus", + "zbus 4.4.0", ] [[package]] @@ -466,8 +504,8 @@ checksum = "a5e6c5de3e524cf967569722446bcd458d5032348554d9a17d7d72b041ab7496" dependencies = [ "atspi-common", "serde", - "zbus", - "zvariant", + "zbus 4.4.0", + "zvariant 4.2.0", ] [[package]] @@ -638,16 +676,6 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "libc", - "system-deps", -] - [[package]] name = "calloop" version = "0.13.0" @@ -697,16 +725,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -1139,6 +1157,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dlib" version = "0.5.2" @@ -1171,7 +1200,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" [[package]] name = "ecolor" -version = "0.29.1" +version = "0.30.0" dependencies = [ "bytemuck", "cint", @@ -1183,7 +1212,7 @@ dependencies = [ [[package]] name = "eframe" -version = "0.29.1" +version = "0.30.0" dependencies = [ "ahash", "bytemuck", @@ -1205,8 +1234,8 @@ dependencies = [ "objc2-foundation", "parking_lot", "percent-encoding", - "pollster", - "puffin", + "pollster 0.4.0", + "profiling", "raw-window-handle 0.6.2", "ron", "serde", @@ -1223,25 +1252,24 @@ dependencies = [ [[package]] name = "egui" -version = "0.29.1" +version = "0.30.0" dependencies = [ "accesskit", "ahash", "backtrace", "document-features", - "egui_kittest", "emath", "epaint", "log", "nohash-hasher", - "puffin", + "profiling", "ron", "serde", ] [[package]] name = "egui-wgpu" -version = "0.29.1" +version = "0.30.0" dependencies = [ "ahash", "bytemuck", @@ -1249,7 +1277,7 @@ dependencies = [ "egui", "epaint", "log", - "puffin", + "profiling", "thiserror", "type-map", "web-time", @@ -1259,7 +1287,7 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.29.1" +version = "0.30.0" dependencies = [ "accesskit_winit", "ahash", @@ -1267,7 +1295,7 @@ dependencies = [ "document-features", "egui", "log", - "puffin", + "profiling", "raw-window-handle 0.6.2", "serde", "smithay-clipboard", @@ -1279,7 +1307,7 @@ dependencies = [ [[package]] name = "egui_demo_app" -version = "0.29.1" +version = "0.30.0" dependencies = [ "bytemuck", "chrono", @@ -1292,6 +1320,7 @@ dependencies = [ "image", "log", "poll-promise", + "profiling", "puffin", "puffin_http", "rfd", @@ -1304,13 +1333,12 @@ dependencies = [ [[package]] name = "egui_demo_lib" -version = "0.29.1" +version = "0.30.0" dependencies = [ "chrono", "criterion", "document-features", "egui", - "egui_demo_lib", "egui_extras", "egui_kittest", "serde", @@ -1320,7 +1348,7 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.29.1" +version = "0.30.0" dependencies = [ "ahash", "chrono", @@ -1331,7 +1359,7 @@ dependencies = [ "image", "log", "mime_guess2", - "puffin", + "profiling", "resvg", "serde", "syntect", @@ -1339,7 +1367,7 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.29.1" +version = "0.30.0" dependencies = [ "ahash", "bytemuck", @@ -1351,7 +1379,7 @@ dependencies = [ "glutin-winit", "log", "memoffset", - "puffin", + "profiling", "wasm-bindgen", "web-sys", "winit", @@ -1359,16 +1387,15 @@ dependencies = [ [[package]] name = "egui_kittest" -version = "0.29.1" +version = "0.30.0" dependencies = [ "dify", "document-features", "egui", "egui-wgpu", - "egui_kittest", "image", "kittest", - "pollster", + "pollster 0.4.0", "wgpu", ] @@ -1394,7 +1421,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "emath" -version = "0.29.1" +version = "0.30.0" dependencies = [ "bytemuck", "document-features", @@ -1461,6 +1488,16 @@ dependencies = [ "syn", ] +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" version = "0.10.2" @@ -1475,7 +1512,7 @@ dependencies = [ [[package]] name = "epaint" -version = "0.29.1" +version = "0.30.0" dependencies = [ "ab_glyph", "ahash", @@ -1489,14 +1526,14 @@ dependencies = [ "log", "nohash-hasher", "parking_lot", - "puffin", + "profiling", "rayon", "serde", ] [[package]] name = "epaint_default_fonts" -version = "0.29.1" +version = "0.30.0" [[package]] name = "equivalent" @@ -1633,6 +1670,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.31" @@ -1698,36 +1744,6 @@ dependencies = [ "slab", ] -[[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gdk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -1784,19 +1800,6 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "gio-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "winapi", -] - [[package]] name = "gl_generator" version = "0.14.0" @@ -1808,16 +1811,6 @@ dependencies = [ "xml-rs", ] -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - [[package]] name = "glow" version = "0.14.2" @@ -1908,17 +1901,6 @@ dependencies = [ "gl_generator", ] -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - [[package]] name = "gpu-alloc" version = "0.6.0" @@ -1958,24 +1940,6 @@ dependencies = [ "bitflags 2.6.0", ] -[[package]] -name = "gtk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" -dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", -] - [[package]] name = "half" version = "2.4.1" @@ -1997,10 +1961,15 @@ dependencies = [ ] [[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +name = "hello_android" +version = "0.1.0" +dependencies = [ + "android_logger", + "eframe", + "egui_extras", + "log", + "winit", +] [[package]] name = "hello_world" @@ -2084,14 +2053,143 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -2250,7 +2348,8 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kittest" version = "0.1.0" -source = "git+https://github.com/rerun-io/kittest?branch=main#06e01f17fed36a997e1541f37b2d47e3771d7533" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f659954571a3c132356bd15c25f0dcf14d270a28ec5c58797adc2f432831bed5" dependencies = [ "accesskit", "accesskit_consumer", @@ -2311,6 +2410,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "litrs" version = "0.4.1" @@ -2551,17 +2656,6 @@ dependencies = [ "malloc_buf", ] -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - [[package]] name = "objc-sys" version = "0.3.5" @@ -2765,15 +2859,6 @@ dependencies = [ "objc2-foundation", ] -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - [[package]] name = "object" version = "0.36.5" @@ -2829,18 +2914,6 @@ dependencies = [ "ttf-parser", ] -[[package]] -name = "pango-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - [[package]] name = "parking" version = "2.2.1" @@ -2988,6 +3061,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + [[package]] name = "pollster" version = "0.4.0" @@ -2996,7 +3075,7 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" [[package]] name = "popups" -version = "0.29.1" +version = "0.30.0" dependencies = [ "eframe", "env_logger", @@ -3040,6 +3119,20 @@ name = "profiling" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +dependencies = [ + "profiling-procmacros", + "puffin", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +dependencies = [ + "quote", + "syn", +] [[package]] name = "puffin" @@ -3078,6 +3171,7 @@ dependencies = [ "eframe", "env_logger", "log", + "profiling", "puffin", "puffin_http", ] @@ -3273,21 +3367,20 @@ dependencies = [ [[package]] name = "rfd" -version = "0.13.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d8ab342bcc5436e04d3a4c1e09e17d74958bfaddf8d5fad6f85607df0f994f" +checksum = "46f6f80a9b882647d9014673ca9925d30ffc9750f2eed2b4490e189eaebd01e8" dependencies = [ - "block", - "dispatch", - "glib-sys", - "gobject-sys", - "gtk-sys", + "ashpd", + "block2", "js-sys", "log", - "objc", - "objc-foundation", - "objc_id", - "raw-window-handle 0.5.2", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "pollster 0.3.0", + "raw-window-handle 0.6.2", + "urlencoding", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -3485,15 +3578,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_spanned" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" -dependencies = [ - "serde", -] - [[package]] name = "serial_windows" version = "0.1.0" @@ -3634,6 +3718,12 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -3676,6 +3766,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syntect" version = "5.2.0" @@ -3698,25 +3799,6 @@ dependencies = [ "yaml-rust", ] -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - [[package]] name = "tempfile" version = "3.13.0" @@ -3858,40 +3940,23 @@ dependencies = [ ] [[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "tinyvec" -version = "1.8.0" +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ - "tinyvec_macros", + "displaydoc", + "zerovec", ] [[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "toml" -version = "0.8.19" +name = "tinytemplate" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", + "serde_json", ] [[package]] @@ -3899,9 +3964,6 @@ 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" @@ -3910,8 +3972,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", - "serde", - "serde_spanned", "toml_datetime", "winnow", ] @@ -3985,27 +4045,12 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" -[[package]] -name = "unicode-bidi" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" - [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" -[[package]] -name = "unicode-normalization" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = [ - "tinyvec", -] - [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -4054,15 +4099,22 @@ dependencies = [ [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "user_attention" version = "0.1.0" @@ -4116,10 +4168,16 @@ dependencies = [ ] [[package]] -name = "version-compare" -version = "0.2.0" +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "version_check" @@ -4495,7 +4553,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -4852,6 +4910,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "x11-dl" version = "2.21.0" @@ -4933,7 +5003,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xtask" -version = "0.29.1" +version = "0.30.0" [[package]] name = "yaml-rust" @@ -4944,6 +5014,30 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zbus" version = "4.4.0" @@ -4977,9 +5071,45 @@ dependencies = [ "uds_windows", "windows-sys 0.52.0", "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", + "zbus_macros 4.4.0", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1162094dc63b1629fcc44150bcceeaa80798cd28bcbe7fa987b65a034c258608" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-util", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow", + "xdg-home", + "zbus_macros 5.1.1", + "zbus_names 4.1.0", + "zvariant 5.1.0", ] [[package]] @@ -4989,7 +5119,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca2c5dceb099bddaade154055c926bb8ae507a18756ba1d8963fd7b51d8ed1d" dependencies = [ "zbus_xml", - "zvariant", + "zvariant 4.2.0", ] [[package]] @@ -5003,7 +5133,7 @@ dependencies = [ "syn", "zbus-lockstep", "zbus_xml", - "zvariant", + "zvariant 4.2.0", ] [[package]] @@ -5016,7 +5146,22 @@ dependencies = [ "proc-macro2", "quote", "syn", - "zvariant_utils", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zbus_macros" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd2dcdce3e2727f7d74b7e33b5a89539b3cc31049562137faf7ae4eb86cd16d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names 4.1.0", + "zvariant 5.1.0", + "zvariant_utils 3.0.2", ] [[package]] @@ -5027,7 +5172,19 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", - "zvariant", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus_names" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "856b7a38811f71846fd47856ceee8bccaec8399ff53fb370247e66081ace647b" +dependencies = [ + "serde", + "static_assertions", + "winnow", + "zvariant 5.1.0", ] [[package]] @@ -5039,8 +5196,8 @@ dependencies = [ "quick-xml 0.30.0", "serde", "static_assertions", - "zbus_names", - "zvariant", + "zbus_names 3.0.0", + "zvariant 4.2.0", ] [[package]] @@ -5064,12 +5221,55 @@ dependencies = [ "syn", ] +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zune-core" version = "0.4.12" @@ -5095,7 +5295,23 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "zvariant_derive", + "zvariant_derive 4.2.0", +] + +[[package]] +name = "zvariant" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1200ee6ac32f1e5a312e455a949a4794855515d34f9909f4a3e082d14e1a56f" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "url", + "winnow", + "zvariant_derive 5.1.0", + "zvariant_utils 3.0.2", ] [[package]] @@ -5108,7 +5324,20 @@ dependencies = [ "proc-macro2", "quote", "syn", - "zvariant_utils", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zvariant_derive" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687e3b97fae6c9104fbbd36c73d27d149abf04fb874e2efbd84838763daa8916" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils 3.0.2", ] [[package]] @@ -5121,3 +5350,17 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zvariant_utils" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20d1d011a38f12360e5fcccceeff5e2c42a8eb7f27f0dcba97a0862ede05c9c6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "static_assertions", + "syn", + "winnow", +] diff --git a/Cargo.toml b/Cargo.toml index ab53c6cfbb8..b551f9720de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,8 +23,8 @@ members = [ [workspace.package] edition = "2021" license = "MIT OR Apache-2.0" -rust-version = "1.79" -version = "0.29.1" +rust-version = "1.80" +version = "0.30.0" [profile.release] @@ -55,18 +55,18 @@ opt-level = 2 [workspace.dependencies] -emath = { version = "0.29.1", path = "crates/emath", default-features = false } -ecolor = { version = "0.29.1", path = "crates/ecolor", default-features = false } -epaint = { version = "0.29.1", path = "crates/epaint", default-features = false } -epaint_default_fonts = { version = "0.29.1", path = "crates/epaint_default_fonts" } -egui = { version = "0.29.1", path = "crates/egui", default-features = false } -egui-winit = { version = "0.29.1", path = "crates/egui-winit", default-features = false } -egui_extras = { version = "0.29.1", path = "crates/egui_extras", default-features = false } -egui-wgpu = { version = "0.29.1", path = "crates/egui-wgpu", default-features = false } -egui_demo_lib = { version = "0.29.1", path = "crates/egui_demo_lib", default-features = false } -egui_glow = { version = "0.29.1", path = "crates/egui_glow", default-features = false } -egui_kittest = { version = "0.29.1", path = "crates/egui_kittest", default-features = false } -eframe = { version = "0.29.1", path = "crates/eframe", default-features = false } +emath = { version = "0.30.0", path = "crates/emath", default-features = false } +ecolor = { version = "0.30.0", path = "crates/ecolor", default-features = false } +epaint = { version = "0.30.0", path = "crates/epaint", default-features = false } +epaint_default_fonts = { version = "0.30.0", path = "crates/epaint_default_fonts" } +egui = { version = "0.30.0", path = "crates/egui", default-features = false } +egui-winit = { version = "0.30.0", path = "crates/egui-winit", default-features = false } +egui_extras = { version = "0.30.0", path = "crates/egui_extras", default-features = false } +egui-wgpu = { version = "0.30.0", path = "crates/egui-wgpu", default-features = false } +egui_demo_lib = { version = "0.30.0", path = "crates/egui_demo_lib", default-features = false } +egui_glow = { version = "0.30.0", path = "crates/egui_glow", default-features = false } +egui_kittest = { version = "0.30.0", path = "crates/egui_kittest", default-features = false } +eframe = { version = "0.30.0", path = "crates/eframe", default-features = false } ahash = { version = "0.8.11", default-features = false, features = [ "no-rng", # we don't need DOS-protection, so we let users opt-in to it instead @@ -82,11 +82,12 @@ glutin = { version = "0.32.0", default-features = false } glutin-winit = { version = "0.5.0", default-features = false } home = "0.5.9" image = { version = "0.25", default-features = false } -kittest = { git = "https://github.com/rerun-io/kittest", version = "0.1", branch = "main" } +kittest = { version = "0.1" } log = { version = "0.4", features = ["std"] } nohash-hasher = "0.2" parking_lot = "0.12" pollster = "0.4" +profiling = { version = "1.0.16", default-features = false } puffin = "0.19" puffin_http = "0.16" raw-window-handle = "0.6.0" @@ -106,13 +107,13 @@ winit = { version = "0.30.5", default-features = false } unsafe_code = "deny" elided_lifetimes_in_paths = "warn" -future_incompatible = "warn" -nonstandard_style = "warn" -rust_2018_idioms = "warn" +future_incompatible = { level = "warn", priority = -1 } +nonstandard_style = { level = "warn", priority = -1 } +rust_2018_idioms = { level = "warn", priority = -1 } rust_2021_prelude_collisions = "warn" semicolon_in_expressions_from_macros = "warn" trivial_numeric_casts = "warn" -unsafe_op_in_unsafe_fn = "warn" # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668 +unsafe_op_in_unsafe_fn = "warn" # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668 unused_extern_crates = "warn" unused_import_braces = "warn" unused_lifetimes = "warn" @@ -233,6 +234,7 @@ ref_patterns = "warn" rest_pat_in_fully_bound_structs = "warn" same_functions_in_if_condition = "warn" semicolon_if_nothing_returned = "warn" +single_char_pattern = "warn" single_match_else = "warn" str_split_at_newline = "warn" str_to_string = "warn" diff --git a/RELEASES.md b/RELEASES.md index b3777a3aedc..79bf23bc614 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -53,8 +53,7 @@ We don't update the MSRV in a patch release, unless we really, really need to. * [ ] run `scripts/generate_example_screenshots.sh` if needed * [ ] write a short release note that fits in a tweet * [ ] record gif for `CHANGELOG.md` release note (and later twitter post) -* [ ] update changelogs using `scripts/generate_changelog.py --write` - - For major releases, always diff to the latest MAJOR release, e.g. `--commit-range 0.29.0..HEAD` +* [ ] update changelogs using `scripts/generate_changelog.py --version 0.x.0 --write` * [ ] bump version numbers in workspace `Cargo.toml` ## Actual release @@ -79,8 +78,9 @@ I usually do this all on the `master` branch, but doing it in a release branch i (cd crates/epaint && cargo publish --quiet) && echo "βœ… epaint" (cd crates/egui && cargo publish --quiet) && echo "βœ… egui" (cd crates/egui-winit && cargo publish --quiet) && echo "βœ… egui-winit" -(cd crates/egui_extras && cargo publish --quiet) && echo "βœ… egui_extras" (cd crates/egui-wgpu && cargo publish --quiet) && echo "βœ… egui-wgpu" +(cd crates/egui_kittest && cargo publish --quiet) && echo "βœ… egui_kittest" +(cd crates/egui_extras && cargo publish --quiet) && echo "βœ… egui_extras" (cd crates/egui_demo_lib && cargo publish --quiet) && echo "βœ… egui_demo_lib" (cd crates/egui_glow && cargo publish --quiet) && echo "βœ… egui_glow" (cd crates/eframe && cargo publish --quiet) && echo "βœ… eframe" diff --git a/clippy.toml b/clippy.toml index cd4a25cab4f..9e5fdd1e593 100644 --- a/clippy.toml +++ b/clippy.toml @@ -3,7 +3,7 @@ # ----------------------------------------------------------------------------- # Section identical to scripts/clippy_wasm/clippy.toml: -msrv = "1.79" +msrv = "1.80" allow-unwrap-in-tests = true diff --git a/crates/ecolor/CHANGELOG.md b/crates/ecolor/CHANGELOG.md index ec42258baf8..37b5555b60a 100644 --- a/crates/ecolor/CHANGELOG.md +++ b/crates/ecolor/CHANGELOG.md @@ -6,6 +6,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.30.0 - 2024-12-16 +* Use boxed slice for lookup table to avoid stack overflow [#5212](https://github.com/emilk/egui/pull/5212) by [@YgorSouza](https://github.com/YgorSouza) +* Add `Color32::mul` [#5437](https://github.com/emilk/egui/pull/5437) by [@emilk](https://github.com/emilk) + + ## 0.29.1 - 2024-10-01 Nothing new diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index 65b5a731985..cbb6cf411ca 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -7,6 +7,25 @@ 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.30.0 - 2024-12-16 - Android support +NOTE: you now need to enable the `wayland` or `x11` features to get Linux support, including getting it to work on most CI systems. + +### ⭐ Added +* Support `ViewportCommand::Screenshot` on web [#5438](https://github.com/emilk/egui/pull/5438) by [@lucasmerlin](https://github.com/lucasmerlin) + +### πŸ”§ Changed +* Android support [#5318](https://github.com/emilk/egui/pull/5318) by [@parasyte](https://github.com/parasyte) +* Update MSRV to 1.80 [#5457](https://github.com/emilk/egui/pull/5457) by [@emilk](https://github.com/emilk) +* Use `profiling` crate to support more profiler backends [#5150](https://github.com/emilk/egui/pull/5150) by [@teddemunnik](https://github.com/teddemunnik) +* Update glow to 0.16 [#5395](https://github.com/emilk/egui/pull/5395) by [@sagudev](https://github.com/sagudev) +* Forward `x11` and `wayland` features to `glutin` [#5391](https://github.com/emilk/egui/pull/5391) by [@e00E](https://github.com/e00E) + +### πŸ› Fixed +* iOS: Support putting UI next to the dynamic island [#5211](https://github.com/emilk/egui/pull/5211) by [@frederik-uni](https://github.com/frederik-uni) +* Prevent panic when copying text outside of a secure context [#5326](https://github.com/emilk/egui/pull/5326) by [@YgorSouza](https://github.com/YgorSouza) +* Fix accidental change of `FallbackEgl` to `PreferEgl` [#5408](https://github.com/emilk/egui/pull/5408) by [@e00E](https://github.com/e00E) + + ## 0.29.1 - 2024-10-01 - Fix backspace/arrow keys on X11 * Linux: Disable IME to fix backspace/arrow keys [#5188](https://github.com/emilk/egui/pull/5188) by [@emilk](https://github.com/emilk) diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 87ab1d3b2c9..eb4766dfaab 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -35,7 +35,7 @@ default = [ "accesskit", "default_fonts", "glow", - "wayland", + "wayland", # Required for Linux support (including CI!) "web_screen_reader", "winit/default", "x11", @@ -71,22 +71,17 @@ persistence = [ "serde", ] -## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -## -## `eframe` will call `puffin::GlobalProfiler::lock().new_frame()` for you +## Enables wayland support and fixes clipboard issue. ## -## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. -puffin = [ - "dep:puffin", - "egui/puffin", - "egui_glow?/puffin", - "egui-wgpu?/puffin", - "egui-winit/puffin", +## If you are compiling for Linux (or want to test on a CI system using Linux), you should enable this feature. +wayland = [ + "egui-winit/wayland", + "egui-wgpu?/wayland", + "egui_glow?/wayland", + "glutin?/wayland", + "glutin-winit?/wayland", ] -## Enables wayland support and fixes clipboard issue. -wayland = ["egui-winit/wayland", "egui-wgpu?/wayland", "egui_glow?/wayland", "glutin?/wayland", "glutin-winit?/wayland"] - ## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`) on web. ## ## For other platforms, use the `accesskit` feature instead. @@ -111,7 +106,15 @@ web_screen_reader = [ wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"] ## Enables compiling for x11. -x11 = ["egui-winit/x11", "egui-wgpu?/x11", "egui_glow?/x11", "glutin?/x11", "glutin?/glx", "glutin-winit?/x11", "glutin-winit?/glx"] +x11 = [ + "egui-winit/x11", + "egui-wgpu?/x11", + "egui_glow?/x11", + "glutin?/x11", + "glutin?/glx", + "glutin-winit?/x11", + "glutin-winit?/glx", +] ## If set, eframe will look for the env-var `EFRAME_SCREENSHOT_TO` and write a screenshot to that location, and then quit. ## This is used to generate images for examples. @@ -127,6 +130,7 @@ ahash.workspace = true document-features.workspace = true log.workspace = true parking_lot.workspace = true +profiling.workspace = true raw-window-handle.workspace = true static_assertions = "1.1.0" web-time.workspace = true @@ -154,10 +158,15 @@ egui-wgpu = { workspace = true, optional = true, features = [ ] } # if wgpu is used, use it with winit pollster = { workspace = true, optional = true } # needed for wgpu -glutin = { workspace = true, optional = true, default-features = false, features = ["egl", "wgl"] } -glutin-winit = { workspace = true, optional = true, default-features = false, features = ["egl", "wgl"] } +glutin = { workspace = true, optional = true, default-features = false, features = [ + "egl", + "wgl", +] } +glutin-winit = { workspace = true, optional = true, default-features = false, features = [ + "egl", + "wgl", +] } home = { workspace = true, optional = true } -puffin = { workspace = true, optional = true } wgpu = { workspace = true, optional = true, features = [ # Let's enable some backends so that users can use `eframe` out-of-the-box # without having to explicitly opt-in to backends diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 9f4f6dde8b1..45d8335018e 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -364,6 +364,16 @@ pub struct NativeOptions { /// /// Defaults to true. pub dithering: bool, + + /// Android application for `winit`'s event loop. + /// + /// This value is required on Android to correctly create the event loop. See + /// [`EventLoopBuilder::build`] and [`with_android_app`] for details. + /// + /// [`EventLoopBuilder::build`]: winit::event_loop::EventLoopBuilder::build + /// [`with_android_app`]: winit::platform::android::EventLoopBuilderExtAndroid::with_android_app + #[cfg(target_os = "android")] + pub android_app: Option, } #[cfg(not(target_arch = "wasm32"))] @@ -383,6 +393,9 @@ impl Clone for NativeOptions { persistence_path: self.persistence_path.clone(), + #[cfg(target_os = "android")] + android_app: self.android_app.clone(), + ..*self } } @@ -424,6 +437,9 @@ impl Default for NativeOptions { persistence_path: None, dithering: true, + + #[cfg(target_os = "android")] + android_app: None, } } } @@ -772,8 +788,7 @@ pub struct IntegrationInfo { /// /// This includes [`App::update`] as well as rendering (except for vsync waiting). /// - /// For a more detailed view of cpu usage, use the [`puffin`](https://crates.io/crates/puffin) - /// profiler together with the `puffin` feature of `eframe`. + /// For a more detailed view of cpu usage, connect your preferred profiler by enabling it's feature in [`profiling`](https://crates.io/crates/profiling). /// /// `None` if this is the first frame. pub cpu_usage: Option, @@ -815,7 +830,7 @@ impl Storage for DummyStorage { /// Get and deserialize the [RON](https://github.com/ron-rs/ron) stored at the given key. #[cfg(feature = "ron")] pub fn get_value(storage: &dyn Storage, key: &str) -> Option { - crate::profile_function!(key); + profiling::function_scope!(key); storage .get_string(key) .and_then(|value| match ron::from_str(&value) { @@ -831,7 +846,7 @@ pub fn get_value(storage: &dyn Storage, key: &st /// Serialize the given value as [RON](https://github.com/ron-rs/ron) and store with the given key. #[cfg(feature = "ron")] pub fn set_value(storage: &mut dyn Storage, key: &str, value: &T) { - crate::profile_function!(key); + profiling::function_scope!(key); match ron::ser::to_string(value) { Ok(string) => storage.set_string(key, string), Err(err) => log::error!("eframe failed to encode data using ron: {}", err), diff --git a/crates/eframe/src/icon_data.rs b/crates/eframe/src/icon_data.rs index ed514d00e1f..4851bee64b3 100644 --- a/crates/eframe/src/icon_data.rs +++ b/crates/eframe/src/icon_data.rs @@ -22,7 +22,7 @@ pub trait IconDataExt { /// # Errors /// If this is not a valid png. pub fn from_png_bytes(png_bytes: &[u8]) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let image = image::load_from_memory(png_bytes)?; Ok(from_image(image)) } @@ -38,7 +38,7 @@ fn from_image(image: image::DynamicImage) -> IconData { impl IconDataExt for IconData { fn to_image(&self) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let Self { rgba, width, @@ -48,7 +48,7 @@ impl IconDataExt for IconData { } fn to_png_bytes(&self) -> Result, String> { - crate::profile_function!(); + profiling::function_scope!(); let image = self.to_image()?; let mut png_bytes: Vec = Vec::new(); image diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 80da6235792..7b342a4c21f 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -129,6 +129,17 @@ //! ## Feature flags #![doc = document_features::document_features!()] //! +//! ## Instrumentation +//! This crate supports using the [profiling](https://crates.io/crates/profiling) crate for instrumentation. +//! You can enable features on the profiling crates in your application to add instrumentation for all +//! crates that support it, including egui. See the profiling crate docs for more information. +//! ```toml +//! [dependencies] +//! profiling = "1.0" +//! [features] +//! profile-with-puffin = ["profiling/profile-with-puffin"] +//! ``` +//! #![warn(missing_docs)] // let's keep eframe well-documented #![allow(clippy::needless_doctest_main)] @@ -445,33 +456,3 @@ impl std::fmt::Display for Error { /// Short for `Result`. pub type Result = std::result::Result; - -// --------------------------------------------------------------------------- - -mod profiling_scopes { - #![allow(unused_macros)] - #![allow(unused_imports)] - - /// Profiling macro for feature "puffin" - macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} - -#[allow(unused_imports)] -pub(crate) use profiling_scopes::{profile_function, profile_scope}; diff --git a/crates/eframe/src/native/app_icon.rs b/crates/eframe/src/native/app_icon.rs index 840bf367b27..8591ba2a8cc 100644 --- a/crates/eframe/src/native/app_icon.rs +++ b/crates/eframe/src/native/app_icon.rs @@ -59,7 +59,7 @@ enum AppIconStatus { /// Since window creation can be lazy, call this every frame until it's either successfully or gave up. /// (See [`AppIconStatus`]) fn set_title_and_icon(_title: &str, _icon_data: Option<&IconData>) -> AppIconStatus { - crate::profile_function!(); + profiling::function_scope!(); #[cfg(target_os = "windows")] { @@ -201,7 +201,7 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus { #[allow(unsafe_code)] fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus { use crate::icon_data::IconDataExt as _; - crate::profile_function!(); + profiling::function_scope!(); use objc2::ClassType; use objc2_app_kit::{NSApplication, NSImage}; @@ -237,7 +237,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS log::trace!("NSImage::initWithData…"); let app_icon = NSImage::initWithData(NSImage::alloc(), &data); - crate::profile_scope!("setApplicationIconImage_"); + profiling::scope!("setApplicationIconImage_"); log::trace!("setApplicationIconImage…"); app.setApplicationIconImage(app_icon.as_deref()); } @@ -246,7 +246,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS if let Some(main_menu) = app.mainMenu() { if let Some(item) = main_menu.itemAtIndex(0) { if let Some(app_menu) = item.submenu() { - crate::profile_scope!("setTitle_"); + profiling::scope!("setTitle_"); app_menu.setTitle(&NSString::from_str(title)); } } diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 5f9d555e3a5..03b5f2dcd46 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -19,7 +19,7 @@ pub fn viewport_builder( native_options: &mut epi::NativeOptions, window_settings: Option, ) -> ViewportBuilder { - crate::profile_function!(); + profiling::function_scope!(); let mut viewport_builder = native_options.viewport.clone(); @@ -67,7 +67,7 @@ pub fn viewport_builder( #[cfg(not(target_os = "ios"))] if native_options.centered { - crate::profile_scope!("center"); + profiling::scope!("center"); if let Some(monitor) = event_loop .primary_monitor() .or_else(|| event_loop.available_monitors().next()) @@ -94,8 +94,7 @@ pub fn apply_window_settings( window: &winit::window::Window, window_settings: Option, ) { - crate::profile_function!(); - + profiling::function_scope!(); if let Some(window_settings) = window_settings { window_settings.initialize_window(window); } @@ -103,12 +102,11 @@ pub fn apply_window_settings( #[cfg(not(target_os = "ios"))] fn largest_monitor_point_size(egui_zoom_factor: f32, event_loop: &ActiveEventLoop) -> egui::Vec2 { - crate::profile_function!(); - + profiling::function_scope!(); let mut max_size = egui::Vec2::ZERO; let available_monitors = { - crate::profile_scope!("available_monitors"); + profiling::scope!("available_monitors"); event_loop.available_monitors() }; @@ -238,7 +236,7 @@ impl EpiIntegration { egui_winit: &mut egui_winit::State, event: &winit::event::WindowEvent, ) -> EventResponse { - crate::profile_function!(egui_winit::short_window_event_description(event)); + profiling::function_scope!(egui_winit::short_window_event_description(event)); use winit::event::{ElementState, MouseButton, WindowEvent}; @@ -276,10 +274,10 @@ impl EpiIntegration { let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { if let Some(viewport_ui_cb) = viewport_ui_cb { // Child viewport - crate::profile_scope!("viewport_callback"); + profiling::scope!("viewport_callback"); viewport_ui_cb(egui_ctx); } else { - crate::profile_scope!("App::update"); + profiling::scope!("App::update"); app.update(egui_ctx, &mut self.frame); } }); @@ -306,7 +304,7 @@ impl EpiIntegration { } pub fn post_rendering(&mut self, window: &winit::window::Window) { - crate::profile_function!(); + profiling::function_scope!(); if std::mem::take(&mut self.is_first_frame) { // We keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279 window.set_visible(true); @@ -332,11 +330,11 @@ impl EpiIntegration { pub fn save(&mut self, _app: &mut dyn epi::App, _window: Option<&winit::window::Window>) { #[cfg(feature = "persistence")] if let Some(storage) = self.frame.storage_mut() { - crate::profile_function!(); + profiling::function_scope!(); if let Some(window) = _window { if self.persist_window { - crate::profile_scope!("native_window"); + profiling::scope!("native_window"); epi::set_value( storage, STORAGE_WINDOW_KEY, @@ -345,23 +343,23 @@ impl EpiIntegration { } } if _app.persist_egui_memory() { - crate::profile_scope!("egui_memory"); + profiling::scope!("egui_memory"); self.egui_ctx .memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem)); } { - crate::profile_scope!("App::save"); + profiling::scope!("App::save"); _app.save(storage); } - crate::profile_scope!("Storage::flush"); + profiling::scope!("Storage::flush"); storage.flush(); } } } fn load_default_egui_icon() -> egui::IconData { - crate::profile_function!(); + profiling::function_scope!(); crate::icon_data::from_png_bytes(&include_bytes!("../../data/icon.png")[..]).unwrap() } @@ -372,7 +370,7 @@ const STORAGE_EGUI_MEMORY_KEY: &str = "egui"; const STORAGE_WINDOW_KEY: &str = "window"; pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option { - crate::profile_function!(); + profiling::function_scope!(); #[cfg(feature = "persistence")] { epi::get_value(_storage?, STORAGE_WINDOW_KEY) @@ -382,7 +380,7 @@ pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option) -> Option { - crate::profile_function!(); + profiling::function_scope!(); #[cfg(feature = "persistence")] { epi::get_value(_storage?, STORAGE_EGUI_MEMORY_KEY) diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index c47a71e6867..346c46b4254 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -100,7 +100,7 @@ pub struct FileStorage { impl Drop for FileStorage { fn drop(&mut self) { if let Some(join_handle) = self.last_save_join_handle.take() { - crate::profile_scope!("wait_for_save"); + profiling::scope!("wait_for_save"); join_handle.join().ok(); } } @@ -109,7 +109,7 @@ impl Drop for FileStorage { impl FileStorage { /// Store the state in this .ron file. pub(crate) fn from_ron_filepath(ron_filepath: impl Into) -> Self { - crate::profile_function!(); + profiling::function_scope!(); let ron_filepath: PathBuf = ron_filepath.into(); log::debug!("Loading app state from {:?}…", ron_filepath); Self { @@ -122,7 +122,7 @@ impl FileStorage { /// Find a good place to put the files that the OS likes. pub fn from_app_id(app_id: &str) -> Option { - crate::profile_function!(app_id); + profiling::function_scope!(); if let Some(data_dir) = storage_dir(app_id) { if let Err(err) = std::fs::create_dir_all(&data_dir) { log::warn!( @@ -155,7 +155,7 @@ impl crate::Storage for FileStorage { fn flush(&mut self) { if self.dirty { - crate::profile_function!(); + profiling::scope!("FileStorage::flush"); self.dirty = false; let file_path = self.ron_filepath.clone(); @@ -184,7 +184,7 @@ impl crate::Storage for FileStorage { } fn save_to_disk(file_path: &PathBuf, kv: &HashMap) { - crate::profile_function!(); + profiling::function_scope!(); if let Some(parent_dir) = file_path.parent() { if !parent_dir.exists() { @@ -199,7 +199,7 @@ fn save_to_disk(file_path: &PathBuf, kv: &HashMap) { let mut writer = std::io::BufWriter::new(file); let config = Default::default(); - crate::profile_scope!("ron::serialize"); + profiling::scope!("ron::serialize"); if let Err(err) = ron::ser::to_writer_pretty(&mut writer, &kv, config) .and_then(|_| writer.flush().map_err(|err| err.into())) { @@ -220,7 +220,7 @@ fn read_ron(ron_path: impl AsRef) -> Option where T: serde::de::DeserializeOwned, { - crate::profile_function!(); + profiling::function_scope!(); match std::fs::File::open(ron_path) { Ok(file) => { let reader = std::io::BufReader::new(file); diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 51c0cadce2a..f17c6ad5091 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -129,7 +129,7 @@ impl<'app> GlowWinitApp<'app> { native_options: NativeOptions, app_creator: AppCreator<'app>, ) -> Self { - crate::profile_function!(); + profiling::function_scope!(); Self { repaint_proxy: Arc::new(egui::mutex::Mutex::new(event_loop.create_proxy())), app_name: app_name.to_owned(), @@ -146,8 +146,7 @@ impl<'app> GlowWinitApp<'app> { storage: Option<&dyn Storage>, native_options: &mut NativeOptions, ) -> Result<(GlutinWindowContext, egui_glow::Painter)> { - crate::profile_function!(); - + profiling::function_scope!(); let window_settings = epi_integration::load_window_settings(storage); let winit_window_builder = epi_integration::viewport_builder( @@ -172,7 +171,7 @@ impl<'app> GlowWinitApp<'app> { } let gl = unsafe { - crate::profile_scope!("glow::Context::from_loader_function"); + profiling::scope!("glow::Context::from_loader_function"); Arc::new(glow::Context::from_loader_function(|s| { let s = std::ffi::CString::new(s) .expect("failed to construct C string from string for gl proc address"); @@ -195,7 +194,7 @@ impl<'app> GlowWinitApp<'app> { &mut self, event_loop: &ActiveEventLoop, ) -> Result<&mut GlowWinitRunning<'app>> { - crate::profile_function!(); + profiling::function_scope!(); let storage = if let Some(file) = &self.native_options.persistence_path { epi_integration::create_storage_with_file(file) @@ -308,7 +307,7 @@ impl<'app> GlowWinitApp<'app> { raw_display_handle: window.display_handle().map(|h| h.as_raw()), raw_window_handle: window.window_handle().map(|h| h.as_raw()), }; - crate::profile_scope!("app_creator"); + profiling::scope!("app_creator"); app_creator(&cc).map_err(crate::Error::AppCreation)? }; @@ -369,7 +368,7 @@ impl<'app> WinitApp for GlowWinitApp<'app> { fn save_and_destroy(&mut self) { if let Some(mut running) = self.running.take() { - crate::profile_function!(); + profiling::function_scope!(); running.integration.save( running.app.as_mut(), @@ -486,7 +485,7 @@ impl<'app> GlowWinitRunning<'app> { event_loop: &ActiveEventLoop, window_id: WindowId, ) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let Some(viewport_id) = self .glutin @@ -498,8 +497,7 @@ impl<'app> GlowWinitRunning<'app> { return Ok(EventResult::Wait); }; - #[cfg(feature = "puffin")] - puffin::GlobalProfiler::lock().new_frame(); + profiling::finish_frame!(); let mut frame_timer = crate::stopwatch::Stopwatch::new(); frame_timer.start(); @@ -698,7 +696,7 @@ impl<'app> GlowWinitRunning<'app> { { // vsync - don't count as frame-time: frame_timer.pause(); - crate::profile_scope!("swap_buffers"); + profiling::scope!("swap_buffers"); let context = current_gl_context .as_ref() .ok_or(egui_glow::PainterError::from( @@ -726,7 +724,7 @@ impl<'app> GlowWinitRunning<'app> { if window.is_minimized() == Some(true) { // On Mac, a minimized Window uses up all CPU: // https://github.com/emilk/egui/issues/325 - crate::profile_scope!("minimized_sleep"); + profiling::scope!("minimized_sleep"); std::thread::sleep(std::time::Duration::from_millis(10)); } @@ -857,7 +855,7 @@ fn change_gl_context( not_current_gl_context: &mut Option, gl_surface: &glutin::surface::Surface, ) { - crate::profile_function!(); + profiling::function_scope!(); if !cfg!(target_os = "windows") { // According to https://github.com/emilk/egui/issues/4289 @@ -866,7 +864,7 @@ fn change_gl_context( // See https://github.com/emilk/egui/issues/4173 if let Some(current_gl_context) = current_gl_context { - crate::profile_scope!("is_current"); + profiling::scope!("is_current"); if gl_surface.is_current(current_gl_context) { return; // Early-out to save a lot of time. } @@ -876,7 +874,7 @@ fn change_gl_context( let not_current = if let Some(not_current_context) = not_current_gl_context.take() { not_current_context } else { - crate::profile_scope!("make_not_current"); + profiling::scope!("make_not_current"); current_gl_context .take() .unwrap() @@ -884,7 +882,7 @@ fn change_gl_context( .unwrap() }; - crate::profile_scope!("make_current"); + profiling::scope!("make_current"); *current_gl_context = Some(not_current.make_current(gl_surface).unwrap()); } @@ -896,7 +894,7 @@ impl GlutinWindowContext { native_options: &NativeOptions, event_loop: &ActiveEventLoop, ) -> Result { - crate::profile_function!(); + profiling::function_scope!(); // There is a lot of complexity with opengl creation, // so prefer extensive logging to get all the help we can to debug issues. @@ -952,7 +950,7 @@ impl GlutinWindowContext { ))); let (window, gl_config) = { - crate::profile_scope!("DisplayBuilder::build"); + profiling::scope!("DisplayBuilder::build"); display_builder .build( @@ -995,7 +993,7 @@ impl GlutinWindowContext { .build(glutin_raw_window_handle); let gl_context_result = unsafe { - crate::profile_scope!("create_context"); + profiling::scope!("create_context"); gl_config .display() .create_context(&gl_config, &context_attributes) @@ -1070,7 +1068,7 @@ impl GlutinWindowContext { /// /// Errors will be logged. fn initialize_all_windows(&mut self, event_loop: &ActiveEventLoop) { - crate::profile_function!(); + profiling::function_scope!(); let viewports: Vec = self.viewports.keys().copied().collect(); @@ -1088,7 +1086,7 @@ impl GlutinWindowContext { viewport_id: ViewportId, event_loop: &ActiveEventLoop, ) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let viewport = self .viewports @@ -1268,7 +1266,7 @@ impl GlutinWindowContext { egui_ctx: &egui::Context, viewport_output: &ViewportIdMap, ) { - crate::profile_function!(); + profiling::function_scope!(); for ( viewport_id, @@ -1329,7 +1327,7 @@ fn initialize_or_update_viewport( mut builder: ViewportBuilder, viewport_ui_cb: Option>, ) -> &mut Viewport { - crate::profile_function!(); + profiling::function_scope!(); if builder.icon.is_none() { // Inherit icon from parent @@ -1393,7 +1391,7 @@ fn render_immediate_viewport( beginning: Instant, immediate_viewport: ImmediateViewport<'_>, ) { - crate::profile_function!(); + profiling::function_scope!(); let ImmediateViewport { ids, @@ -1516,7 +1514,7 @@ fn render_immediate_viewport( ); { - crate::profile_scope!("swap_buffers"); + profiling::scope!("swap_buffers"); if let Err(err) = gl_surface.swap_buffers(current_gl_context) { log::error!("swap_buffers failed: {err}"); } diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 43dd07ee0da..e328877a4be 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -17,14 +17,25 @@ use crate::{ // ---------------------------------------------------------------------------- fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result> { - crate::profile_function!(); + #[cfg(target_os = "android")] + use winit::platform::android::EventLoopBuilderExtAndroid as _; + + profiling::function_scope!(); let mut builder = winit::event_loop::EventLoop::with_user_event(); + #[cfg(target_os = "android")] + let mut builder = + builder.with_android_app(native_options.android_app.take().ok_or_else(|| { + crate::Error::AppCreation(Box::from( + "`NativeOptions` is missing required `android_app`", + )) + })?); + if let Some(hook) = std::mem::take(&mut native_options.event_loop_builder) { hook(&mut builder); } - crate::profile_scope!("EventLoopBuilder::build"); + profiling::scope!("EventLoopBuilder::build"); Ok(builder.build()?) } @@ -175,7 +186,7 @@ impl WinitAppWrapper { impl ApplicationHandler for WinitAppWrapper { fn suspended(&mut self, event_loop: &ActiveEventLoop) { - crate::profile_function!("Event::Suspended"); + profiling::scope!("Event::Suspended"); event_loop_context::with_event_loop_context(event_loop, move || { let event_result = self.winit_app.suspended(event_loop); @@ -184,7 +195,7 @@ impl ApplicationHandler for WinitAppWrapper { } fn resumed(&mut self, event_loop: &ActiveEventLoop) { - crate::profile_function!("Event::Resumed"); + profiling::scope!("Event::Resumed"); // Nb: Make sure this guard is dropped after this function returns. event_loop_context::with_event_loop_context(event_loop, move || { @@ -208,7 +219,7 @@ impl ApplicationHandler for WinitAppWrapper { device_id: winit::event::DeviceId, event: winit::event::DeviceEvent, ) { - crate::profile_function!(egui_winit::short_device_event_description(&event)); + profiling::function_scope!(egui_winit::short_device_event_description(&event)); // Nb: Make sure this guard is dropped after this function returns. event_loop_context::with_event_loop_context(event_loop, move || { @@ -218,7 +229,7 @@ impl ApplicationHandler for WinitAppWrapper { } fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) { - crate::profile_function!(match &event { + profiling::function_scope!(match &event { UserEvent::RequestRepaint { .. } => "UserEvent::RequestRepaint", #[cfg(feature = "accesskit")] UserEvent::AccessKitActionRequest(_) => "UserEvent::AccessKitActionRequest", @@ -274,7 +285,7 @@ impl ApplicationHandler for WinitAppWrapper { window_id: WindowId, event: winit::event::WindowEvent, ) { - crate::profile_function!(egui_winit::short_window_event_description(&event)); + profiling::function_scope!(egui_winit::short_window_event_description(&event)); // Nb: Make sure this guard is dropped after this function returns. event_loop_context::with_event_loop_context(event_loop, move || { diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index d13bed0bf41..93643c2230e 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -102,7 +102,7 @@ impl<'app> WgpuWinitApp<'app> { native_options: NativeOptions, app_creator: AppCreator<'app>, ) -> Self { - crate::profile_function!(); + profiling::function_scope!(); #[cfg(feature = "__screenshot")] assert!( @@ -181,10 +181,10 @@ impl<'app> WgpuWinitApp<'app> { window: Window, builder: ViewportBuilder, ) -> crate::Result<&mut WgpuWinitRunning<'app>> { - crate::profile_function!(); - + profiling::function_scope!(); #[allow(unsafe_code, unused_mut, unused_unsafe)] let mut painter = egui_wgpu::winit::Painter::new( + egui_ctx.clone(), self.native_options.wgpu_options.clone(), self.native_options.multisampling.max(1) as _, egui_wgpu::depth_format_from_bits( @@ -198,7 +198,7 @@ impl<'app> WgpuWinitApp<'app> { let window = Arc::new(window); { - crate::profile_scope!("set_window"); + profiling::scope!("set_window"); pollster::block_on(painter.set_window(ViewportId::ROOT, Some(window.clone())))?; } @@ -267,7 +267,7 @@ impl<'app> WgpuWinitApp<'app> { raw_window_handle: window.window_handle().map(|h| h.as_raw()), }; let app = { - crate::profile_scope!("user_app_creator"); + profiling::scope!("user_app_creator"); app_creator(&cc).map_err(crate::Error::AppCreation)? }; @@ -489,7 +489,7 @@ impl<'app> WinitApp for WgpuWinitApp<'app> { impl<'app> WgpuWinitRunning<'app> { fn save_and_destroy(&mut self) { - crate::profile_function!(); + profiling::function_scope!(); let mut shared = self.shared.borrow_mut(); if let Some(Viewport { window, .. }) = shared.viewports.get(&ViewportId::ROOT) { @@ -507,7 +507,7 @@ impl<'app> WgpuWinitRunning<'app> { /// This is called both for the root viewport, and all deferred viewports fn run_ui_and_paint(&mut self, window_id: WindowId) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let Some(viewport_id) = self .shared @@ -519,8 +519,7 @@ impl<'app> WgpuWinitRunning<'app> { return Ok(EventResult::Wait); }; - #[cfg(feature = "puffin")] - puffin::GlobalProfiler::lock().new_frame(); + profiling::finish_frame!(); let Self { app, @@ -532,7 +531,7 @@ impl<'app> WgpuWinitRunning<'app> { frame_timer.start(); let (viewport_ui_cb, raw_input) = { - crate::profile_scope!("Prepare"); + profiling::scope!("Prepare"); let mut shared_lock = shared.borrow_mut(); let SharedState { @@ -576,7 +575,7 @@ impl<'app> WgpuWinitRunning<'app> { egui_winit::update_viewport_info(info, &integration.egui_ctx, window, false); { - crate::profile_scope!("set_window"); + profiling::scope!("set_window"); pollster::block_on(painter.set_window(viewport_id, Some(window.clone())))?; } @@ -593,6 +592,8 @@ impl<'app> WgpuWinitRunning<'app> { .map(|(id, viewport)| (*id, viewport.info.clone())) .collect(); + painter.handle_screenshots(&mut raw_input.events); + (viewport_ui_cb, raw_input) }; @@ -652,37 +653,14 @@ impl<'app> WgpuWinitRunning<'app> { true } }); - let screenshot_requested = !screenshot_commands.is_empty(); - let (vsync_secs, screenshot) = painter.paint_and_update_textures( + let vsync_secs = painter.paint_and_update_textures( viewport_id, pixels_per_point, app.clear_color(&egui_ctx.style().visuals), &clipped_primitives, &textures_delta, - screenshot_requested, + screenshot_commands, ); - match (screenshot_requested, screenshot) { - (false, None) => {} - (true, Some(screenshot)) => { - let screenshot = Arc::new(screenshot); - for user_data in screenshot_commands { - egui_winit - .egui_input_mut() - .events - .push(egui::Event::Screenshot { - viewport_id, - user_data, - image: screenshot.clone(), - }); - } - } - (true, None) => { - log::error!("Bug in egui_wgpu: screenshot requested, but no screenshot was taken"); - } - (false, Some(_)) => { - log::warn!("Bug in egui_wgpu: Got screenshot without requesting it"); - } - } for action in viewport.actions_requested.drain() { match action { @@ -739,7 +717,7 @@ impl<'app> WgpuWinitRunning<'app> { if window.is_minimized() == Some(true) { // On Mac, a minimized Window uses up all CPU: // https://github.com/emilk/egui/issues/325 - crate::profile_scope!("minimized_sleep"); + profiling::scope!("minimized_sleep"); std::thread::sleep(std::time::Duration::from_millis(10)); } } @@ -866,7 +844,7 @@ impl Viewport { return; // we already have one } - crate::profile_function!(); + profiling::function_scope!(); let viewport_id = self.ids.this; @@ -907,7 +885,7 @@ fn create_window( storage: Option<&dyn Storage>, native_options: &mut NativeOptions, ) -> Result<(Window, ViewportBuilder), winit::error::OsError> { - crate::profile_function!(); + profiling::function_scope!(); let window_settings = epi_integration::load_window_settings(storage); let viewport_builder = epi_integration::viewport_builder( @@ -928,7 +906,7 @@ fn render_immediate_viewport( shared: &RefCell, immediate_viewport: ImmediateViewport<'_>, ) { - crate::profile_function!(); + profiling::function_scope!(); let ImmediateViewport { ids, @@ -1008,7 +986,7 @@ fn render_immediate_viewport( }; { - crate::profile_scope!("set_window"); + profiling::scope!("set_window"); if let Err(err) = pollster::block_on(painter.set_window(ids.this, Some(window.clone()))) { log::error!( "when rendering viewport_id={:?}, set_window Error {err}", @@ -1024,7 +1002,7 @@ fn render_immediate_viewport( [0.0, 0.0, 0.0, 0.0], &clipped_primitives, &textures_delta, - false, + vec![], ); egui_winit.handle_platform_output(window, platform_output); @@ -1116,7 +1094,7 @@ fn initialize_or_update_viewport<'a>( viewport_ui_cb: Option>, painter: &mut egui_wgpu::winit::Painter, ) -> &'a mut Viewport { - crate::profile_function!(); + profiling::function_scope!(); if builder.icon.is_none() { // Inherit icon from parent diff --git a/crates/eframe/src/native/winit_integration.rs b/crates/eframe/src/native/winit_integration.rs index e9d214103b8..2b6c54a67f9 100644 --- a/crates/eframe/src/native/winit_integration.rs +++ b/crates/eframe/src/native/winit_integration.rs @@ -11,7 +11,7 @@ use egui_winit::accesskit_winit; /// Create an egui context, restoring it from storage if possible. pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context { - crate::profile_function!(); + profiling::function_scope!(); pub const IS_DESKTOP: bool = cfg!(any( target_os = "freebsd", diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 00cc8f0c182..13ad762874a 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -1,4 +1,4 @@ -use egui::TexturesDelta; +use egui::{TexturesDelta, UserData, ViewportCommand}; use crate::{epi, App}; @@ -16,6 +16,10 @@ pub struct AppRunner { last_save_time: f64, pub(crate) text_agent: TextAgent, + // If not empty, the painter should capture n frames from now. + // zero means capture the exact next frame. + screenshot_commands_with_frame_delay: Vec<(UserData, usize)>, + // Output for the last run: textures_delta: TexturesDelta, clipped_primitives: Option>, @@ -36,7 +40,8 @@ impl AppRunner { app_creator: epi::AppCreator<'static>, text_agent: TextAgent, ) -> Result { - let painter = super::ActiveWebPainter::new(canvas, &web_options).await?; + let egui_ctx = egui::Context::default(); + let painter = super::ActiveWebPainter::new(egui_ctx.clone(), canvas, &web_options).await?; let info = epi::IntegrationInfo { web_info: epi::WebInfo { @@ -47,7 +52,6 @@ impl AppRunner { }; let storage = LocalStorage::default(); - let egui_ctx = egui::Context::default(); egui_ctx.set_os(egui::os::OperatingSystem::from_user_agent( &super::user_agent().unwrap_or_default(), )); @@ -110,6 +114,7 @@ impl AppRunner { needs_repaint, last_save_time: now_sec(), text_agent, + screenshot_commands_with_frame_delay: vec![], textures_delta: Default::default(), clipped_primitives: None, }; @@ -205,6 +210,8 @@ impl AppRunner { pub fn logic(&mut self) { // We sometimes miss blur/focus events due to the text agent, so let's just poll each frame: self.update_focus(); + // We might have received a screenshot + self.painter.handle_screenshots(&mut self.input.raw.events); let canvas_size = super::canvas_size_in_points(self.canvas(), self.egui_ctx()); let mut raw_input = self.input.new_frame(canvas_size); @@ -225,12 +232,20 @@ impl AppRunner { if viewport_output.len() > 1 { log::warn!("Multiple viewports not yet supported on the web"); } - for viewport_output in viewport_output.values() { - for command in &viewport_output.commands { - // TODO(emilk): handle some of the commands - log::warn!( - "Unhandled egui viewport command: {command:?} - not implemented in web backend" - ); + for (_viewport_id, viewport_output) in viewport_output { + for command in viewport_output.commands { + match command { + ViewportCommand::Screenshot(user_data) => { + self.screenshot_commands_with_frame_delay + .push((user_data, 1)); + } + _ => { + // TODO(emilk): handle some of the commands + log::warn!( + "Unhandled egui viewport command: {command:?} - not implemented in web backend" + ); + } + } } } @@ -245,11 +260,27 @@ impl AppRunner { let clipped_primitives = std::mem::take(&mut self.clipped_primitives); if let Some(clipped_primitives) = clipped_primitives { + let mut screenshot_commands = vec![]; + self.screenshot_commands_with_frame_delay + .retain_mut(|(user_data, frame_delay)| { + if *frame_delay == 0 { + screenshot_commands.push(user_data.clone()); + false + } else { + *frame_delay -= 1; + true + } + }); + if !self.screenshot_commands_with_frame_delay.is_empty() { + self.egui_ctx().request_repaint(); + } + if let Err(err) = self.painter.paint_and_update_textures( self.app.clear_color(&self.egui_ctx.style().visuals), &clipped_primitives, self.egui_ctx.pixels_per_point(), &textures_delta, + screenshot_commands, ) { log::error!("Failed to paint: {}", super::string_from_js_value(&err)); } @@ -260,7 +291,7 @@ impl AppRunner { self.frame.info.cpu_usage = Some(cpu_usage_seconds); } - fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) { + fn handle_platform_output(&self, platform_output: egui::PlatformOutput) { #[cfg(feature = "web_screen_reader")] if self.egui_ctx.options(|o| o.screen_reader) { super::screen_reader::speak(&platform_output.events_description()); diff --git a/crates/eframe/src/web/web_painter.rs b/crates/eframe/src/web/web_painter.rs index b5164b915c0..fe751bf169f 100644 --- a/crates/eframe/src/web/web_painter.rs +++ b/crates/eframe/src/web/web_painter.rs @@ -1,3 +1,4 @@ +use egui::{Event, UserData}; use wasm_bindgen::JsValue; /// Renderer for a browser canvas. @@ -16,14 +17,19 @@ pub(crate) trait WebPainter { fn max_texture_side(&self) -> usize; /// Update all internal textures and paint gui. + /// When `capture` isn't empty, the rendered screen should be captured. + /// Once the screenshot is ready, the screenshot should be returned via [`Self::handle_screenshots`]. fn paint_and_update_textures( &mut self, clear_color: [f32; 4], clipped_primitives: &[egui::ClippedPrimitive], pixels_per_point: f32, textures_delta: &egui::TexturesDelta, + capture: Vec, ) -> Result<(), JsValue>; + fn handle_screenshots(&mut self, events: &mut Vec); + /// Destroy all resources. fn destroy(&mut self); } diff --git a/crates/eframe/src/web/web_painter_glow.rs b/crates/eframe/src/web/web_painter_glow.rs index e13cb0018cd..876a6d78e2d 100644 --- a/crates/eframe/src/web/web_painter_glow.rs +++ b/crates/eframe/src/web/web_painter_glow.rs @@ -1,9 +1,10 @@ +use egui::{Event, UserData, ViewportId}; +use egui_glow::glow; +use std::sync::Arc; use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; use web_sys::HtmlCanvasElement; -use egui_glow::glow; - use crate::{WebGlContextOption, WebOptions}; use super::web_painter::WebPainter; @@ -11,6 +12,7 @@ use super::web_painter::WebPainter; pub(crate) struct WebPainterGlow { canvas: HtmlCanvasElement, painter: egui_glow::Painter, + screenshots: Vec<(egui::ColorImage, Vec)>, } impl WebPainterGlow { @@ -18,7 +20,11 @@ impl WebPainterGlow { self.painter.gl() } - pub async fn new(canvas: HtmlCanvasElement, options: &WebOptions) -> Result { + pub async fn new( + _ctx: egui::Context, + canvas: HtmlCanvasElement, + options: &WebOptions, + ) -> Result { let (gl, shader_prefix) = init_glow_context_from_canvas(&canvas, options.webgl_context_option)?; #[allow(clippy::arc_with_non_send_sync)] @@ -27,7 +33,11 @@ impl WebPainterGlow { let painter = egui_glow::Painter::new(gl, shader_prefix, None, options.dithering) .map_err(|err| format!("Error starting glow painter: {err}"))?; - Ok(Self { canvas, painter }) + Ok(Self { + canvas, + painter, + screenshots: Vec::new(), + }) } } @@ -46,6 +56,7 @@ impl WebPainter for WebPainterGlow { clipped_primitives: &[egui::ClippedPrimitive], pixels_per_point: f32, textures_delta: &egui::TexturesDelta, + capture: Vec, ) -> Result<(), JsValue> { let canvas_dimension = [self.canvas.width(), self.canvas.height()]; @@ -57,6 +68,11 @@ impl WebPainter for WebPainterGlow { self.painter .paint_primitives(canvas_dimension, pixels_per_point, clipped_primitives); + if !capture.is_empty() { + let image = self.painter.read_screen_rgba(canvas_dimension); + self.screenshots.push((image, capture)); + } + for &id in &textures_delta.free { self.painter.free_texture(id); } @@ -67,6 +83,19 @@ impl WebPainter for WebPainterGlow { fn destroy(&mut self) { self.painter.destroy(); } + + fn handle_screenshots(&mut self, events: &mut Vec) { + for (image, data) in self.screenshots.drain(..) { + let image = Arc::new(image); + for data in data { + events.push(Event::Screenshot { + viewport_id: ViewportId::default(), + image: image.clone(), + user_data: data, + }); + } + } + } } /// Returns glow context and shader prefix. diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index ec487622e38..591d4224d3b 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -1,14 +1,13 @@ use std::sync::Arc; +use super::web_painter::WebPainter; +use crate::WebOptions; +use egui::{Event, UserData, ViewportId}; +use egui_wgpu::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState}; +use egui_wgpu::{RenderState, SurfaceErrorAction, WgpuSetup}; use wasm_bindgen::JsValue; use web_sys::HtmlCanvasElement; -use egui_wgpu::{RenderState, SurfaceErrorAction, WgpuSetup}; - -use crate::WebOptions; - -use super::web_painter::WebPainter; - pub(crate) struct WebPainterWgpu { canvas: HtmlCanvasElement, surface: wgpu::Surface<'static>, @@ -17,6 +16,10 @@ pub(crate) struct WebPainterWgpu { on_surface_error: Arc SurfaceErrorAction>, depth_format: Option, depth_texture_view: Option, + screen_capture_state: Option, + capture_tx: CaptureSender, + capture_rx: CaptureReceiver, + ctx: egui::Context, } impl WebPainterWgpu { @@ -54,6 +57,7 @@ impl WebPainterWgpu { #[allow(unused)] // only used if `wgpu` is the only active feature. pub async fn new( + ctx: egui::Context, canvas: web_sys::HtmlCanvasElement, options: &WebOptions, ) -> Result { @@ -119,17 +123,21 @@ impl WebPainterWgpu { .await .map_err(|err| err.to_string())?; + let default_configuration = surface + .get_default_config(&render_state.adapter, 0, 0) // Width/height is set later. + .ok_or("The surface isn't supported by this adapter")?; + let surface_configuration = wgpu::SurfaceConfiguration { format: render_state.target_format, present_mode: options.wgpu_options.present_mode, view_formats: vec![render_state.target_format], - ..surface - .get_default_config(&render_state.adapter, 0, 0) // Width/height is set later. - .ok_or("The surface isn't supported by this adapter")? + ..default_configuration }; log::debug!("wgpu painter initialized."); + let (capture_tx, capture_rx) = capture_channel(); + Ok(Self { canvas, render_state: Some(render_state), @@ -138,6 +146,10 @@ impl WebPainterWgpu { depth_format, depth_texture_view: None, on_surface_error: options.wgpu_options.on_surface_error.clone(), + screen_capture_state: None, + capture_tx, + capture_rx, + ctx, }) } } @@ -159,7 +171,10 @@ impl WebPainter for WebPainterWgpu { clipped_primitives: &[egui::ClippedPrimitive], pixels_per_point: f32, textures_delta: &egui::TexturesDelta, + capture_data: Vec, ) -> Result<(), JsValue> { + let capture = !capture_data.is_empty(); + let size_in_pixels = [self.canvas.width(), self.canvas.height()]; let Some(render_state) = &self.render_state else { @@ -203,7 +218,7 @@ impl WebPainter for WebPainterWgpu { // Resize surface if needed let is_zero_sized_surface = size_in_pixels[0] == 0 || size_in_pixels[1] == 0; - let frame = if is_zero_sized_surface { + let frame_and_capture_buffer = if is_zero_sized_surface { None } else { if size_in_pixels[0] != self.surface_configuration.width @@ -220,7 +235,7 @@ impl WebPainter for WebPainterWgpu { ); } - let frame = match self.surface.get_current_texture() { + let output_frame = match self.surface.get_current_texture() { Ok(frame) => frame, Err(err) => match (*self.on_surface_error)(err) { SurfaceErrorAction::RecreateSurface => { @@ -236,12 +251,23 @@ impl WebPainter for WebPainterWgpu { { let renderer = render_state.renderer.read(); - let frame_view = frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); + + let target_texture = if capture { + let capture_state = self.screen_capture_state.get_or_insert_with(|| { + CaptureState::new(&render_state.device, &output_frame.texture) + }); + capture_state.update(&render_state.device, &output_frame.texture); + + &capture_state.texture + } else { + &output_frame.texture + }; + let target_view = + target_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &frame_view, + view: &target_view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { @@ -280,7 +306,19 @@ impl WebPainter for WebPainterWgpu { ); } - Some(frame) + 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, + )); + } + }; + + Some((output_frame, capture_buffer)) }; { @@ -295,13 +333,38 @@ impl WebPainter for WebPainterWgpu { .queue .submit(user_cmd_bufs.into_iter().chain([encoder.finish()])); - if let Some(frame) = frame { + 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, + ); + } + } + frame.present(); } Ok(()) } + fn handle_screenshots(&mut self, events: &mut Vec) { + for (viewport_id, user_data, screenshot) in self.capture_rx.try_iter() { + let screenshot = Arc::new(screenshot); + for data in user_data { + events.push(Event::Screenshot { + viewport_id, + user_data: data, + image: screenshot.clone(), + }); + } + } + } + fn destroy(&mut self) { self.render_state = None; } diff --git a/crates/egui-wgpu/CHANGELOG.md b/crates/egui-wgpu/CHANGELOG.md index a25b2a21548..d92e867a77e 100644 --- a/crates/egui-wgpu/CHANGELOG.md +++ b/crates/egui-wgpu/CHANGELOG.md @@ -6,6 +6,14 @@ 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.30.0 - 2024-12-16 +* Fix docs.rs build [#5204](https://github.com/emilk/egui/pull/5204) by [@lucasmerlin](https://github.com/lucasmerlin) +* Free textures after submitting queue instead of before with wgpu renderer [#5226](https://github.com/emilk/egui/pull/5226) by [@Rusty-Cube](https://github.com/Rusty-Cube) +* Add option to initialize with existing wgpu instance/adapter/device/queue [#5319](https://github.com/emilk/egui/pull/5319) by [@ArthurBrussee](https://github.com/ArthurBrussee) +* Updare to `wgpu` 23.0.0 and `wasm-bindgen` to 0.2.95 [#5330](https://github.com/emilk/egui/pull/5330) by [@torokati44](https://github.com/torokati44) +* Support wgpu-tracing with same mechanism as wgpu examples [#5450](https://github.com/emilk/egui/pull/5450) by [@EriKWDev](https://github.com/EriKWDev) + + ## 0.29.1 - 2024-10-01 Nothing new diff --git a/crates/egui-wgpu/Cargo.toml b/crates/egui-wgpu/Cargo.toml index 4575ce0069b..2f4330236d0 100644 --- a/crates/egui-wgpu/Cargo.toml +++ b/crates/egui-wgpu/Cargo.toml @@ -33,9 +33,6 @@ rustdoc-args = ["--generate-link-to-definition"] [features] default = ["fragile-send-sync-non-atomic-wasm"] -## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -puffin = ["dep:puffin"] - ## Enable [`winit`](https://docs.rs/winit) integration. On Linux, requires either `wayland` or `x11` winit = ["dep:winit", "winit/rwh_06"] @@ -60,6 +57,7 @@ ahash.workspace = true bytemuck.workspace = true document-features.workspace = true log.workspace = true +profiling.workspace = true thiserror.workspace = true type-map.workspace = true web-time.workspace = true @@ -68,7 +66,3 @@ wgpu = { workspace = true, features = ["wgsl"] } # Optional dependencies: winit = { workspace = true, optional = true, default-features = false } - -# Native: -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -puffin = { workspace = true, optional = true } diff --git a/crates/egui-wgpu/src/capture.rs b/crates/egui-wgpu/src/capture.rs new file mode 100644 index 00000000000..1ce780d054f --- /dev/null +++ b/crates/egui-wgpu/src/capture.rs @@ -0,0 +1,257 @@ +use egui::{UserData, ViewportId}; +use epaint::ColorImage; +use std::sync::{mpsc, Arc}; +use wgpu::{BindGroupLayout, MultisampleState, StoreOp}; + +/// A texture and a buffer for reading the rendered frame back to the cpu. +/// The texture is required since [`wgpu::TextureUsages::COPY_SRC`] is not an allowed +/// flag for the surface texture on all platforms. This means that anytime we want to +/// capture the frame, we first render it to this texture, and then we can copy it to +/// both the surface texture (via a render pass) and the buffer (via a texture to buffer copy), +/// from where we can pull it back +/// to the cpu. +pub struct CaptureState { + padding: BufferPadding, + pub texture: wgpu::Texture, + pipeline: wgpu::RenderPipeline, + bind_group: wgpu::BindGroup, +} + +pub type CaptureReceiver = mpsc::Receiver<(ViewportId, Vec, ColorImage)>; +pub type CaptureSender = mpsc::Sender<(ViewportId, Vec, ColorImage)>; +pub use mpsc::channel as capture_channel; + +impl CaptureState { + pub fn new(device: &wgpu::Device, surface_texture: &wgpu::Texture) -> Self { + let shader = device.create_shader_module(wgpu::include_wgsl!("texture_copy.wgsl")); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("texture_copy"), + layout: None, + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + compilation_options: Default::default(), + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + compilation_options: Default::default(), + targets: &[Some(surface_texture.format().into())], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: None, + multisample: MultisampleState::default(), + multiview: None, + cache: None, + }); + + let bind_group_layout = pipeline.get_bind_group_layout(0); + + let (texture, padding, bind_group) = + Self::create_texture(device, surface_texture, &bind_group_layout); + + Self { + padding, + texture, + pipeline, + bind_group, + } + } + + fn create_texture( + device: &wgpu::Device, + surface_texture: &wgpu::Texture, + layout: &BindGroupLayout, + ) -> (wgpu::Texture, BufferPadding, wgpu::BindGroup) { + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("egui_screen_capture_texture"), + size: surface_texture.size(), + mip_level_count: surface_texture.mip_level_count(), + sample_count: surface_texture.sample_count(), + dimension: surface_texture.dimension(), + format: surface_texture.format(), + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + + let padding = BufferPadding::new(surface_texture.width()); + + let view = texture.create_view(&Default::default()); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&view), + }], + label: None, + }); + + (texture, padding, bind_group) + } + + /// Updates the [`CaptureState`] if the size of the surface texture has changed + pub fn update(&mut self, device: &wgpu::Device, texture: &wgpu::Texture) { + if self.texture.size() != texture.size() { + let (new_texture, padding, bind_group) = + Self::create_texture(device, texture, &self.pipeline.get_bind_group_layout(0)); + self.texture = new_texture; + self.padding = padding; + self.bind_group = bind_group; + } + } + + /// Handles copying from the [`CaptureState`] texture to the surface texture and the buffer. + /// Pass the returned buffer to [`CaptureState::read_screen_rgba`] to read the data back to the cpu. + pub fn copy_textures( + &mut self, + device: &wgpu::Device, + output_frame: &wgpu::SurfaceTexture, + encoder: &mut wgpu::CommandEncoder, + ) -> wgpu::Buffer { + debug_assert_eq!( + self.texture.size(), + output_frame.texture.size(), + "Texture sizes must match, `CaptureState::update` was probably not called" + ); + + // 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)] + 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, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + let padding = self.padding; + let tex = &mut self.texture; + + let tex_extent = tex.size(); + + encoder.copy_texture_to_buffer( + tex.as_image_copy(), + wgpu::ImageCopyBuffer { + buffer: &buffer, + layout: wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(padding.padded_bytes_per_row), + rows_per_image: None, + }, + }, + tex_extent, + ); + + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("texture_copy"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &output_frame.texture.create_view(&Default::default()), + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, &self.bind_group, &[]); + pass.draw(0..3, 0..1); + + buffer + } + + /// Handles copying from the [`CaptureState`] texture to the surface texture and the cpu + /// This function is non-blocking and will send the data to the given sender when it's ready. + /// Pass in the buffer returned from [`CaptureState::copy_textures`]. + /// Make sure to call this after the encoder has been submitted. + pub fn read_screen_rgba( + &self, + ctx: egui::Context, + buffer: wgpu::Buffer, + data: Vec, + tx: CaptureSender, + viewport_id: ViewportId, + ) { + #[allow(clippy::arc_with_non_send_sync)] + let buffer = Arc::new(buffer); + let buffer_clone = buffer.clone(); + let buffer_slice = buffer_clone.slice(..); + let format = self.texture.format(); + let tex_extent = self.texture.size(); + let padding = self.padding; + let to_rgba = match format { + wgpu::TextureFormat::Rgba8Unorm => [0, 1, 2, 3], + wgpu::TextureFormat::Bgra8Unorm => [2, 1, 0, 3], + _ => { + log::error!("Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {:?}", format); + return; + } + }; + buffer_slice.map_async(wgpu::MapMode::Read, move |result| { + if let Err(err) = result { + log::error!("Failed to map buffer for reading: {:?}", err); + return; + } + let buffer_slice = buffer.slice(..); + + let mut pixels = Vec::with_capacity((tex_extent.width * tex_extent.height) as usize); + for padded_row in buffer_slice + .get_mapped_range() + .chunks(padding.padded_bytes_per_row as usize) + { + let row = &padded_row[..padding.unpadded_bytes_per_row as usize]; + for color in row.chunks(4) { + pixels.push(epaint::Color32::from_rgba_premultiplied( + color[to_rgba[0]], + color[to_rgba[1]], + color[to_rgba[2]], + color[to_rgba[3]], + )); + } + } + buffer.unmap(); + + tx.send(( + viewport_id, + data, + ColorImage { + size: [tex_extent.width as usize, tex_extent.height as usize], + pixels, + }, + )) + .ok(); + ctx.request_repaint(); + }); + } +} + +#[derive(Copy, Clone)] +struct BufferPadding { + unpadded_bytes_per_row: u32, + padded_bytes_per_row: u32, +} + +impl BufferPadding { + fn new(width: u32) -> Self { + let bytes_per_pixel = std::mem::size_of::() as u32; + let unpadded_bytes_per_row = width * bytes_per_pixel; + let padded_bytes_per_row = + wgpu::util::align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT); + Self { + unpadded_bytes_per_row, + padded_bytes_per_row, + } + } +} diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 29dc8d8f5c2..a91662f45b1 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -26,6 +26,9 @@ mod renderer; pub use renderer::*; use wgpu::{Adapter, Device, Instance, Queue}; +/// Helpers for capturing screenshots of the UI. +pub mod capture; + /// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`]. #[cfg(feature = "winit")] pub mod winit; @@ -93,7 +96,7 @@ impl RenderState { msaa_samples: u32, dithering: bool, ) -> Result { - crate::profile_scope!("RenderState::create"); // async yield give bad names using `profile_function` + profiling::scope!("RenderState::create"); // async yield give bad names using `profile_function` // This is always an empty list on web. #[cfg(not(target_arch = "wasm32"))] @@ -106,7 +109,7 @@ impl RenderState { device_descriptor, } => { let adapter = { - crate::profile_scope!("request_adapter"); + profiling::scope!("request_adapter"); instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference, @@ -161,7 +164,7 @@ impl RenderState { let trace_path = std::env::var("WGPU_TRACE"); let (device, queue) = { - crate::profile_scope!("request_device"); + profiling::scope!("request_device"); adapter .request_device( &(*device_descriptor)(&adapter), @@ -184,7 +187,7 @@ impl RenderState { }; let capabilities = { - crate::profile_scope!("get_capabilities"); + profiling::scope!("get_capabilities"); surface.get_capabilities(&adapter).formats }; let target_format = crate::preferred_framebuffer_format(&capabilities)?; @@ -471,33 +474,3 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String { summary } - -// --------------------------------------------------------------------------- - -mod profiling_scopes { - #![allow(unused_macros)] - #![allow(unused_imports)] - - /// Profiling macro for feature "puffin" - macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} - -#[allow(unused_imports)] -pub(crate) use profiling_scopes::{profile_function, profile_scope}; diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index b6d49d22d4f..2c1fa0428f1 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -214,14 +214,14 @@ impl Renderer { msaa_samples: u32, dithering: bool, ) -> Self { - crate::profile_function!(); + profiling::function_scope!(); let shader = wgpu::ShaderModuleDescriptor { label: Some("egui"), source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("egui.wgsl"))), }; let module = { - crate::profile_scope!("create_shader_module"); + profiling::scope!("create_shader_module"); device.create_shader_module(shader) }; @@ -236,7 +236,7 @@ impl Renderer { }); let uniform_bind_group_layout = { - crate::profile_scope!("create_bind_group_layout"); + profiling::scope!("create_bind_group_layout"); device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("egui_uniform_bind_group_layout"), entries: &[wgpu::BindGroupLayoutEntry { @@ -253,7 +253,7 @@ impl Renderer { }; let uniform_bind_group = { - crate::profile_scope!("create_bind_group"); + profiling::scope!("create_bind_group"); device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("egui_uniform_bind_group"), layout: &uniform_bind_group_layout, @@ -269,7 +269,7 @@ impl Renderer { }; let texture_bind_group_layout = { - crate::profile_scope!("create_bind_group_layout"); + profiling::scope!("create_bind_group_layout"); device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("egui_texture_bind_group_layout"), entries: &[ @@ -308,7 +308,7 @@ impl Renderer { }); let pipeline = { - crate::profile_scope!("create_render_pipeline"); + profiling::scope!("create_render_pipeline"); device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("egui_pipeline"), layout: Some(&pipeline_layout), @@ -420,7 +420,7 @@ impl Renderer { paint_jobs: &[epaint::ClippedPrimitive], screen_descriptor: &ScreenDescriptor, ) { - crate::profile_function!(); + profiling::function_scope!(); let pixels_per_point = screen_descriptor.pixels_per_point; let size_in_pixels = screen_descriptor.size_in_pixels; @@ -506,7 +506,7 @@ impl Renderer { let viewport_px = info.viewport_in_pixels(); if viewport_px.width_px > 0 && viewport_px.height_px > 0 { - crate::profile_scope!("callback"); + profiling::scope!("callback"); needs_reset = true; @@ -544,7 +544,7 @@ impl Renderer { id: epaint::TextureId, image_delta: &epaint::ImageDelta, ) { - crate::profile_function!(); + profiling::function_scope!(); let width = image_delta.image.width() as u32; let height = image_delta.image.height() as u32; @@ -570,14 +570,14 @@ impl Renderer { image.pixels.len(), "Mismatch between texture size and texel count" ); - crate::profile_scope!("font -> sRGBA"); + profiling::scope!("font -> sRGBA"); Cow::Owned(image.srgba_pixels(None).collect::>()) } }; let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice()); let queue_write_data_to_texture = |texture, origin| { - crate::profile_scope!("write_texture"); + profiling::scope!("write_texture"); queue.write_texture( wgpu::ImageCopyTexture { texture, @@ -631,7 +631,7 @@ impl Renderer { } else { // allocate a new texture let texture = { - crate::profile_scope!("create_texture"); + profiling::scope!("create_texture"); device.create_texture(&wgpu::TextureDescriptor { label, size, @@ -756,7 +756,7 @@ impl Renderer { texture: &wgpu::TextureView, sampler_descriptor: wgpu::SamplerDescriptor<'_>, ) -> epaint::TextureId { - crate::profile_function!(); + profiling::function_scope!(); let sampler = device.create_sampler(&wgpu::SamplerDescriptor { compare: None, @@ -804,7 +804,7 @@ impl Renderer { sampler_descriptor: wgpu::SamplerDescriptor<'_>, id: epaint::TextureId, ) { - crate::profile_function!(); + profiling::function_scope!(); let Texture { bind_group: user_texture_binding, @@ -849,7 +849,7 @@ impl Renderer { paint_jobs: &[epaint::ClippedPrimitive], screen_descriptor: &ScreenDescriptor, ) -> Vec { - crate::profile_function!(); + profiling::function_scope!(); let screen_size_in_points = screen_descriptor.screen_size_in_points(); @@ -859,7 +859,7 @@ impl Renderer { _padding: Default::default(), }; if uniform_buffer_content != self.previous_uniform_buffer_content { - crate::profile_scope!("update uniforms"); + profiling::scope!("update uniforms"); queue.write_buffer( &self.uniform_buffer, 0, @@ -871,7 +871,7 @@ impl Renderer { // Determine how many vertices & indices need to be rendered, and gather prepare callbacks let mut callbacks = Vec::new(); let (vertex_count, index_count) = { - crate::profile_scope!("count_vertices_indices"); + profiling::scope!("count_vertices_indices"); paint_jobs.iter().fold((0, 0), |acc, clipped_primitive| { match &clipped_primitive.primitive { Primitive::Mesh(mesh) => { @@ -890,7 +890,7 @@ impl Renderer { }; if index_count > 0 { - crate::profile_scope!("indices", index_count.to_string()); + profiling::scope!("indices", index_count.to_string().as_str()); self.index_buffer.slices.clear(); @@ -928,7 +928,7 @@ impl Renderer { } } if vertex_count > 0 { - crate::profile_scope!("vertices", vertex_count.to_string()); + profiling::scope!("vertices", vertex_count.to_string().as_str()); self.vertex_buffer.slices.clear(); @@ -969,7 +969,7 @@ impl Renderer { let mut user_cmd_bufs = Vec::new(); { - crate::profile_scope!("prepare callbacks"); + profiling::scope!("prepare callbacks"); for callback in &callbacks { user_cmd_bufs.extend(callback.prepare( device, @@ -981,7 +981,7 @@ impl Renderer { } } { - crate::profile_scope!("finish prepare callbacks"); + profiling::scope!("finish prepare callbacks"); for callback in &callbacks { user_cmd_bufs.extend(callback.finish_prepare( device, @@ -1026,7 +1026,7 @@ fn create_sampler( } fn create_vertex_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer { - crate::profile_function!(); + profiling::function_scope!(); device.create_buffer(&wgpu::BufferDescriptor { label: Some("egui_vertex_buffer"), usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, @@ -1036,7 +1036,7 @@ fn create_vertex_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer { } fn create_index_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer { - crate::profile_function!(); + profiling::function_scope!(); device.create_buffer(&wgpu::BufferDescriptor { label: Some("egui_index_buffer"), usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, diff --git a/crates/egui-wgpu/src/texture_copy.wgsl b/crates/egui-wgpu/src/texture_copy.wgsl new file mode 100644 index 00000000000..4096d164cc3 --- /dev/null +++ b/crates/egui-wgpu/src/texture_copy.wgsl @@ -0,0 +1,43 @@ +struct VertexOutput { + @builtin(position) position: vec4, +}; + +var positions: array = array( + vec2f(-1.0, -3.0), + vec2f(-1.0, 1.0), + vec2f(3.0, 1.0) +); + +// meant to be called with 3 vertex indices: 0, 1, 2 +// draws one large triangle over the clip space like this: +// (the asterisks represent the clip space bounds) +//-1,1 1,1 +// --------------------------------- +// | * . +// | * . +// | * . +// | * . +// | * . +// | * . +// |*************** +// | . 1,-1 +// | . +// | . +// | . +// | . +// |. +@vertex +fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { + var result: VertexOutput; + result.position = vec4f(positions[vertex_index], 0.0, 1.0); + return result; +} + +@group(0) +@binding(0) +var r_color: texture_2d; + +@fragment +fn fs_main(vertex: VertexOutput) -> @location(0) vec4 { + return textureLoad(r_color, vec2i(vertex.position.xy), 0); +} diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index abf88518368..dea2e7fa329 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -1,77 +1,16 @@ #![allow(clippy::missing_errors_doc)] #![allow(clippy::undocumented_unsafe_blocks)] -use std::{num::NonZeroU32, sync::Arc}; - -use egui::{ViewportId, ViewportIdMap, ViewportIdSet}; - +use crate::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState}; use crate::{renderer, RenderState, SurfaceErrorAction, WgpuConfiguration}; +use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet}; +use std::{num::NonZeroU32, sync::Arc}; struct SurfaceState { surface: wgpu::Surface<'static>, alpha_mode: wgpu::CompositeAlphaMode, width: u32, height: u32, - supports_screenshot: bool, -} - -/// A texture and a buffer for reading the rendered frame back to the cpu. -/// The texture is required since [`wgpu::TextureUsages::COPY_DST`] is not an allowed -/// flag for the surface texture on all platforms. This means that anytime we want to -/// capture the frame, we first render it to this texture, and then we can copy it to -/// both the surface texture and the buffer, from where we can pull it back to the cpu. -struct CaptureState { - texture: wgpu::Texture, - buffer: wgpu::Buffer, - padding: BufferPadding, -} - -impl CaptureState { - fn new(device: &Arc, surface_texture: &wgpu::Texture) -> Self { - let texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("egui_screen_capture_texture"), - size: surface_texture.size(), - mip_level_count: surface_texture.mip_level_count(), - sample_count: surface_texture.sample_count(), - dimension: surface_texture.dimension(), - format: surface_texture.format(), - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, - view_formats: &[], - }); - - let padding = BufferPadding::new(surface_texture.width()); - - let buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("egui_screen_capture_buffer"), - size: (padding.padded_bytes_per_row * texture.height()) as u64, - usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, - mapped_at_creation: false, - }); - - Self { - texture, - buffer, - padding, - } - } -} - -struct BufferPadding { - unpadded_bytes_per_row: u32, - padded_bytes_per_row: u32, -} - -impl BufferPadding { - fn new(width: u32) -> Self { - let bytes_per_pixel = std::mem::size_of::() as u32; - let unpadded_bytes_per_row = width * bytes_per_pixel; - let padded_bytes_per_row = - wgpu::util::align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT); - Self { - unpadded_bytes_per_row, - padded_bytes_per_row, - } - } } /// Everything you need to paint egui with [`wgpu`] on [`winit`]. @@ -80,6 +19,7 @@ impl BufferPadding { /// /// NOTE: all egui viewports share the same painter. pub struct Painter { + context: Context, configuration: WgpuConfiguration, msaa_samples: u32, support_transparent_backbuffer: bool, @@ -94,6 +34,8 @@ pub struct Painter { depth_texture_view: ViewportIdMap, msaa_texture_view: ViewportIdMap, surfaces: ViewportIdMap, + capture_tx: CaptureSender, + capture_rx: CaptureReceiver, } impl Painter { @@ -110,6 +52,7 @@ impl Painter { /// a [`winit::window::Window`] with a valid `.raw_window_handle()` /// associated. pub fn new( + context: Context, configuration: WgpuConfiguration, msaa_samples: u32, depth_format: Option, @@ -126,7 +69,10 @@ impl Painter { crate::WgpuSetup::Existing { instance, .. } => instance.clone(), }; + let (capture_tx, capture_rx) = capture_channel(); + Self { + context, configuration, msaa_samples, support_transparent_backbuffer, @@ -140,6 +86,9 @@ impl Painter { depth_texture_view: Default::default(), surfaces: Default::default(), msaa_texture_view: Default::default(), + + capture_tx, + capture_rx, } } @@ -155,19 +104,13 @@ impl Painter { render_state: &RenderState, config: &WgpuConfiguration, ) { - crate::profile_function!(); - - let usage = if surface_state.supports_screenshot { - wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST - } else { - wgpu::TextureUsages::RENDER_ATTACHMENT - }; + profiling::function_scope!(); let width = surface_state.width; let height = surface_state.height; let mut surf_config = wgpu::SurfaceConfiguration { - usage, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: render_state.target_format, present_mode: config.present_mode, alpha_mode: surface_state.alpha_mode, @@ -213,7 +156,7 @@ impl Painter { viewport_id: ViewportId, window: Option>, ) -> Result<(), crate::WgpuError> { - crate::profile_scope!("Painter::set_window"); // profile_function gives bad names for async functions + profiling::scope!("Painter::set_window"); // profile_function gives bad names for async functions if let Some(window) = window { let size = window.inner_size(); @@ -239,7 +182,7 @@ impl Painter { viewport_id: ViewportId, window: Option<&winit::window::Window>, ) -> Result<(), crate::WgpuError> { - crate::profile_scope!("Painter::set_window_unsafe"); // profile_function gives bad names for async functions + profiling::scope!("Painter::set_window_unsafe"); // profile_function gives bad names for async functions if let Some(window) = window { let size = window.inner_size(); @@ -292,8 +235,6 @@ impl Painter { } else { wgpu::CompositeAlphaMode::Auto }; - let supports_screenshot = - !matches!(render_state.adapter.get_info().backend, wgpu::Backend::Gl); self.surfaces.insert( viewport_id, SurfaceState { @@ -301,7 +242,6 @@ impl Painter { width: size.width, height: size.height, alpha_mode, - supports_screenshot, }, ); let Some(width) = NonZeroU32::new(size.width) else { @@ -333,7 +273,7 @@ impl Painter { width_in_pixels: NonZeroU32, height_in_pixels: NonZeroU32, ) { - crate::profile_function!(); + profiling::function_scope!(); let width = width_in_pixels.get(); let height = height_in_pixels.get(); @@ -404,7 +344,7 @@ impl Painter { width_in_pixels: NonZeroU32, height_in_pixels: NonZeroU32, ) { - crate::profile_function!(); + profiling::function_scope!(); if self.surfaces.contains_key(&viewport_id) { self.resize_and_generate_depth_texture_view_and_msaa_view( @@ -417,109 +357,12 @@ impl Painter { } } - // CaptureState only needs to be updated when the size of the two textures don't match and we want to - // capture a frame - fn update_capture_state( - screen_capture_state: &mut Option, - surface_texture: &wgpu::SurfaceTexture, - render_state: &RenderState, - ) { - let surface_texture = &surface_texture.texture; - match screen_capture_state { - Some(capture_state) => { - if capture_state.texture.size() != surface_texture.size() { - *capture_state = CaptureState::new(&render_state.device, surface_texture); - } - } - None => { - *screen_capture_state = - Some(CaptureState::new(&render_state.device, surface_texture)); - } - } - } - - // Handles copying from the CaptureState texture to the surface texture and the cpu - fn read_screen_rgba( - screen_capture_state: &CaptureState, - render_state: &RenderState, - output_frame: &wgpu::SurfaceTexture, - ) -> Option { - let CaptureState { - texture: tex, - buffer, - padding, - } = screen_capture_state; - - let device = &render_state.device; - let queue = &render_state.queue; - - let tex_extent = tex.size(); - - let mut encoder = device.create_command_encoder(&Default::default()); - encoder.copy_texture_to_buffer( - tex.as_image_copy(), - wgpu::ImageCopyBuffer { - buffer, - layout: wgpu::ImageDataLayout { - offset: 0, - bytes_per_row: Some(padding.padded_bytes_per_row), - rows_per_image: None, - }, - }, - tex_extent, - ); - - encoder.copy_texture_to_texture( - tex.as_image_copy(), - output_frame.texture.as_image_copy(), - tex.size(), - ); - - let id = queue.submit(Some(encoder.finish())); - let buffer_slice = buffer.slice(..); - let (sender, receiver) = std::sync::mpsc::channel(); - buffer_slice.map_async(wgpu::MapMode::Read, move |v| { - drop(sender.send(v)); - }); - device.poll(wgpu::Maintain::WaitForSubmissionIndex(id)); - receiver.recv().ok()?.ok()?; - - let to_rgba = match tex.format() { - wgpu::TextureFormat::Rgba8Unorm => [0, 1, 2, 3], - wgpu::TextureFormat::Bgra8Unorm => [2, 1, 0, 3], - _ => { - log::error!("Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {:?}", tex.format()); - return None; - } - }; - - let mut pixels = Vec::with_capacity((tex.width() * tex.height()) as usize); - for padded_row in buffer_slice - .get_mapped_range() - .chunks(padding.padded_bytes_per_row as usize) - { - let row = &padded_row[..padding.unpadded_bytes_per_row as usize]; - for color in row.chunks(4) { - pixels.push(epaint::Color32::from_rgba_premultiplied( - color[to_rgba[0]], - color[to_rgba[1]], - color[to_rgba[2]], - color[to_rgba[3]], - )); - } - } - buffer.unmap(); - - Some(epaint::ColorImage { - size: [tex.width() as usize, tex.height() as usize], - pixels, - }) - } - /// Returns two things: /// /// The approximate number of seconds spent on vsync-waiting (if any), /// and the captures captured screenshot if it was requested. + /// + /// If `capture_data` isn't empty, a screenshot will be captured. pub fn paint_and_update_textures( &mut self, viewport_id: ViewportId, @@ -527,17 +370,18 @@ impl Painter { clear_color: [f32; 4], clipped_primitives: &[epaint::ClippedPrimitive], textures_delta: &epaint::textures::TexturesDelta, - capture: bool, - ) -> (f32, Option) { - crate::profile_function!(); + capture_data: Vec, + ) -> f32 { + profiling::function_scope!(); + let capture = !capture_data.is_empty(); let mut vsync_sec = 0.0; let Some(render_state) = self.render_state.as_mut() else { - return (vsync_sec, None); + return vsync_sec; }; let Some(surface_state) = self.surfaces.get(&viewport_id) else { - return (vsync_sec, None); + return vsync_sec; }; let mut encoder = @@ -573,17 +417,8 @@ impl Painter { ) }; - let capture = match (capture, surface_state.supports_screenshot) { - (false, _) => false, - (true, true) => true, - (true, false) => { - log::error!("The active render surface doesn't support taking screenshots."); - false - } - }; - let output_frame = { - crate::profile_scope!("get_current_texture"); + profiling::scope!("get_current_texture"); // This is what vsync-waiting happens on my Mac. let start = web_time::Instant::now(); let output_frame = surface_state.surface.get_current_texture(); @@ -596,40 +431,35 @@ impl Painter { Err(err) => match (*self.configuration.on_surface_error)(err) { SurfaceErrorAction::RecreateSurface => { Self::configure_surface(surface_state, render_state, &self.configuration); - return (vsync_sec, None); + return vsync_sec; } SurfaceErrorAction::SkipFrame => { - return (vsync_sec, None); + return vsync_sec; } }, }; + let mut capture_buffer = None; { let renderer = render_state.renderer.read(); - let frame_view = if capture { - Self::update_capture_state( - &mut self.screen_capture_state, - &output_frame, - render_state, - ); - self.screen_capture_state - .as_ref() - .map_or_else( - || &output_frame.texture, - |capture_state| &capture_state.texture, - ) - .create_view(&wgpu::TextureViewDescriptor::default()) + + let target_texture = if capture { + let capture_state = self.screen_capture_state.get_or_insert_with(|| { + CaptureState::new(&render_state.device, &output_frame.texture) + }); + capture_state.update(&render_state.device, &output_frame.texture); + + &capture_state.texture } else { - output_frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()) + &output_frame.texture }; + let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default()); let (view, resolve_target) = (self.msaa_samples > 1) .then_some(self.msaa_texture_view.get(&viewport_id)) .flatten() - .map_or((&frame_view, None), |texture_view| { - (texture_view, Some(&frame_view)) + .map_or((&target_view, None), |texture_view| { + (texture_view, Some(&target_view)) }); let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -671,16 +501,26 @@ impl Painter { clipped_primitives, &screen_descriptor, ); + + 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, + )); + } + } } let encoded = { - crate::profile_scope!("CommandEncoder::finish"); + profiling::scope!("CommandEncoder::finish"); encoder.finish() }; // Submit the commands: both the main buffer and user-defined ones. { - crate::profile_scope!("Queue::submit"); + profiling::scope!("Queue::submit"); // wgpu doesn't document where vsync can happen. Maybe here? let start = web_time::Instant::now(); render_state @@ -699,25 +539,41 @@ impl Painter { } } - let screenshot = if capture { - self.screen_capture_state - .as_ref() - .and_then(|screen_capture_state| { - Self::read_screen_rgba(screen_capture_state, render_state, &output_frame) - }) - } else { - None - }; + if let Some(capture_buffer) = capture_buffer { + if let Some(screen_capture_state) = &mut self.screen_capture_state { + screen_capture_state.read_screen_rgba( + self.context.clone(), + capture_buffer, + capture_data, + self.capture_tx.clone(), + viewport_id, + ); + } + } { - crate::profile_scope!("present"); + profiling::scope!("present"); // wgpu doesn't document where vsync can happen. Maybe here? let start = web_time::Instant::now(); output_frame.present(); vsync_sec += start.elapsed().as_secs_f32(); } - (vsync_sec, screenshot) + vsync_sec + } + + /// Call this at the beginning of each frame to receive the requested screenshots. + pub fn handle_screenshots(&self, events: &mut Vec) { + for (viewport_id, user_data, screenshot) in self.capture_rx.try_iter() { + let screenshot = Arc::new(screenshot); + for data in user_data { + events.push(Event::Screenshot { + viewport_id, + user_data: data, + image: screenshot.clone(), + }); + } + } } pub fn gc_viewports(&mut self, active_viewports: &ViewportIdSet) { @@ -728,7 +584,7 @@ impl Painter { .retain(|id, _| active_viewports.contains(id)); } - #[allow(clippy::unused_self)] + #[allow(clippy::needless_pass_by_ref_mut, clippy::unused_self)] pub fn destroy(&mut self) { // TODO(emilk): something here? } diff --git a/crates/egui-winit/CHANGELOG.md b/crates/egui-winit/CHANGELOG.md index cf9def985f7..d164464208d 100644 --- a/crates/egui-winit/CHANGELOG.md +++ b/crates/egui-winit/CHANGELOG.md @@ -5,6 +5,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.30.0 - 2024-12-16 +* iOS: Support putting UI next to the dynamic island [#5211](https://github.com/emilk/egui/pull/5211) by [@frederik-uni](https://github.com/frederik-uni) +* Remove implicit `accesskit_winit` feature [#5316](https://github.com/emilk/egui/pull/5316) by [@waywardmonkeys](https://github.com/waywardmonkeys) + + ## 0.29.1 - 2024-10-01 - Fix backspace/arrow keys on X11 * Linux: Disable IME to fix backspace/arrow keys [#5188](https://github.com/emilk/egui/pull/5188) by [@emilk](https://github.com/emilk) diff --git a/crates/egui-winit/Cargo.toml b/crates/egui-winit/Cargo.toml index 2f65548757d..c584db85e70 100644 --- a/crates/egui-winit/Cargo.toml +++ b/crates/egui-winit/Cargo.toml @@ -45,9 +45,6 @@ clipboard = ["arboard", "smithay-clipboard"] ## Enable opening links in a browser when an egui hyperlink is clicked. links = ["webbrowser"] -## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -puffin = ["dep:puffin", "egui/puffin"] - ## Allow serialization of [`WindowSettings`] using [`serde`](https://docs.rs/serde). serde = ["egui/serde", "dep:serde"] @@ -62,6 +59,7 @@ egui = { workspace = true, default-features = false, features = ["log"] } ahash.workspace = true log.workspace = true +profiling.workspace = true raw-window-handle.workspace = true web-time.workspace = true winit = { workspace = true, default-features = false } @@ -74,7 +72,6 @@ accesskit_winit = { version = "0.23", optional = true } ## Enable this when generating docs. document-features = { workspace = true, optional = true } -puffin = { workspace = true, optional = true } serde = { workspace = true, optional = true } webbrowser = { version = "1.0.0", optional = true } diff --git a/crates/egui-winit/src/clipboard.rs b/crates/egui-winit/src/clipboard.rs index 44e3840b64f..c4192f78d55 100644 --- a/crates/egui-winit/src/clipboard.rs +++ b/crates/egui-winit/src/clipboard.rs @@ -112,7 +112,7 @@ impl Clipboard { #[cfg(all(feature = "arboard", not(target_os = "android")))] fn init_arboard() -> Option { - crate::profile_function!(); + profiling::function_scope!(); log::trace!("Initializing arboard clipboard…"); match arboard::Clipboard::new() { @@ -139,7 +139,7 @@ fn init_smithay_clipboard( ) -> Option { #![allow(clippy::undocumented_unsafe_blocks)] - crate::profile_function!(); + profiling::function_scope!(); if let Some(RawDisplayHandle::Wayland(display)) = raw_display_handle { log::trace!("Initializing smithay clipboard…"); diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 1cb2d502c5d..50ff2d31b4b 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -25,9 +25,6 @@ pub use window_settings::WindowSettings; use ahash::HashSet; use raw_window_handle::HasDisplayHandle; -#[allow(unused_imports)] -pub(crate) use profiling_scopes::{profile_function, profile_scope}; - use winit::{ dpi::{PhysicalPosition, PhysicalSize}, event::ElementState, @@ -121,7 +118,7 @@ impl State { theme: Option, max_texture_side: Option, ) -> Self { - crate::profile_function!(); + profiling::function_scope!(); let egui_input = egui::RawInput { focused: false, // winit will tell us when we have focus @@ -172,7 +169,7 @@ impl State { window: &Window, event_loop_proxy: winit::event_loop::EventLoopProxy, ) { - crate::profile_function!(); + profiling::function_scope!(); self.accesskit = Some(accesskit_winit::Adapter::with_event_loop_proxy( window, @@ -233,7 +230,7 @@ impl State { /// Use [`update_viewport_info`] to update the info for each /// viewport. pub fn take_egui_input(&mut self, window: &Window) -> egui::RawInput { - crate::profile_function!(); + profiling::function_scope!(); self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64()); @@ -268,7 +265,7 @@ impl State { window: &Window, event: &winit::event::WindowEvent, ) -> EventResponse { - crate::profile_function!(short_window_event_description(event)); + profiling::function_scope!(short_window_event_description(event)); #[cfg(feature = "accesskit")] if let Some(accesskit) = self.accesskit.as_mut() { @@ -823,7 +820,7 @@ impl State { window: &Window, platform_output: egui::PlatformOutput, ) { - crate::profile_function!(); + profiling::function_scope!(); let egui::PlatformOutput { cursor_icon, @@ -851,7 +848,7 @@ impl State { let allow_ime = ime.is_some(); if self.allow_ime != allow_ime { self.allow_ime = allow_ime; - crate::profile_scope!("set_ime_allowed"); + profiling::scope!("set_ime_allowed"); window.set_ime_allowed(allow_ime); } @@ -862,7 +859,7 @@ impl State { || self.egui_ctx.input(|i| !i.events.is_empty()) { self.ime_rect_px = Some(ime_rect_px); - crate::profile_scope!("set_ime_cursor_area"); + profiling::scope!("set_ime_cursor_area"); window.set_ime_cursor_area( winit::dpi::PhysicalPosition { x: ime_rect_px.min.x, @@ -881,7 +878,7 @@ impl State { #[cfg(feature = "accesskit")] if let Some(accesskit) = self.accesskit.as_mut() { if let Some(update) = accesskit_update { - crate::profile_scope!("accesskit"); + profiling::scope!("accesskit"); accesskit.update_if_active(|| update); } } @@ -953,8 +950,7 @@ pub fn update_viewport_info( window: &Window, is_init: bool, ) { - crate::profile_function!(); - + profiling::function_scope!(); let pixels_per_point = pixels_per_point(egui_ctx, window); let has_a_position = match window.is_minimized() { @@ -975,7 +971,7 @@ pub fn update_viewport_info( }; let monitor_size = { - crate::profile_scope!("monitor_size"); + profiling::scope!("monitor_size"); if let Some(monitor) = window.current_monitor() { let size = monitor.size().to_logical::(pixels_per_point.into()); Some(egui::vec2(size.width, size.height)) @@ -1326,7 +1322,7 @@ fn process_viewport_command( info: &mut ViewportInfo, actions_requested: &mut HashSet, ) { - crate::profile_function!(); + profiling::function_scope!(); use winit::window::ResizeDirection; @@ -1542,7 +1538,7 @@ pub fn create_window( event_loop: &ActiveEventLoop, viewport_builder: &ViewportBuilder, ) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let window_attributes = create_winit_window_attributes(egui_ctx, event_loop, viewport_builder.clone()); @@ -1556,7 +1552,7 @@ pub fn create_winit_window_attributes( event_loop: &ActiveEventLoop, viewport_builder: ViewportBuilder, ) -> winit::window::WindowAttributes { - crate::profile_function!(); + profiling::function_scope!(); // We set sizes and positions in egui:s own ui points, which depends on the egui // zoom_factor and the native pixels per point, so we need to know that here. @@ -1752,7 +1748,7 @@ fn to_winit_icon(icon: &egui::IconData) -> Option { if icon.is_empty() { None } else { - crate::profile_function!(); + profiling::function_scope!(); match winit::window::Icon::from_rgba(icon.rgba.clone(), icon.width, icon.height) { Ok(winit_icon) => Some(winit_icon), Err(err) => { @@ -1867,30 +1863,3 @@ pub fn short_window_event_description(event: &winit::event::WindowEvent) -> &'st WindowEvent::PanGesture { .. } => "WindowEvent::PanGesture", } } - -// --------------------------------------------------------------------------- - -mod profiling_scopes { - #![allow(unused_macros)] - #![allow(unused_imports)] - - /// Profiling macro for feature "puffin" - macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} diff --git a/crates/egui-winit/src/window_settings.rs b/crates/egui-winit/src/window_settings.rs index 627d88158c0..168a086c70f 100644 --- a/crates/egui-winit/src/window_settings.rs +++ b/crates/egui-winit/src/window_settings.rs @@ -56,7 +56,7 @@ impl WindowSettings { event_loop: &winit::event_loop::ActiveEventLoop, mut viewport_builder: ViewportBuilder, ) -> ViewportBuilder { - crate::profile_function!(); + profiling::function_scope!(); // `WindowBuilder::with_position` expects inner position in Macos, and outer position elsewhere // See [`winit::window::WindowBuilder::with_position`] for details. @@ -143,8 +143,7 @@ fn find_active_monitor( window_size_pts: egui::Vec2, position_px: &egui::Pos2, ) -> Option { - crate::profile_function!(); - + profiling::function_scope!(); let monitors = event_loop.available_monitors(); // default to primary monitor, in case the correct monitor was disconnected. @@ -178,7 +177,7 @@ fn clamp_pos_to_monitors( window_size_pts: egui::Vec2, position_px: &mut egui::Pos2, ) { - crate::profile_function!(); + profiling::function_scope!(); let Some(active_monitor) = find_active_monitor(egui_zoom_factor, event_loop, window_size_pts, position_px) diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index 9bb1dc0f4e4..ce7999dd1f4 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -62,10 +62,6 @@ mint = ["epaint/mint"] ## Enable persistence of memory (window positions etc). persistence = ["serde", "epaint/serde", "ron"] -## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -## -## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. -puffin = ["dep:puffin", "epaint/puffin"] ## Enable parallel tessellation using [`rayon`](https://docs.rs/rayon). ## @@ -85,6 +81,7 @@ epaint = { workspace = true, default-features = false } ahash.workspace = true nohash-hasher.workspace = true +profiling.workspace = true #! ### Optional dependencies accesskit = { version = "0.17.0", optional = true } @@ -95,10 +92,5 @@ backtrace = { workspace = true, optional = true } document-features = { workspace = true, optional = true } log = { workspace = true, optional = true } -puffin = { workspace = true, optional = true } ron = { workspace = true, optional = true } serde = { workspace = true, optional = true, features = ["derive", "rc"] } - - -[dev-dependencies] -egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } diff --git a/crates/egui/assets/ferris.png b/crates/egui/assets/ferris.png index 129c7f931c0..8741baa19d0 100644 Binary files a/crates/egui/assets/ferris.png and b/crates/egui/assets/ferris.png differ diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index ab9a0b88d09..a2bfbe1905f 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -467,7 +467,7 @@ impl Area { id: interact_id, layer_id, rect: state.rect(), - interact_rect: state.rect(), + interact_rect: state.rect().intersect(constrain_rect), sense, enabled, }, diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index a90e615aca7..81bf84a2f4f 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -93,8 +93,8 @@ pub fn show_tooltip_at_pointer( pointer_rect.min.x = pointer_pos.x; // Transform global coords to layer coords: - if let Some(transform) = ctx.memory(|m| m.layer_transforms.get(&parent_layer).copied()) { - pointer_rect = transform.inverse() * pointer_rect; + if let Some(from_global) = ctx.layer_transform_from_global(parent_layer) { + pointer_rect = from_global * pointer_rect; } show_tooltip_at_dyn( @@ -162,8 +162,8 @@ fn show_tooltip_at_dyn<'c, R>( ) -> R { // Transform layer coords to global coords: let mut widget_rect = *widget_rect; - if let Some(transform) = ctx.memory(|m| m.layer_transforms.get(&parent_layer).copied()) { - widget_rect = transform * widget_rect; + if let Some(to_global) = ctx.layer_transform_to_global(parent_layer) { + widget_rect = to_global * widget_rect; } remember_that_tooltip_was_shown(ctx); @@ -404,11 +404,12 @@ pub fn popup_above_or_below_widget( AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM), AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP), }; - if let Some(transform) = parent_ui + + if let Some(to_global) = parent_ui .ctx() - .memory(|m| m.layer_transforms.get(&parent_ui.layer_id()).copied()) + .layer_transform_to_global(parent_ui.layer_id()) { - pos = transform * pos; + pos = to_global * pos; } let frame = Frame::popup(parent_ui.style()); diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 458bf11229d..84f687783aa 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -205,7 +205,7 @@ struct Prepared { } impl Resize { - fn begin(&mut self, ui: &mut Ui) -> Prepared { + fn begin(&self, ui: &mut Ui) -> Prepared { let position = ui.available_rect_before_wrap().min; let id = self.id.unwrap_or_else(|| { let id_salt = self.id_salt.unwrap_or_else(|| Id::new("resize")); @@ -295,7 +295,7 @@ impl Resize { } } - pub fn show(mut self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R { + pub fn show(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R { let mut prepared = self.begin(ui); let ret = add_contents(&mut prepared.content_ui); self.end(ui, prepared); diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index aa449849e32..322007113ba 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -109,13 +109,13 @@ struct Plugins { impl Plugins { fn call(ctx: &Context, _cb_name: &str, callbacks: &[NamedContextCallback]) { - crate::profile_scope!("plugins", _cb_name); + profiling::scope!("plugins", _cb_name); for NamedContextCallback { debug_name: _name, callback, } in callbacks { - crate::profile_scope!("plugin", _name); + profiling::scope!("plugin", _name); (callback)(ctx); } } @@ -498,19 +498,8 @@ impl ContextImpl { viewport.this_pass.begin_pass(screen_rect); { - let area_order = self.memory.areas().order_map(); - let mut layers: Vec = viewport.prev_pass.widgets.layer_ids().collect(); - - layers.sort_by(|a, b| { - if a.order == b.order { - // Maybe both are windows, so respect area order: - area_order.get(a).cmp(&area_order.get(b)) - } else { - // comparing e.g. background to tooltips - a.order.cmp(&b.order) - } - }); + layers.sort_by(|&a, &b| self.memory.areas().compare_order(a, b)); viewport.hits = if let Some(pos) = viewport.input.pointer.interact_pos() { let interact_radius = self.memory.options.style().interaction.interact_radius; @@ -518,7 +507,7 @@ impl ContextImpl { crate::hit_test::hit_test( &viewport.prev_pass.widgets, &layers, - &self.memory.layer_transforms, + &self.memory.to_global, pos, interact_radius, ) @@ -549,7 +538,7 @@ impl ContextImpl { #[cfg(feature = "accesskit")] if self.is_accesskit_enabled { - crate::profile_scope!("accesskit"); + profiling::scope!("accesskit"); use crate::pass_state::AccessKitPassState; let id = crate::accesskit_root_id(); let mut root_node = accesskit::Node::new(accesskit::Role::Window); @@ -568,8 +557,7 @@ impl ContextImpl { /// Load fonts unless already loaded. fn update_fonts_mut(&mut self) { - crate::profile_function!(); - + profiling::function_scope!(); let input = &self.viewport().input; let pixels_per_point = input.pixels_per_point(); let max_texture_side = input.max_texture_side; @@ -616,7 +604,7 @@ impl ContextImpl { log::trace!("Creating new Fonts for pixels_per_point={pixels_per_point}"); is_new = true; - crate::profile_scope!("Fonts::new"); + profiling::scope!("Fonts::new"); Fonts::new( pixels_per_point, max_texture_side, @@ -625,12 +613,12 @@ impl ContextImpl { }); { - crate::profile_scope!("Fonts::begin_pass"); + profiling::scope!("Fonts::begin_pass"); fonts.begin_pass(pixels_per_point, max_texture_side); } if is_new && self.memory.options.preload_font_glyphs { - crate::profile_scope!("preload_font_glyphs"); + profiling::scope!("preload_font_glyphs"); // Preload the most common characters for the most common fonts. // This is not very important to do, but may save a few GPU operations. for font_id in self.memory.options.style().text_styles.values() { @@ -812,8 +800,7 @@ impl Context { /// ``` #[must_use] pub fn run(&self, mut new_input: RawInput, mut run_ui: impl FnMut(&Self)) -> FullOutput { - crate::profile_function!(); - + profiling::function_scope!(); let viewport_id = new_input.viewport_id; let max_passes = self.write(|ctx| ctx.memory.options.max_passes.get()); @@ -821,9 +808,13 @@ impl Context { debug_assert_eq!(output.platform_output.num_completed_passes, 0); loop { - crate::profile_scope!( + profiling::scope!( "pass", - output.platform_output.num_completed_passes.to_string() + output + .platform_output + .num_completed_passes + .to_string() + .as_str() ); // We must move the `num_passes` (back) to the viewport output so that [`Self::will_discard`] @@ -886,7 +877,7 @@ impl Context { /// // handle full_output /// ``` pub fn begin_pass(&self, new_input: RawInput) { - crate::profile_function!(); + profiling::function_scope!(); self.write(|ctx| ctx.begin_pass(new_input)); @@ -1329,11 +1320,11 @@ impl Context { res.is_pointer_button_down_on || res.long_touched || clicked || res.drag_stopped; if is_interacted_with { res.interact_pointer_pos = input.pointer.interact_pos(); - if let (Some(transform), Some(pos)) = ( - memory.layer_transforms.get(&res.layer_id), + if let (Some(to_global), Some(pos)) = ( + memory.to_global.get(&res.layer_id), &mut res.interact_pointer_pos, ) { - *pos = transform.inverse() * *pos; + *pos = to_global.inverse() * *pos; } } @@ -1760,7 +1751,7 @@ impl Context { /// The new fonts will become active at the start of the next pass. /// This will overwrite the existing fonts. pub fn set_fonts(&self, font_definitions: FontDefinitions) { - crate::profile_function!(); + profiling::function_scope!(); let pixels_per_point = self.pixels_per_point(); @@ -1788,7 +1779,7 @@ impl Context { /// The new font will become active at the start of the next pass. /// This will keep the existing fonts. pub fn add_font(&self, new_font: FontInsert) { - crate::profile_function!(); + profiling::function_scope!(); let pixels_per_point = self.pixels_per_point(); @@ -2152,7 +2143,7 @@ impl Context { /// Call at the end of each frame if you called [`Context::begin_pass`]. #[must_use] pub fn end_pass(&self) -> FullOutput { - crate::profile_function!(); + profiling::function_scope!(); if self.options(|o| o.zoom_with_keyboard) { crate::gui_zoom::zoom_with_keyboard(self); @@ -2246,7 +2237,8 @@ impl Context { for id in contains_pointer { let mut widget_text = format!("{id:?}"); if let Some(rect) = widget_rects.get(id) { - widget_text += &format!(" {:?} {:?}", rect.rect, rect.sense); + widget_text += + &format!(" {:?} {:?} {:?}", rect.layer_id, rect.rect, rect.sense); } if let Some(info) = widget_rects.info(id) { widget_text += &format!(" {info:?}"); @@ -2272,11 +2264,17 @@ impl Context { if self.style().debug.show_widget_hits { let hits = self.write(|ctx| ctx.viewport().hits.clone()); let WidgetHits { + close, contains_pointer, click, drag, } = hits; + if false { + for widget in &close { + paint_widget(widget, "close", Color32::from_gray(70)); + } + } if true { for widget in &contains_pointer { paint_widget(widget, "contains_pointer", Color32::BLUE); @@ -2342,7 +2340,7 @@ impl ContextImpl { // https://github.com/emilk/egui/issues/3664 // at the cost of a lot of performance. // (This will override any smaller delta that was uploaded above.) - crate::profile_scope!("full_font_atlas_update"); + profiling::scope!("full_font_atlas_update"); let full_delta = ImageDelta::full(fonts.image(), TextureAtlas::texture_options()); tex_mngr.set(TextureId::default(), full_delta); } @@ -2356,7 +2354,7 @@ impl ContextImpl { #[cfg(feature = "accesskit")] { - crate::profile_scope!("accesskit"); + profiling::scope!("accesskit"); let state = viewport.this_pass.accesskit_state.take(); if let Some(state) = state { let root_id = crate::accesskit_root_id().accesskit_id(); @@ -2381,12 +2379,12 @@ impl ContextImpl { let shapes = viewport .graphics - .drain(self.memory.areas().order(), &self.memory.layer_transforms); + .drain(self.memory.areas().order(), &self.memory.to_global); let mut repaint_needed = false; if self.memory.options.repaint_on_widget_change { - crate::profile_function!("compare-widget-rects"); + profiling::scope!("compare-widget-rects"); if viewport.prev_pass.widgets != viewport.this_pass.widgets { repaint_needed = true; // Some widget has moved } @@ -2525,7 +2523,7 @@ impl Context { shapes: Vec, pixels_per_point: f32, ) -> Vec { - crate::profile_function!(); + profiling::function_scope!(); // A tempting optimization is to reuse the tessellation from last frame if the // shapes are the same, but just comparing the shapes takes about 50% of the time @@ -2552,7 +2550,7 @@ impl Context { let paint_stats = PaintStats::from_shapes(&shapes); let clipped_primitives = { - crate::profile_scope!("tessellator::tessellate_shapes"); + profiling::scope!("tessellator::tessellate_shapes"); tessellator::Tessellator::new( pixels_per_point, tessellation_options, @@ -2697,6 +2695,7 @@ impl Context { /// Transform the graphics of the given layer. /// /// This will also affect input. + /// The direction of the given transform is "into the global coordinate system". /// /// This is a sticky setting, remembered from one frame to the next. /// @@ -2706,13 +2705,28 @@ impl Context { pub fn set_transform_layer(&self, layer_id: LayerId, transform: TSTransform) { self.memory_mut(|m| { if transform == TSTransform::IDENTITY { - m.layer_transforms.remove(&layer_id) + m.to_global.remove(&layer_id) } else { - m.layer_transforms.insert(layer_id, transform) + m.to_global.insert(layer_id, transform) } }); } + /// Return how to transform the graphics of the given layer into the global coordinate system. + /// + /// Set this with [`Self::layer_transform_to_global`]. + pub fn layer_transform_to_global(&self, layer_id: LayerId) -> Option { + self.memory(|m| m.to_global.get(&layer_id).copied()) + } + + /// Return how to transform the graphics of the global coordinate system into the local coordinate system of the given layer. + /// + /// This returns the inverse of [`Self::layer_transform_to_global`]. + pub fn layer_transform_from_global(&self, layer_id: LayerId) -> Option { + self.layer_transform_to_global(layer_id) + .map(|t| t.inverse()) + } + /// Move all the graphics at the given layer. /// /// Is used to implement drag-and-drop preview. @@ -2777,12 +2791,11 @@ impl Context { /// /// See also [`Response::contains_pointer`]. pub fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool { - let rect = - if let Some(transform) = self.memory(|m| m.layer_transforms.get(&layer_id).copied()) { - transform * rect - } else { - rect - }; + let rect = if let Some(to_global) = self.layer_transform_to_global(layer_id) { + to_global * rect + } else { + rect + }; if !rect.is_positive() { return false; } @@ -3144,28 +3157,26 @@ impl Context { self.memory_mut(|mem| *mem.areas_mut() = Default::default()); } }); - ui.indent("areas", |ui| { - ui.label("Visible areas, ordered back to front."); - ui.label("Hover to highlight"); + ui.indent("layers", |ui| { + ui.label("Layers, ordered back to front."); let layers_ids: Vec = self.memory(|mem| mem.areas().order().to_vec()); for layer_id in layers_ids { - let area = AreaState::load(self, layer_id.id); - if let Some(area) = area { + if let Some(area) = AreaState::load(self, layer_id.id) { let is_visible = self.memory(|mem| mem.areas().is_visible(&layer_id)); if !is_visible { continue; } let text = format!("{} - {:?}", layer_id.short_debug_format(), area.rect(),); // TODO(emilk): `Sense::hover_highlight()` - if ui - .add(Label::new(RichText::new(text).monospace()).sense(Sense::click())) - .hovered - && is_visible - { + let response = + ui.add(Label::new(RichText::new(text).monospace()).sense(Sense::click())); + if response.hovered && is_visible { ui.ctx() .debug_painter() .debug_rect(area.rect(), Color32::RED, ""); } + } else { + ui.monospace(layer_id.short_debug_format()); } } }); @@ -3353,7 +3364,7 @@ impl Context { pub fn forget_image(&self, uri: &str) { use load::BytesLoader as _; - crate::profile_function!(); + profiling::function_scope!(); let loaders = self.loaders(); @@ -3375,7 +3386,7 @@ impl Context { pub fn forget_all_images(&self) { use load::BytesLoader as _; - crate::profile_function!(); + profiling::function_scope!(); let loaders = self.loaders(); @@ -3410,7 +3421,7 @@ impl Context { /// [not_supported]: crate::load::LoadError::NotSupported /// [custom]: crate::load::LoadError::Loading pub fn try_load_bytes(&self, uri: &str) -> load::BytesLoadResult { - crate::profile_function!(uri); + profiling::function_scope!(uri); let loaders = self.loaders(); let bytes_loaders = loaders.bytes.lock(); @@ -3447,7 +3458,7 @@ impl Context { /// [not_supported]: crate::load::LoadError::NotSupported /// [custom]: crate::load::LoadError::Loading pub fn try_load_image(&self, uri: &str, size_hint: load::SizeHint) -> load::ImageLoadResult { - crate::profile_function!(uri); + profiling::function_scope!(uri); let loaders = self.loaders(); let image_loaders = loaders.image.lock(); @@ -3498,7 +3509,7 @@ impl Context { texture_options: TextureOptions, size_hint: load::SizeHint, ) -> load::TextureLoadResult { - crate::profile_function!(uri); + profiling::function_scope!(uri); let loaders = self.loaders(); let texture_loaders = loaders.texture.lock(); @@ -3516,7 +3527,7 @@ impl Context { /// The loaders of bytes, images, and textures. pub fn loaders(&self) -> Arc { - crate::profile_function!(); + profiling::function_scope!(); self.read(|this| this.loaders.clone()) } } @@ -3648,7 +3659,7 @@ impl Context { viewport_builder: ViewportBuilder, viewport_ui_cb: impl Fn(&Self, ViewportClass) + Send + Sync + 'static, ) { - crate::profile_function!(); + profiling::function_scope!(); if self.embed_viewports() { viewport_ui_cb(self, ViewportClass::Embedded); @@ -3700,7 +3711,7 @@ impl Context { builder: ViewportBuilder, mut viewport_ui_cb: impl FnMut(&Self, ViewportClass) -> T, ) -> T { - crate::profile_function!(); + profiling::function_scope!(); if self.embed_viewports() { return viewport_ui_cb(self, ViewportClass::Embedded); diff --git a/crates/egui/src/drag_and_drop.rs b/crates/egui/src/drag_and_drop.rs index e1997800834..fc9f29f5e03 100644 --- a/crates/egui/src/drag_and_drop.rs +++ b/crates/egui/src/drag_and_drop.rs @@ -57,7 +57,13 @@ impl DragAndDrop { if abort_dnd_due_to_mouse_release { Self::clear_payload(ctx); } else { - ctx.set_cursor_icon(CursorIcon::Grabbing); + // We set the cursor icon only if its default, as the user code might have + // explicitly set it already. + ctx.output_mut(|o| { + if o.cursor_icon == CursorIcon::Default { + o.cursor_icon = CursorIcon::Grabbing; + } + }); } } } diff --git a/crates/egui/src/grid.rs b/crates/egui/src/grid.rs index 3c4986fcad0..0342f6f5227 100644 --- a/crates/egui/src/grid.rs +++ b/crates/egui/src/grid.rs @@ -227,7 +227,7 @@ impl GridLayout { self.col += 1; } - fn paint_row(&mut self, cursor: &Rect, painter: &Painter) { + fn paint_row(&self, cursor: &Rect, painter: &Painter) { // handle row color painting based on color-picker function let Some(color_picker) = self.color_picker.as_ref() else { return; @@ -450,7 +450,7 @@ impl Grid { ui.allocate_new_ui(ui_builder, |ui| { ui.horizontal(|ui| { let is_color = color_picker.is_some(); - let mut grid = GridLayout { + let grid = GridLayout { num_columns, color_picker, min_cell_size: vec2(min_col_width, min_row_height), diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 142645f24e6..8741f5f8b41 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -2,7 +2,7 @@ use ahash::HashMap; use emath::TSTransform; -use crate::{ahash, emath, LayerId, Pos2, WidgetRect, WidgetRects}; +use crate::{ahash, emath, LayerId, Pos2, Rect, WidgetRect, WidgetRects}; /// Result of a hit-test against [`WidgetRects`]. /// @@ -12,11 +12,18 @@ use crate::{ahash, emath, LayerId, Pos2, WidgetRect, WidgetRects}; /// or if we're currently already dragging something. #[derive(Clone, Debug, Default)] pub struct WidgetHits { + /// All widgets close to the pointer, back-to-front. + /// + /// This is a superset of all other widgets in this struct. + pub close: Vec, + /// All widgets that contains the pointer, back-to-front. /// - /// i.e. both a Window and the button in it can contain the pointer. + /// i.e. both a Window and the Button in it can contain the pointer. /// /// Some of these may be widgets in a layer below the top-most layer. + /// + /// This will be used for hovering. pub contains_pointer: Vec, /// If the user would start a clicking now, this is what would be clicked. @@ -35,18 +42,18 @@ pub struct WidgetHits { pub fn hit_test( widgets: &WidgetRects, layer_order: &[LayerId], - layer_transforms: &HashMap, + layer_to_global: &HashMap, pos: Pos2, search_radius: f32, ) -> WidgetHits { - crate::profile_function!(); + profiling::function_scope!(); let search_radius_sq = search_radius * search_radius; // Transform the position into the local coordinate space of each layer: - let pos_in_layers: HashMap = layer_transforms + let pos_in_layers: HashMap = layer_to_global .iter() - .map(|(layer_id, t)| (*layer_id, t.inverse() * pos)) + .map(|(layer_id, to_global)| (*layer_id, to_global.inverse() * pos)) .collect(); let mut closest_dist_sq = f32::INFINITY; @@ -63,6 +70,7 @@ pub fn hit_test( } let pos_in_layer = pos_in_layers.get(&w.layer_id).copied().unwrap_or(pos); + // TODO(emilk): we should probably do the distance testing in global space instead let dist_sq = w.interact_rect.distance_sq_to_pos(pos_in_layer); // In tie, pick last = topmost. @@ -76,51 +84,103 @@ pub fn hit_test( .copied() .collect(); - // We need to pick one single layer for the interaction. - if let Some(closest_hit) = closest_hit { - // Select the top layer, and ignore widgets in any other layer: - let top_layer = closest_hit.layer_id; - close.retain(|w| w.layer_id == top_layer); - - // If the widget is disabled, treat it as if it isn't sensing anything. - // This simplifies the code in `hit_test_on_close` so it doesn't have to check - // the `enabled` flag everywhere: - for w in &mut close { - if !w.enabled { - w.sense.click = false; - w.sense.drag = false; - } + // Transform to global coordinates: + for hit in &mut close { + if let Some(to_global) = layer_to_global.get(&hit.layer_id).copied() { + *hit = hit.transform(to_global); } + } - let pos_in_layer = pos_in_layers.get(&top_layer).copied().unwrap_or(pos); - let hits = hit_test_on_close(&close, pos_in_layer); - - if let Some(drag) = hits.drag { - debug_assert!(drag.sense.drag); - } - if let Some(click) = hits.click { - debug_assert!(click.sense.click); + // When using layer transforms it is common to stack layers close to each other. + // For instance, you may have a resize-separator on a panel, with two + // transform-layers on either side. + // The resize-separator is technically in a layer _behind_ the transform-layers, + // but the user doesn't perceive it as such. + // So how do we handle this case? + // + // If we just allow interactions with ALL close widgets, + // then we might accidentally allow clicks through windows and other bad stuff. + // + // Let's try this: + // * Set up a hit-area (based on search_radius) + // * Iterate over all hits top-to-bottom + // * Stop if any hit covers the whole hit-area, otherwise keep going + // * Collect the layers ids in a set + // * Remove all widgets not in the above layer set + // + // This will most often result in only one layer, + // but if the pointer is at the edge of a layer, we might include widgets in + // a layer behind it. + + let mut included_layers: ahash::HashSet = Default::default(); + for hit in close.iter().rev() { + included_layers.insert(hit.layer_id); + let hit_covers_search_area = contains_circle(hit.interact_rect, pos, search_radius); + if hit_covers_search_area { + break; // nothing behind this layer could ever be interacted with } + } + + close.retain(|hit| included_layers.contains(&hit.layer_id)); - hits - } else { - // No close widgets. - Default::default() + // If a widget is disabled, treat it as if it isn't sensing anything. + // This simplifies the code in `hit_test_on_close` so it doesn't have to check + // the `enabled` flag everywhere: + for w in &mut close { + if !w.enabled { + w.sense.click = false; + w.sense.drag = false; + } } -} -fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { - #![allow(clippy::collapsible_else_if)] + let mut hits = hit_test_on_close(&close, pos); - // Only those widgets directly under the `pos`. - let hits: Vec = close + hits.contains_pointer = close .iter() .filter(|widget| widget.interact_rect.contains(pos)) .copied() .collect(); - let hit_click = hits.iter().copied().filter(|w| w.sense.click).last(); - let hit_drag = hits.iter().copied().filter(|w| w.sense.drag).last(); + hits.close = close; + + { + // Undo the to_global-transform we applied earlier, + // go back to local layer-coordinates: + + let restore_widget_rect = |w: &mut WidgetRect| { + *w = widgets.get(w.id).copied().unwrap_or(*w); + }; + + for wr in &mut hits.close { + restore_widget_rect(wr); + } + for wr in &mut hits.contains_pointer { + restore_widget_rect(wr); + } + if let Some(wr) = &mut hits.drag { + debug_assert!(wr.sense.drag); + restore_widget_rect(wr); + } + if let Some(wr) = &mut hits.click { + debug_assert!(wr.sense.click); + restore_widget_rect(wr); + } + } + + hits +} + +/// Returns true if the rectangle contains the whole circle. +fn contains_circle(interact_rect: emath::Rect, pos: Pos2, radius: f32) -> bool { + interact_rect.shrink(radius).contains(pos) +} + +fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { + #![allow(clippy::collapsible_else_if)] + + // First find the best direct hits: + let hit_click = find_closest_within(close.iter().copied().filter(|w| w.sense.click), pos, 0.0); + let hit_drag = find_closest_within(close.iter().copied().filter(|w| w.sense.drag), pos, 0.0); match (hit_click, hit_drag) { (None, None) => { @@ -136,16 +196,16 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { if let Some(closest) = closest { WidgetHits { - contains_pointer: hits, click: closest.sense.click.then_some(closest), drag: closest.sense.drag.then_some(closest), + ..Default::default() } } else { // Found nothing WidgetHits { - contains_pointer: hits, click: None, drag: None, + ..Default::default() } } } @@ -170,17 +230,17 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { // This is a smaller thing on a big background - help the user hit it, // and ignore the big drag background. WidgetHits { - contains_pointer: hits, click: Some(closest_click), drag: Some(closest_click), + ..Default::default() } } else { - // The drag wiudth is separate from the click wiudth, - // so return only the drag widget + // The drag-widget is separate from the click-widget, + // so return only the drag-widget WidgetHits { - contains_pointer: hits, click: None, drag: Some(hit_drag), + ..Default::default() } } } else { @@ -194,17 +254,17 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { // The drag widget is a big background thing (scroll area), // so returning a separate click widget should not be confusing WidgetHits { - contains_pointer: hits, click: Some(closest_click), drag: Some(hit_drag), + ..Default::default() } } else { // The two widgets are just two normal small widgets close to each other. // Highlighting both would be very confusing. WidgetHits { - contains_pointer: hits, click: None, drag: Some(hit_drag), + ..Default::default() } } } @@ -229,17 +289,17 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { // `hit_drag` is a big background thing and `closest_drag` is something small on top of it. // Be helpful and return the small things: return WidgetHits { - contains_pointer: hits, click: None, drag: Some(closest_drag), + ..Default::default() }; } } WidgetHits { - contains_pointer: hits, click: None, drag: Some(hit_drag), + ..Default::default() } } } @@ -253,57 +313,57 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { // where when hovering directly over a drag-widget (like a big ScrollArea), // we look for close click-widgets (e.g. buttons). // This is because big background drag-widgets (ScrollArea, Window) are common, - // but bit clickable things aren't. + // but big clickable things aren't. // Even if they were, I think it would be confusing for a user if clicking // a drag-only widget would click something _behind_ it. WidgetHits { - contains_pointer: hits, click: Some(hit_click), drag: None, + ..Default::default() } } (Some(hit_click), Some(hit_drag)) => { // We have a perfect hit on both click and drag. Which is the topmost? - let click_idx = hits.iter().position(|w| *w == hit_click).unwrap(); - let drag_idx = hits.iter().position(|w| *w == hit_drag).unwrap(); + let click_idx = close.iter().position(|w| *w == hit_click).unwrap(); + let drag_idx = close.iter().position(|w| *w == hit_drag).unwrap(); let click_is_on_top_of_drag = drag_idx < click_idx; if click_is_on_top_of_drag { if hit_click.sense.drag { // The top thing senses both clicks and drags. WidgetHits { - contains_pointer: hits, click: Some(hit_click), drag: Some(hit_click), + ..Default::default() } } else { // They are interested in different things, // and click is on top. Report both hits, // e.g. the top Button and the ScrollArea behind it. WidgetHits { - contains_pointer: hits, click: Some(hit_click), drag: Some(hit_drag), + ..Default::default() } } } else { if hit_drag.sense.click { // The top thing senses both clicks and drags. WidgetHits { - contains_pointer: hits, click: Some(hit_drag), drag: Some(hit_drag), + ..Default::default() } } else { // The top things senses only drags, // so we ignore the click-widget, because it would be confusing // if clicking a drag-widget would actually click something else below it. WidgetHits { - contains_pointer: hits, click: None, drag: Some(hit_drag), + ..Default::default() } } } @@ -312,8 +372,16 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { } fn find_closest(widgets: impl Iterator, pos: Pos2) -> Option { - let mut closest = None; - let mut closest_dist_sq = f32::INFINITY; + find_closest_within(widgets, pos, f32::INFINITY) +} + +fn find_closest_within( + widgets: impl Iterator, + pos: Pos2, + max_dist: f32, +) -> Option { + let mut closest: Option = None; + let mut closest_dist_sq = max_dist * max_dist; for widget in widgets { if widget.interact_rect.is_negative() { continue; @@ -321,6 +389,16 @@ fn find_closest(widgets: impl Iterator, pos: Pos2) -> Option< let dist_sq = widget.interact_rect.distance_sq_to_pos(pos); + if let Some(closest) = closest { + if dist_sq == closest_dist_sq { + // It's a tie! Pick the thin candidate over the thick one. + // This makes it easier to hit a thin resize-handle, for instance: + if should_prioritizie_hits_on_back(closest.interact_rect, widget.interact_rect) { + continue; + } + } + } + // In case of a tie, take the last one = the one on top. if dist_sq <= closest_dist_sq { closest_dist_sq = dist_sq; @@ -331,6 +409,27 @@ fn find_closest(widgets: impl Iterator, pos: Pos2) -> Option< closest } +/// Should we prioritizie hits on `back` over those on `front`? +/// +/// `back` should be behind the `front` widget. +/// +/// Returns true if `back` is a small hit-target and `front` is not. +fn should_prioritizie_hits_on_back(back: Rect, front: Rect) -> bool { + if front.contains_rect(back) { + return false; // back widget is fully occluded; no way to hit it + } + + // Reduce each rect to its width or height, whichever is smaller: + let back = back.width().min(back.height()); + let front = front.width().min(front.height()); + + // These are hard-coded heuristics that could surely be improved. + let back_is_much_thinner = back <= 0.5 * front; + let back_is_thin = back <= 16.0; + + back_is_much_thinner && back_is_thin +} + #[cfg(test)] mod tests { use emath::{pos2, vec2, Rect}; diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index 99c166cf443..048e880e3d4 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -269,7 +269,7 @@ impl InputState { pixels_per_point: f32, options: &crate::Options, ) -> Self { - crate::profile_function!(); + profiling::function_scope!(); let time = new.time.unwrap_or(self.time + new.predicted_dt as f64); let unstable_dt = (time - self.time) as f32; diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index 7cbadbf1193..04f8f7dbf6f 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -113,7 +113,7 @@ pub(crate) fn interact( input: &InputState, interaction: &mut InteractionState, ) -> InteractionSnapshot { - crate::profile_function!(); + profiling::function_scope!(); if let Some(id) = interaction.potential_click_id { if !widgets.contains(id) { @@ -249,7 +249,7 @@ pub(crate) fn interact( .copied() .collect() } else { - // We may be hovering a an interactive widget or two. + // We may be hovering an interactive widget or two. // We must also consider the case where non-interactive widgets // are _on top_ of an interactive widget. // For instance: a label in a draggable window. @@ -264,9 +264,9 @@ pub(crate) fn interact( // but none below it (an interactive widget stops the hover search). // // To know when to stop we need to first know the order of the widgets, - // which luckily we have in the `WidgetRects`. + // which luckily we already have in `hits.close`. - let order = |id| widgets.order(id).map(|(_layer, order)| order); // we ignore the layer, since all widgets at this point is in the same layer + let order = |id| hits.close.iter().position(|w| w.id == id); let click_order = hits.click.and_then(|w| order(w.id)).unwrap_or(0); let drag_order = hits.drag.and_then(|w| order(w.id)).unwrap_or(0); diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index b44daa41858..81a60812ece 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -11,9 +11,6 @@ pub enum Order { /// Painted behind all floating windows Background, - /// Special layer between panels and windows - PanelResizeLine, - /// Normal moveable windows that you reorder by click Middle, @@ -30,10 +27,9 @@ pub enum Order { } impl Order { - const COUNT: usize = 6; + const COUNT: usize = 5; const ALL: [Self; Self::COUNT] = [ Self::Background, - Self::PanelResizeLine, Self::Middle, Self::Foreground, Self::Tooltip, @@ -44,12 +40,9 @@ impl Order { #[inline(always)] pub fn allow_interaction(&self) -> bool { match self { - Self::Background - | Self::PanelResizeLine - | Self::Middle - | Self::Foreground - | Self::Tooltip - | Self::Debug => true, + Self::Background | Self::Middle | Self::Foreground | Self::Tooltip | Self::Debug => { + true + } } } @@ -57,7 +50,6 @@ impl Order { pub fn short_debug_format(&self) -> &'static str { match self { Self::Background => "backg", - Self::PanelResizeLine => "panel", Self::Middle => "middl", Self::Foreground => "foreg", Self::Tooltip => "toolt", @@ -68,7 +60,7 @@ impl Order { /// An identifier for a paint layer. /// Also acts as an identifier for [`crate::Area`]:s. -#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)] +#[derive(Clone, Copy, Hash, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct LayerId { pub order: Order, @@ -110,6 +102,13 @@ impl LayerId { } } +impl std::fmt::Debug for LayerId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self { order, id } = self; + write!(f, "LayerId {{ {order:?} {id:?} }}") + } +} + /// A unique identifier of a specific [`Shape`] in a [`PaintList`]. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -221,9 +220,9 @@ impl GraphicLayers { pub fn drain( &mut self, area_order: &[LayerId], - transforms: &ahash::HashMap, + to_global: &ahash::HashMap, ) -> Vec { - crate::profile_function!(); + profiling::function_scope!(); let mut all_shapes: Vec<_> = Default::default(); @@ -239,10 +238,10 @@ impl GraphicLayers { for layer_id in area_order { if layer_id.order == order { if let Some(list) = order_map.get_mut(&layer_id.id) { - if let Some(transform) = transforms.get(layer_id) { + if let Some(to_global) = to_global.get(layer_id) { for clipped_shape in &mut list.0 { - clipped_shape.clip_rect = *transform * clipped_shape.clip_rect; - clipped_shape.shape.transform(*transform); + clipped_shape.clip_rect = *to_global * clipped_shape.clip_rect; + clipped_shape.shape.transform(*to_global); } } all_shapes.append(&mut list.0); @@ -254,10 +253,10 @@ impl GraphicLayers { for (id, list) in order_map { let layer_id = LayerId::new(order, *id); - if let Some(transform) = transforms.get(&layer_id) { + if let Some(to_global) = to_global.get(&layer_id) { for clipped_shape in &mut list.0 { - clipped_shape.clip_rect = *transform * clipped_shape.clip_rect; - clipped_shape.shape.transform(*transform); + clipped_shape.clip_rect = *to_global * clipped_shape.clip_rect; + clipped_shape.shape.transform(*to_global); } } diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 6d8c6a3459d..1afaada95e1 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -3,7 +3,7 @@ //! Try the live web demo: . Read more about egui at . //! //! `egui` is in heavy development, with each new version having breaking changes. -//! You need to have rust 1.79.0 or later to use `egui`. +//! You need to have rust 1.80.0 or later to use `egui`. //! //! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template) //! which uses [`eframe`](https://docs.rs/eframe). @@ -388,6 +388,18 @@ //! ## Installing additional fonts //! The default egui fonts only support latin and cryllic characters, and some emojis. //! To use egui with e.g. asian characters you need to install your own font (`.ttf` or `.otf`) using [`Context::set_fonts`]. +//! +//! ## Instrumentation +//! This crate supports using the [profiling](https://crates.io/crates/profiling) crate for instrumentation. +//! You can enable features on the profiling crates in your application to add instrumentation for all +//! crates that support it, including egui. See the profiling crate docs for more information. +//! ```toml +//! [dependencies] +//! profiling = "1.0" +//! [features] +//! profile-with-puffin = ["profiling/profile-with-puffin"] +//! ``` +//! #![allow(clippy::float_cmp)] #![allow(clippy::manual_range_contains)] @@ -691,33 +703,3 @@ pub fn __run_test_ui(add_contents: impl Fn(&mut Ui)) { pub fn accesskit_root_id() -> Id { Id::new("accesskit_root") } - -// --------------------------------------------------------------------------- - -mod profiling_scopes { - #![allow(unused_macros)] - #![allow(unused_imports)] - - /// Profiling macro for feature "puffin" - macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} - -#[allow(unused_imports)] -pub(crate) use profiling_scopes::{profile_function, profile_scope}; diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 5e94e6c0915..976ad2d95ef 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -95,8 +95,13 @@ pub struct Memory { #[cfg_attr(feature = "persistence", serde(skip))] everything_is_visible: bool, - /// Transforms per layer - pub layer_transforms: HashMap, + /// Transforms per layer. + /// + /// Instead of using this directly, use: + /// * [`crate::Context::set_transform_layer`] + /// * [`crate::Context::layer_transform_to_global`] + /// * [`crate::Context::layer_transform_from_global`] + pub to_global: HashMap, // ------------------------------------------------- // Per-viewport: @@ -120,7 +125,7 @@ impl Default for Memory { focus: Default::default(), viewport_id: Default::default(), areas: Default::default(), - layer_transforms: Default::default(), + to_global: Default::default(), popup: Default::default(), everything_is_visible: Default::default(), add_fonts: Default::default(), @@ -774,7 +779,7 @@ impl Focus { impl Memory { pub(crate) fn begin_pass(&mut self, new_raw_input: &RawInput, viewports: &ViewportIdSet) { - crate::profile_function!(); + profiling::function_scope!(); self.viewport_id = new_raw_input.viewport_id; @@ -819,7 +824,7 @@ impl Memory { /// Top-most layer at the given position. pub fn layer_id_at(&self, pos: Pos2) -> Option { self.areas() - .layer_id_at(pos, &self.layer_transforms) + .layer_id_at(pos, &self.to_global) .and_then(|layer_id| { if self.is_above_modal_layer(layer_id) { Some(layer_id) @@ -829,6 +834,12 @@ impl Memory { }) } + /// The currently set transform of a layer. + #[deprecated = "Use `Context::layer_transform_to_global` instead"] + pub fn layer_transforms(&self, layer_id: LayerId) -> Option { + self.to_global.get(&layer_id).copied() + } + /// An iterator over all layers. Back-to-front, top is last. pub fn layer_ids(&self) -> impl ExactSizeIterator + '_ { self.areas().order().iter().copied() @@ -1121,15 +1132,18 @@ type OrderMap = HashMap; pub struct Areas { areas: IdMap, + visible_areas_last_frame: ahash::HashSet, + visible_areas_current_frame: ahash::HashSet, + + // ---------------------------- + // Everything below this is general to all layers, not just areas. + // TODO(emilk): move this to a separate struct. /// Back-to-front, top is last. order: Vec, - /// Actual order of the layers, pre-calculated each frame. + /// Inverse of [`Self::order`], calculated at the end of the frame. order_map: OrderMap, - visible_last_frame: ahash::HashSet, - visible_current_frame: ahash::HashSet, - /// When an area wants to be on top, it is assigned here. /// This is used to reorder the layers at the end of the frame. /// If several layers want to be on top, they will keep their relative order. @@ -1137,9 +1151,9 @@ pub struct Areas { /// results in them being sent to the top and keeping their previous internal order. wants_to_be_on_top: ahash::HashSet, - /// List of sublayers for each layer. + /// The sublayers that each layer has. /// - /// When a layer has sublayers, they are moved directly above it in the ordering. + /// The parent sublayer is moved directly above the child sublayers in the ordering. sublayers: ahash::HashMap>, } @@ -1152,17 +1166,13 @@ impl Areas { self.areas.get(&id) } - /// Back-to-front, top is last. + /// All layers back-to-front, top is last. pub(crate) fn order(&self) -> &[LayerId] { &self.order } - /// For each layer, which [`Self::order`] is it in? - pub(crate) fn order_map(&self) -> &OrderMap { - &self.order_map - } - /// Compare the order of two layers, based on the order list from last frame. + /// /// May return [`std::cmp::Ordering::Equal`] if the layers are not in the order list. pub(crate) fn compare_order(&self, a: LayerId, b: LayerId) -> std::cmp::Ordering { if let (Some(a), Some(b)) = (self.order_map.get(&a), self.order_map.get(&b)) { @@ -1172,18 +1182,8 @@ impl Areas { } } - /// Calculates the order map. - fn calculate_order_map(&mut self) { - self.order_map = self - .order - .iter() - .enumerate() - .map(|(i, id)| (*id, i)) - .collect(); - } - pub(crate) fn set_state(&mut self, layer_id: LayerId, state: area::AreaState) { - self.visible_current_frame.insert(layer_id); + self.visible_areas_current_frame.insert(layer_id); self.areas.insert(layer_id.id, state); if !self.order.iter().any(|x| *x == layer_id) { self.order.push(layer_id); @@ -1194,15 +1194,15 @@ impl Areas { pub fn layer_id_at( &self, pos: Pos2, - layer_transforms: &HashMap, + layer_to_global: &HashMap, ) -> Option { for layer in self.order.iter().rev() { if self.is_visible(layer) { if let Some(state) = self.areas.get(&layer.id) { let mut rect = state.rect(); if state.interactable { - if let Some(transform) = layer_transforms.get(layer) { - rect = *transform * rect; + if let Some(to_global) = layer_to_global.get(layer) { + rect = *to_global * rect; } if rect.contains(pos) { @@ -1216,18 +1216,19 @@ impl Areas { } pub fn visible_last_frame(&self, layer_id: &LayerId) -> bool { - self.visible_last_frame.contains(layer_id) + self.visible_areas_last_frame.contains(layer_id) } pub fn is_visible(&self, layer_id: &LayerId) -> bool { - self.visible_last_frame.contains(layer_id) || self.visible_current_frame.contains(layer_id) + self.visible_areas_last_frame.contains(layer_id) + || self.visible_areas_current_frame.contains(layer_id) } pub fn visible_layer_ids(&self) -> ahash::HashSet { - self.visible_last_frame + self.visible_areas_last_frame .iter() .copied() - .chain(self.visible_current_frame.iter().copied()) + .chain(self.visible_areas_current_frame.iter().copied()) .collect() } @@ -1240,7 +1241,7 @@ impl Areas { } pub fn move_to_top(&mut self, layer_id: LayerId) { - self.visible_current_frame.insert(layer_id); + self.visible_areas_current_frame.insert(layer_id); self.wants_to_be_on_top.insert(layer_id); if !self.order.iter().any(|x| *x == layer_id) { @@ -1255,8 +1256,21 @@ impl Areas { /// /// This currently only supports one level of nesting. If `parent` is a sublayer of another /// layer, the behavior is unspecified. + /// + /// The two layers must have the same [`LayerId::order`]. pub fn set_sublayer(&mut self, parent: LayerId, child: LayerId) { + debug_assert_eq!(parent.order, child.order, + "DEBUG ASSERT: Trying to set sublayers across layers of different order ({:?}, {:?}), which is currently undefined behavior in egui", parent.order, child.order); + self.sublayers.entry(parent).or_default().insert(child); + + // Make sure the layers are in the order list: + if !self.order.iter().any(|x| *x == parent) { + self.order.push(parent); + } + if !self.order.iter().any(|x| *x == child) { + self.order.push(child); + } } pub fn top_layer_id(&self, order: Order) -> Option { @@ -1267,26 +1281,42 @@ impl Areas { .copied() } + /// If this layer is the sublayer of another layer, return the parent. + pub fn parent_layer(&self, layer_id: LayerId) -> Option { + self.sublayers.iter().find_map(|(parent, children)| { + if children.contains(&layer_id) { + Some(*parent) + } else { + None + } + }) + } + + /// All the child layers of this layer. + pub fn child_layers(&self, layer_id: LayerId) -> impl Iterator + '_ { + self.sublayers.get(&layer_id).into_iter().flatten().copied() + } + pub(crate) fn is_sublayer(&self, layer: &LayerId) -> bool { - self.sublayers - .iter() - .any(|(_, children)| children.contains(layer)) + self.parent_layer(*layer).is_some() } pub(crate) fn end_pass(&mut self) { let Self { - visible_last_frame, - visible_current_frame, + visible_areas_last_frame, + visible_areas_current_frame, order, wants_to_be_on_top, sublayers, .. } = self; - std::mem::swap(visible_last_frame, visible_current_frame); - visible_current_frame.clear(); + std::mem::swap(visible_areas_last_frame, visible_areas_current_frame); + visible_areas_current_frame.clear(); + order.sort_by_key(|layer| (layer.order, wants_to_be_on_top.contains(layer))); wants_to_be_on_top.clear(); + // For all layers with sublayers, put the sublayers directly after the parent layer: let sublayers = std::mem::take(sublayers); for (parent, children) in sublayers { @@ -1304,7 +1334,13 @@ impl Areas { }; order.splice(parent_pos..=parent_pos, moved_layers); } - self.calculate_order_map(); + + self.order_map = self + .order + .iter() + .enumerate() + .map(|(i, id)| (*id, i)) + .collect(); } } diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 8faf8e77ceb..166e18da04b 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -406,11 +406,8 @@ impl MenuRoot { } } - if let Some(transform) = button - .ctx - .memory(|m| m.layer_transforms.get(&button.layer_id).copied()) - { - pos = transform * pos; + if let Some(to_global) = button.ctx.layer_transform_to_global(button.layer_id) { + pos = to_global * pos; } return MenuResponse::Create(pos, id); diff --git a/crates/egui/src/pass_state.rs b/crates/egui/src/pass_state.rs index da42e0932ae..5501220f769 100644 --- a/crates/egui/src/pass_state.rs +++ b/crates/egui/src/pass_state.rs @@ -248,7 +248,7 @@ impl Default for PassState { impl PassState { pub(crate) fn begin_pass(&mut self, screen_rect: Rect) { - crate::profile_function!(); + profiling::function_scope!(); let Self { used_ids, widgets, diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index f49383c70d1..18ddf793cc6 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -392,11 +392,8 @@ impl Response { pub fn drag_delta(&self) -> Vec2 { if self.dragged() { let mut delta = self.ctx.input(|i| i.pointer.delta()); - if let Some(scaling) = self - .ctx - .memory(|m| m.layer_transforms.get(&self.layer_id).map(|t| t.scaling)) - { - delta /= scaling; + if let Some(from_global) = self.ctx.layer_transform_from_global(self.layer_id) { + delta *= from_global.scaling; } delta } else { @@ -478,11 +475,8 @@ impl Response { pub fn hover_pos(&self) -> Option { if self.hovered() { let mut pos = self.ctx.input(|i| i.pointer.hover_pos())?; - if let Some(transform) = self - .ctx - .memory(|m| m.layer_transforms.get(&self.layer_id).copied()) - { - pos = transform.inverse() * pos; + if let Some(from_global) = self.ctx.layer_transform_from_global(self.layer_id) { + pos = from_global * pos; } Some(pos) } else { diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 786b93d53c4..6cc474e141b 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -301,7 +301,7 @@ impl Ui { min_rect: placer.min_rect(), max_rect: placer.max_rect(), }; - let child_ui = Ui { + let mut child_ui = Ui { id: stable_id, unique_id, next_auto_id_salt, @@ -316,6 +316,10 @@ impl Ui { min_rect_already_remembered: false, }; + if disabled { + child_ui.disable(); + } + // Register in the widget stack early, to ensure we are behind all widgets we contain: let start_rect = Rect::NOTHING; // This will be overwritten when `remember_min_rect` is called child_ui.ctx().create_widget( diff --git a/crates/egui/src/util/id_type_map.rs b/crates/egui/src/util/id_type_map.rs index 7e812ae5572..a7b8f18c9c3 100644 --- a/crates/egui/src/util/id_type_map.rs +++ b/crates/egui/src/util/id_type_map.rs @@ -308,7 +308,7 @@ fn from_ron_str(ron: &str) -> Option { use crate::Id; // TODO(emilk): make IdTypeMap generic over the key (`Id`), and make a library of IdTypeMap. -/// Stores values identified by an [`Id`] AND a the [`std::any::TypeId`] of the value. +/// Stores values identified by an [`Id`] AND the [`std::any::TypeId`] of the value. /// /// In other words, it maps `(Id, TypeId)` to any value you want. /// @@ -574,7 +574,7 @@ struct PersistedMap(Vec<(u64, SerializedElement)>); #[cfg(feature = "persistence")] impl PersistedMap { fn from_map(map: &IdTypeMap) -> Self { - crate::profile_function!(); + profiling::function_scope!(); use std::collections::BTreeMap; @@ -593,7 +593,7 @@ impl PersistedMap { let max_bytes_per_type = map.max_bytes_per_type; { - crate::profile_scope!("gather"); + profiling::scope!("gather"); for (hash, element) in &map.map { if let Some(element) = element.to_serialize() { let stats = types_map.entry(element.type_id).or_default(); @@ -610,7 +610,7 @@ impl PersistedMap { let mut persisted = vec![]; { - crate::profile_scope!("gc"); + profiling::scope!("gc"); for stats in types_map.values() { let mut bytes_written = 0; @@ -634,7 +634,7 @@ impl PersistedMap { } fn into_map(self) -> IdTypeMap { - crate::profile_function!(); + profiling::function_scope!(); let map = self .0 .into_iter() @@ -671,7 +671,7 @@ impl serde::Serialize for IdTypeMap { where S: serde::Serializer, { - crate::profile_scope!("IdTypeMap::serialize"); + profiling::scope!("IdTypeMap::serialize"); PersistedMap::from_map(self).serialize(serializer) } } @@ -682,7 +682,7 @@ impl<'de> serde::Deserialize<'de> for IdTypeMap { where D: serde::Deserializer<'de>, { - crate::profile_scope!("IdTypeMap::deserialize"); + profiling::scope!("IdTypeMap::deserialize"); ::deserialize(deserializer).map(PersistedMap::into_map) } } diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index d8b26429c77..91cd12e2b60 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -188,7 +188,7 @@ impl std::fmt::Debug for IconData { impl From for epaint::ColorImage { fn from(icon: IconData) -> Self { - crate::profile_function!(); + profiling::function_scope!(); let IconData { rgba, width, @@ -200,7 +200,7 @@ impl From for epaint::ColorImage { impl From<&IconData> for epaint::ColorImage { fn from(icon: &IconData) -> Self { - crate::profile_function!(); + profiling::function_scope!(); let IconData { rgba, width, @@ -1056,7 +1056,7 @@ pub enum ViewportCommand { /// Enable mouse pass-through: mouse clicks pass through the window, used for non-interactable overlays. MousePassthrough(bool), - /// Take a screenshot. + /// Take a screenshot of the next frame after this. /// /// The results are returned in [`crate::Event::Screenshot`]. Screenshot(crate::UserData), diff --git a/crates/egui/src/widget_rect.rs b/crates/egui/src/widget_rect.rs index dd900af1f11..3725d62a7b0 100644 --- a/crates/egui/src/widget_rect.rs +++ b/crates/egui/src/widget_rect.rs @@ -20,10 +20,10 @@ pub struct WidgetRect { /// What layer the widget is on. pub layer_id: LayerId, - /// The full widget rectangle. + /// The full widget rectangle, in local layer coordinates. pub rect: Rect, - /// Where the widget is. + /// Where the widget is, in local layer coordinates. /// /// This is after clipping with the parent ui clip rect. pub interact_rect: Rect, @@ -42,6 +42,27 @@ pub struct WidgetRect { pub enabled: bool, } +impl WidgetRect { + pub fn transform(self, transform: emath::TSTransform) -> Self { + let Self { + id, + layer_id, + rect, + interact_rect, + sense, + enabled, + } = self; + Self { + id, + layer_id, + rect: transform * rect, + interact_rect: transform * interact_rect, + sense, + enabled, + } + } +} + /// Stores the [`WidgetRect`]s of all widgets generated during a single egui update/frame. /// /// All [`crate::Ui`]s have a [`WidgetRect`]. It is created in [`crate::Ui::new`] with [`Rect::NOTHING`] diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs index a00d6f9ac3b..9a605905968 100644 --- a/crates/egui/src/widgets/color_picker.rs +++ b/crates/egui/src/widgets/color_picker.rs @@ -165,8 +165,11 @@ fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color /// * `x_value` - X axis, either saturation or value (0.0-1.0). /// * `y_value` - Y axis, either saturation or value (0.0-1.0). /// * `color_at` - A function that dictates how the mix of saturation and value will be displayed in the 2d slider. -/// E.g.: `|x_value, y_value| HsvaGamma { h: 1.0, s: x_value, v: y_value, a: 1.0 }.into()` displays the colors as follows: top-left: white \[s: 0.0, v: 1.0], top-right: fully saturated color \[s: 1.0, v: 1.0], bottom-right: black \[s: 0.0, v: 1.0]. /// +/// e.g.: `|x_value, y_value| HsvaGamma { h: 1.0, s: x_value, v: y_value, a: 1.0 }.into()` displays the colors as follows: +/// * top-left: white `[s: 0.0, v: 1.0]` +/// * top-right: fully saturated color `[s: 1.0, v: 1.0]` +/// * bottom-right: black `[s: 0.0, v: 1.0].` fn color_slider_2d( ui: &mut Ui, x_value: &mut f32, diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 012d8c8f0d5..587f498b0a2 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -766,14 +766,15 @@ impl<'t> TextEdit<'t> { } // Set IME output (in screen coords) when text is editable and visible - let transform = ui - .memory(|m| m.layer_transforms.get(&ui.layer_id()).copied()) + let to_global = ui + .ctx() + .layer_transform_to_global(ui.layer_id()) .unwrap_or_default(); ui.ctx().output_mut(|o| { o.ime = Some(crate::output::IMEOutput { - rect: transform * rect, - cursor_rect: transform * primary_cursor_rect, + rect: to_global * rect, + cursor_rect: to_global * primary_cursor_rect, }); }); } diff --git a/crates/egui/src/widgets/text_edit/state.rs b/crates/egui/src/widgets/text_edit/state.rs index cbbb6071484..c10a88274ef 100644 --- a/crates/egui/src/widgets/text_edit/state.rs +++ b/crates/egui/src/widgets/text_edit/state.rs @@ -89,6 +89,7 @@ impl TextEditState { self.undoer.lock().clone() } + #[allow(clippy::needless_pass_by_ref_mut)] // Intentionally hide interiority of mutability pub fn set_undoer(&mut self, undoer: TextEditUndoer) { *self.undoer.lock() = undoer; } diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 23ef8599dba..c3083d11caf 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -8,6 +8,9 @@ rust-version.workspace = true publish = false default-run = "egui_demo_app" +[package.metadata.cargo-machete] +ignored = ["profiling"] + [lints] workspace = true @@ -25,10 +28,15 @@ default = ["glow", "persistence"] # image_viewer adds about 0.9 MB of WASM web_app = ["http", "persistence"] -http = ["ehttp", "image", "poll-promise", "egui_extras/image"] -image_viewer = ["image", "egui_extras/all_loaders", "rfd"] -persistence = ["eframe/persistence", "egui/persistence", "serde", "egui_extras/serde"] -puffin = ["eframe/puffin", "dep:puffin", "dep:puffin_http"] +http = ["ehttp", "image/jpeg", "poll-promise", "egui_extras/image"] +image_viewer = ["image/jpeg", "egui_extras/all_loaders", "rfd"] +persistence = [ + "eframe/persistence", + "egui_extras/serde", + "egui/persistence", + "serde", +] +puffin = ["dep:puffin", "dep:puffin_http", "profiling/profile-with-puffin"] serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"] syntect = ["egui_demo_lib/syntect"] @@ -48,7 +56,12 @@ eframe = { workspace = true, default-features = false, features = [ egui = { workspace = true, features = ["callstack", "default", "log"] } egui_demo_lib = { workspace = true, features = ["default", "chrono"] } egui_extras = { workspace = true, features = ["default", "image"] } +image = { workspace = true, default-features = false, features = [ + # Ensure we can display the test images + "png", +] } log.workspace = true +profiling.workspace = true # Optional dependencies: @@ -61,7 +74,6 @@ wgpu = { workspace = true, features = ["webgpu", "webgl"], optional = true } # feature "http": ehttp = { version = "0.5", optional = true } -image = { workspace = true, optional = true, features = ["jpeg", "png"] } poll-promise = { version = "0.3", optional = true, default-features = false } # feature "persistence": @@ -74,7 +86,7 @@ env_logger = { version = "0.10", default-features = false, features = [ "auto-color", "humantime", ] } -rfd = { version = "0.13", optional = true } +rfd = { version = "0.15", optional = true } # web: [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/crates/egui_demo_app/src/frame_history.rs b/crates/egui_demo_app/src/frame_history.rs index 535d6d9f95f..a6a3fbeeb3f 100644 --- a/crates/egui_demo_app/src/frame_history.rs +++ b/crates/egui_demo_app/src/frame_history.rs @@ -32,7 +32,7 @@ impl FrameHistory { 1.0 / self.frame_times.mean_time_interval().unwrap_or_default() } - pub fn ui(&mut self, ui: &mut egui::Ui) { + pub fn ui(&self, ui: &mut egui::Ui) { ui.label(format!( "Mean CPU usage: {:.2} ms / frame", 1e3 * self.mean_frame_time() diff --git a/crates/egui_demo_app/src/main.rs b/crates/egui_demo_app/src/main.rs index 9f42b422d04..6a2bb2da796 100644 --- a/crates/egui_demo_app/src/main.rs +++ b/crates/egui_demo_app/src/main.rs @@ -51,6 +51,7 @@ fn main() -> eframe::Result { ..Default::default() }; + eframe::run_native( "egui demo app", options, diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index bd313fb0c08..cbed988fa68 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -111,11 +111,19 @@ impl Anchor { Self::Rendering, ] } + + #[cfg(target_arch = "wasm32")] + fn from_str_case_insensitive(anchor: &str) -> Option { + let anchor = anchor.to_lowercase(); + Self::all().into_iter().find(|x| x.to_string() == anchor) + } } impl std::fmt::Display for Anchor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{self:?}") + let mut name = format!("{self:?}"); + name.make_ascii_lowercase(); + f.write_str(&name) } } @@ -263,11 +271,15 @@ impl eframe::App for WrapApp { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { #[cfg(target_arch = "wasm32")] - if let Some(anchor) = frame.info().web_info.location.hash.strip_prefix('#') { - let anchor = Anchor::all().into_iter().find(|x| x.to_string() == anchor); - if let Some(v) = anchor { - self.state.selected_anchor = v; - } + if let Some(anchor) = frame + .info() + .web_info + .location + .hash + .strip_prefix('#') + .and_then(Anchor::from_str_case_insensitive) + { + self.state.selected_anchor = anchor; } #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index f8f1f47f686..b494e18a99b 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -55,9 +55,6 @@ serde = { workspace = true, optional = true } [dev-dependencies] -# when running tests we always want to use the `chrono` feature -egui_demo_lib = { workspace = true, features = ["chrono"] } - criterion.workspace = true egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } wgpu = { workspace = true, features = ["metal"] } diff --git a/crates/egui_demo_lib/data/icon.png b/crates/egui_demo_lib/data/icon.png index eb17ab1a223..87f15e746e4 100644 Binary files a/crates/egui_demo_lib/data/icon.png and b/crates/egui_demo_lib/data/icon.png differ diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 5b57c675b5a..2cfcdfaeeba 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -38,6 +38,7 @@ impl Default for Demos { Box::::default(), Box::::default(), Box::::default(), + Box::::default(), Box::::default(), Box::::default(), Box::::default(), diff --git a/crates/egui_demo_lib/src/demo/mod.rs b/crates/egui_demo_lib/src/demo/mod.rs index 8c9034868e4..c00725fbd59 100644 --- a/crates/egui_demo_lib/src/demo/mod.rs +++ b/crates/egui_demo_lib/src/demo/mod.rs @@ -24,6 +24,7 @@ pub mod painting; pub mod pan_zoom; pub mod panels; pub mod password; +pub mod screenshot; pub mod scrolling; pub mod sliders; pub mod strip_demo; diff --git a/crates/egui_demo_lib/src/demo/screenshot.rs b/crates/egui_demo_lib/src/demo/screenshot.rs new file mode 100644 index 00000000000..eb62611c863 --- /dev/null +++ b/crates/egui_demo_lib/src/demo/screenshot.rs @@ -0,0 +1,84 @@ +use egui::{Image, UserData, ViewportCommand, Widget}; +use std::sync::Arc; + +/// Showcase [`ViewportCommand::Screenshot`]. +#[derive(PartialEq, Eq, Default)] +pub struct Screenshot { + image: Option<(Arc, egui::TextureHandle)>, + continuous: bool, +} + +impl crate::Demo for Screenshot { + fn name(&self) -> &'static str { + "πŸ“· Screenshot" + } + + fn show(&mut self, ctx: &egui::Context, open: &mut bool) { + egui::Window::new(self.name()) + .open(open) + .resizable(false) + .default_width(250.0) + .show(ctx, |ui| { + use crate::View as _; + self.ui(ui); + }); + } +} + +impl crate::View for Screenshot { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.set_width(300.0); + ui.vertical_centered(|ui| { + ui.add(crate::egui_github_link_file!()); + }); + + ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("This demo showcases how to take screenshots via "); + ui.code("ViewportCommand::Screenshot"); + ui.label("."); + }); + + ui.horizontal_top(|ui| { + let capture = ui.button("πŸ“· Take Screenshot").clicked(); + ui.checkbox(&mut self.continuous, "Capture continuously"); + if capture || self.continuous { + ui.ctx() + .send_viewport_cmd(ViewportCommand::Screenshot(UserData::default())); + } + }); + + let image = ui.ctx().input(|i| { + i.events + .iter() + .filter_map(|e| { + if let egui::Event::Screenshot { image, .. } = e { + Some(image.clone()) + } else { + None + } + }) + .last() + }); + + if let Some(image) = image { + self.image = Some(( + image.clone(), + ui.ctx() + .load_texture("screenshot_demo", image, Default::default()), + )); + } + + if let Some((_, texture)) = &self.image { + Image::new(texture).shrink_to_fit().ui(ui); + } else { + ui.group(|ui| { + ui.set_width(ui.available_width()); + ui.set_height(100.0); + ui.centered_and_justified(|ui| { + ui.label("No screenshot taken yet."); + }); + }); + } + } +} diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index c579576b85b..b69d0f1c8d2 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -286,6 +286,7 @@ fn doc_link_label_with_crate<'a>( } } +#[cfg(feature = "chrono")] #[cfg(test)] mod tests { use super::*; diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png new file mode 100644 index 00000000000..56978de2001 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:579a7a66f86ade628e9f469b0014e9010aa56312ad5bd1e8de2faaae7e0d1af6 +size 23770 diff --git a/crates/egui_extras/CHANGELOG.md b/crates/egui_extras/CHANGELOG.md index d0f5aa868bf..7617b1863b0 100644 --- a/crates/egui_extras/CHANGELOG.md +++ b/crates/egui_extras/CHANGELOG.md @@ -5,6 +5,12 @@ 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.30.0 - 2024-12-16 +* Use `Table::id_salt` on `ScrollArea` [#5282](https://github.com/emilk/egui/pull/5282) by [@jwhear](https://github.com/jwhear) +* Use proper `image` crate URI and MIME support detection [#5324](https://github.com/emilk/egui/pull/5324) by [@xangelix](https://github.com/xangelix) +* Support loading images with weird urls and improve error message [#5431](https://github.com/emilk/egui/pull/5431) by [@lucasmerlin](https://github.com/lucasmerlin) + + ## 0.29.1 - 2024-10-01 - Fix table interaction * Bug fix: click anywhere on a `Table` row to select it [#5193](https://github.com/emilk/egui/pull/5193) by [@emilk](https://github.com/emilk) diff --git a/crates/egui_extras/Cargo.toml b/crates/egui_extras/Cargo.toml index 3bc9a4e8672..89465f6d130 100644 --- a/crates/egui_extras/Cargo.toml +++ b/crates/egui_extras/Cargo.toml @@ -56,11 +56,6 @@ http = ["dep:ehttp"] ## ``` image = ["dep:image"] -## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -## -## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. -puffin = ["dep:puffin", "egui/puffin"] - ## Derive serde Serialize/Deserialize on stateful structs serde = ["egui/serde", "dep:serde"] @@ -77,6 +72,7 @@ egui = { workspace = true, default-features = false } ahash.workspace = true enum-map = { version = "2", features = ["serde"] } log.workspace = true +profiling.workspace = true #! ### Optional dependencies @@ -99,7 +95,6 @@ image = { workspace = true, optional = true } # file feature mime_guess2 = { version = "2", optional = true, default-features = false } -puffin = { workspace = true, optional = true } syntect = { version = "5", optional = true, default-features = false, features = [ "default-fancy", diff --git a/crates/egui_extras/src/image.rs b/crates/egui_extras/src/image.rs index 1d2f6afa480..46d9df170a0 100644 --- a/crates/egui_extras/src/image.rs +++ b/crates/egui_extras/src/image.rs @@ -199,7 +199,7 @@ impl RetainedImage { /// On invalid image or unsupported image format. #[cfg(feature = "image")] pub fn load_image_bytes(image_bytes: &[u8]) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let image = image::load_from_memory(image_bytes).map_err(|err| match err { image::ImageError::Unsupported(err) => match err.kind() { image::error::UnsupportedErrorKind::Format(format) => { @@ -245,7 +245,8 @@ pub fn load_svg_bytes_with_size( use resvg::tiny_skia::{IntSize, Pixmap}; use resvg::usvg::{Options, Tree, TreeParsing}; - crate::profile_function!(); + profiling::function_scope!(); + let opt = Options::default(); let mut rtree = Tree::from_data(svg_bytes, &opt).map_err(|err| err.to_string())?; diff --git a/crates/egui_extras/src/lib.rs b/crates/egui_extras/src/lib.rs index ab2dde735b9..6f339010224 100644 --- a/crates/egui_extras/src/lib.rs +++ b/crates/egui_extras/src/lib.rs @@ -37,36 +37,6 @@ pub use loaders::install_image_loaders; // --------------------------------------------------------------------------- -mod profiling_scopes { - #![allow(unused_macros)] - #![allow(unused_imports)] - - /// Profiling macro for feature "puffin" - macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} - -#[allow(unused_imports)] -pub(crate) use profiling_scopes::profile_function; - -// --------------------------------------------------------------------------- - /// Panic in debug builds, log otherwise. macro_rules! log_or_panic { ($fmt: literal) => {$crate::log_or_panic!($fmt,)}; diff --git a/crates/egui_extras/src/loaders/image_loader.rs b/crates/egui_extras/src/loaders/image_loader.rs index 088ef5e4587..4c1a846e26b 100644 --- a/crates/egui_extras/src/loaders/image_loader.rs +++ b/crates/egui_extras/src/loaders/image_loader.rs @@ -78,6 +78,12 @@ impl ImageLoader for ImageCrateLoader { } } + if bytes.starts_with(b"version https://git-lfs") { + return Err(LoadError::FormatNotSupported { + detected_format: Some("git-lfs".to_owned()), + }); + } + // (3) log::trace!("started loading {uri:?}"); let result = crate::image::load_image_bytes(&bytes).map(Arc::new); diff --git a/crates/egui_extras/src/syntax_highlighting.rs b/crates/egui_extras/src/syntax_highlighting.rs index 9275d345b5b..027ba5ee9da 100644 --- a/crates/egui_extras/src/syntax_highlighting.rs +++ b/crates/egui_extras/src/syntax_highlighting.rs @@ -403,7 +403,7 @@ struct Highlighter { #[cfg(feature = "syntect")] impl Default for Highlighter { fn default() -> Self { - crate::profile_function!(); + profiling::function_scope!(); Self { ps: syntect::parsing::SyntaxSet::load_defaults_newlines(), ts: syntect::highlighting::ThemeSet::load_defaults(), @@ -437,8 +437,7 @@ impl Highlighter { #[cfg(feature = "syntect")] fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option { - crate::profile_function!(); - + profiling::function_scope!(); use syntect::easy::HighlightLines; use syntect::highlighting::FontStyle; use syntect::util::LinesWithEndings; @@ -512,7 +511,7 @@ impl Highlighter { mut text: &str, language: &str, ) -> Option { - crate::profile_function!(); + profiling::function_scope!(); let language = Language::new(language)?; diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index 2be2e25bb77..ec88990305b 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -1227,7 +1227,7 @@ impl<'a> TableBody<'a> { // Capture the hover information for the just created row. This is used in the next render // to ensure that the entire row is highlighted. - fn capture_hover_state(&mut self, response: &Option, row_index: usize) { + fn capture_hover_state(&self, response: &Option, row_index: usize) { let is_row_hovered = response.as_ref().map_or(false, |r| r.hovered()); if is_row_hovered { self.layout diff --git a/crates/egui_glow/CHANGELOG.md b/crates/egui_glow/CHANGELOG.md index 0ebbad6d95c..dc3d6dfda46 100644 --- a/crates/egui_glow/CHANGELOG.md +++ b/crates/egui_glow/CHANGELOG.md @@ -6,6 +6,10 @@ Changes since the last release can be found at { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} - -#[allow(unused_imports)] -pub(crate) use profiling_scopes::{profile_function, profile_scope}; diff --git a/crates/egui_glow/src/painter.rs b/crates/egui_glow/src/painter.rs index 86d86bedf7c..bec46cf085f 100644 --- a/crates/egui_glow/src/painter.rs +++ b/crates/egui_glow/src/painter.rs @@ -144,7 +144,7 @@ impl Painter { shader_version: Option, dithering: bool, ) -> Result { - crate::profile_function!(); + profiling::function_scope!(); crate::check_for_gl_error_even_in_release!(&gl, "before Painter::new"); // some useful debug info. all three of them are present in gl 1.1. @@ -366,7 +366,7 @@ impl Painter { clipped_primitives: &[egui::ClippedPrimitive], textures_delta: &egui::TexturesDelta, ) { - crate::profile_function!(); + profiling::function_scope!(); for (id, image_delta) in &textures_delta.set { self.set_texture(*id, image_delta); @@ -405,7 +405,7 @@ impl Painter { pixels_per_point: f32, clipped_primitives: &[egui::ClippedPrimitive], ) { - crate::profile_function!(); + profiling::function_scope!(); self.assert_not_destroyed(); unsafe { self.prepare_painting(screen_size_px, pixels_per_point) }; @@ -423,7 +423,7 @@ impl Painter { } Primitive::Callback(callback) => { if callback.rect.is_positive() { - crate::profile_scope!("callback"); + profiling::scope!("callback"); let info = egui::PaintCallbackInfo { viewport: callback.rect, @@ -508,7 +508,7 @@ impl Painter { // ------------------------------------------------------------------------ pub fn set_texture(&mut self, tex_id: egui::TextureId, delta: &egui::epaint::ImageDelta) { - crate::profile_function!(); + profiling::function_scope!(); self.assert_not_destroyed(); @@ -540,7 +540,7 @@ impl Painter { ); let data: Vec = { - crate::profile_scope!("font -> sRGBA"); + profiling::scope!("font -> sRGBA"); image .srgba_pixels(None) .flat_map(|a| a.to_array()) @@ -559,7 +559,7 @@ impl Painter { options: egui::TextureOptions, data: &[u8], ) { - crate::profile_function!(); + profiling::function_scope!(); assert_eq!(data.len(), w * h * 4); assert!( w <= self.max_texture_side && h <= self.max_texture_side, @@ -610,7 +610,7 @@ impl Painter { let level = 0; if let Some([x, y]) = pos { - crate::profile_scope!("gl.tex_sub_image_2d"); + profiling::scope!("gl.tex_sub_image_2d"); self.gl.tex_sub_image_2d( glow::TEXTURE_2D, level, @@ -625,7 +625,7 @@ impl Painter { check_for_gl_error!(&self.gl, "tex_sub_image_2d"); } else { let border = 0; - crate::profile_scope!("gl.tex_image_2d"); + profiling::scope!("gl.tex_image_2d"); self.gl.tex_image_2d( glow::TEXTURE_2D, level, @@ -675,7 +675,7 @@ impl Painter { } pub fn read_screen_rgba(&self, [w, h]: [u32; 2]) -> egui::ColorImage { - crate::profile_function!(); + profiling::function_scope!(); let mut pixels = vec![0_u8; (w * h * 4) as usize]; unsafe { @@ -700,8 +700,7 @@ impl Painter { } pub fn read_screen_rgb(&self, [w, h]: [u32; 2]) -> Vec { - crate::profile_function!(); - + profiling::function_scope!(); let mut pixels = vec![0_u8; (w * h * 3) as usize]; unsafe { self.gl.read_pixels( @@ -748,7 +747,7 @@ impl Painter { } pub fn clear(gl: &glow::Context, screen_size_in_pixels: [u32; 2], clear_color: [f32; 4]) { - crate::profile_function!(); + profiling::function_scope!(); unsafe { gl.disable(glow::SCISSOR_TEST); diff --git a/crates/egui_kittest/CHANGELOG.md b/crates/egui_kittest/CHANGELOG.md new file mode 100644 index 00000000000..aa8b7cac577 --- /dev/null +++ b/crates/egui_kittest/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog for egui_kittest +All notable changes to the `egui_kittest` crate will be noted in this file. + + +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.30.0 - 2024-12-16 - Initial relrease +* Support for egui 0.30.0 +* Automate clicks and text input +* Automatic screenshot testing with wgpu diff --git a/crates/egui_kittest/Cargo.toml b/crates/egui_kittest/Cargo.toml index f9f98198c82..d93f0348368 100644 --- a/crates/egui_kittest/Cargo.toml +++ b/crates/egui_kittest/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "egui_kittest" version.workspace = true -authors = ["Lucas Meurer ", "Emil Ernerfeldt "] +authors = [ + "Lucas Meurer ", + "Emil Ernerfeldt ", +] description = "Testing library for egui based on kittest and AccessKit" edition.workspace = true rust-version.workspace = true @@ -39,10 +42,9 @@ dify = { workspace = true, optional = true } document-features = { workspace = true, optional = true } [dev-dependencies] -egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } -wgpu = { workspace = true, features = ["metal"] } -image = { workspace = true, features = ["png"] } egui = { workspace = true, features = ["default_fonts"] } +image = { workspace = true, features = ["png"] } +wgpu = { workspace = true, features = ["metal"] } [lints] workspace = true diff --git a/crates/egui_kittest/README.md b/crates/egui_kittest/README.md index 2ffda785c05..c124fac3a62 100644 --- a/crates/egui_kittest/README.md +++ b/crates/egui_kittest/README.md @@ -4,21 +4,16 @@ Ui testing library for egui, based on [kittest](https://github.com/rerun-io/kitt ## Example usage ```rust -use egui::accesskit::{Role, Toggled}; -use egui::{CentralPanel, Context, TextEdit, Vec2}; -use egui_kittest::Harness; -use kittest::Queryable; -use std::cell::RefCell; +use egui::accesskit::Toggled; +use egui_kittest::{Harness, kittest::Queryable}; fn main() { let mut checked = false; - let app = |ctx: &Context| { - CentralPanel::default().show(ctx, |ui| { - ui.checkbox(&mut checked, "Check me!"); - }); + let app = |ui: &mut egui::Ui| { + ui.checkbox(&mut checked, "Check me!"); }; - let mut harness = Harness::builder().with_size(egui::Vec2::new(200.0, 100.0)).build(app); + let mut harness = Harness::new_ui(app); let checkbox = harness.get_by_label("Check me!"); assert_eq!(checkbox.toggled(), Some(Toggled::False)); @@ -28,6 +23,9 @@ fn main() { let checkbox = harness.get_by_label("Check me!"); assert_eq!(checkbox.toggled(), Some(Toggled::True)); + + // Shrink the window size to the smallest size possible + harness.fit_contents(); // You can even render the ui and do image snapshot tests #[cfg(all(feature = "wgpu", feature = "snapshot"))] diff --git a/crates/egui_kittest/src/wgpu.rs b/crates/egui_kittest/src/wgpu.rs index d2cda112af7..4c3001fa72b 100644 --- a/crates/egui_kittest/src/wgpu.rs +++ b/crates/egui_kittest/src/wgpu.rs @@ -60,7 +60,7 @@ impl TestRenderer { } /// Render the [`Harness`] and return the resulting image. - pub fn render(&mut self, harness: &Harness<'_, State>) -> RgbaImage { + pub fn render(&self, harness: &Harness<'_, State>) -> RgbaImage { // We need to create a new renderer each time we render, since the renderer stores // textures related to the Harnesses' egui Context. // Calling the renderer from different Harnesses would cause problems if we store the renderer. diff --git a/crates/egui/tests/accesskit.rs b/crates/egui_kittest/tests/accesskit.rs similarity index 97% rename from crates/egui/tests/accesskit.rs rename to crates/egui_kittest/tests/accesskit.rs index 83efeab3f37..02afddefc80 100644 --- a/crates/egui/tests/accesskit.rs +++ b/crates/egui_kittest/tests/accesskit.rs @@ -1,8 +1,9 @@ //! Tests the accesskit accessibility output of egui. -#![cfg(feature = "accesskit")] -use accesskit::{NodeId, Role, TreeUpdate}; -use egui::{CentralPanel, Context, RawInput, Window}; +use egui::{ + accesskit::{NodeId, Role, TreeUpdate}, + CentralPanel, Context, RawInput, Window, +}; /// Baseline test that asserts there are no spurious nodes in the /// accesskit output when the ui is empty. diff --git a/crates/egui/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs similarity index 91% rename from crates/egui/tests/regression_tests.rs rename to crates/egui_kittest/tests/regression_tests.rs index f54cf1ffcf6..9493d5443f4 100644 --- a/crates/egui/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -1,6 +1,5 @@ use egui::Button; -use egui_kittest::kittest::Queryable; -use egui_kittest::Harness; +use egui_kittest::{kittest::Queryable, Harness}; #[test] pub fn focus_should_skip_over_disabled_buttons() { diff --git a/crates/egui_kittest/tests/snapshots/readme_example.png b/crates/egui_kittest/tests/snapshots/readme_example.png index 66b21e7f4bf..ef0774162da 100644 --- a/crates/egui_kittest/tests/snapshots/readme_example.png +++ b/crates/egui_kittest/tests/snapshots/readme_example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36c1b432140456ea5cbb687076b1c910aea8b31affd33a0ece22218f60af2d6e -size 2296 +oid sha256:31bd906040fcc356c19dc36036fbfd2a28dfcef54c7a073f584f4a9abddbdb4c +size 1699 diff --git a/crates/egui_kittest/tests/tests.rs b/crates/egui_kittest/tests/tests.rs index 4978cbeb775..6799b9a3567 100644 --- a/crates/egui_kittest/tests/tests.rs +++ b/crates/egui_kittest/tests/tests.rs @@ -10,5 +10,6 @@ fn test_shrink() { harness.fit_contents(); + #[cfg(all(feature = "snapshot", feature = "wgpu"))] harness.wgpu_snapshot("test_shrink"); } diff --git a/crates/epaint/CHANGELOG.md b/crates/epaint/CHANGELOG.md index b044cddee2d..742cf0acd78 100644 --- a/crates/epaint/CHANGELOG.md +++ b/crates/epaint/CHANGELOG.md @@ -5,6 +5,13 @@ 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.30.0 - 2024-12-16 +* Expand max font atlas size from 8k to 16k [#5257](https://github.com/emilk/egui/pull/5257) by [@rustbasic](https://github.com/rustbasic) +* Put font data into `Arc` to reduce memory consumption [#5276](https://github.com/emilk/egui/pull/5276) by [@StarStarJ](https://github.com/StarStarJ) +* Reduce aliasing when painting thin box outlines [#5484](https://github.com/emilk/egui/pull/5484) by [@emilk](https://github.com/emilk) +* Fix zero-width strokes still affecting the feathering color of boxes [#5485](https://github.com/emilk/egui/pull/5485) by [@emilk](https://github.com/emilk) + + ## 0.29.1 - 2024-10-01 Nothing new diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml index 7815189dc5a..0a4afde26b5 100644 --- a/crates/epaint/Cargo.toml +++ b/crates/epaint/Cargo.toml @@ -55,11 +55,6 @@ log = ["dep:log"] ## [`mint`](https://docs.rs/mint) enables interoperability with other math libraries such as [`glam`](https://docs.rs/glam) and [`nalgebra`](https://docs.rs/nalgebra). mint = ["emath/mint"] -## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -## -## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. -puffin = ["dep:puffin"] - ## Enable parallel tessellation using [`rayon`](https://docs.rs/rayon). ## ## This can help performance for graphics-intense applications. @@ -79,6 +74,7 @@ ab_glyph = "0.2.11" ahash.workspace = true nohash-hasher.workspace = true parking_lot.workspace = true # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios. +profiling = { workspace = true} #! ### Optional dependencies bytemuck = { workspace = true, optional = true, features = ["derive"] } @@ -87,7 +83,6 @@ bytemuck = { workspace = true, optional = true, features = ["derive"] } document-features = { workspace = true, optional = true } log = { workspace = true, optional = true } -puffin = { workspace = true, optional = true } rayon = { version = "1.7", optional = true } ## Allow serialization using [`serde`](https://docs.rs/serde) . diff --git a/crates/epaint/src/bezier.rs b/crates/epaint/src/bezier.rs index 05fff89298b..cab0d29f918 100644 --- a/crates/epaint/src/bezier.rs +++ b/crates/epaint/src/bezier.rs @@ -207,17 +207,21 @@ impl CubicBezierShape { /// B.x = (P3.x - 3 * P2.x + 3 * P1.x - P0.x) * t^3 + (3 * P2.x - 6 * P1.x + 3 * P0.x) * t^2 + (3 * P1.x - 3 * P0.x) * t + P0.x /// B.y = (P3.y - 3 * P2.y + 3 * P1.y - P0.y) * t^3 + (3 * P2.y - 6 * P1.y + 3 * P0.y) * t^2 + (3 * P1.y - 3 * P0.y) * t + P0.y /// Combine the above three equations and iliminate B.x and B.y, we get: + /// ```text /// t^3 * ( (P3.x - 3*P2.x + 3*P1.x - P0.x) * (P3.y - P0.y) - (P3.y - 3*P2.y + 3*P1.y - P0.y) * (P3.x - P0.x)) /// + t^2 * ( (3 * P2.x - 6 * P1.x + 3 * P0.x) * (P3.y - P0.y) - (3 * P2.y - 6 * P1.y + 3 * P0.y) * (P3.x - P0.x)) /// + t^1 * ( (3 * P1.x - 3 * P0.x) * (P3.y - P0.y) - (3 * P1.y - 3 * P0.y) * (P3.x - P0.x)) /// + (P0.x * (P3.y - P0.y) - P0.y * (P3.x - P0.x)) + P0.x * (P0.y - P3.y) + P0.y * (P3.x - P0.x) /// = 0 - /// or a * t^3 + b * t^2 + c * t + d = 0 + /// ``` + /// or `a * t^3 + b * t^2 + c * t + d = 0` /// /// let x = t - b / (3 * a), then we have: + /// ```text /// x^3 + p * x + q = 0, where: /// p = (3.0 * a * c - b^2) / (3.0 * a^2) /// q = (2.0 * b^3 - 9.0 * a * b * c + 27.0 * a^2 * d) / (27.0 * a^3) + /// ``` /// /// when p > 0, there will be one real root, two complex roots /// when p = 0, there will be two real roots, when p=q=0, there will be three real roots but all 0. diff --git a/crates/epaint/src/image.rs b/crates/epaint/src/image.rs index 286c09ad765..9a204a12199 100644 --- a/crates/epaint/src/image.rs +++ b/crates/epaint/src/image.rs @@ -154,8 +154,10 @@ impl ColorImage { let max_x = (region.max.x * pixels_per_point) as usize; let min_y = (region.min.y * pixels_per_point) as usize; let max_y = (region.max.y * pixels_per_point) as usize; - assert!(min_x <= max_x); - assert!(min_y <= max_y); + assert!( + min_x <= max_x && min_y <= max_y, + "Screenshot region is invalid: {region:?}" + ); let width = max_x - min_x; let height = max_y - min_y; let mut output = Vec::with_capacity(width * height); diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index 4dee42381cd..89664cdb844 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -143,33 +143,3 @@ pub enum Primitive { /// Was epaint compiled with the `rayon` feature? pub const HAS_RAYON: bool = cfg!(feature = "rayon"); - -// --------------------------------------------------------------------------- - -mod profiling_scopes { - #![allow(unused_macros)] - #![allow(unused_imports)] - - /// Profiling macro for feature "puffin" - macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} - -#[allow(unused_imports)] -pub(crate) use profiling_scopes::{profile_function, profile_scope}; diff --git a/crates/epaint/src/mesh.rs b/crates/epaint/src/mesh.rs index ba37e715856..2447cad293e 100644 --- a/crates/epaint/src/mesh.rs +++ b/crates/epaint/src/mesh.rs @@ -85,7 +85,7 @@ impl Mesh { /// Are all indices within the bounds of the contained vertices? pub fn is_valid(&self) -> bool { - crate::profile_function!(); + profiling::function_scope!(); if let Ok(n) = u32::try_from(self.vertices.len()) { self.indices.iter().all(|&i| i < n) @@ -111,7 +111,7 @@ impl Mesh { /// /// Panics when `other` mesh has a different texture. pub fn append(&mut self, other: Self) { - crate::profile_function!(); + profiling::function_scope!(); debug_assert!(other.is_valid()); if self.is_empty() { diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 399a602c259..63412cdc013 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -161,10 +161,15 @@ where impl From for PathStroke { fn from(value: Stroke) -> Self { - Self { - width: value.width, - color: ColorMode::Solid(value.color), - kind: StrokeKind::default(), + if value.is_empty() { + // Important, since we use the stroke color when doing feathering of the fill! + Self::NONE + } else { + Self { + width: value.width, + color: ColorMode::Solid(value.color), + kind: StrokeKind::default(), + } } } } diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index fdbe270914e..ebd22f55303 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -502,6 +502,8 @@ impl Path { /// Calling this may reverse the vertices in the path if they are wrong winding order. /// /// The preferred winding order is clockwise. + /// + /// The stroke colors is used for color-correct feathering. pub fn fill(&mut self, feathering: f32, color: Color32, stroke: &PathStroke, out: &mut Mesh) { fill_closed_path(feathering, &mut self.0, color, stroke, out); } @@ -918,7 +920,7 @@ fn stroke_path( ) { let n = path.len() as u32; - if stroke.width <= 0.0 || stroke.color == ColorMode::TRANSPARENT || n < 2 { + if stroke.is_empty() || n < 2 { return; } @@ -1277,6 +1279,11 @@ impl Tessellator { ((point * self.pixels_per_point - 0.5).round() + 0.5) / self.pixels_per_point } + #[inline(always)] + pub fn round_pos_to_pixel(&self, pos: Pos2) -> Pos2 { + pos2(self.round_to_pixel(pos.x), self.round_to_pixel(pos.y)) + } + #[inline(always)] pub fn round_pos_to_pixel_center(&self, pos: Pos2) -> Pos2 { pos2( @@ -1363,7 +1370,7 @@ impl Tessellator { self.tessellate_ellipse(ellipse, out); } Shape::Mesh(mesh) => { - crate::profile_scope!("mesh"); + profiling::scope!("mesh"); if self.options.validate_meshes && !mesh.is_valid() { debug_assert!(false, "Invalid Mesh in Shape::Mesh"); @@ -1596,7 +1603,7 @@ impl Tessellator { return; } - crate::profile_function!(); + profiling::function_scope!(); let PathShape { points, @@ -1702,6 +1709,20 @@ impl Tessellator { self.tessellate_line(line, stroke, out); // …and forth } } else { + let rect = if !stroke.is_empty() && stroke.width < self.feathering { + // Very thin rectangle strokes create extreme aliasing when they move around. + // We can fix that by rounding the rectangle corners to pixel centers. + // TODO(#5164): maybe do this for all shapes and stroke sizes + // TODO(emilk): since we use StrokeKind::Outside, we should probably round the + // corners after offsetting them with half the stroke width (see `translate_stroke_point`). + Rect { + min: self.round_pos_to_pixel_center(rect.min), + max: self.round_pos_to_pixel_center(rect.max), + } + } else { + rect + }; + let path = &mut self.scratchpad_path; path.clear(); path::rounded_rectangle(&mut self.scratchpad_points, rect, rounding); @@ -1977,7 +1998,7 @@ impl Tessellator { /// A list of clip rectangles with matching [`Mesh`]. #[allow(unused_mut)] pub fn tessellate_shapes(&mut self, mut shapes: Vec) -> Vec { - crate::profile_function!(); + profiling::function_scope!(); #[cfg(feature = "rayon")] if self.options.parallel_tessellation { @@ -1987,7 +2008,7 @@ impl Tessellator { let mut clipped_primitives: Vec = Vec::default(); { - crate::profile_scope!("tessellate"); + profiling::scope!("tessellate"); for clipped_shape in shapes { self.tessellate_clipped_shape(clipped_shape, &mut clipped_primitives); } @@ -2024,7 +2045,7 @@ impl Tessellator { /// then replace the original shape with their tessellated meshes. #[cfg(feature = "rayon")] fn parallel_tessellation_of_large_shapes(&self, shapes: &mut [ClippedShape]) { - crate::profile_function!(); + profiling::function_scope!(); use rayon::prelude::*; @@ -2054,7 +2075,7 @@ impl Tessellator { .enumerate() .filter(|(_, clipped_shape)| should_parallelize(&clipped_shape.shape)) .map(|(index, clipped_shape)| { - crate::profile_scope!("tessellate_big_shape"); + profiling::scope!("tessellate_big_shape"); // TODO(emilk): reuse tessellator in a thread local let mut tessellator = (*self).clone(); let mut mesh = Mesh::default(); @@ -2063,7 +2084,7 @@ impl Tessellator { }) .collect(); - crate::profile_scope!("distribute results", tessellated.len().to_string()); + profiling::scope!("distribute results", tessellated.len().to_string()); for (index, mesh) in tessellated { shapes[index].shape = Shape::Mesh(mesh); } diff --git a/crates/epaint/src/texture_handle.rs b/crates/epaint/src/texture_handle.rs index f4142d91510..1f640a171de 100644 --- a/crates/epaint/src/texture_handle.rs +++ b/crates/epaint/src/texture_handle.rs @@ -66,6 +66,7 @@ impl TextureHandle { } /// Assign a new image to an existing texture. + #[allow(clippy::needless_pass_by_ref_mut)] // Intentionally hide interiority of mutability pub fn set(&mut self, image: impl Into, options: TextureOptions) { self.tex_mngr .write() @@ -73,6 +74,7 @@ impl TextureHandle { } /// Assign a new image to a subregion of the whole texture. + #[allow(clippy::needless_pass_by_ref_mut)] // Intentionally hide interiority of mutability pub fn set_partial( &mut self, pos: [usize; 2], diff --git a/crates/epaint_default_fonts/CHANGELOG.md b/crates/epaint_default_fonts/CHANGELOG.md index 3e8e6959b3c..42cd89ba51a 100644 --- a/crates/epaint_default_fonts/CHANGELOG.md +++ b/crates/epaint_default_fonts/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.30.0 - 2024-12-16 +Nothing new + + ## 0.29.1 - 2024-10-01 Nothing new diff --git a/deny.toml b/deny.toml index 87ca0fb2fb5..ede37dea405 100644 --- a/deny.toml +++ b/deny.toml @@ -85,6 +85,7 @@ allow = [ "MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11. Used by webpki-roots on Linux. "OFL-1.1", # https://spdx.org/licenses/OFL-1.1.html "OpenSSL", # https://www.openssl.org/source/license.html - used on Linux + "Unicode-3.0", # https://www.unicode.org/license.txt "Unicode-DFS-2016", # https://spdx.org/licenses/Unicode-DFS-2016.html "Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib) ] diff --git a/examples/confirm_exit/Cargo.toml b/examples/confirm_exit/Cargo.toml index d4a21060b76..62e8bc29c14 100644 --- a/examples/confirm_exit/Cargo.toml +++ b/examples/confirm_exit/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/custom_3d_glow/Cargo.toml b/examples/custom_3d_glow/Cargo.toml index d1dcc056949..1ddec02f5f8 100644 --- a/examples/custom_3d_glow/Cargo.toml +++ b/examples/custom_3d_glow/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/custom_font/Cargo.toml b/examples/custom_font/Cargo.toml index ee769cc62d5..5214bdc1f5f 100644 --- a/examples/custom_font/Cargo.toml +++ b/examples/custom_font/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/custom_font_style/Cargo.toml b/examples/custom_font_style/Cargo.toml index f25676e87a1..241b893401d 100644 --- a/examples/custom_font_style/Cargo.toml +++ b/examples/custom_font_style/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["tami5 "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/custom_keypad/Cargo.toml b/examples/custom_keypad/Cargo.toml index dc3c62dddb8..73c6b0e7aa8 100644 --- a/examples/custom_keypad/Cargo.toml +++ b/examples/custom_keypad/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Varphone Wong "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/custom_style/Cargo.toml b/examples/custom_style/Cargo.toml index 6299e1aee35..f87ce0bf82a 100644 --- a/examples/custom_style/Cargo.toml +++ b/examples/custom_style/Cargo.toml @@ -3,7 +3,7 @@ name = "custom_style" version = "0.1.0" license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/custom_window_frame/Cargo.toml b/examples/custom_window_frame/Cargo.toml index 848189084ad..4a53ee48745 100644 --- a/examples/custom_window_frame/Cargo.toml +++ b/examples/custom_window_frame/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/file_dialog/Cargo.toml b/examples/file_dialog/Cargo.toml index dc58e0ba2e7..1a9f86c40c0 100644 --- a/examples/file_dialog/Cargo.toml +++ b/examples/file_dialog/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] @@ -20,4 +20,4 @@ env_logger = { version = "0.10", default-features = false, features = [ "auto-color", "humantime", ] } -rfd = "0.13" +rfd = "0.15" diff --git a/examples/hello_android/Cargo.toml b/examples/hello_android/Cargo.toml new file mode 100644 index 00000000000..dcb0a5a5c01 --- /dev/null +++ b/examples/hello_android/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "hello_android" +version = "0.1.0" +authors = ["Emil Ernerfeldt "] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.76" +publish = false + +# `unsafe_code` is required for `#[no_mangle]`, disable workspace lints to workaround lint error. +# [lints] +# workspace = true + +[lib] +crate-type = ["cdylib"] + + +[dependencies] +eframe = { workspace = true, features = [ + "default", + "android-native-activity", +] } + +# For image support: +egui_extras = { workspace = true, features = ["default", "image"] } + +log = { workspace = true } +winit = { workspace = true } +android_logger = "0.14" + +[package.metadata.android] +build_targets = [ "armv7-linux-androideabi", "aarch64-linux-android" ] diff --git a/examples/hello_android/README.md b/examples/hello_android/README.md new file mode 100644 index 00000000000..fe14eb9face --- /dev/null +++ b/examples/hello_android/README.md @@ -0,0 +1,20 @@ +Hello world example for Android. + +Use `cargo-apk` to build and run. Requires a patch to workaround [an upstream bug](https://github.com/rust-mobile/cargo-subcommand/issues/29). + +One-time setup: + +```sh +cargo install \ + --git https://github.com/parasyte/cargo-apk.git \ + --rev 282639508eeed7d73f2e1eaeea042da2716436d5 \ + cargo-apk +``` + +Build and run: + +```sh +cargo apk run -p hello_android +``` + +![](screenshot.png) diff --git a/examples/hello_android/screenshot.png b/examples/hello_android/screenshot.png new file mode 100644 index 00000000000..91179fa2f41 --- /dev/null +++ b/examples/hello_android/screenshot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7add91d7d6b73f48e98f20d84cba3bd3a950cf97aa31f5e9fa93da9af98e876c +size 120019 diff --git a/examples/hello_android/src/lib.rs b/examples/hello_android/src/lib.rs new file mode 100644 index 00000000000..adda66ca5f7 --- /dev/null +++ b/examples/hello_android/src/lib.rs @@ -0,0 +1,65 @@ +#![cfg(target_os = "android")] +#![allow(rustdoc::missing_crate_level_docs)] // it's an example + +use android_logger::Config; +use eframe::egui; +use log::LevelFilter; +use winit::platform::android::activity::AndroidApp; + +#[no_mangle] +fn android_main(app: AndroidApp) { + // Log to android output + android_logger::init_once(Config::default().with_max_level(LevelFilter::Info)); + + let options = eframe::NativeOptions { + android_app: Some(app), + ..Default::default() + }; + eframe::run_native( + "My egui App", + options, + Box::new(|cc| { + // This gives us image support: + egui_extras::install_image_loaders(&cc.egui_ctx); + + Ok(Box::::default()) + }), + ) + .unwrap() +} + +struct MyApp { + name: String, + age: u32, +} + +impl Default for MyApp { + fn default() -> Self { + Self { + name: "Arthur".to_owned(), + age: 42, + } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::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 self.name) + .labelled_by(name_label.id); + }); + ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age")); + if ui.button("Increment").clicked() { + self.age += 1; + } + ui.label(format!("Hello '{}', age {}", self.name, self.age)); + + ui.image(egui::include_image!( + "../../../crates/egui/assets/ferris.png" + )); + }); + } +} diff --git a/examples/hello_world/Cargo.toml b/examples/hello_world/Cargo.toml index 6e7dd8d00f1..8816b425598 100644 --- a/examples/hello_world/Cargo.toml +++ b/examples/hello_world/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/hello_world_par/Cargo.toml b/examples/hello_world_par/Cargo.toml index d486e35791d..b3e00b20897 100644 --- a/examples/hello_world_par/Cargo.toml +++ b/examples/hello_world_par/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Maxim Osipenko "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/hello_world_simple/Cargo.toml b/examples/hello_world_simple/Cargo.toml index 0d77c65e316..7197c60337b 100644 --- a/examples/hello_world_simple/Cargo.toml +++ b/examples/hello_world_simple/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/images/Cargo.toml b/examples/images/Cargo.toml index 4759e2d128e..f1b4f97ddf2 100644 --- a/examples/images/Cargo.toml +++ b/examples/images/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Jan ProchΓ‘zka "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/keyboard_events/Cargo.toml b/examples/keyboard_events/Cargo.toml index e587764bacf..1af09849319 100644 --- a/examples/keyboard_events/Cargo.toml +++ b/examples/keyboard_events/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Jose Palazon "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/multiple_viewports/Cargo.toml b/examples/multiple_viewports/Cargo.toml index 9910aed5a2d..1644d6a72a5 100644 --- a/examples/multiple_viewports/Cargo.toml +++ b/examples/multiple_viewports/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/puffin_profiler/Cargo.toml b/examples/puffin_profiler/Cargo.toml index d2cbce19052..d0e9e485a8d 100644 --- a/examples/puffin_profiler/Cargo.toml +++ b/examples/puffin_profiler/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false +[package.metadata.cargo-machete] +ignored = ["profiling"] + [lints] workspace = true @@ -18,7 +21,6 @@ wgpu = ["eframe/wgpu"] [dependencies] eframe = { workspace = true, features = [ "default", - "puffin", "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO ] } env_logger = { version = "0.10", default-features = false, features = [ @@ -28,3 +30,4 @@ env_logger = { version = "0.10", default-features = false, features = [ log = { workspace = true } puffin = "0.19" puffin_http = "0.16" +profiling = {workspace = true, features = ["profile-with-puffin"] } diff --git a/examples/screenshot/Cargo.toml b/examples/screenshot/Cargo.toml index 2e74482a64e..5963ea3ad71 100644 --- a/examples/screenshot/Cargo.toml +++ b/examples/screenshot/Cargo.toml @@ -7,7 +7,7 @@ authors = [ ] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/serial_windows/Cargo.toml b/examples/serial_windows/Cargo.toml index c377524c69c..1f6d5ea431e 100644 --- a/examples/serial_windows/Cargo.toml +++ b/examples/serial_windows/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/user_attention/Cargo.toml b/examples/user_attention/Cargo.toml index 3fbc75e260e..25aa473486c 100644 --- a/examples/user_attention/Cargo.toml +++ b/examples/user_attention/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["TicClick "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/rust-toolchain b/rust-toolchain index 9fdafb7a67a..38e5e90f3ac 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -5,6 +5,6 @@ # to the user in the error, instead of "error: invalid channel name '[toolchain]'". [toolchain] -channel = "1.79.0" +channel = "1.80.0" components = ["rustfmt", "clippy"] targets = ["wasm32-unknown-unknown"] diff --git a/scripts/check.sh b/scripts/check.sh index 3129f2ac114..7db1a6aa3e2 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -9,7 +9,7 @@ set -x # Checks all tests, lints etc. # Basically does what the CI does. -cargo +1.79.0 install --quiet typos-cli +cargo +1.80.0 install --quiet typos-cli export RUSTFLAGS="-D warnings" export RUSTDOCFLAGS="-D warnings" # https://github.com/emilk/egui/pull/1454 diff --git a/scripts/clippy_wasm/clippy.toml b/scripts/clippy_wasm/clippy.toml index 0f7fc92dcc7..f06033c71b9 100644 --- a/scripts/clippy_wasm/clippy.toml +++ b/scripts/clippy_wasm/clippy.toml @@ -6,7 +6,7 @@ # ----------------------------------------------------------------------------- # Section identical to the root clippy.toml: -msrv = "1.79" +msrv = "1.80" allow-unwrap-in-tests = true diff --git a/scripts/generate_changelog.py b/scripts/generate_changelog.py index 551d9c9d29e..1621fd29793 100755 --- a/scripts/generate_changelog.py +++ b/scripts/generate_changelog.py @@ -214,18 +214,42 @@ def add_to_changelog_file(crate: str, content: str, version: str) -> None: file.write(content) +def calc_commit_range(new_version: str) -> str: + parts = new_version.split(".") + assert len(parts) == 3, "Expected version to be on the format X.Y.Z" + major = int(parts[0]) + minor = int(parts[1]) + patch = int(parts[2]) + + if 0 < patch: + # A patch release. + # Include changes since last patch release. + # This assumes we've cherry-picked stuff for this release. + diff_since_version = f"0.{minor}.{patch - 1}" + elif 0 < minor: + # A minor release + # The diff should span everything since the last minor release. + # The script later excludes duplicated automatically, so we don't include stuff that + # was part of intervening patch releases. + diff_since_version = f"{major}.{minor - 1}.0" + else: + # A major release + # The diff should span everything since the last major release. + # The script later excludes duplicated automatically, so we don't include stuff that + # was part of intervening minor/patch releases. + diff_since_version = f"{major - 1}.{minor}.0" + + return f"{diff_since_version}..HEAD" + + def main() -> None: parser = argparse.ArgumentParser(description="Generate a changelog.") - parser.add_argument("--commit-range", help="e.g. 0.24.0..HEAD", required=True) + parser.add_argument("--version", help="What release is this?", required=True) parser.add_argument( "--write", help="Write into the different changelogs?", action="store_true" ) - parser.add_argument("--version", help="What release is this?") args = parser.parse_args() - - if args.write and not args.version: - print("ERROR: --version is required when --write is used") - sys.exit(1) + commit_range = calc_commit_range(args.version) crate_names = [ "ecolor", @@ -251,7 +275,7 @@ def main() -> None: all_changelogs += file.read() repo = Repo(".") - commits = list(repo.iter_commits(args.commit_range)) + commits = list(repo.iter_commits(commit_range)) commits.reverse() # Most recent last commit_infos = list(map(get_commit_info, commits)) @@ -307,7 +331,7 @@ def main() -> None: unsorted_prs.append(pr_summary(pr_info)) print() - print(f"Full diff at https://github.com/emilk/egui/compare/{args.commit_range}") + print(f"Full diff at https://github.com/emilk/egui/compare/{commit_range}") print() for crate in crate_names: if crate in crate_sections: diff --git a/tests/test_egui_extras_compilation/Cargo.toml b/tests/test_egui_extras_compilation/Cargo.toml index 1a310566f2a..f1d67f4bb8b 100644 --- a/tests/test_egui_extras_compilation/Cargo.toml +++ b/tests/test_egui_extras_compilation/Cargo.toml @@ -3,7 +3,7 @@ name = "test_egui_extras_compilation" version = "0.1.0" license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/tests/test_inline_glow_paint/Cargo.toml b/tests/test_inline_glow_paint/Cargo.toml index 5ded3cc356b..bcda5b3ddea 100644 --- a/tests/test_inline_glow_paint/Cargo.toml +++ b/tests/test_inline_glow_paint/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/tests/test_size_pass/Cargo.toml b/tests/test_size_pass/Cargo.toml index d6ee661e6b6..e3819a107e9 100644 --- a/tests/test_size_pass/Cargo.toml +++ b/tests/test_size_pass/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/tests/test_ui_stack/Cargo.toml b/tests/test_ui_stack/Cargo.toml index df2e2bf15c2..12ba9961be7 100644 --- a/tests/test_ui_stack/Cargo.toml +++ b/tests/test_ui_stack/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Antoine Beyeler "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/tests/test_viewports/Cargo.toml b/tests/test_viewports/Cargo.toml index cb962411558..cb877a7107a 100644 --- a/tests/test_viewports/Cargo.toml +++ b/tests/test_viewports/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["konkitoman"] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints]