diff --git a/.github/workflows/cargo_machete.yml b/.github/workflows/cargo_machete.yml new file mode 100644 index 00000000000..dab6725553c --- /dev/null +++ b/.github/workflows/cargo_machete.yml @@ -0,0 +1,12 @@ +name: Cargo Machete + +on: [push, pull_request] + +jobs: + cargo-machete: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Machete + uses: bnjbvr/cargo-machete@main diff --git a/.github/workflows/preview_build.yml b/.github/workflows/preview_build.yml new file mode 100644 index 00000000000..70cd5ce2aae --- /dev/null +++ b/.github/workflows/preview_build.yml @@ -0,0 +1,52 @@ +# This action builds and deploys egui_demo_app on each pull request created +# Security notes: +# The preview deployment is split in two workflows, preview_build and preview_deploy. +# `preview_build` runs on pull_request, so it won't have any access to the repositories secrets, so it is safe to +# build / execute untrusted code. +# `preview_deploy` has access to the repositories secrets (so it can push to the pr preview repo) but won't run +# any untrusted code (it will just extract the build artifact and push it to the pages branch where it will +# automatically be deployed). + +name: Preview Build + +on: + - pull_request + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: rustup toolchain install stable --profile minimal --target wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: "pr-preview-" + + - name: "Install wasmopt / binaryen" + run: | + sudo apt-get update && sudo apt-get install binaryen + + - run: | + scripts/build_demo_web.sh --release + + - name: Remove gitignore file + # We need to remove the .gitignore, otherwise the deploy via git will not include the js and wasm files + run: | + rm -rf web_demo/.gitignore + + - uses: actions/upload-artifact@v4 + with: + name: web_demo + path: web_demo + + - name: Generate meta.json + env: + PR_NUMBER: ${{ github.event.number }} + PR_BRANCH: ${{ github.head_ref }} + run: | + echo "{\"pr_number\": \"$PR_NUMBER\", \"pr_branch\": \"$PR_BRANCH\"}" > meta.json + + - uses: actions/upload-artifact@v4 + with: + name: meta.json + path: meta.json diff --git a/.github/workflows/preview_cleanup.yml b/.github/workflows/preview_cleanup.yml new file mode 100644 index 00000000000..3aba668a2b3 --- /dev/null +++ b/.github/workflows/preview_cleanup.yml @@ -0,0 +1,31 @@ +name: Preview Cleanup + +permissions: + contents: write + +on: + pull_request_target: + types: + - closed + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - run: mkdir -p empty_dir + - name: Url slug variable + run: | + echo "URL_SLUG=${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.ref }}" >> $GITHUB_ENV + - name: Deploy + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: empty_dir + repository-name: egui-pr-preview/pr + branch: 'main' + clean: true + target-folder: ${{ env.URL_SLUG }} + ssh-key: ${{ secrets.DEPLOY_KEY }} + commit-message: "Remove preview for PR ${{ env.URL_SLUG }}" + single-commit: true diff --git a/.github/workflows/preview_deploy.yml b/.github/workflows/preview_deploy.yml new file mode 100644 index 00000000000..9fdcfaf755f --- /dev/null +++ b/.github/workflows/preview_deploy.yml @@ -0,0 +1,68 @@ +name: Preview Deploy + +permissions: + contents: write + pull-requests: write + +on: + workflow_run: + workflows: + - "Preview Build" + types: + - completed + +# Since we use single_commit and force on the deploy action, only one deploy action can run at a time. +# Should this create a bottleneck we might have to set single_commit and force to false which should allow +# for the deployments to run in parallel. +concurrency: + group: preview_deploy + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: 'Download build artifact' + uses: actions/download-artifact@v4 + with: + name: web_demo + path: web_demo_artifact + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + - name: 'Download build meta' + uses: actions/download-artifact@v4 + with: + name: meta.json + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + + - 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 + + - name: Deploy + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: web_demo_artifact + repository-name: egui-pr-preview/pr + branch: 'main' + clean: true + target-folder: ${{ env.URL_SLUG }} + ssh-key: ${{ secrets.DEPLOY_KEY }} + commit-message: "Update preview for PR ${{ env.URL_SLUG }}" + single-commit: true + + - name: Comment PR + uses: thollander/actions-comment-pull-request@v2 + with: + message: | + Preview available at https://egui-pr-preview.github.io/pr/${{ env.URL_SLUG }} + Note that it might take a couple seconds for the update to show up after the preview_build workflow has completed. + pr_number: ${{ env.PR_NUMBER }} + comment_tag: 'egui-preview' diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 1e9f4009188..1e8e31bbcf1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -5,6 +5,7 @@ name: Rust env: RUSTFLAGS: -D warnings RUSTDOCFLAGS: -D warnings + NIGHTLY_VERSION: nightly-2024-09-11 jobs: fmt-crank-check-test: @@ -113,6 +114,25 @@ jobs: - name: clippy wasm32 run: ./scripts/clippy_wasm.sh + # requires a different toolchain from the other checks (nightly) + check_wasm_atomics: + name: Check wasm32+atomics + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev + + - name: Set up cargo cache + uses: Swatinem/rust-cache@v2 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{env.NIGHTLY_VERSION}} + targets: wasm32-unknown-unknown + components: rust-src + + - name: Check wasm32+atomics eframe with wgpu + run: RUSTFLAGS='-C target-feature=+atomics' cargo +${{env.NIGHTLY_VERSION}} check -p eframe --lib --no-default-features --features wgpu --target wasm32-unknown-unknown -Z build-std=std,panic_abort + # --------------------------------------------------------------------------- cargo-deny: diff --git a/CHANGELOG.md b/CHANGELOG.md index ac42b232bff..69437e03cc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,105 @@ 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.29.0 - 2024-09-26 - Multipass, `UiBuilder`, & visual improvements +### ✨ Highlights +This release adds initial support for multi-pass layout, which is a tool to circumvent [a common limitation of immediate mode](https://github.com/emilk/egui#layout). +You can use the new `UiBuilder::sizing_pass` ([#4969](https://github.com/emilk/egui/pull/4969)) to instruct the `Ui` and widgets to shrink to their minimum size, then store that size. +Then call the new `Context::request_discard` ([#5059](https://github.com/emilk/egui/pull/5059)) to discard the visual output and do another _pass_ immediately after the current finishes. +Together, this allows more advanced layouts that is normally not possible in immediate mode. +So far this is only used by `egui::Grid` to hide the "first-frame jitters" that would sometimes happen before, but 3rd party libraries can also use it to do much more advanced things. + +There is also a new `UiBuilder` for more flexible construction of `Ui`s ([#4969](https://github.com/emilk/egui/pull/4969)). +By specifying a `sense` for the `Ui` you can make it respond to clicks and drags, reading the result with the new `Ui::response` ([#5054](https://github.com/emilk/egui/pull/5054)). +Among other things, you can use this to create buttons that contain arbitrary widgets. + +0.29 also adds improve support for automatic switching between light and dark mode. +You can now set up a custom `Style` for both dark and light mode, and have egui follow the system preference ([#4744](https://github.com/emilk/egui/pull/4744) [#4860](https://github.com/emilk/egui/pull/4860)). + +There also has been several small improvements to the look of egui: +* Fix vertical centering of text (e.g. in buttons) ([#5117](https://github.com/emilk/egui/pull/5117)) +* Sharper rendering of lines and outlines ([#4943](https://github.com/emilk/egui/pull/4943)) +* Nicer looking text selection, especially in light mode ([#5017](https://github.com/emilk/egui/pull/5017)) + +#### The new text selection +New text selection in light mode +New text selection in dark mode + + +#### What text selection used to look like +Old text selection in light mode +Old text selection in dark mode + +### 🧳 Migration +* `id_source` is now called `id_salt` everywhere ([#5025](https://github.com/emilk/egui/pull/5025)) +* `Ui::new` now takes a `UiBuilder` ([#4969](https://github.com/emilk/egui/pull/4969)) +* Deprecated (replaced with `UiBuilder`): + * `ui.add_visible_ui` + * `ui.allocate_ui_at_rect` + * `ui.child_ui` + * `ui.child_ui_with_id_source` + * `ui.push_stack_info` + +### ⭐ Added +* Create a `UiBuilder` for building `Ui`s [#4969](https://github.com/emilk/egui/pull/4969) by [@emilk](https://github.com/emilk) +* Add `egui::Sides` for adding UI on left and right sides [#5036](https://github.com/emilk/egui/pull/5036) by [@emilk](https://github.com/emilk) +* Make light & dark visuals customizable when following the system theme [#4744](https://github.com/emilk/egui/pull/4744) [#4860](https://github.com/emilk/egui/pull/4860) by [@bash](https://github.com/bash) +* Interactive `Ui`:s: add `UiBuilder::sense` and `Ui::response` [#5054](https://github.com/emilk/egui/pull/5054) by [@lucasmerlin](https://github.com/lucasmerlin) +* Add a menu button with text and image [#4748](https://github.com/emilk/egui/pull/4748) by [@NicolasBircksZR](https://github.com/NicolasBircksZR) +* Add `Ui::columns_const()` [#4764](https://github.com/emilk/egui/pull/4764) by [@v0x0g](https://github.com/v0x0g) +* Add `Slider::max_decimals_opt` [#4953](https://github.com/emilk/egui/pull/4953) by [@bircni](https://github.com/bircni) +* Add `Label::halign` [#4975](https://github.com/emilk/egui/pull/4975) by [@rustbasic](https://github.com/rustbasic) +* Add `ui.shrink_clip_rect` [#5068](https://github.com/emilk/egui/pull/5068) by [@emilk](https://github.com/emilk) +* Add `ScrollArea::scroll_bar_rect` [#5070](https://github.com/emilk/egui/pull/5070) by [@emilk](https://github.com/emilk) +* Add `Options::input_options` for click-delay etc [#4942](https://github.com/emilk/egui/pull/4942) by [@girtsf](https://github.com/girtsf) +* Add `WidgetType::RadioGroup` [#5081](https://github.com/emilk/egui/pull/5081) by [@bash](https://github.com/bash) +* Add return value to `with_accessibility_parent` [#5083](https://github.com/emilk/egui/pull/5083) by [@bash](https://github.com/bash) +* Add `Ui::with_visual_transform` [#5055](https://github.com/emilk/egui/pull/5055) by [@lucasmerlin](https://github.com/lucasmerlin) +* Make `Slider` and `DragValue` compatible with `NonZeroUsize` etc [#5105](https://github.com/emilk/egui/pull/5105) by [@emilk](https://github.com/emilk) +* Add `Context::request_discard` for multi-pass layouts [#5059](https://github.com/emilk/egui/pull/5059) by [@emilk](https://github.com/emilk) +* Add UI to modify `FontTweak` live [#5125](https://github.com/emilk/egui/pull/5125) by [@emilk](https://github.com/emilk) +* Add `Response::intrinsic_size` to enable better layout in 3rd party crates [#5082](https://github.com/emilk/egui/pull/5082) by [@lucasmerlin](https://github.com/lucasmerlin) +* Add support for mipmap textures [#5146](https://github.com/emilk/egui/pull/5146) by [@nolanderc](https://github.com/nolanderc) +* Add `DebugOptions::show_unaligned` [#5165](https://github.com/emilk/egui/pull/5165) by [@emilk](https://github.com/emilk) +* Add `Slider::clamping` for precise clamp control [#5119](https://github.com/emilk/egui/pull/5119) by [@emilk](https://github.com/emilk) + +### 🚀 Performance +* Optimize `Color32::from_rgba_unmultiplied` with LUT [#5088](https://github.com/emilk/egui/pull/5088) by [@YgorSouza](https://github.com/YgorSouza) + +### 🔧 Changed +* Rename `id_source` to `id_salt` [#5025](https://github.com/emilk/egui/pull/5025) by [@bircni](https://github.com/bircni) +* Avoid some `Id` clashes by seeding auto-ids with child id [#4840](https://github.com/emilk/egui/pull/4840) by [@ironpeak](https://github.com/ironpeak) +* Nicer looking text selection, especially in light mode [#5017](https://github.com/emilk/egui/pull/5017) by [@emilk](https://github.com/emilk) +* Fix blurry lines by aligning to pixel grid [#4943](https://github.com/emilk/egui/pull/4943) by [@juancampa](https://github.com/juancampa) +* Center-align all text vertically [#5117](https://github.com/emilk/egui/pull/5117) by [@emilk](https://github.com/emilk) +* Clamp margin values in `Margin::ui` [#4873](https://github.com/emilk/egui/pull/4873) by [@rustbasic](https://github.com/rustbasic) +* Make `scroll_to_*` animations configurable [#4305](https://github.com/emilk/egui/pull/4305) by [@lucasmerlin](https://github.com/lucasmerlin) +* Update `Button` to correctly align contained image [#4891](https://github.com/emilk/egui/pull/4891) by [@PrimmR](https://github.com/PrimmR) +* Deprecate `ahash` re-exports [#4979](https://github.com/emilk/egui/pull/4979) by [@oscargus](https://github.com/oscargus) +* Fix: Ensures correct IME behavior when the text input area gains or loses focus [#4896](https://github.com/emilk/egui/pull/4896) by [@rustbasic](https://github.com/rustbasic) +* Enable rustdoc `generate-link-to-definition` feature on docs.rs [#5030](https://github.com/emilk/egui/pull/5030) by [@GuillaumeGomez](https://github.com/GuillaumeGomez) +* Make some `Memory` methods public [#5046](https://github.com/emilk/egui/pull/5046) by [@bircni](https://github.com/bircni) +* Deprecate `ui.set_sizing_pass` [#5074](https://github.com/emilk/egui/pull/5074) by [@emilk](https://github.com/emilk) +* Export module `egui::frame` [#5087](https://github.com/emilk/egui/pull/5087) by [@simgt](https://github.com/simgt) +* Use `log` crate instead of `eprintln` & remove some unwraps [#5010](https://github.com/emilk/egui/pull/5010) by [@bircni](https://github.com/bircni) +* Fix: `Event::Copy` and `Event::Cut` behave as if they select the entire text when there is no selection [#5115](https://github.com/emilk/egui/pull/5115) by [@rustbasic](https://github.com/rustbasic) + +### 🐛 Fixed +* Prevent text shrinking in tooltips; round wrap-width to integer [#5161](https://github.com/emilk/egui/pull/5161) by [@emilk](https://github.com/emilk) +* Fix bug causing tooltips with dynamic content to shrink [#5168](https://github.com/emilk/egui/pull/5168) by [@emilk](https://github.com/emilk) +* Remove some debug asserts [#4826](https://github.com/emilk/egui/pull/4826) by [@emilk](https://github.com/emilk) +* Handle the IME event first in `TextEdit` to fix some bugs [#4794](https://github.com/emilk/egui/pull/4794) by [@rustbasic](https://github.com/rustbasic) +* Slider: round to decimals after applying `step_by` [#4822](https://github.com/emilk/egui/pull/4822) by [@AurevoirXavier](https://github.com/AurevoirXavier) +* Fix: hint text follows the alignment set on the `TextEdit` [#4889](https://github.com/emilk/egui/pull/4889) by [@PrimmR](https://github.com/PrimmR) +* Request focus on a `TextEdit` when clicked [#4991](https://github.com/emilk/egui/pull/4991) by [@Zoxc](https://github.com/Zoxc) +* Fix `Id` clash in `Frame` styling widget [#4967](https://github.com/emilk/egui/pull/4967) by [@YgorSouza](https://github.com/YgorSouza) +* Prevent `ScrollArea` contents from exceeding the container size [#5006](https://github.com/emilk/egui/pull/5006) by [@DouglasDwyer](https://github.com/DouglasDwyer) +* Fix bug in size calculation of truncated text [#5076](https://github.com/emilk/egui/pull/5076) by [@emilk](https://github.com/emilk) +* Fix: Make sure `RawInput::take` clears all events, like it says it does [#5104](https://github.com/emilk/egui/pull/5104) by [@emilk](https://github.com/emilk) +* Fix `DragValue` range clamping [#5118](https://github.com/emilk/egui/pull/5118) by [@emilk](https://github.com/emilk) +* Fix: panic when dragging window between monitors of different pixels_per_point [#4868](https://github.com/emilk/egui/pull/4868) by [@rustbasic](https://github.com/rustbasic) + + ## 0.28.1 - 2024-07-05 - Tooltip tweaks ### ⭐ Added * Add `Image::uri()` [#4720](https://github.com/emilk/egui/pull/4720) by [@rustbasic](https://github.com/rustbasic) diff --git a/Cargo.lock b/Cargo.lock index 7cc5aa5c845..5fdfab95d28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1052,6 +1052,17 @@ dependencies = [ "env_logger", ] +[[package]] +name = "custom_style" +version = "0.1.0" +dependencies = [ + "eframe", + "egui_demo_lib", + "egui_extras", + "env_logger", + "image", +] + [[package]] name = "custom_window_frame" version = "0.1.0" @@ -1155,7 +1166,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" [[package]] name = "ecolor" -version = "0.28.1" +version = "0.29.0" dependencies = [ "bytemuck", "cint", @@ -1167,7 +1178,7 @@ dependencies = [ [[package]] name = "eframe" -version = "0.28.1" +version = "0.29.0" dependencies = [ "ahash", "bytemuck", @@ -1177,7 +1188,7 @@ dependencies = [ "egui-wgpu", "egui-winit", "egui_glow", - "glow", + "glow 0.14.0", "glutin", "glutin-winit", "home", @@ -1207,7 +1218,7 @@ dependencies = [ [[package]] name = "egui" -version = "0.28.1" +version = "0.29.0" dependencies = [ "accesskit", "ahash", @@ -1224,7 +1235,7 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.28.1" +version = "0.29.0" dependencies = [ "ahash", "bytemuck", @@ -1242,7 +1253,7 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.28.1" +version = "0.29.0" dependencies = [ "accesskit_winit", "ahash", @@ -1250,7 +1261,6 @@ dependencies = [ "document-features", "egui", "log", - "nix", "puffin", "raw-window-handle 0.6.2", "serde", @@ -1263,7 +1273,7 @@ dependencies = [ [[package]] name = "egui_demo_app" -version = "0.28.1" +version = "0.29.0" dependencies = [ "bytemuck", "chrono", @@ -1288,21 +1298,20 @@ dependencies = [ [[package]] name = "egui_demo_lib" -version = "0.28.1" +version = "0.29.0" dependencies = [ "chrono", "criterion", "document-features", "egui", "egui_extras", - "log", "serde", "unicode_names2", ] [[package]] name = "egui_extras" -version = "0.28.1" +version = "0.29.0" dependencies = [ "ahash", "chrono", @@ -1321,14 +1330,14 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.28.1" +version = "0.29.0" dependencies = [ "ahash", "bytemuck", "document-features", "egui", "egui-winit", - "glow", + "glow 0.14.0", "glutin", "glutin-winit", "log", @@ -1361,7 +1370,7 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "emath" -version = "0.28.1" +version = "0.29.0" dependencies = [ "bytemuck", "document-features", @@ -1436,7 +1445,7 @@ dependencies = [ [[package]] name = "epaint" -version = "0.28.1" +version = "0.29.0" dependencies = [ "ab_glyph", "ahash", @@ -1457,7 +1466,7 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.28.1" +version = "0.29.0" [[package]] name = "equivalent" @@ -1831,6 +1840,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "glow" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f865cbd94bd355b89611211e49508da98a1fce0ad755c1e8448fb96711b24528" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "glutin" version = "0.32.0" @@ -3048,7 +3069,7 @@ checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" [[package]] name = "popups" -version = "0.28.1" +version = "0.29.0" dependencies = [ "eframe", "env_logger", @@ -3132,6 +3153,7 @@ version = "0.1.0" dependencies = [ "eframe", "env_logger", + "log", "puffin", "puffin_http", ] @@ -3560,6 +3582,7 @@ version = "0.1.0" dependencies = [ "eframe", "env_logger", + "log", ] [[package]] @@ -4504,7 +4527,7 @@ dependencies = [ "block", "cfg_aliases 0.1.1", "core-graphics-types", - "glow", + "glow 0.13.1", "glutin_wgl_sys", "gpu-alloc", "gpu-allocator", @@ -5003,7 +5026,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xtask" -version = "0.28.1" +version = "0.29.0" [[package]] name = "yaml-rust" diff --git a/Cargo.toml b/Cargo.toml index 09e2f42d840..ecec4fcca3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ edition = "2021" license = "MIT OR Apache-2.0" rust-version = "1.76" -version = "0.28.1" +version = "0.29.0" [profile.release] @@ -54,17 +54,17 @@ opt-level = 2 [workspace.dependencies] -emath = { version = "0.28.1", path = "crates/emath", default-features = false } -ecolor = { version = "0.28.1", path = "crates/ecolor", default-features = false } -epaint = { version = "0.28.1", path = "crates/epaint", default-features = false } -epaint_default_fonts = { version = "0.28.1", path = "crates/epaint_default_fonts" } -egui = { version = "0.28.1", path = "crates/egui", default-features = false } -egui-winit = { version = "0.28.1", path = "crates/egui-winit", default-features = false } -egui_extras = { version = "0.28.1", path = "crates/egui_extras", default-features = false } -egui-wgpu = { version = "0.28.1", path = "crates/egui-wgpu", default-features = false } -egui_demo_lib = { version = "0.28.1", path = "crates/egui_demo_lib", default-features = false } -egui_glow = { version = "0.28.1", path = "crates/egui_glow", default-features = false } -eframe = { version = "0.28.1", path = "crates/eframe", default-features = false } +emath = { version = "0.29.0", path = "crates/emath", default-features = false } +ecolor = { version = "0.29.0", path = "crates/ecolor", default-features = false } +epaint = { version = "0.29.0", path = "crates/epaint", default-features = false } +epaint_default_fonts = { version = "0.29.0", path = "crates/epaint_default_fonts" } +egui = { version = "0.29.0", path = "crates/egui", default-features = false } +egui-winit = { version = "0.29.0", path = "crates/egui-winit", default-features = false } +egui_extras = { version = "0.29.0", path = "crates/egui_extras", default-features = false } +egui-wgpu = { version = "0.29.0", path = "crates/egui-wgpu", default-features = false } +egui_demo_lib = { version = "0.29.0", path = "crates/egui_demo_lib", default-features = false } +egui_glow = { version = "0.29.0", path = "crates/egui_glow", default-features = false } +eframe = { version = "0.29.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 @@ -74,7 +74,7 @@ backtrace = "0.3" bytemuck = "1.7.2" criterion = { version = "0.5.1", default-features = false } document-features = " 0.2.8" -glow = "0.13" +glow = "0.14" glutin = "0.32.0" glutin-winit = "0.5.0" home = "0.5.9" @@ -92,10 +92,7 @@ web-time = "1.1.0" # Timekeeping for native and web wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" web-sys = "0.3.70" -wgpu = { version = "22.1.0", default-features = false, features = [ - # Make the renderer `Sync` even on wasm32, because it makes the code simpler: - "fragile-send-sync-non-atomic-wasm", -] } +wgpu = { version = "22.1.0", default-features = false } windows-sys = "0.52" winit = { version = "0.30.5", default-features = false } @@ -210,6 +207,7 @@ negative_feature_names = "warn" nonstandard_macro_braces = "warn" option_option = "warn" path_buf_push_overwrite = "warn" +print_stderr = "warn" ptr_as_ptr = "warn" ptr_cast_constness = "warn" pub_without_shorthand = "warn" @@ -254,11 +252,11 @@ wildcard_dependencies = "warn" wildcard_imports = "warn" zero_sized_map_values = "warn" + # TODO(emilk): enable more of these lints: iter_over_hash_type = "allow" let_underscore_untyped = "allow" missing_assert_message = "allow" -print_stderr = "allow" # TODO(emilk): use `log` crate instead should_panic_without_expect = "allow" too_many_lines = "allow" unwrap_used = "allow" # TODO(emilk): We really wanna warn on this one diff --git a/README.md b/README.md index 6fa53cbae11..bdf4229125c 100644 --- a/README.md +++ b/README.md @@ -98,8 +98,7 @@ On Fedora Rawhide you need to run: * Portable: the same code works on the web and as a native app * Easy to integrate into any environment * A simple 2D graphics API for custom painting ([`epaint`](https://docs.rs/epaint)). -* No callbacks -* Pure immediate mode +* Pure immediate mode: no callbacks * Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs) * Modular: You should be able to use small parts of egui and combine them in new ways * Safe: there is no `unsafe` code in egui @@ -113,7 +112,6 @@ egui is *not* a framework. egui is a library you call into, not an environment y * Become the most powerful GUI library * Native looking interface -* Advanced and flexible layouts (that's fundamentally incompatible with immediate mode) ## State @@ -206,6 +204,7 @@ These are the official egui integrations: * [`fltk-egui`](https://crates.io/crates/fltk-egui) for [fltk-rs](https://github.com/fltk-rs/fltk-rs) * [`ggegui`](https://github.com/NemuiSen/ggegui) for the [ggez](https://ggez.rs/) game framework * [`godot-egui`](https://github.com/setzer22/godot-egui) for [godot-rust](https://github.com/godot-rust/godot-rust) +* [`gtk-egui-area`](https://github.com/ilya-zlobintsev/gtk-egui-area) for [gtk-rs](https://github.com/gtk-rs/gtk4-rs) * [`nannou_egui`](https://github.com/nannou-org/nannou/tree/master/nannou_egui) for [nannou](https://nannou.cc) * [`notan_egui`](https://github.com/Nazariglez/notan/tree/main/crates/notan_egui) for [notan](https://github.com/Nazariglez/notan) * [`screen-13-egui`](https://github.com/attackgoat/screen-13/tree/master/contrib/screen-13-egui) for [Screen 13](https://github.com/attackgoat/screen-13) @@ -250,7 +249,8 @@ This is a fundamental shortcoming of immediate mode GUIs, and any attempt to res One workaround is to store the size and use it the next frame. This produces a frame-delay for the correct layout, producing occasional flickering the first frame something shows up. `egui` does this for some things such as windows and grid layouts. -You can also call the layout code twice (once to get the size, once to do the interaction), but that is not only more expensive, it's also complex to implement, and in some cases twice is not enough. `egui` never does this. +The "first-frame jitter" can be covered up with an extra _pass_, which egui supports via `Context::request_discard`. +The downside of this is the added CPU cost of a second pass, so egui only does this in very rare circumstances (the majority of frames are single-pass). For "atomic" widgets (e.g. a button) `egui` knows the size before showing it, so centering buttons, labels etc is possible in `egui` without any special workarounds. diff --git a/RELEASES.md b/RELEASES.md index b825eef8df5..b3777a3aedc 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -33,7 +33,6 @@ We don't update the MSRV in a patch release, unless we really, really need to. * [ ] `./scripts/docs.sh`: read and improve documentation of new stuff * [ ] `cargo update` * [ ] `cargo outdated` (or manually look for outdated crates in each `Cargo.toml`) -* [ ] `cargo machete` ## Release testing * [ ] `cargo r -p egui_demo_app` and click around for while @@ -55,12 +54,13 @@ We don't update the MSRV in a patch release, unless we really, really need to. * [ ] 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.27.0..HEAD` + - For major releases, always diff to the latest MAJOR release, e.g. `--commit-range 0.29.0..HEAD` * [ ] bump version numbers in workspace `Cargo.toml` ## Actual release I usually do this all on the `master` branch, but doing it in a release branch is also fine, as long as you remember to merge it into `master` later. +* [ ] Run `typos` * [ ] `git commit -m 'Release 0.x.0 - summary'` * [ ] `cargo publish` (see below) * [ ] `git tag -a 0.x.0 -m 'Release 0.x.0 - summary'` @@ -75,8 +75,8 @@ I usually do this all on the `master` branch, but doing it in a release branch i ``` (cd crates/emath && cargo publish --quiet) && echo "✅ emath" (cd crates/ecolor && cargo publish --quiet) && echo "✅ ecolor" -(cd crates/epaint && cargo publish --quiet) && echo "✅ epaint" (cd crates/epaint_default_fonts && cargo publish --quiet) && echo "✅ epaint_default_fonts" +(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" @@ -96,4 +96,5 @@ I usually do this all on the `master` branch, but doing it in a release branch i ## After release * [ ] publish new `eframe_template` * [ ] publish new `egui_plot` +* [ ] publish new `egui_table` * [ ] publish new `egui_tiles` diff --git a/crates/ecolor/CHANGELOG.md b/crates/ecolor/CHANGELOG.md index 6b374869f98..27280c70bfb 100644 --- a/crates/ecolor/CHANGELOG.md +++ b/crates/ecolor/CHANGELOG.md @@ -6,6 +6,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.29.0 - 2024-09-26 +* Document the fact that the `hex_color!` macro is not `const` [#5169](https://github.com/emilk/egui/pull/5169) by [@YgorSouza](https://github.com/YgorSouza) + + ## 0.28.1 - 2024-07-05 Nothing new diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index ca257303dae..80c876d192d 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -1,6 +1,4 @@ -use crate::{ - fast_round, gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_linear_u8, Rgba, -}; +use crate::{fast_round, linear_f32_from_linear_u8, Rgba}; /// This format is used for space-efficient color representation (32 bits). /// @@ -12,11 +10,18 @@ use crate::{ /// /// The special value of alpha=0 means the color is to be treated as an additive color. #[repr(C)] -#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))] pub struct Color32(pub(crate) [u8; 4]); +impl std::fmt::Debug for Color32 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let [r, g, b, a] = self.0; + write!(f, "#{r:02X}_{g:02X}_{b:02X}_{a:02X}") + } +} + impl std::ops::Index for Color32 { type Output = u8; @@ -49,6 +54,7 @@ impl Color32 { pub const LIGHT_RED: Self = Self::from_rgb(255, 128, 128); pub const YELLOW: Self = Self::from_rgb(255, 255, 0); + pub const ORANGE: Self = Self::from_rgb(255, 165, 0); pub const LIGHT_YELLOW: Self = Self::from_rgb(255, 255, 0xE0); pub const KHAKI: Self = Self::from_rgb(240, 230, 140); @@ -95,21 +101,28 @@ impl Color32 { /// From `sRGBA` WITHOUT premultiplied alpha. #[inline] pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { - if a == 255 { - Self::from_rgb(r, g, b) // common-case optimization - } else if a == 0 { - Self::TRANSPARENT // common-case optimization - } else { - let r_lin = linear_f32_from_gamma_u8(r); - let g_lin = linear_f32_from_gamma_u8(g); - let b_lin = linear_f32_from_gamma_u8(b); - let a_lin = linear_f32_from_linear_u8(a); - - let r = gamma_u8_from_linear_f32(r_lin * a_lin); - let g = gamma_u8_from_linear_f32(g_lin * a_lin); - let b = gamma_u8_from_linear_f32(b_lin * a_lin); - - Self::from_rgba_premultiplied(r, g, b, a) + use std::sync::OnceLock; + match a { + // common-case optimization + 0 => Self::TRANSPARENT, + // common-case optimization + 255 => Self::from_rgb(r, g, b), + a => { + static LOOKUP_TABLE: OnceLock<[u8; 256 * 256]> = OnceLock::new(); + let lut = LOOKUP_TABLE.get_or_init(|| { + use crate::{gamma_u8_from_linear_f32, linear_f32_from_gamma_u8}; + core::array::from_fn(|i| { + let [value, alpha] = (i as u16).to_ne_bytes(); + let value_lin = linear_f32_from_gamma_u8(value); + let alpha_lin = linear_f32_from_linear_u8(alpha); + gamma_u8_from_linear_f32(value_lin * alpha_lin) + }) + }); + + let [r, g, b] = + [r, g, b].map(|value| lut[usize::from(u16::from_ne_bytes([value, a]))]); + Self::from_rgba_premultiplied(r, g, b, a) + } } } diff --git a/crates/ecolor/src/hex_color_macro.rs b/crates/ecolor/src/hex_color_macro.rs index a0a0729fdd1..fd1075dc639 100644 --- a/crates/ecolor/src/hex_color_macro.rs +++ b/crates/ecolor/src/hex_color_macro.rs @@ -1,15 +1,41 @@ -/// Construct a [`crate::Color32`] from a hex RGB or RGBA string. +/// Construct a [`crate::Color32`] from a hex RGB or RGBA string literal. /// /// Requires the "color-hex" feature. /// +/// The string is checked at compile time. If the format is invalid, compilation fails. The valid +/// format is the one described in . Only 6 (RGB) or 8 (RGBA) +/// digits are supported, and the leading `#` character is optional. +/// +/// Note that despite being checked at compile-time, this macro is not usable in `const` contexts +/// because creating the [`crate::Color32`] instance requires floating-point arithmetic. +/// /// See also [`crate::Color32::from_hex`] and [`crate::Color32::to_hex`]. /// +/// # Examples +/// /// ``` /// # use ecolor::{hex_color, Color32}; /// assert_eq!(hex_color!("#202122"), Color32::from_hex("#202122").unwrap()); /// assert_eq!(hex_color!("#202122"), Color32::from_rgb(0x20, 0x21, 0x22)); +/// assert_eq!(hex_color!("#202122"), hex_color!("202122")); /// assert_eq!(hex_color!("#abcdef12"), Color32::from_rgba_unmultiplied(0xab, 0xcd, 0xef, 0x12)); /// ``` +/// +/// If the literal string has the wrong format, the code does not compile. +/// +/// ```compile_fail +/// let _ = ecolor::hex_color!("#abc"); +/// ``` +/// +/// ```compile_fail +/// let _ = ecolor::hex_color!("#20212x"); +/// ``` +/// +/// The macro cannot be used in a `const` context. +/// +/// ```compile_fail +/// const COLOR: ecolor::Color32 = ecolor::hex_color!("#202122"); +/// ``` #[macro_export] macro_rules! hex_color { ($s:literal) => {{ diff --git a/crates/ecolor/src/hex_color_runtime.rs b/crates/ecolor/src/hex_color_runtime.rs index 3163fc5af7b..5b20258fa48 100644 --- a/crates/ecolor/src/hex_color_runtime.rs +++ b/crates/ecolor/src/hex_color_runtime.rs @@ -123,8 +123,8 @@ impl Color32 { /// Supports the 3, 4, 6, and 8-digit formats, according to the specification in /// /// - /// To parse hex colors at compile-time (e.g. for use in `const` contexts) - /// use the macro [`crate::hex_color!`] instead. + /// To parse hex colors from string literals with compile-time checking, use the macro + /// [`crate::hex_color!`] instead. /// /// # Example /// ```rust diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index f57b1f61eea..ff54dc2e83d 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -7,6 +7,41 @@ 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.29.0 - 2024-09-26 - `winit` 0.30 & fix mobile text input +### ✨ Highlights +* Upgrade winit to 0.30 ([#4849](https://github.com/emilk/egui/pull/4849) [#4939](https://github.com/emilk/egui/pull/4939)) +* Fix virtual keyboard on (mobile) web ([#4848](https://github.com/emilk/egui/pull/4848) [#4855](https://github.com/emilk/egui/pull/4855)) + +### 🧳 Migration +* `WebRunner::start` now expects a `HtmlCanvasElement` rather than the id of it ([#4780](https://github.com/emilk/egui/pull/4780)) +* `NativeOptions::follow_system_theme` and `default_theme` is gone, and is now in `egui::Options` instead ([#4860](https://github.com/emilk/egui/pull/4860) by [@bash](https://github.com/bash)) + +### ⭐ Added +* Conditionally propagate web events using a filter in WebOptions [#5056](https://github.com/emilk/egui/pull/5056) by [@liamrosenfeld](https://github.com/liamrosenfeld) + +### 🔧 Changed +* Pass `HtmlCanvasElement ` element directly in `WebRunner::start` [#4780](https://github.com/emilk/egui/pull/4780) by [@jprochazk](https://github.com/jprochazk) +* Upgrade winit to 0.30.2 [#4849](https://github.com/emilk/egui/pull/4849) [#4939](https://github.com/emilk/egui/pull/4939) by [@ArthurBrussee](https://github.com/ArthurBrussee) +* Allow non-`static` `eframe::App` lifetime [#5060](https://github.com/emilk/egui/pull/5060) by [@timstr](https://github.com/timstr) +* Improve `glow` context switching [#4814](https://github.com/emilk/egui/pull/4814) by [@rustbasic](https://github.com/rustbasic) +* Ignore viewport size/position on iOS [#4922](https://github.com/emilk/egui/pull/4922) by [@frederik-uni](https://github.com/frederik-uni) +* Update `web-sys` & `wasm-bindgen` [#4980](https://github.com/emilk/egui/pull/4980) by [@bircni](https://github.com/bircni) +* Remove the need for setting `web_sys_unstable_apis` [#5000](https://github.com/emilk/egui/pull/5000) by [@emilk](https://github.com/emilk) +* Remove the `directories` dependency [#4904](https://github.com/emilk/egui/pull/4904) by [@YgorSouza](https://github.com/YgorSouza) + +### 🐛 Fixed +* Fix: call `save` when hiding web tab, and `update` when focusing it [#5114](https://github.com/emilk/egui/pull/5114) by [@emilk](https://github.com/emilk) +* Force canvas/text input focus on touch for iOS web browsers [#4848](https://github.com/emilk/egui/pull/4848) by [@BKSalman](https://github.com/BKSalman) +* Fix virtual keyboard on (mobile) web [#4855](https://github.com/emilk/egui/pull/4855) by [@micmonay](https://github.com/micmonay) +* Fix: Backspace not working after IME input [#4912](https://github.com/emilk/egui/pull/4912) by [@rustbasic](https://github.com/rustbasic) +* Fix iOS build, and add iOS step to CI [#4898](https://github.com/emilk/egui/pull/4898) by [@lucasmerlin](https://github.com/lucasmerlin) +* Fix iOS compilation of eframe [#4851](https://github.com/emilk/egui/pull/4851) by [@ardocrat](https://github.com/ardocrat) +* Fix crash when changing viewport settings [#4862](https://github.com/emilk/egui/pull/4862) by [@pm100](https://github.com/pm100) +* Fix eframe centering on multiple monitor systems [#4919](https://github.com/emilk/egui/pull/4919) by [@VinTarZ](https://github.com/VinTarZ) +* Fix viewport not working when minimized [#5042](https://github.com/emilk/egui/pull/5042) by [@rustbasic](https://github.com/rustbasic) +* Clarified `eframe::run_simple_native()` persistence [#4846](https://github.com/emilk/egui/pull/4846) by [@tpstevens](https://github.com/tpstevens) + + ## 0.28.1 - 2024-07-05 * Web: only capture clicks/touches when actually over canvas [#4775](https://github.com/emilk/egui/pull/4775) by [@lucasmerlin](https://github.com/lucasmerlin) diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index b48b33c503b..060e857e941 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -39,6 +39,7 @@ default = [ "web_screen_reader", "winit/default", "x11", + "egui-wgpu?/fragile-send-sync-non-atomic-wasm", ] ## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/). @@ -185,7 +186,11 @@ objc2-app-kit = { version = "0.2.0", features = [ # windows: [target.'cfg(any(target_os = "windows"))'.dependencies] winapi = { version = "0.3.9", features = ["winuser"] } -windows-sys = { workspace = true, features = ["Win32_Foundation", "Win32_UI_Shell", "Win32_System_Com"] } +windows-sys = { workspace = true, features = [ + "Win32_Foundation", + "Win32_UI_Shell", + "Win32_System_Com", +] } # ------------------------------------------- # web: diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 0fec5a070fd..f567507c4da 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -54,7 +54,7 @@ pub struct CreationContext<'s> { /// The egui Context. /// /// You can use this to customize the look of egui, e.g to call [`egui::Context::set_fonts`], - /// [`egui::Context::set_visuals`] etc. + /// [`egui::Context::set_visuals_of`] etc. pub egui_ctx: egui::Context, /// Information about the surrounding environment. diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index c1a3f5eaf89..476b1bdd466 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -5,10 +5,6 @@ //! There is a bunch of improvements we could do, //! like removing a bunch of `unwraps`. -// `clippy::arc_with_non_send_sync`: `glow::Context` was accidentally non-Sync in glow 0.13, -// but that will be fixed in future releases of glow. -// https://github.com/grovesNL/glow/commit/c4a5f7151b9b4bbb380faa06ec27415235d1bf7e -#![allow(clippy::arc_with_non_send_sync)] #![allow(clippy::undocumented_unsafe_blocks)] use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant}; @@ -255,13 +251,13 @@ impl<'app> GlowWinitApp<'app> { .set_request_repaint_callback(move |info| { log::trace!("request_repaint_callback: {info:?}"); let when = Instant::now() + info.delay; - let frame_nr = info.current_frame_nr; + let cumulative_pass_nr = info.current_cumulative_pass_nr; event_loop_proxy .lock() .send_event(UserEvent::RequestRepaint { viewport_id: info.viewport_id, when, - frame_nr, + cumulative_pass_nr, }) .ok(); }); @@ -350,10 +346,8 @@ impl<'app> GlowWinitApp<'app> { } impl<'app> WinitApp for GlowWinitApp<'app> { - fn frame_nr(&self, viewport_id: ViewportId) -> u64 { - self.running - .as_ref() - .map_or(0, |r| r.integration.egui_ctx.frame_nr_for(viewport_id)) + fn egui_ctx(&self) -> Option<&egui::Context> { + self.running.as_ref().map(|r| &r.integration.egui_ctx) } fn window(&self, window_id: WindowId) -> Option> { @@ -716,7 +710,7 @@ impl<'app> GlowWinitRunning<'app> { // give it time to settle: #[cfg(feature = "__screenshot")] - if integration.egui_ctx.frame_nr() == 2 { + if integration.egui_ctx.cumulative_pass_nr() == 2 { if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") { save_screenshot_and_exit(&path, &painter, screen_size_in_pixels); } @@ -1401,7 +1395,7 @@ fn render_immediate_viewport( let ImmediateViewport { ids, builder, - viewport_ui_cb, + mut viewport_ui_cb, } = immediate_viewport; let viewport_id = ids.this; @@ -1553,7 +1547,7 @@ fn save_screenshot_and_exit( .unwrap_or_else(|err| { panic!("Failed to save screenshot to {path:?}: {err}"); }); - eprintln!("Screenshot saved to {path:?}."); + log::info!("Screenshot saved to {path:?}."); #[allow(clippy::exit)] std::process::exit(0); diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index f38c1b8f27f..7964a1e9ad4 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -228,11 +228,16 @@ impl ApplicationHandler for WinitAppWrapper { let event_result = match event { UserEvent::RequestRepaint { when, - frame_nr, + cumulative_pass_nr, viewport_id, } => { - let current_frame_nr = self.winit_app.frame_nr(viewport_id); - if current_frame_nr == frame_nr || current_frame_nr == frame_nr + 1 { + let current_pass_nr = self + .winit_app + .egui_ctx() + .map_or(0, |ctx| ctx.cumulative_pass_nr_for(viewport_id)); + if current_pass_nr == cumulative_pass_nr + || current_pass_nr == cumulative_pass_nr + 1 + { log::trace!("UserEvent::RequestRepaint scheduling repaint at {when:?}"); if let Some(window_id) = self.winit_app.window_id_from_viewport_id(viewport_id) diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index b38b78c9b4e..997383f85c3 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -223,13 +223,13 @@ impl<'app> WgpuWinitApp<'app> { egui_ctx.set_request_repaint_callback(move |info| { log::trace!("request_repaint_callback: {info:?}"); let when = Instant::now() + info.delay; - let frame_nr = info.current_frame_nr; + let cumulative_pass_nr = info.current_cumulative_pass_nr; event_loop_proxy .lock() .send_event(UserEvent::RequestRepaint { when, - frame_nr, + cumulative_pass_nr, viewport_id: info.viewport_id, }) .ok(); @@ -324,10 +324,8 @@ impl<'app> WgpuWinitApp<'app> { } impl<'app> WinitApp for WgpuWinitApp<'app> { - fn frame_nr(&self, viewport_id: ViewportId) -> u64 { - self.running - .as_ref() - .map_or(0, |r| r.integration.egui_ctx.frame_nr_for(viewport_id)) + fn egui_ctx(&self) -> Option<&egui::Context> { + self.running.as_ref().map(|r| &r.integration.egui_ctx) } fn window(&self, window_id: WindowId) -> Option> { @@ -916,7 +914,7 @@ fn render_immediate_viewport( let ImmediateViewport { ids, builder, - viewport_ui_cb, + mut viewport_ui_cb, } = immediate_viewport; let input = { diff --git a/crates/eframe/src/native/winit_integration.rs b/crates/eframe/src/native/winit_integration.rs index 049c90a63ca..e9d214103b8 100644 --- a/crates/eframe/src/native/winit_integration.rs +++ b/crates/eframe/src/native/winit_integration.rs @@ -25,6 +25,11 @@ pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Contex egui_ctx.set_embed_viewports(!IS_DESKTOP); + egui_ctx.options_mut(|o| { + // eframe supports multi-pass (Context::request_discard). + o.max_passes = 2.try_into().unwrap(); + }); + let memory = crate::native::epi_integration::load_egui_memory(storage).unwrap_or_default(); egui_ctx.memory_mut(|mem| *mem = memory); @@ -42,8 +47,8 @@ pub enum UserEvent { /// When to repaint. when: Instant, - /// What the frame number was when the repaint was _requested_. - frame_nr: u64, + /// What the cumulative pass number was when the repaint was _requested_. + cumulative_pass_nr: u64, }, /// A request related to [`accesskit`](https://accesskit.dev/). @@ -59,8 +64,7 @@ impl From for UserEvent { } pub trait WinitApp { - /// The current frame number, as reported by egui. - fn frame_nr(&self, viewport_id: ViewportId) -> u64; + fn egui_ctx(&self) -> Option<&egui::Context>; fn window(&self, window_id: WindowId) -> Option>; diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 88673118a91..00cc8f0c182 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -176,6 +176,12 @@ impl AppRunner { /// /// Technically: does either the canvas or the [`TextAgent`] have focus? pub fn has_focus(&self) -> bool { + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + if document.hidden() { + return false; + } + super::has_focus(self.canvas()) || self.text_agent.has_focus() } @@ -269,6 +275,8 @@ impl AppRunner { ime, #[cfg(feature = "accesskit")] accesskit_update: _, // not currently implemented + num_completed_passes: _, // handled by `Context::run` + request_discard_reasons: _, // handled by `Context::run` } = platform_output; super::set_cursor_icon(cursor_icon); diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index ebb1bda2551..cdecf3b701e 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -61,6 +61,7 @@ pub(crate) fn install_event_handlers(runner_ref: &WebRunner) -> Result<(), JsVal let document = window.document().unwrap(); let canvas = runner_ref.try_lock().unwrap().canvas().clone(); + install_blur_focus(runner_ref, &document)?; install_blur_focus(runner_ref, &canvas)?; prevent_default_and_stop_propagation( @@ -106,15 +107,10 @@ pub(crate) fn install_event_handlers(runner_ref: &WebRunner) -> Result<(), JsVal fn install_blur_focus(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { // NOTE: because of the text agent we sometime miss 'blur' events, // so we also poll the focus state each frame in `AppRunner::logic`. - for event_name in ["blur", "focus"] { + for event_name in ["blur", "focus", "visibilitychange"] { let closure = move |_event: web_sys::MouseEvent, runner: &mut AppRunner| { log::trace!("{} {event_name:?}", runner.canvas().id()); runner.update_focus(); - - if event_name == "blur" { - // This might be a good time to save the state - runner.save(); - } }; runner_ref.add_event_listener(target, event_name, closure)?; diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index 7d845a969e5..bbf38e06a15 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -302,7 +302,7 @@ impl WebPainter for WebPainterWgpu { let frame_view = frame .texture .create_view(&wgpu::TextureViewDescriptor::default()); - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &frame_view, resolve_target: None, @@ -333,7 +333,14 @@ impl WebPainter for WebPainterWgpu { timestamp_writes: None, }); - renderer.render(&mut render_pass, clipped_primitives, &screen_descriptor); + // Forgetting the pass' lifetime means that we are no longer compile-time protected from + // runtime errors caused by accessing the parent encoder before the render pass is dropped. + // Since we don't pass it on to the renderer, we should be perfectly safe against this mistake here! + renderer.render( + &mut render_pass.forget_lifetime(), + clipped_primitives, + &screen_descriptor, + ); } Some(frame) diff --git a/crates/egui-wgpu/CHANGELOG.md b/crates/egui-wgpu/CHANGELOG.md index 97a19d6105d..e254517b1c9 100644 --- a/crates/egui-wgpu/CHANGELOG.md +++ b/crates/egui-wgpu/CHANGELOG.md @@ -6,6 +6,20 @@ 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.29.0 - 2024-09-26 - `wgpu` 22.0 +### ⭐ Added +* Add opt-out `fragile-send-sync-non-atomic-wasm` feature for wgpu [#5098](https://github.com/emilk/egui/pull/5098) by [@9SMTM6](https://github.com/9SMTM6) + +### 🔧 Changed +* Upgrade to wgpu 22.0.0 [#4847](https://github.com/emilk/egui/pull/4847) by [@KeKsBoTer](https://github.com/KeKsBoTer) +* Introduce dithering to reduce banding [#4497](https://github.com/emilk/egui/pull/4497) by [@jwagner](https://github.com/jwagner) +* Ensure that `WgpuConfiguration` is `Send + Sync` [#4803](https://github.com/emilk/egui/pull/4803) by [@murl-digital](https://github.com/murl-digital) +* Wgpu render pass on paint callback has now `'static` lifetime [#5149](https://github.com/emilk/egui/pull/5149) by [@Wumpf](https://github.com/Wumpf) + +### 🐛 Fixed +* Update sampler along with texture on wgpu backend [#5122](https://github.com/emilk/egui/pull/5122) by [@valadaptive](https://github.com/valadaptive) + + ## 0.28.1 - 2024-07-05 Nothing new diff --git a/crates/egui-wgpu/Cargo.toml b/crates/egui-wgpu/Cargo.toml index 88b81b0c635..2cc9852921f 100644 --- a/crates/egui-wgpu/Cargo.toml +++ b/crates/egui-wgpu/Cargo.toml @@ -31,7 +31,7 @@ all-features = true rustdoc-args = ["--generate-link-to-definition"] [features] -default = [] +default = ["fragile-send-sync-non-atomic-wasm"] ## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. puffin = ["dep:puffin"] @@ -45,6 +45,12 @@ wayland = ["winit?/wayland"] ## Enables x11 support for winit. x11 = ["winit?/x11"] +## Make the renderer `Sync` on wasm, exploiting that by default wasm isn't multithreaded. +## It may make code easier, expecially when targeting both native and web. +## On native most wgpu objects are send and sync, on the web they are not (by nature of the WebGPU specification). +## This is not supported in [multithreaded WASM](https://gpuweb.github.io/gpuweb/explainer/#multithreading-transfer). +## Thus that usage is guarded against with compiler errors in wgpu. +fragile-send-sync-non-atomic-wasm = ["wgpu/fragile-send-sync-non-atomic-wasm"] [dependencies] egui = { workspace = true, default-features = false } diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 972351ee64c..d5c2d309ba3 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -173,6 +173,9 @@ impl RenderState { dithering, ); + // On wasm, depending on feature flags, wgpu objects may or may not implement sync. + // It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint. + #[allow(clippy::arc_with_non_send_sync)] Ok(Self { adapter: Arc::new(adapter), #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index 476685707ad..a3fcd667f5f 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -7,8 +7,19 @@ use epaint::{emath::NumExt, PaintCallbackInfo, Primitive, Vertex}; use wgpu::util::DeviceExt as _; +// Only implements Send + Sync on wasm32 in order to allow storing wgpu resources on the type map. +#[cfg(not(all( + target_arch = "wasm32", + not(feature = "fragile-send-sync-non-atomic-wasm"), +)))] /// You can use this for storage when implementing [`CallbackTrait`]. pub type CallbackResources = type_map::concurrent::TypeMap; +#[cfg(all( + target_arch = "wasm32", + not(feature = "fragile-send-sync-non-atomic-wasm"), +))] +/// You can use this for storage when implementing [`CallbackTrait`]. +pub type CallbackResources = type_map::TypeMap; /// You can use this to do custom [`wgpu`] rendering in an egui app. /// @@ -101,11 +112,11 @@ pub trait CallbackTrait: Send + Sync { /// /// It is given access to the [`wgpu::RenderPass`] so that it can issue draw commands /// into the same [`wgpu::RenderPass`] that is used for all other egui elements. - fn paint<'a>( - &'a self, + fn paint( + &self, info: PaintCallbackInfo, - render_pass: &mut wgpu::RenderPass<'a>, - callback_resources: &'a CallbackResources, + render_pass: &mut wgpu::RenderPass<'static>, + callback_resources: &CallbackResources, ); } @@ -152,6 +163,18 @@ struct SlicedBuffer { capacity: wgpu::BufferAddress, } +pub struct Texture { + /// The texture may be None if the `TextureId` is just a handle to a user-provided bind-group. + pub texture: Option, + + /// Bindgroup for the texture + sampler. + pub bind_group: wgpu::BindGroup, + + /// Options describing the sampler used in the bind group. This may be None if the `TextureId` + /// is just a handle to a user-provided bind-group. + pub options: Option, +} + /// Renderer for a egui based GUI. pub struct Renderer { pipeline: wgpu::RenderPipeline, @@ -167,7 +190,7 @@ pub struct Renderer { /// Map of egui texture IDs to textures and their associated bindgroups (texture view + /// sampler). The texture may be None if the `TextureId` is just a handle to a user-provided /// sampler. - textures: HashMap, wgpu::BindGroup)>, + textures: HashMap, next_user_texture_id: u64, samplers: HashMap, @@ -385,10 +408,16 @@ impl Renderer { } /// Executes the egui renderer onto an existing wgpu renderpass. - pub fn render<'rp>( - &'rp self, - render_pass: &mut wgpu::RenderPass<'rp>, - paint_jobs: &'rp [epaint::ClippedPrimitive], + /// + /// Note that the lifetime of `render_pass` is `'static` which requires a call to [`wgpu::RenderPass::forget_lifetime`]. + /// This allows users to pass resources that live outside of the callback resources to the render pass. + /// The render pass internally keeps all referenced resources alive as long as necessary. + /// The only consequence of `forget_lifetime` is that any operation on the parent encoder will cause a runtime error + /// instead of a compile time error. + pub fn render( + &self, + render_pass: &mut wgpu::RenderPass<'static>, + paint_jobs: &[epaint::ClippedPrimitive], screen_descriptor: &ScreenDescriptor, ) { crate::profile_function!(); @@ -443,7 +472,7 @@ impl Renderer { let index_buffer_slice = index_buffer_slices.next().unwrap(); let vertex_buffer_slice = vertex_buffer_slices.next().unwrap(); - if let Some((_texture, bind_group)) = self.textures.get(&mesh.texture_id) { + if let Some(Texture { bind_group, .. }) = self.textures.get(&mesh.texture_id) { render_pass.set_bind_group(1, bind_group, &[]); render_pass.set_index_buffer( self.index_buffer.buffer.slice( @@ -542,7 +571,7 @@ impl Renderer { "Mismatch between texture size and texel count" ); crate::profile_scope!("font -> sRGBA"); - Cow::Owned(image.srgba_pixels(None).collect::>()) + Cow::Owned(image.srgba_pixels(None).collect::>()) } }; let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice()); @@ -566,26 +595,41 @@ impl Renderer { ); }; - if let Some(pos) = image_delta.pos { + // Use same label for all resources associated with this texture id (no point in retyping the type) + let label_str = format!("egui_texid_{id:?}"); + let label = Some(label_str.as_str()); + + let (texture, origin, bind_group) = if let Some(pos) = image_delta.pos { // update the existing texture - let (texture, _bind_group) = self + let Texture { + texture, + bind_group, + options, + } = self .textures - .get(&id) + .remove(&id) .expect("Tried to update a texture that has not been allocated yet."); + let texture = texture.expect("Tried to update user texture."); + let options = options.expect("Tried to update user texture."); let origin = wgpu::Origin3d { x: pos[0] as u32, y: pos[1] as u32, z: 0, }; - queue_write_data_to_texture( - texture.as_ref().expect("Tried to update user texture."), + + ( + texture, origin, - ); + // If the TextureOptions are the same as the previous ones, we can reuse the bind group. Otherwise we + // have to recreate it. + if image_delta.options == options { + Some(bind_group) + } else { + None + }, + ) } else { // allocate a new texture - // Use same label for all resources associated with this texture id (no point in retyping the type) - let label_str = format!("egui_texid_{id:?}"); - let label = Some(label_str.as_str()); let texture = { crate::profile_scope!("create_texture"); device.create_texture(&wgpu::TextureDescriptor { @@ -599,11 +643,16 @@ impl Renderer { view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb], }) }; + let origin = wgpu::Origin3d::ZERO; + (texture, origin, None) + }; + + let bind_group = bind_group.unwrap_or_else(|| { let sampler = self .samplers .entry(image_delta.options) .or_insert_with(|| create_sampler(image_delta.options, device)); - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + device.create_bind_group(&wgpu::BindGroupDescriptor { label, layout: &self.texture_bind_group_layout, entries: &[ @@ -618,25 +667,31 @@ impl Renderer { resource: wgpu::BindingResource::Sampler(sampler), }, ], - }); - let origin = wgpu::Origin3d::ZERO; - queue_write_data_to_texture(&texture, origin); - self.textures.insert(id, (Some(texture), bind_group)); - }; + }) + }); + + queue_write_data_to_texture(&texture, origin); + self.textures.insert( + id, + Texture { + texture: Some(texture), + bind_group, + options: Some(image_delta.options), + }, + ); } pub fn free_texture(&mut self, id: &epaint::TextureId) { - self.textures.remove(id); + if let Some(texture) = self.textures.remove(id).and_then(|t| t.texture) { + texture.destroy(); + } } /// Get the WGPU texture and bind group associated to a texture that has been allocated by egui. /// /// This could be used by custom paint hooks to render images that have been added through /// [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture). - pub fn texture( - &self, - id: &epaint::TextureId, - ) -> Option<&(Option, wgpu::BindGroup)> { + pub fn texture(&self, id: &epaint::TextureId) -> Option<&Texture> { self.textures.get(id) } @@ -724,7 +779,14 @@ impl Renderer { }); let id = epaint::TextureId::User(self.next_user_texture_id); - self.textures.insert(id, (None, bind_group)); + self.textures.insert( + id, + Texture { + texture: None, + bind_group, + options: None, + }, + ); self.next_user_texture_id += 1; id @@ -744,7 +806,10 @@ impl Renderer { ) { crate::profile_function!(); - let (_user_texture, user_texture_binding) = self + let Texture { + bind_group: user_texture_binding, + .. + } = self .textures .get_mut(&id) .expect("Tried to update a texture that has not been allocated yet."); @@ -1017,6 +1082,11 @@ impl ScissorRect { } } +// Look at the feature flag for an explanation. +#[cfg(not(all( + target_arch = "wasm32", + not(feature = "fragile-send-sync-non-atomic-wasm"), +)))] #[test] fn renderer_impl_send_sync() { fn assert_send_sync() {} diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 46db8821e02..26b3c86ae2c 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -627,7 +627,7 @@ impl Painter { (texture_view, Some(&frame_view)) }); - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("egui_render"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view, @@ -658,7 +658,14 @@ impl Painter { occlusion_query_set: None, }); - renderer.render(&mut render_pass, clipped_primitives, &screen_descriptor); + // Forgetting the pass' lifetime means that we are no longer compile-time protected from + // runtime errors caused by accessing the parent encoder before the render pass is dropped. + // Since we don't pass it on to the renderer, we should be perfectly safe against this mistake here! + renderer.render( + &mut render_pass.forget_lifetime(), + clipped_primitives, + &screen_descriptor, + ); } { diff --git a/crates/egui-winit/CHANGELOG.md b/crates/egui-winit/CHANGELOG.md index 3b7faf14559..785c57b9404 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.29.0 - 2024-09-26 - `winit` 0.30 +* Upgrade to `winit` 0.30 [#4849](https://github.com/emilk/egui/pull/4849) [#4939](https://github.com/emilk/egui/pull/4939) by [@ArthurBrussee](https://github.com/ArthurBrussee) +* Fix: Backspace not working after IME input [#4912](https://github.com/emilk/egui/pull/4912) by [@rustbasic](https://github.com/rustbasic) + + ## 0.28.1 - 2024-07-05 Nothing new diff --git a/crates/egui-winit/Cargo.toml b/crates/egui-winit/Cargo.toml index 4472c70f552..f8a11ec7a36 100644 --- a/crates/egui-winit/Cargo.toml +++ b/crates/egui-winit/Cargo.toml @@ -62,7 +62,6 @@ egui = { workspace = true, default-features = false, features = ["log"] } ahash.workspace = true log.workspace = true -nix = { version = "0.26.4", default-features = false, optional = true } raw-window-handle.workspace = true web-time.workspace = true winit = { workspace = true, default-features = false } diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 1a546794cb1..7110ef3fe21 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -823,6 +823,8 @@ impl State { ime, #[cfg(feature = "accesskit")] accesskit_update, + num_completed_passes: _, // `egui::Context::run` handles this + request_discard_reasons: _, // `egui::Context::run` handles this } = platform_output; self.set_cursor_icon(window, cursor_icon); diff --git a/crates/egui/src/callstack.rs b/crates/egui/src/callstack.rs index 6b1959b0b79..03eeaf5fc6e 100644 --- a/crates/egui/src/callstack.rs +++ b/crates/egui/src/callstack.rs @@ -94,7 +94,7 @@ pub fn capture() -> String { "", "egui_plot::", "egui_extras::", - "core::ptr::drop_in_place::", + "core::ptr::drop_in_place", "eframe::", "core::ops::function::FnOnce::call_once", " as core::ops::function::FnOnce>::call_once", diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 38d84ca7128..fa8e5fc203b 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -462,14 +462,17 @@ impl Area { } }); - let move_response = ctx.create_widget(WidgetRect { - id: interact_id, - layer_id, - rect: state.rect(), - interact_rect: state.rect(), - sense, - enabled, - }); + let move_response = ctx.create_widget( + WidgetRect { + id: interact_id, + layer_id, + rect: state.rect(), + interact_rect: state.rect(), + sense, + enabled, + }, + true, + ); if movable && move_response.dragged() { if let Some(pivot_pos) = &mut state.pivot_pos { diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index 9883e2c755a..3d2bdd50b53 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -22,10 +22,10 @@ pub type IconPainter = Box ctx.frame_state_mut(|state| { + Side::Left => ctx.pass_state_mut(|state| { state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max)); }), - Side::Right => ctx.frame_state_mut(|state| { + Side::Right => ctx.pass_state_mut(|state| { state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max)); }), } @@ -885,12 +885,12 @@ impl TopBottomPanel { match side { TopBottomSide::Top => { - ctx.frame_state_mut(|state| { + ctx.pass_state_mut(|state| { state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max)); }); } TopBottomSide::Bottom => { - ctx.frame_state_mut(|state| { + ctx.pass_state_mut(|state| { state.allocate_bottom_panel(Rect::from_min_max(rect.min, available_rect.max)); }); } @@ -1149,7 +1149,7 @@ impl CentralPanel { let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); // Only inform ctx about what we actually used, so we can shrink the native window to fit. - ctx.frame_state_mut(|state| state.allocate_central_panel(inner_response.response.rect)); + ctx.pass_state_mut(|state| state.allocate_central_panel(inner_response.response.rect)); inner_response } diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 959d653fffb..45304245ca4 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -1,9 +1,9 @@ //! Show popup windows, tooltips, context menus etc. -use frame_state::PerWidgetTooltipState; +use pass_state::PerWidgetTooltipState; use crate::{ - frame_state, vec2, AboveOrBelow, Align, Align2, Area, AreaState, Context, Frame, Id, + pass_state, vec2, AboveOrBelow, Align, Align2, Area, AreaState, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order, Pos2, Rect, Response, Sense, Ui, UiKind, Vec2, Widget, WidgetText, }; @@ -162,7 +162,7 @@ fn show_tooltip_at_dyn<'c, R>( remember_that_tooltip_was_shown(ctx); - let mut state = ctx.frame_state_mut(|fs| { + let mut state = ctx.pass_state_mut(|fs| { // Remember that this is the widget showing the tooltip: fs.layers .entry(parent_layer) @@ -213,14 +213,14 @@ fn show_tooltip_at_dyn<'c, R>( state.tooltip_count += 1; state.bounding_rect = state.bounding_rect.union(response.rect); - ctx.frame_state_mut(|fs| fs.tooltips.widget_tooltips.insert(widget_id, state)); + ctx.pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(widget_id, state)); inner } /// What is the id of the next tooltip for this widget? pub fn next_tooltip_id(ctx: &Context, widget_id: Id) -> Id { - let tooltip_count = ctx.frame_state(|fs| { + let tooltip_count = ctx.pass_state(|fs| { fs.tooltips .widget_tooltips .get(&widget_id) @@ -409,7 +409,7 @@ pub fn popup_above_or_below_widget( let frame_margin = frame.total_margin(); let inner_width = widget_response.rect.width() - frame_margin.sum().x; - parent_ui.ctx().frame_state_mut(|fs| { + parent_ui.ctx().pass_state_mut(|fs| { fs.layers .entry(parent_ui.layer_id()) .or_default() diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 0921fa24615..ab7da8aff29 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -1,7 +1,7 @@ #![allow(clippy::needless_range_loop)] use crate::{ - emath, epaint, frame_state, lerp, pos2, remap, remap_clamp, vec2, Context, Id, NumExt, Pos2, + emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, vec2, Context, Id, NumExt, Pos2, Rangef, Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, }; @@ -819,10 +819,10 @@ impl Prepared { let scroll_delta = content_ui .ctx() - .frame_state_mut(|state| std::mem::take(&mut state.scroll_delta)); + .pass_state_mut(|state| std::mem::take(&mut state.scroll_delta)); for d in 0..2 { - // FrameState::scroll_delta is inverted from the way we apply the delta, so we need to negate it. + // PassState::scroll_delta is inverted from the way we apply the delta, so we need to negate it. let mut delta = -scroll_delta.0[d]; let mut animation = scroll_delta.1; @@ -830,11 +830,11 @@ impl Prepared { // is to avoid them leaking to other scroll areas. let scroll_target = content_ui .ctx() - .frame_state_mut(|state| state.scroll_target[d].take()); + .pass_state_mut(|state| state.scroll_target[d].take()); if scroll_enabled[d] { if let Some(target) = scroll_target { - let frame_state::ScrollTarget { + let pass_state::ScrollTarget { range, align, animation: animation_update, diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index c183a4dc731..438d562ede7 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -833,14 +833,17 @@ fn resize_interaction( } let is_dragging = |rect, id| { - let response = ctx.create_widget(WidgetRect { - layer_id, - id, - rect, - interact_rect: rect, - sense: Sense::drag(), - enabled: true, - }); + let response = ctx.create_widget( + WidgetRect { + layer_id, + id, + rect, + interact_rect: rect, + sense: Sense::drag(), + enabled: true, + }, + true, + ); SideResponse { hover: response.hovered(), drag: response.dragged(), diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 162ff4a739e..c8875dc3bf0 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -4,27 +4,32 @@ use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration use containers::area::AreaState; use epaint::{ - emath, emath::TSTransform, mutex::RwLock, pos2, stats::PaintStats, tessellator, text::Fonts, - util::OrderedFloat, vec2, ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, - Rect, TessellationOptions, TextureAtlas, TextureId, Vec2, + emath::{self, TSTransform}, + mutex::RwLock, + pos2, + stats::PaintStats, + tessellator, + text::Fonts, + util::OrderedFloat, + vec2, ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, + TessellationOptions, TextureAtlas, TextureId, Vec2, }; use crate::{ animation_manager::AnimationManager, containers, data::output::PlatformOutput, - epaint, - frame_state::FrameState, - hit_test, + epaint, hit_test, input_state::{InputState, MultiTouchInfo, PointerEvent}, interaction, layers::GraphicLayers, load, load::{Bytes, Loaders, SizedTexture}, - memory::Options, + memory::{Options, Theme}, menu, os::OperatingSystem, output::FullOutput, + pass_state::PassState, resize, scroll_area, util::IdTypeMap, viewport::ViewportClass, @@ -51,11 +56,11 @@ pub struct RequestRepaintInfo { /// Repaint after this duration. If zero, repaint as soon as possible. pub delay: Duration, - /// The current frame number. + /// The number of fully completed passes, of the entire lifetime of the [`Context`]. /// - /// This can be compared to [`Context::frame_nr`] to see if we've already - /// triggered the painting of the next frame. - pub current_frame_nr: u64, + /// This can be compared to [`Context::cumulative_pass_nr`] to see if we we still + /// need another repaint (ui pass / frame), or if one has already happened. + pub current_cumulative_pass_nr: u64, } // ---------------------------------------------------------------------------- @@ -98,8 +103,8 @@ struct NamedContextCallback { /// Callbacks that users can register #[derive(Clone, Default)] struct Plugins { - pub on_begin_frame: Vec, - pub on_end_frame: Vec, + pub on_begin_pass: Vec, + pub on_end_pass: Vec, } impl Plugins { @@ -115,12 +120,12 @@ impl Plugins { } } - fn on_begin_frame(&self, ctx: &Context) { - Self::call(ctx, "on_begin_frame", &self.on_begin_frame); + fn on_begin_pass(&self, ctx: &Context) { + Self::call(ctx, "on_begin_pass", &self.on_begin_pass); } - fn on_end_frame(&self, ctx: &Context) { - Self::call(ctx, "on_end_frame", &self.on_end_frame); + fn on_end_pass(&self, ctx: &Context) { + Self::call(ctx, "on_end_pass", &self.on_end_pass); } } @@ -129,7 +134,7 @@ impl Plugins { /// Repaint-logic impl ContextImpl { /// This is where we update the repaint logic. - fn begin_frame_repaint_logic(&mut self, viewport_id: ViewportId) { + fn begin_pass_repaint_logic(&mut self, viewport_id: ViewportId) { let viewport = self.viewports.entry(viewport_id).or_default(); std::mem::swap( @@ -138,7 +143,7 @@ impl ContextImpl { ); viewport.repaint.causes.clear(); - viewport.repaint.prev_frame_paint_delay = viewport.repaint.repaint_delay; + viewport.repaint.prev_pass_paint_delay = viewport.repaint.repaint_delay; if viewport.repaint.outstanding == 0 { // We are repainting now, so we can wait a while for the next repaint. @@ -150,7 +155,7 @@ impl ContextImpl { (callback)(RequestRepaintInfo { viewport_id, delay: Duration::ZERO, - current_frame_nr: viewport.repaint.frame_nr, + current_cumulative_pass_nr: viewport.repaint.cumulative_pass_nr, }); } } @@ -196,17 +201,17 @@ impl ContextImpl { (callback)(RequestRepaintInfo { viewport_id, delay, - current_frame_nr: viewport.repaint.frame_nr, + current_cumulative_pass_nr: viewport.repaint.cumulative_pass_nr, }); } } } #[must_use] - fn requested_immediate_repaint_prev_frame(&self, viewport_id: &ViewportId) -> bool { - self.viewports.get(viewport_id).map_or(false, |v| { - v.repaint.requested_immediate_repaint_prev_frame() - }) + fn requested_immediate_repaint_prev_pass(&self, viewport_id: &ViewportId) -> bool { + self.viewports + .get(viewport_id) + .map_or(false, |v| v.repaint.requested_immediate_repaint_prev_pass()) } #[must_use] @@ -241,53 +246,66 @@ pub struct ViewportState { pub input: InputState, - /// State that is collected during a frame and then cleared. - pub this_frame: FrameState, + /// State that is collected during a pass and then cleared. + pub this_pass: PassState, - /// The final [`FrameState`] from last frame. + /// The final [`PassState`] from last pass. /// /// Only read from. - pub prev_frame: FrameState, + pub prev_pass: PassState, - /// Has this viewport been updated this frame? + /// Has this viewport been updated this pass? pub used: bool, /// State related to repaint scheduling. repaint: ViewportRepaintInfo, // ---------------------- - // Updated at the start of the frame: + // Updated at the start of the pass: // /// Which widgets are under the pointer? pub hits: WidgetHits, - /// What widgets are being interacted with this frame? + /// What widgets are being interacted with this pass? /// - /// Based on the widgets from last frame, and input in this frame. + /// Based on the widgets from last pass, and input in this pass. pub interact_widgets: InteractionSnapshot, // ---------------------- - // The output of a frame: + // The output of a pass: // pub graphics: GraphicLayers, // Most of the things in `PlatformOutput` are not actually viewport dependent. pub output: PlatformOutput, pub commands: Vec, + + // ---------------------- + // Cross-frame statistics: + pub num_multipass_in_row: usize, } -/// What called [`Context::request_repaint`]? -#[derive(Clone)] +/// What called [`Context::request_repaint`] or [`Context::request_discard`]? +#[derive(Clone, PartialEq, Eq, Hash)] pub struct RepaintCause { /// What file had the call that requested the repaint? pub file: &'static str, /// What line number of the call that requested the repaint? pub line: u32, + + /// Explicit reason; human readable. + pub reason: Cow<'static, str>, } impl std::fmt::Debug for RepaintCause { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}:{}", self.file, self.line) + write!(f, "{}:{} {}", self.file, self.line, self.reason) + } +} + +impl std::fmt::Display for RepaintCause { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{} {}", self.file, self.line, self.reason) } } @@ -300,50 +318,58 @@ impl RepaintCause { Self { file: caller.file(), line: caller.line(), + reason: "".into(), } } -} -impl std::fmt::Display for RepaintCause { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}:{}", self.file, self.line) + /// Capture the file and line number of the call site, + /// as well as add a reason. + #[allow(clippy::new_without_default)] + #[track_caller] + pub fn new_reason(reason: impl Into>) -> Self { + let caller = Location::caller(); + Self { + file: caller.file(), + line: caller.line(), + reason: reason.into(), + } } } /// Per-viewport state related to repaint scheduling. struct ViewportRepaintInfo { /// Monotonically increasing counter. - frame_nr: u64, + cumulative_pass_nr: u64, /// The duration which the backend will poll for new events /// before forcing another egui update, even if there's no new events. /// - /// Also used to suppress multiple calls to the repaint callback during the same frame. + /// Also used to suppress multiple calls to the repaint callback during the same pass. /// /// This is also returned in [`crate::ViewportOutput`]. repaint_delay: Duration, - /// While positive, keep requesting repaints. Decrement at the start of each frame. + /// While positive, keep requesting repaints. Decrement at the start of each pass. outstanding: u8, - /// What caused repaints during this frame? + /// What caused repaints during this pass? causes: Vec, - /// What triggered a repaint the previous frame? + /// What triggered a repaint the previous pass? /// (i.e: why are we updating now?) prev_causes: Vec, - /// What was the output of `repaint_delay` on the previous frame? + /// What was the output of `repaint_delay` on the previous pass? /// /// If this was zero, we are repainting as quickly as possible /// (as far as we know). - prev_frame_paint_delay: Duration, + prev_pass_paint_delay: Duration, } impl Default for ViewportRepaintInfo { fn default() -> Self { Self { - frame_nr: 0, + cumulative_pass_nr: 0, // We haven't scheduled a repaint yet. repaint_delay: Duration::MAX, @@ -354,14 +380,14 @@ impl Default for ViewportRepaintInfo { causes: Default::default(), prev_causes: Default::default(), - prev_frame_paint_delay: Duration::MAX, + prev_pass_paint_delay: Duration::MAX, } } } impl ViewportRepaintInfo { - pub fn requested_immediate_repaint_prev_frame(&self) -> bool { - self.prev_frame_paint_delay == Duration::ZERO + pub fn requested_immediate_repaint_prev_pass(&self) -> bool { + self.prev_pass_paint_delay == Duration::ZERO } } @@ -390,7 +416,7 @@ struct ContextImpl { /// See . tex_manager: WrappedTextureManager, - /// Set during the frame, becomes active at the start of the next frame. + /// Set during the pass, becomes active at the start of the next pass. new_zoom_factor: Option, os: OperatingSystem, @@ -417,7 +443,7 @@ struct ContextImpl { } impl ContextImpl { - fn begin_frame_mut(&mut self, mut new_raw_input: RawInput) { + fn begin_pass(&mut self, mut new_raw_input: RawInput) { let viewport_id = new_raw_input.viewport_id; let parent_id = new_raw_input .viewports @@ -429,7 +455,7 @@ impl ContextImpl { let is_outermost_viewport = self.viewport_stack.is_empty(); // not necessarily root, just outermost immediate viewport self.viewport_stack.push(ids); - self.begin_frame_repaint_logic(viewport_id); + self.begin_pass_repaint_logic(viewport_id); let viewport = self.viewports.entry(viewport_id).or_default(); @@ -458,23 +484,23 @@ impl ContextImpl { let viewport = self.viewports.entry(self.viewport_id()).or_default(); - self.memory.begin_frame(&new_raw_input, &all_viewport_ids); + self.memory.begin_pass(&new_raw_input, &all_viewport_ids); - viewport.input = std::mem::take(&mut viewport.input).begin_frame( + viewport.input = std::mem::take(&mut viewport.input).begin_pass( new_raw_input, - viewport.repaint.requested_immediate_repaint_prev_frame(), + viewport.repaint.requested_immediate_repaint_prev_pass(), pixels_per_point, &self.memory.options, ); let screen_rect = viewport.input.screen_rect; - viewport.this_frame.begin_frame(screen_rect); + viewport.this_pass.begin_pass(screen_rect); { let area_order = self.memory.areas().order_map(); - let mut layers: Vec = viewport.prev_frame.widgets.layer_ids().collect(); + let mut layers: Vec = viewport.prev_pass.widgets.layer_ids().collect(); layers.sort_by(|a, b| { if a.order == b.order { @@ -487,10 +513,10 @@ impl ContextImpl { }); viewport.hits = if let Some(pos) = viewport.input.pointer.interact_pos() { - let interact_radius = self.memory.options.style.interaction.interact_radius; + let interact_radius = self.memory.options.style().interaction.interact_radius; crate::hit_test::hit_test( - &viewport.prev_frame.widgets, + &viewport.prev_pass.widgets, &layers, &self.memory.layer_transforms, pos, @@ -502,7 +528,7 @@ impl ContextImpl { viewport.interact_widgets = crate::interaction::interact( &viewport.interact_widgets, - &viewport.prev_frame.widgets, + &viewport.prev_pass.widgets, &viewport.hits, &viewport.input, self.memory.interaction_mut(), @@ -524,14 +550,14 @@ impl ContextImpl { #[cfg(feature = "accesskit")] if self.is_accesskit_enabled { crate::profile_scope!("accesskit"); - use crate::frame_state::AccessKitFrameState; + use crate::pass_state::AccessKitPassState; let id = crate::accesskit_root_id(); let mut builder = accesskit::NodeBuilder::new(accesskit::Role::Window); let pixels_per_point = viewport.input.pixels_per_point(); builder.set_transform(accesskit::Affine::scale(pixels_per_point.into())); let mut node_builders = IdMap::default(); node_builders.insert(id, builder); - viewport.this_frame.accesskit_state = Some(AccessKitFrameState { + viewport.this_pass.accesskit_state = Some(AccessKitPassState { node_builders, parent_stack: vec![id], }); @@ -553,7 +579,7 @@ impl ContextImpl { self.fonts.clear(); self.font_definitions = font_definitions; #[cfg(feature = "log")] - log::debug!("Loading new font definitions"); + log::trace!("Loading new font definitions"); } let mut is_new = false; @@ -575,15 +601,15 @@ impl ContextImpl { }); { - crate::profile_scope!("Fonts::begin_frame"); - fonts.begin_frame(pixels_per_point, max_texture_side); + crate::profile_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"); // 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() { + for font_id in self.memory.options.style().text_styles.values() { fonts.lock().fonts.font(font_id).preload_common_characters(); } } @@ -591,7 +617,7 @@ impl ContextImpl { #[cfg(feature = "accesskit")] fn accesskit_node_builder(&mut self, id: Id) -> &mut accesskit::NodeBuilder { - let state = self.viewport().this_frame.accesskit_state.as_mut().unwrap(); + let state = self.viewport().this_pass.accesskit_state.as_mut().unwrap(); let builders = &mut state.node_builders; if let std::collections::hash_map::Entry::Vacant(entry) = builders.entry(id) { entry.insert(Default::default()); @@ -737,14 +763,15 @@ impl Context { writer(&mut self.0.write()) } - /// Run the ui code for one frame. + /// Run the ui code for one 1. /// - /// Put your widgets into a [`crate::SidePanel`], [`crate::TopBottomPanel`], [`crate::CentralPanel`], [`crate::Window`] or [`crate::Area`]. + /// At most [`Options::max_passes`] calls will be issued to `run_ui`, + /// and only on the rare occasion that [`Context::request_discard`] is called. + /// Usually, it `run_ui` will only be called once. /// - /// This will modify the internal reference to point to a new generation of [`Context`]. - /// Any old clones of this [`Context`] will refer to the old [`Context`], which will not get new input. + /// Put your widgets into a [`crate::SidePanel`], [`crate::TopBottomPanel`], [`crate::CentralPanel`], [`crate::Window`] or [`crate::Area`]. /// - /// You can alternatively run [`Self::begin_frame`] and [`Context::end_frame`]. + /// Instead of calling `run`, you can alternatively use [`Self::begin_pass`] and [`Context::end_pass`]. /// /// ``` /// // One egui context that you keep reusing: @@ -760,38 +787,93 @@ impl Context { /// // handle full_output /// ``` #[must_use] - pub fn run(&self, new_input: RawInput, run_ui: impl FnOnce(&Self)) -> FullOutput { + pub fn run(&self, mut new_input: RawInput, mut run_ui: impl FnMut(&Self)) -> FullOutput { crate::profile_function!(); - self.begin_frame(new_input); - run_ui(self); - self.end_frame() + let viewport_id = new_input.viewport_id; + let max_passes = self.write(|ctx| ctx.memory.options.max_passes.get()); + + let mut output = FullOutput::default(); + debug_assert_eq!(output.platform_output.num_completed_passes, 0); + + loop { + crate::profile_scope!( + "pass", + output.platform_output.num_completed_passes.to_string() + ); + + // We must move the `num_passes` (back) to the viewport output so that [`Self::will_discard`] + // has access to the latest pass count. + self.write(|ctx| { + let viewport = ctx.viewport_for(viewport_id); + viewport.output.num_completed_passes = + std::mem::take(&mut output.platform_output.num_completed_passes); + output.platform_output.request_discard_reasons.clear(); + }); + + self.begin_pass(new_input.take()); + run_ui(self); + output.append(self.end_pass()); + debug_assert!(0 < output.platform_output.num_completed_passes); + + if !output.platform_output.requested_discard() { + break; // no need for another pass + } + + if max_passes <= output.platform_output.num_completed_passes { + #[cfg(feature = "log")] + log::debug!("Ignoring call request_discard, because max_passes={max_passes}. Requested from {:?}", output.platform_output.request_discard_reasons); + + break; + } + } + + self.write(|ctx| { + let did_multipass = 1 < output.platform_output.num_completed_passes; + let viewport = ctx.viewport_for(viewport_id); + if did_multipass { + viewport.num_multipass_in_row += 1; + } else { + viewport.num_multipass_in_row = 0; + } + }); + + output } /// An alternative to calling [`Self::run`]. /// + /// It is usually better to use [`Self::run`], because + /// `run` supports multi-pass layout using [`Self::request_discard`]. + /// /// ``` /// // One egui context that you keep reusing: /// let mut ctx = egui::Context::default(); /// /// // Each frame: /// let input = egui::RawInput::default(); - /// ctx.begin_frame(input); + /// ctx.begin_pass(input); /// /// egui::CentralPanel::default().show(&ctx, |ui| { /// ui.label("Hello egui!"); /// }); /// - /// let full_output = ctx.end_frame(); + /// let full_output = ctx.end_pass(); /// // handle full_output /// ``` - pub fn begin_frame(&self, new_input: RawInput) { + pub fn begin_pass(&self, new_input: RawInput) { crate::profile_function!(); - self.write(|ctx| ctx.begin_frame_mut(new_input)); + self.write(|ctx| ctx.begin_pass(new_input)); + + // Plugins run just after the pass starts: + self.read(|ctx| ctx.plugins.clone()).on_begin_pass(self); + } - // Plugins run just after the frame has started: - self.read(|ctx| ctx.plugins.clone()).on_begin_frame(self); + /// See [`Self::begin_pass`]. + #[deprecated = "Renamed begin_pass"] + pub fn begin_frame(&self, new_input: RawInput) { + self.begin_pass(new_input); } } @@ -874,7 +956,7 @@ impl Context { /// Read-only access to [`PlatformOutput`]. /// - /// This is what egui outputs each frame. + /// This is what egui outputs each pass and frame. /// /// ``` /// # let mut ctx = egui::Context::default(); @@ -891,28 +973,28 @@ impl Context { self.write(move |ctx| writer(&mut ctx.viewport().output)) } - /// Read-only access to [`FrameState`]. + /// Read-only access to [`PassState`]. /// - /// This is only valid between [`Context::begin_frame`] and [`Context::end_frame`]. + /// This is only valid during the call to [`Self::run`] (between [`Self::begin_pass`] and [`Self::end_pass`]). #[inline] - pub(crate) fn frame_state(&self, reader: impl FnOnce(&FrameState) -> R) -> R { - self.write(move |ctx| reader(&ctx.viewport().this_frame)) + pub(crate) fn pass_state(&self, reader: impl FnOnce(&PassState) -> R) -> R { + self.write(move |ctx| reader(&ctx.viewport().this_pass)) } - /// Read-write access to [`FrameState`]. + /// Read-write access to [`PassState`]. /// - /// This is only valid between [`Context::begin_frame`] and [`Context::end_frame`]. + /// This is only valid during the call to [`Self::run`] (between [`Self::begin_pass`] and [`Self::end_pass`]). #[inline] - pub(crate) fn frame_state_mut(&self, writer: impl FnOnce(&mut FrameState) -> R) -> R { - self.write(move |ctx| writer(&mut ctx.viewport().this_frame)) + pub(crate) fn pass_state_mut(&self, writer: impl FnOnce(&mut PassState) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.viewport().this_pass)) } - /// Read-only access to the [`FrameState`] from the previous frame. + /// Read-only access to the [`PassState`] from the previous pass. /// - /// This is swapped at the end of each frame. + /// This is swapped at the end of each pass. #[inline] - pub(crate) fn prev_frame_state(&self, reader: impl FnOnce(&FrameState) -> R) -> R { - self.write(move |ctx| reader(&ctx.viewport().prev_frame)) + pub(crate) fn prev_pass_state(&self, reader: impl FnOnce(&PassState) -> R) -> R { + self.write(move |ctx| reader(&ctx.viewport().prev_pass)) } /// Read-only access to [`Fonts`]. @@ -958,7 +1040,7 @@ impl Context { self.write(move |ctx| writer(&mut ctx.memory.options.tessellation_options)) } - /// If the given [`Id`] has been used previously the same frame at different position, + /// If the given [`Id`] has been used previously the same pass at different position, /// then an error will be printed on screen. /// /// This function is already called for all widgets that do any interaction, @@ -968,7 +1050,7 @@ impl Context { /// The most important thing is that [`Rect::min`] is approximately correct, /// because that's where the warning will be painted. If you don't know what size to pick, just pick [`Vec2::ZERO`]. pub fn check_for_id_clash(&self, id: Id, new_rect: Rect, what: &str) { - let prev_rect = self.frame_state_mut(move |state| state.used_ids.insert(id, new_rect)); + let prev_rect = self.pass_state_mut(move |state| state.used_ids.insert(id, new_rect)); if !self.options(|opt| opt.warn_on_id_clash) { return; @@ -976,7 +1058,7 @@ impl Context { let Some(prev_rect) = prev_rect else { return }; - // it is ok to reuse the same ID for e.g. a frame around a widget, + // It is ok to reuse the same ID for e.g. a frame around a widget, // or to check for interaction with the same widget twice: let is_same_rect = prev_rect.expand(0.1).contains_rect(new_rect) || new_rect.expand(0.1).contains_rect(prev_rect); @@ -1049,8 +1131,11 @@ impl Context { /// You should use [`Ui::interact`] instead. /// /// If the widget already exists, its state (sense, Rect, etc) will be updated. + /// + /// `allow_focus` should usually be true, unless you call this function multiple times with the + /// same widget, then `allow_focus` should only be true once (like in [`Ui::new`] (true) and [`Ui::remember_min_rect`] (false)). #[allow(clippy::too_many_arguments)] - pub(crate) fn create_widget(&self, w: WidgetRect) -> Response { + pub(crate) fn create_widget(&self, w: WidgetRect, allow_focus: bool) -> Response { // Remember this widget self.write(|ctx| { let viewport = ctx.viewport(); @@ -1058,14 +1143,14 @@ impl Context { // We add all widgets here, even non-interactive ones, // because we need this list not only for checking for blocking widgets, // but also to know when we have reached the widget we are checking for cover. - viewport.this_frame.widgets.insert(w.layer_id, w); + viewport.this_pass.widgets.insert(w.layer_id, w); - if w.sense.focusable { + if allow_focus && w.sense.focusable { ctx.memory.interested_in_focus(w.id); } }); - if !w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction() { + if allow_focus && (!w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction()) { // Not interested or allowed input: self.memory_mut(|mem| mem.surrender_focus(w.id)); } @@ -1078,7 +1163,7 @@ impl Context { let res = self.get_response(w); #[cfg(feature = "accesskit")] - if w.sense.focusable { + if allow_focus && w.sense.focusable { // Make sure anything that can receive focus has an AccessKit node. // TODO(mwcampbell): For nodes that are filled from widget info, // some information is written to the node twice. @@ -1090,17 +1175,17 @@ impl Context { /// Read the response of some widget, which may be called _before_ creating the widget (!). /// - /// This is because widget interaction happens at the start of the frame, using the previous frame's widgets. + /// This is because widget interaction happens at the start of the pass, using the widget rects from the previous pass. /// - /// If the widget was not visible the previous frame (or this frame), this will return `None`. + /// If the widget was not visible the previous pass (or this pass), this will return `None`. pub fn read_response(&self, id: Id) -> Option { self.write(|ctx| { let viewport = ctx.viewport(); viewport - .this_frame + .this_pass .widgets .get(id) - .or_else(|| viewport.prev_frame.widgets.get(id)) + .or_else(|| viewport.prev_pass.widgets.get(id)) .copied() }) .map(|widget_rect| self.get_response(widget_rect)) @@ -1114,7 +1199,7 @@ impl Context { } /// Do all interaction for an existing widget, without (re-)registering it. - fn get_response(&self, widget_rect: WidgetRect) -> Response { + pub(crate) fn get_response(&self, widget_rect: WidgetRect) -> Response { let WidgetRect { id, layer_id, @@ -1124,8 +1209,8 @@ impl Context { enabled, } = widget_rect; - // previous frame + "highlight next frame" == "highlight this frame" - let highlighted = self.prev_frame_state(|fs| fs.highlight_next_frame.contains(&id)); + // previous pass + "highlight next pass" == "highlight this pass" + let highlighted = self.prev_pass_state(|fs| fs.highlight_next_pass.contains(&id)); let mut res = Response { ctx: self.clone(), @@ -1147,6 +1232,7 @@ impl Context { is_pointer_button_down_on: false, interact_pointer_pos: None, changed: false, + intrinsic_size: None, }; self.write(|ctx| { @@ -1245,8 +1331,8 @@ impl Context { pub fn register_widget_info(&self, id: Id, make_info: impl Fn() -> crate::WidgetInfo) { #[cfg(debug_assertions)] self.write(|ctx| { - if ctx.memory.options.style.debug.show_interactive_widgets { - ctx.viewport().this_frame.widgets.set_info(id, make_info()); + if ctx.memory.options.style().debug.show_interactive_widgets { + ctx.viewport().this_pass.widgets.set_info(id, make_info()); } }); @@ -1267,7 +1353,7 @@ impl Context { Self::layer_painter(self, LayerId::debug()) } - /// Print this text next to the cursor at the end of the frame. + /// Print this text next to the cursor at the end of the pass. /// /// If you call this multiple times, the text will be appended. /// @@ -1375,22 +1461,22 @@ impl Context { } } - /// The current frame number for the current viewport. - /// - /// Starts at zero, and is incremented at the end of [`Self::run`] or by [`Self::end_frame`]. + /// The total number of completed passes (usually there is one pass per rendered frame). /// - /// Between calls to [`Self::run`], this is the frame number of the coming frame. - pub fn frame_nr(&self) -> u64 { - self.frame_nr_for(self.viewport_id()) + /// Starts at zero, and is incremented for each completed pass inside of [`Self::run`] (usually once). + pub fn cumulative_pass_nr(&self) -> u64 { + self.cumulative_pass_nr_for(self.viewport_id()) } - /// The current frame number. + /// The total number of completed passes (usually there is one pass per rendered frame). /// - /// Starts at zero, and is incremented at the end of [`Self::run`] or by [`Self::end_frame`]. - /// - /// Between calls to [`Self::run`], this is the frame number of the coming frame. - pub fn frame_nr_for(&self, id: ViewportId) -> u64 { - self.read(|ctx| ctx.viewports.get(&id).map_or(0, |v| v.repaint.frame_nr)) + /// Starts at zero, and is incremented for each completed pass inside of [`Self::run`] (usually once). + pub fn cumulative_pass_nr_for(&self, id: ViewportId) -> u64 { + self.read(|ctx| { + ctx.viewports + .get(&id) + .map_or(0, |v| v.repaint.cumulative_pass_nr) + }) } /// Call this if there is need to repaint the UI, i.e. if you are showing an animation. @@ -1455,7 +1541,7 @@ impl Context { /// So, it's not that we are requesting repaint within X duration. We are rather timing out /// during app idle time where we are not receiving any new input events. /// - /// This repaints the current viewport + /// This repaints the current viewport. #[track_caller] pub fn request_repaint_after(&self, duration: Duration) { self.request_repaint_after_for(duration, self.viewport_id()); @@ -1498,23 +1584,23 @@ impl Context { /// So, it's not that we are requesting repaint within X duration. We are rather timing out /// during app idle time where we are not receiving any new input events. /// - /// This repaints the specified viewport + /// This repaints the specified viewport. #[track_caller] pub fn request_repaint_after_for(&self, duration: Duration, id: ViewportId) { let cause = RepaintCause::new(); self.write(|ctx| ctx.request_repaint_after(duration, id, cause)); } - /// Was a repaint requested last frame for the current viewport? + /// Was a repaint requested last pass for the current viewport? #[must_use] - pub fn requested_repaint_last_frame(&self) -> bool { - self.requested_repaint_last_frame_for(&self.viewport_id()) + pub fn requested_repaint_last_pass(&self) -> bool { + self.requested_repaint_last_pass_for(&self.viewport_id()) } - /// Was a repaint requested last frame for the given viewport? + /// Was a repaint requested last pass for the given viewport? #[must_use] - pub fn requested_repaint_last_frame_for(&self, viewport_id: &ViewportId) -> bool { - self.read(|ctx| ctx.requested_immediate_repaint_prev_frame(viewport_id)) + pub fn requested_repaint_last_pass_for(&self, viewport_id: &ViewportId) -> bool { + self.read(|ctx| ctx.requested_immediate_repaint_prev_pass(viewport_id)) } /// Has a repaint been requested for the current viewport? @@ -1553,34 +1639,84 @@ impl Context { let callback = Box::new(callback); self.write(|ctx| ctx.request_repaint_callback = Some(callback)); } + + /// Request to discard the visual output of this pass, + /// and to immediately do another one. + /// + /// This can be called to cover up visual glitches during a "sizing pass". + /// For instance, when a [`crate::Grid`] is first shown we don't yet know the + /// width and heights of its columns and rows. egui will do a best guess, + /// but it will likely be wrong. Next pass it can read the sizes from the previous + /// pass, and from there on the widths will be stable. + /// This means the first pass will look glitchy, and ideally should not be shown to the user. + /// So [`crate::Grid`] calls [`Self::request_discard`] to cover up this glitches. + /// + /// There is a limit to how many passes egui will perform, set by [`Options::max_passes`]. + /// Therefore, the request might be declined. + /// + /// You can check if the current pass will be discarded with [`Self::will_discard`]. + /// + /// You should be very conservative with when you call [`Self::request_discard`], + /// as it will cause an extra ui pass, potentially leading to extra CPU use and frame judder. + /// + /// The given reason should be a human-readable string that explains why `request_discard` + /// was called. This will be shown in certain debug situations, to help you figure out + /// why a pass was discarded. + #[track_caller] + pub fn request_discard(&self, reason: impl Into>) { + let cause = RepaintCause::new_reason(reason); + self.output_mut(|o| o.request_discard_reasons.push(cause)); + + #[cfg(feature = "log")] + log::trace!( + "request_discard: {}", + if self.will_discard() { + "allowed" + } else { + "denied" + } + ); + } + + /// Will the visual output of this pass be discarded? + /// + /// If true, you can early-out from expensive graphics operations. + /// + /// See [`Self::request_discard`] for more. + pub fn will_discard(&self) -> bool { + self.write(|ctx| { + let vp = ctx.viewport(); + // NOTE: `num_passes` is incremented + vp.output.requested_discard() + && vp.output.num_completed_passes + 1 < ctx.memory.options.max_passes.get() + }) + } } /// Callbacks impl Context { - /// Call the given callback at the start of each frame - /// of each viewport. + /// Call the given callback at the start of each pass of each viewport. /// /// This can be used for egui _plugins_. /// See [`crate::debug_text`] for an example. - pub fn on_begin_frame(&self, debug_name: &'static str, cb: ContextCallback) { + pub fn on_begin_pass(&self, debug_name: &'static str, cb: ContextCallback) { let named_cb = NamedContextCallback { debug_name, callback: cb, }; - self.write(|ctx| ctx.plugins.on_begin_frame.push(named_cb)); + self.write(|ctx| ctx.plugins.on_begin_pass.push(named_cb)); } - /// Call the given callback at the end of each frame - /// of each viewport. + /// Call the given callback at the end of each pass of each viewport. /// /// This can be used for egui _plugins_. /// See [`crate::debug_text`] for an example. - pub fn on_end_frame(&self, debug_name: &'static str, cb: ContextCallback) { + pub fn on_end_pass(&self, debug_name: &'static str, cb: ContextCallback) { let named_cb = NamedContextCallback { debug_name, callback: cb, }; - self.write(|ctx| ctx.plugins.on_end_frame.push(named_cb)); + self.write(|ctx| ctx.plugins.on_end_pass.push(named_cb)); } } @@ -1590,7 +1726,7 @@ impl Context { /// The default `egui` fonts only support latin and cyrillic alphabets, /// but you can call this to install additional fonts that support e.g. korean characters. /// - /// The new fonts will become active at the start of the next frame. + /// The new fonts will become active at the start of the next pass. pub fn set_fonts(&self, font_definitions: FontDefinitions) { crate::profile_function!(); @@ -1612,12 +1748,37 @@ impl Context { } } - /// The [`Style`] used by all subsequent windows, panels etc. + /// Does the OS use dark or light mode? + /// This is used when the theme preference is set to [`crate::ThemePreference::System`]. + pub fn system_theme(&self) -> Option { + self.memory(|mem| mem.options.system_theme) + } + + /// The [`Theme`] used to select the appropriate [`Style`] (dark or light) + /// used by all subsequent windows, panels etc. + pub fn theme(&self) -> Theme { + self.options(|opt| opt.theme()) + } + + /// The [`Theme`] used to select between dark and light [`Self::style`] + /// as the active style used by all subsequent windows, panels etc. + /// + /// Example: + /// ``` + /// # let mut ctx = egui::Context::default(); + /// ctx.set_theme(egui::Theme::Light); // Switch to light mode + /// ``` + pub fn set_theme(&self, theme_preference: impl Into) { + self.options_mut(|opt| opt.theme_preference = theme_preference.into()); + } + + /// The currently active [`Style`] used by all subsequent windows, panels etc. pub fn style(&self) -> Arc