diff --git a/.gitattributes b/.gitattributes index b1f5e1192e4..b5348bf23b4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,8 @@ * text=auto eol=lf Cargo.lock linguist-generated=false +*.png filter=lfs diff=lfs merge=lfs -text + +# Exclude some small files from LFS: +crates/eframe/data/* !filter !diff !merge text=auto eol=lf +crates/egui_demo_lib/data/* !filter !diff !merge text=auto eol=lf +crates/egui/assets/* !filter !diff !merge text=auto eol=lf diff --git a/.github/workflows/cargo_machete.yml b/.github/workflows/cargo_machete.yml index dab6725553c..2b06dfdef36 100644 --- a/.github/workflows/cargo_machete.yml +++ b/.github/workflows/cargo_machete.yml @@ -9,4 +9,4 @@ jobs: - name: Checkout uses: actions/checkout@v3 - name: Machete - uses: bnjbvr/cargo-machete@main + run: cargo install cargo-machete --locked && cargo machete diff --git a/.github/workflows/deploy_web_demo.yml b/.github/workflows/deploy_web_demo.yml index 717266d6362..6eb54696139 100644 --- a/.github/workflows/deploy_web_demo.yml +++ b/.github/workflows/deploy_web_demo.yml @@ -39,7 +39,7 @@ jobs: with: profile: minimal target: wasm32-unknown-unknown - toolchain: 1.76.0 + toolchain: 1.80.0 override: true - uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 7a9a94bcc73..b8304066709 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -29,4 +29,4 @@ jobs: with: mode: minimum count: 1 - labels: "CI, dependencies, docs and examples, ecolor, eframe, egui_extras, egui_glow, egui-wgpu, egui-winit, egui, epaint, epaint_default_fonts, exclude from changelog, typo" + labels: "CI, dependencies, docs and examples, ecolor, eframe, egui_extras, egui_glow, egui_kittest, egui-wgpu, egui-winit, egui, epaint, epaint_default_fonts, exclude from changelog, typo" diff --git a/.github/workflows/png_only_on_lfs.yml b/.github/workflows/png_only_on_lfs.yml new file mode 100644 index 00000000000..624a7f4502d --- /dev/null +++ b/.github/workflows/png_only_on_lfs.yml @@ -0,0 +1,31 @@ +name: All; .png on git LFS + +on: [push, pull_request] + +jobs: + check-binary-files: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Check that png files are on git LFS + run: | + binary_extensions="png" + exclude_paths=( + "crates/eframe/data" + "crates/egui_demo_lib/data/" + "crates/egui/assets/" + ) + + # Find binary files that are not tracked by Git LFS + for ext in $binary_extensions; do + # Create grep pattern to exclude multiple paths + exclude_pattern=$(printf "|^%s" "${exclude_paths[@]}" | sed 's/^|//') + + if comm -23 <(git ls-files | grep -Ev "$exclude_pattern" | sort) <(git lfs ls-files -n | sort) | grep "\.${ext}$"; then + echo "Error: Found binary file with extension .$ext not tracked by git LFS. See CONTRIBUTING.md" + exit 1 + fi + done diff --git a/.github/workflows/preview_build.yml b/.github/workflows/preview_build.yml index 70cd5ce2aae..c44c10f0c83 100644 --- a/.github/workflows/preview_build.yml +++ b/.github/workflows/preview_build.yml @@ -42,9 +42,11 @@ jobs: - name: Generate meta.json env: PR_NUMBER: ${{ github.event.number }} - PR_BRANCH: ${{ github.head_ref }} + URL_SLUG: ${{ github.event.number }}-${{ github.head_ref }} run: | - echo "{\"pr_number\": \"$PR_NUMBER\", \"pr_branch\": \"$PR_BRANCH\"}" > meta.json + # Sanitize the URL_SLUG to only contain alphanumeric characters and dashes + URL_SLUG=$(echo $URL_SLUG | tr -cd '[:alnum:]-') + echo "{\"pr_number\": \"$PR_NUMBER\", \"url_slug\": \"$URL_SLUG\"}" > meta.json - uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/preview_cleanup.yml b/.github/workflows/preview_cleanup.yml index 3aba668a2b3..6e2b94b83aa 100644 --- a/.github/workflows/preview_cleanup.yml +++ b/.github/workflows/preview_cleanup.yml @@ -15,9 +15,14 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - run: mkdir -p empty_dir - - name: Url slug variable + - name: Generate URL_SLUG + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + URL_SLUG: ${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.ref }} run: | - echo "URL_SLUG=${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.ref }}" >> $GITHUB_ENV + # Sanitize the URL_SLUG to only contain alphanumeric characters and dashes + URL_SLUG=$(echo $URL_SLUG | tr -cd '[:alnum:]-') + echo "URL_SLUG=$URL_SLUG" >> $GITHUB_ENV - name: Deploy uses: JamesIves/github-pages-deploy-action@v4 with: diff --git a/.github/workflows/preview_deploy.yml b/.github/workflows/preview_deploy.yml index 9fdcfaf755f..f98f96da162 100644 --- a/.github/workflows/preview_deploy.yml +++ b/.github/workflows/preview_deploy.yml @@ -40,11 +40,7 @@ jobs: - name: Parse meta.json run: | echo "PR_NUMBER=$(jq -r .pr_number meta.json)" >> $GITHUB_ENV - echo "PR_BRANCH=$(jq -r .pr_branch meta.json)" >> $GITHUB_ENV - - - name: Url slug variable - run: | - echo "URL_SLUG=${{ env.PR_NUMBER }}-${{ env.PR_BRANCH }}" >> $GITHUB_ENV + echo "URL_SLUG=$(jq -r .url_slug meta.json)" >> $GITHUB_ENV - name: Deploy uses: JamesIves/github-pages-deploy-action@v4 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 1e8e31bbcf1..e2a5896980e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -13,10 +13,12 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 + with: + lfs: true - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.76.0 + toolchain: 1.80.0 - name: Install packages (Linux) if: runner.os == 'Linux' @@ -60,18 +62,12 @@ jobs: - name: cargo check -p test_egui_extras_compilation run: cargo check -p test_egui_extras_compilation - - name: Test doc-tests - run: cargo test --doc --all-features - - name: cargo doc --lib run: cargo doc --lib --no-deps --all-features - name: cargo doc --document-private-items run: cargo doc --document-private-items --no-deps --all-features - - name: Test - run: cargo test --all-features - - name: clippy run: cargo clippy --all-targets --all-features -- -D warnings @@ -87,7 +83,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.76.0 + toolchain: 1.80.0 targets: wasm32-unknown-unknown - run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev @@ -107,7 +103,7 @@ jobs: - name: wasm-bindgen uses: jetli/wasm-bindgen-action@v0.1.0 with: - version: "0.2.93" + version: "0.2.95" - run: ./scripts/wasm_bindgen_check.sh --skip-setup @@ -159,7 +155,7 @@ jobs: - uses: actions/checkout@v4 - uses: EmbarkStudios/cargo-deny-action@v1 with: - rust-version: "1.76.0" + rust-version: "1.80.0" log-level: error command: check arguments: --target ${{ matrix.target }} @@ -174,7 +170,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.76.0 + toolchain: 1.80.0 targets: aarch64-linux-android - name: Set up cargo cache @@ -193,7 +189,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.76.0 + toolchain: 1.80.0 targets: aarch64-apple-ios - name: Set up cargo cache @@ -212,7 +208,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.76.0 + toolchain: 1.80.0 - name: Set up cargo cache uses: Swatinem/rust-cache@v2 @@ -222,3 +218,36 @@ jobs: - name: Check hello_world run: cargo check -p hello_world + + # --------------------------------------------------------------------------- + + tests: + name: Run tests + # We run the tests on macOS because it will run with a actual GPU + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + with: + lfs: true + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.80.0 + + - name: Set up cargo cache + uses: Swatinem/rust-cache@v2 + + - name: Run tests + # TODO(lucasmerlin): Enable --all-features (currently this breaks the rendering in the tests because of the `unity` feature) + run: cargo test + + - name: Run doc-tests + # TODO(lucasmerlin): Enable --all-features (currently this breaks the rendering in the tests because of the `unity` feature) + run: cargo test --doc + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results + path: "**/tests/snapshots" diff --git a/.gitignore b/.gitignore index 7db0b9d06fb..887de9da3de 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ **/target **/target_ra **/target_wasm +**/tests/snapshots/**/*.diff.png +**/tests/snapshots/**/*.new.png /.*.json /.vscode /media/* diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index cf2c2d60a31..000f2b7ac9e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -50,6 +50,9 @@ This contains a bunch of uses of `egui` and looks like the ui code you would wri Thin wrapper around `egui_demo_lib` so we can compile it to a web site or a native app executable. Depends on `egui_demo_lib` + `eframe`. +### `egui_kittest` +A test harness for egui based on [kittest](https://github.com/rerun/kittest) and [AccessKit](https://github.com/AccessKit/accesskit/). + ### Other integrations There are also many great integrations for game engines such as `bevy` and `miniquad` which you can find at . diff --git a/CHANGELOG.md b/CHANGELOG.md index 69437e03cc1..4642da637f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,113 @@ # egui changelog All notable changes to the `egui` crate will be documented in this file. -NOTE: this is just the changelog for the core `egui` crate. [`eframe`](crates/eframe/CHANGELOG.md), [`ecolor`](crates/ecolor/CHANGELOG.md), [`epaint`](crates/epaint/CHANGELOG.md), [`egui-winit`](crates/egui-winit/CHANGELOG.md), [`egui_glow`](crates/egui_glow/CHANGELOG.md) and [`egui-wgpu`](crates/egui-wgpu/CHANGELOG.md) have their own changelogs! +This is just the changelog for the core `egui` crate. Every crate in this repository has their own changelog: +* [`epaint` changelog](crates/epaint/CHANGELOG.md) +* [`egui-winit` changelog](crates/egui-winit/CHANGELOG.md) +* [`egui-wgpu` changelog](crates/egui-wgpu/CHANGELOG.md) +* [`egui_kittest` changelog](crates/egui_kittest/CHANGELOG.md) +* [`egui_glow` changelog](crates/egui_glow/CHANGELOG.md) +* [`ecolor` changelog](crates/ecolor/CHANGELOG.md) +* [`eframe` changelog](crates/eframe/CHANGELOG.md) This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.30.0 - 2024-12-16 - Modals and better layer support + +### ✨ Highlights +* Add `Modal`, a popup that blocks input to the rest of the application ([#5358](https://github.com/emilk/egui/pull/5358) by [@lucasmerlin](https://github.com/lucasmerlin)) +* Improved support for transform layers ([#5465](https://github.com/emilk/egui/pull/5465), [#5468](https://github.com/emilk/egui/pull/5468), [#5429](https://github.com/emilk/egui/pull/5429)) + +#### `egui_kittest` +This release welcomes a new crate to the family: [egui_kittest](https://github.com/emilk/egui/tree/master/crates/egui_kittest). +`egui_kittest` is a testing framework for egui, allowing you to test both automation (simulated clicks and other events), +and also do screenshot testing (useful for regression tests). +`egui_kittest` is built using [`kittest`](https://github.com/rerun-io/kittest), which is a general GUI testing framework that aims to work with any Rust GUI (not just egui!). +`kittest` uses the accessibility library [`AccessKit`](https://github.com/AccessKit/accesskit/) for automatation and to query the widget tree. + +`kittest` and `egui_kittest` are written by [@lucasmerlin](https://github.com/lucasmerlin). + +Here's a quick example of how to use `egui_kittest` to test a checkbox: + +```rust +use egui::accesskit::Toggled; +use egui_kittest::{Harness, kittest::Queryable}; + +fn main() { + let mut checked = false; + let app = |ui: &mut egui::Ui| { + ui.checkbox(&mut checked, "Check me!"); + }; + + let mut harness = egui_kittest::Harness::new_ui(app); + + let checkbox = harness.get_by_label("Check me!"); + assert_eq!(checkbox.toggled(), Some(Toggled::False)); + checkbox.click(); + + harness.run(); + + let checkbox = harness.get_by_label("Check me!"); + assert_eq!(checkbox.toggled(), Some(Toggled::True)); + + // You can even render the ui and do image snapshot tests + #[cfg(all(feature = "wgpu", feature = "snapshot"))] + harness.wgpu_snapshot("readme_example"); +} +``` + +### ⭐ Added +* Add `Modal` and `Memory::set_modal_layer` [#5358](https://github.com/emilk/egui/pull/5358) by [@lucasmerlin](https://github.com/lucasmerlin) +* Add `UiBuilder::layer_id` and remove `layer_id` from `Ui::new` [#5195](https://github.com/emilk/egui/pull/5195) by [@emilk](https://github.com/emilk) +* Allow easier setting of background color for `TextEdit` [#5203](https://github.com/emilk/egui/pull/5203) by [@bircni](https://github.com/bircni) +* Set `Response::intrinsic_size` for `TextEdit` [#5266](https://github.com/emilk/egui/pull/5266) by [@lucasmerlin](https://github.com/lucasmerlin) +* Expose center position in `MultiTouchInfo` [#5247](https://github.com/emilk/egui/pull/5247) by [@lucasmerlin](https://github.com/lucasmerlin) +* `Context::add_font` [#5228](https://github.com/emilk/egui/pull/5228) by [@frederik-uni](https://github.com/frederik-uni) +* Impl from `Box` for `WidgetText`, `RichText` [#5309](https://github.com/emilk/egui/pull/5309) by [@dimtpap](https://github.com/dimtpap) +* Add `Window::scroll_bar_visibility` [#5231](https://github.com/emilk/egui/pull/5231) by [@Zeenobit](https://github.com/Zeenobit) +* Add `ComboBox::close_behavior` [#5305](https://github.com/emilk/egui/pull/5305) by [@avalsch](https://github.com/avalsch) +* Add `painter.line()` [#5291](https://github.com/emilk/egui/pull/5291) by [@bircni](https://github.com/bircni) +* Allow attaching custom user data to a screenshot command [#5416](https://github.com/emilk/egui/pull/5416) by [@emilk](https://github.com/emilk) +* Add `Button::image_tint_follows_text_color` [#5430](https://github.com/emilk/egui/pull/5430) by [@emilk](https://github.com/emilk) +* Consume escape keystroke when bailing out from a drag operation [#5433](https://github.com/emilk/egui/pull/5433) by [@abey79](https://github.com/abey79) +* Add `Context::layer_transform_to_global` & `layer_transform_from_global` [#5465](https://github.com/emilk/egui/pull/5465) by [@emilk](https://github.com/emilk) + +### 🔧 Changed +* Update MSRV to Rust 1.80 [#5421](https://github.com/emilk/egui/pull/5421), [#5457](https://github.com/emilk/egui/pull/5457) by [@emilk](https://github.com/emilk) +* Expand max font atlas size from 8k to 16k [#5257](https://github.com/emilk/egui/pull/5257) by [@rustbasic](https://github.com/rustbasic) +* Put font data into `Arc` to reduce memory consumption [#5276](https://github.com/emilk/egui/pull/5276) by [@StarStarJ](https://github.com/StarStarJ) +* Move `egui::util::cache` to `egui::cache`; add `FramePublisher` [#5426](https://github.com/emilk/egui/pull/5426) by [@emilk](https://github.com/emilk) +* Remove `Order::PanelResizeLine` [#5455](https://github.com/emilk/egui/pull/5455) by [@emilk](https://github.com/emilk) +* Drag-and-drop: keep cursor set by user, if any [#5467](https://github.com/emilk/egui/pull/5467) by [@abey79](https://github.com/abey79) +* Use `profiling` crate to support more profiler backends [#5150](https://github.com/emilk/egui/pull/5150) by [@teddemunnik](https://github.com/teddemunnik) +* Improve hit-test of thin widgets, and widgets across layers [#5468](https://github.com/emilk/egui/pull/5468) by [@emilk](https://github.com/emilk) + +### 🐛 Fixed +* Update `ScrollArea` drag velocity when drag stopped [#5175](https://github.com/emilk/egui/pull/5175) by [@valadaptive](https://github.com/valadaptive) +* Fix bug causing wrong-fire of `ViewportCommand::Visible` [#5244](https://github.com/emilk/egui/pull/5244) by [@rustbasic](https://github.com/rustbasic) +* Fix: `Ui::new_child` does not consider the `sizing_pass` field of `UiBuilder` [#5262](https://github.com/emilk/egui/pull/5262) by [@zhatuokun](https://github.com/zhatuokun) +* Fix Ctrl+Shift+Z redo shortcut [#5258](https://github.com/emilk/egui/pull/5258) by [@YgorSouza](https://github.com/YgorSouza) +* Fix: `Window::default_pos` does not work [#5315](https://github.com/emilk/egui/pull/5315) by [@rustbasic](https://github.com/rustbasic) +* Fix: `Sides` did not apply the layout position correctly [#5303](https://github.com/emilk/egui/pull/5303) by [@zhatuokun](https://github.com/zhatuokun) +* Respect `Style::override_font_id` in `RichText` [#5310](https://github.com/emilk/egui/pull/5310) by [@MStarha](https://github.com/MStarha) +* Fix disabled widgets "eating" focus [#5370](https://github.com/emilk/egui/pull/5370) by [@lucasmerlin](https://github.com/lucasmerlin) +* Fix cursor clipping in `TextEdit` inside a `ScrollArea` [#3660](https://github.com/emilk/egui/pull/3660) by [@juancampa](https://github.com/juancampa) +* Make text cursor always appear on click [#5420](https://github.com/emilk/egui/pull/5420) by [@juancampa](https://github.com/juancampa) +* Fix `on_hover_text_at_pointer` for transformed layers [#5429](https://github.com/emilk/egui/pull/5429) by [@emilk](https://github.com/emilk) +* Fix: don't interact with `Area` outside its `constrain_rect` [#5459](https://github.com/emilk/egui/pull/5459) by [@MScottMcBee](https://github.com/MScottMcBee) +* Fix broken images on egui.rs (move from git lfs to normal git) [#5480](https://github.com/emilk/egui/pull/5480) by [@emilk](https://github.com/emilk) +* Fix: `ui.new_child` should now respect `disabled` [#5483](https://github.com/emilk/egui/pull/5483) by [@emilk](https://github.com/emilk) +* Fix zero-width strokes still affecting the feathering color of boxes [#5485](https://github.com/emilk/egui/pull/5485) by [@emilk](https://github.com/emilk) + + +## 0.29.1 - 2024-10-01 - Bug fixes +* Remove debug-assert triggered by `with_layer_id/dnd_drag_source` [#5191](https://github.com/emilk/egui/pull/5191) by [@emilk](https://github.com/emilk) +* Fix id clash in `Ui::response` [#5192](https://github.com/emilk/egui/pull/5192) by [@emilk](https://github.com/emilk) +* Do not round panel rectangles to pixel grid [#5196](https://github.com/emilk/egui/pull/5196) by [@emilk](https://github.com/emilk) + + ## 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). @@ -855,7 +956,7 @@ egui_extras::install_image_loaders(egui_ctx); * Added `Slider::step_by` ([1225](https://github.com/emilk/egui/pull/1225)). * Added `Context::move_to_top` and `Context::top_most_layer` for managing the layer on the top ([#1242](https://github.com/emilk/egui/pull/1242)). * Support a subset of macOS' emacs input field keybindings in `TextEdit` ([#1243](https://github.com/emilk/egui/pull/1243)). -* Added ability to scroll an UI into view without specifying an alignment ([1247](https://github.com/emilk/egui/pull/1247)). +* Added ability to scroll a UI into view without specifying an alignment ([1247](https://github.com/emilk/egui/pull/1247)). * Added `Ui::scroll_to_rect` ([1252](https://github.com/emilk/egui/pull/1252)). ### 🔧 Changed diff --git a/CODEOWNERS b/CODEOWNERS index 40b72a3140a..8b71b22c10e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1,2 @@ +/crates/egui_kittest @lucasmerlin /crates/egui-wgpu @Wumpf diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index be55d133c22..6777c9a156b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,6 +32,16 @@ For small things, just go ahead an open a PR. For bigger things, please file an Browse through [`ARCHITECTURE.md`](ARCHITECTURE.md) to get a sense of how all pieces connects. You can test your code locally by running `./scripts/check.sh`. +There are snapshots test that might need to be updated. Run the tests with `UPDATE_SNAPSHOTS=true` to update them. +For more info about the tests see [egui_kittest](./crates/egui_kittest/README.md). + +We use [git-lfs](https://git-lfs.com/) to store big files in the repository. +Make sure you have it installed (running `git lfs ls-files` from the repository root should list some files). +Don't forget to run `git lfs install` after installing the git-lfs binary. +You need to add any .png images to `git lfs`. +If the CI complains about this, make sure you run `git add --renormalize .`. + +If you see an `InvalidSignature` error when running snapshot tests, it's probably a problem related to git-lfs. When you have something that works, open a draft PR. You may get some helpful feedback early! When you feel the PR is ready to go, do a self-review of the code, and then open it for review. diff --git a/Cargo.lock b/Cargo.lock index 5fdfab95d28..5073cd8b64b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "ab_glyph" -version = "0.2.21" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5110f1c78cf582855d895ecd0746b653db010cec6d9f5575293f27934d980a39" +checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0" dependencies = [ "ab_glyph_rasterizer", "owned_ttf_parser", @@ -20,9 +20,9 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" [[package]] name = "accesskit" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4700bdc115b306d6c43381c344dc307f03b7f0460c304e4892c309930322bd7" +checksum = "45c97bb3cc1dacbdc6d1147040fc61309590d3e1ab5efd92a8a09c7a2e07284c" dependencies = [ "enumn", "serde", @@ -30,23 +30,23 @@ dependencies = [ [[package]] name = "accesskit_atspi_common" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1de72dc7093910a1284cef784b6b143bab0a34d67f6178e4fc3aaaf29a09f8b" +checksum = "03db49d2948db6875c69a1ef17816efa8e3d9f36c7cd79e467d8562a6695662b" dependencies = [ "accesskit", "accesskit_consumer", "atspi-common", "serde", "thiserror", - "zvariant", + "zvariant 4.2.0", ] [[package]] name = "accesskit_consumer" -version = "0.24.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe3a07a32ab5837ad83db3230ac490c8504c2cd5b90ac8c00db6535f6ed65d0b" +checksum = "fa3a17950ce0d911f132387777b9b3d05eddafb59b773ccaa53fceefaeb0228e" dependencies = [ "accesskit", "immutable-chunkmap", @@ -54,9 +54,9 @@ dependencies = [ [[package]] name = "accesskit_macos" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a189d159c153ae0fce5f9eefdcfec4a27885f453ce5ef0ccf078f72a73c39d34" +checksum = "e8d94b7544775dddce398e2500a8b3cc2be3655190879071ce6a9e5610195be4" dependencies = [ "accesskit", "accesskit_consumer", @@ -68,9 +68,9 @@ dependencies = [ [[package]] name = "accesskit_unix" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b76c448cfd96d16131a9ad3ab786d06951eb341cdac1db908978ab010245a19d" +checksum = "3a88d913b144104dd825f75db1b82c63d754b01c53c2f9b7545dcdfae63bb0ed" dependencies = [ "accesskit", "accesskit_atspi_common", @@ -78,30 +78,31 @@ dependencies = [ "async-executor", "async-task", "atspi", - "futures-lite 1.13.0", + "futures-lite", "futures-util", "serde", - "zbus", + "zbus 4.4.0", ] [[package]] name = "accesskit_windows" -version = "0.22.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "682d8c4fb425606f97408e7577793f32e96310b646fa77662eb4216293eddc7f" +checksum = "0aaa870a5d047338f03707706141f22c98c20e79d5403bf3c9b195549e6cdeea" dependencies = [ "accesskit", "accesskit_consumer", "paste", "static_assertions", - "windows 0.54.0", + "windows", + "windows-core 0.58.0", ] [[package]] name = "accesskit_winit" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afbd6d598b7c035639ad2b664aa0edc94c93dc1fc3ebb4b40d8a95fcd43ffac" +checksum = "3555a67a9bb208f620cfc3746f1502d1512f0ffbdb19c6901aa90b111aa56ec5" dependencies = [ "accesskit", "accesskit_macos", @@ -113,18 +114,18 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.21.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "ahash" @@ -142,18 +143,18 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "android-activity" @@ -188,6 +189,23 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +[[package]] +name = "android_log-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937" + +[[package]] +name = "android_logger" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -205,21 +223,21 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" [[package]] name = "arboard" -version = "3.4.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb4009533e8ff8f1450a5bcbc30f4242a1d34442221f72314bea1f5dc9c7f89" +checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" dependencies = [ "clipboard-win", "log", @@ -232,21 +250,21 @@ dependencies = [ [[package]] name = "arrayref" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "as-raw-xcb-connection" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5f312b0a56c5cdf967c0aeb67f6289603354951683bc97ddc595ab974ba9aa" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" [[package]] name = "ash" @@ -257,14 +275,38 @@ dependencies = [ "libloading", ] +[[package]] +name = "ashpd" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c39d707614dbcc6bed00015539f488d8e3fe3e66ed60961efc0c90f4b380b3" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand", + "raw-window-handle 0.6.2", + "serde", + "serde_repr", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus 5.1.1", +] + [[package]] name = "async-broadcast" -version = "0.5.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" +checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" dependencies = [ - "event-listener 2.5.3", + "event-listener", + "event-listener-strategy", "futures-core", + "pin-project-lite", ] [[package]] @@ -281,133 +323,132 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.5.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f2db9467baa66a700abce2a18c5ad793f6f83310aca1284796fc3921d113fd" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" dependencies = [ - "async-lock", "async-task", "concurrent-queue", - "fastrand 2.0.1", - "futures-lite 1.13.0", + "fastrand", + "futures-lite", "slab", ] [[package]] name = "async-fs" -version = "1.6.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" dependencies = [ "async-lock", - "autocfg", "blocking", - "futures-lite 1.13.0", + "futures-lite", ] [[package]] name = "async-io" -version = "1.13.0" +version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" dependencies = [ "async-lock", - "autocfg", "cfg-if", "concurrent-queue", - "futures-lite 1.13.0", - "log", + "futures-io", + "futures-lite", "parking", - "polling 2.8.0", - "rustix 0.37.25", + "polling", + "rustix", "slab", - "socket2", - "waker-fn", + "tracing", + "windows-sys 0.59.0", ] [[package]] name = "async-lock" -version = "2.8.0" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" dependencies = [ - "event-listener 2.5.3", + "async-io", + "blocking", + "futures-lite", ] [[package]] name = "async-process" -version = "1.8.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf012553ce51eb7aa6dc2143804cc8252bd1cb681a1c5cb7fa94ca88682dee1d" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" dependencies = [ + "async-channel", "async-io", "async-lock", "async-signal", + "async-task", "blocking", "cfg-if", - "event-listener 3.0.0", - "futures-lite 1.13.0", - "rustix 0.38.21", - "windows-sys 0.48.0", + "event-listener", + "futures-lite", + "rustix", + "tracing", ] [[package]] name = "async-recursion" -version = "1.0.5" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] name = "async-signal" -version = "0.2.1" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4af361a844928cb7d36590d406709473a1b574f443094422ef166daa3b493208" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" dependencies = [ "async-io", "async-lock", "atomic-waker", "cfg-if", - "concurrent-queue", "futures-core", "futures-io", - "libc", + "rustix", "signal-hook-registry", "slab", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] name = "async-task" -version = "4.4.1" +version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9441c6b2fe128a7c2bf680a44c34d0df31ce09e5b7e401fcca3faa483dbc921" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.73" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", -] - -[[package]] -name = "atk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", + "syn", ] [[package]] @@ -418,9 +459,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "atspi" -version = "0.19.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6059f350ab6f593ea00727b334265c4dfc7fd442ee32d264794bd9bdc68e87ca" +checksum = "be534b16650e35237bb1ed189ba2aab86ce65e88cc84c66f4935ba38575cecbf" dependencies = [ "atspi-common", "atspi-connection", @@ -429,67 +470,76 @@ dependencies = [ [[package]] name = "atspi-common" -version = "0.3.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92af95f966d2431f962bc632c2e68eda7777330158bf640c4af4249349b2cdf5" +checksum = "1909ed2dc01d0a17505d89311d192518507e8a056a48148e3598fef5e7bb6ba7" dependencies = [ "enumflags2", "serde", "static_assertions", - "zbus", - "zbus_names", - "zvariant", + "zbus 4.4.0", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names 3.0.0", + "zvariant 4.2.0", ] [[package]] name = "atspi-connection" -version = "0.3.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c65e7d70f86d4c0e3b2d585d9bf3f979f0b19d635a336725a88d279f76b939" +checksum = "430c5960624a4baaa511c9c0fcc2218e3b58f5dbcc47e6190cafee344b873333" dependencies = [ "atspi-common", "atspi-proxies", - "futures-lite 1.13.0", - "zbus", + "futures-lite", + "zbus 4.4.0", ] [[package]] name = "atspi-proxies" -version = "0.3.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6495661273703e7a229356dcbe8c8f38223d697aacfaf0e13590a9ac9977bb52" +checksum = "a5e6c5de3e524cf967569722446bcd458d5032348554d9a17d7d72b041ab7496" dependencies = [ "atspi-common", "serde", - "zbus", + "zbus 4.4.0", + "zvariant 4.2.0", ] [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] name = "base64" -version = "0.21.4" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bincode" @@ -511,11 +561,11 @@ dependencies = [ [[package]] name = "bit-set" -version = "0.6.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec 0.7.0", + "bit-vec 0.8.0", ] [[package]] @@ -526,9 +576,9 @@ checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bit-vec" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" @@ -578,57 +628,53 @@ dependencies = [ "async-channel", "async-task", "futures-io", - "futures-lite 2.3.0", + "futures-lite", "piper", ] [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.14.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.5.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" +checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] -name = "bytes" -version = "1.5.0" +name = "byteorder-lite" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] -name = "cairo-sys-rs" -version = "0.18.2" +name = "bytes" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "libc", - "system-deps", -] +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "calloop" @@ -638,8 +684,8 @@ checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ "bitflags 2.6.0", "log", - "polling 3.3.0", - "rustix 0.38.21", + "polling", + "rustix", "slab", "thiserror", ] @@ -651,7 +697,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ "calloop", - "rustix 0.38.21", + "rustix", "wayland-backend", "wayland-client", ] @@ -664,12 +710,13 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.83" +version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" dependencies = [ "jobserver", "libc", + "shlex", ] [[package]] @@ -678,16 +725,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cfg-expr" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -717,23 +754,23 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] name = "ciborium" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", @@ -742,15 +779,15 @@ dependencies = [ [[package]] name = "ciborium-io" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", @@ -764,18 +801,18 @@ checksum = "7a0e87cdf78571d9fbeff16861c37a006cd718d2433dc6d5b80beaae367d899a" [[package]] name = "clap" -version = "4.4.11" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.4.11" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ "anstyle", "clap_lex", @@ -783,15 +820,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.6.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "clipboard-win" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" dependencies = [ "error-code", ] @@ -819,41 +856,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] -name = "com" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e17887fd17353b65b1b2ef1c526c83e26cd72e74f598a8dc1bee13a48f3d9f6" -dependencies = [ - "com_macros", -] - -[[package]] -name = "com_macros" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d375883580a668c7481ea6631fc1a8863e33cc335bf56bfad8d7e6d4b04b13a5" -dependencies = [ - "com_macros_support", - "proc-macro2", - "syn 1.0.109", -] - -[[package]] -name = "com_macros_support" -version = "0.6.0" +name = "colored" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "lazy_static", + "windows-sys 0.48.0", ] [[package]] name = "combine" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "memchr", @@ -888,15 +904,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -918,18 +934,18 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -970,11 +986,10 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.8" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ - "cfg-if", "crossbeam-utils", ] @@ -999,9 +1014,15 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crunchy" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-common" @@ -1079,22 +1100,24 @@ checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" [[package]] name = "deranged" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", ] [[package]] -name = "derivative" -version = "2.2.0" +name = "dify" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +checksum = "11217d469eafa3b809ad84651eb9797ccbb440b4a916d5d85cb1b994e89787f6" dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "anyhow", + "colored", + "getopts", + "image", + "rayon", ] [[package]] @@ -1134,6 +1157,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dlib" version = "0.5.2" @@ -1154,9 +1188,9 @@ dependencies = [ [[package]] name = "downcast-rs" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "dpi" @@ -1166,7 +1200,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" [[package]] name = "ecolor" -version = "0.29.0" +version = "0.30.0" dependencies = [ "bytemuck", "cint", @@ -1178,7 +1212,7 @@ dependencies = [ [[package]] name = "eframe" -version = "0.29.0" +version = "0.30.0" dependencies = [ "ahash", "bytemuck", @@ -1188,7 +1222,7 @@ dependencies = [ "egui-wgpu", "egui-winit", "egui_glow", - "glow 0.14.0", + "glow 0.16.0", "glutin", "glutin-winit", "home", @@ -1200,8 +1234,8 @@ dependencies = [ "objc2-foundation", "parking_lot", "percent-encoding", - "pollster", - "puffin", + "pollster 0.4.0", + "profiling", "raw-window-handle 0.6.2", "ron", "serde", @@ -1212,13 +1246,13 @@ dependencies = [ "web-time", "wgpu", "winapi", - "windows-sys 0.52.0", + "windows-sys 0.59.0", "winit", ] [[package]] name = "egui" -version = "0.29.0" +version = "0.30.0" dependencies = [ "accesskit", "ahash", @@ -1228,14 +1262,14 @@ dependencies = [ "epaint", "log", "nohash-hasher", - "puffin", + "profiling", "ron", "serde", ] [[package]] name = "egui-wgpu" -version = "0.29.0" +version = "0.30.0" dependencies = [ "ahash", "bytemuck", @@ -1243,7 +1277,7 @@ dependencies = [ "egui", "epaint", "log", - "puffin", + "profiling", "thiserror", "type-map", "web-time", @@ -1253,7 +1287,7 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.29.0" +version = "0.30.0" dependencies = [ "accesskit_winit", "ahash", @@ -1261,7 +1295,7 @@ dependencies = [ "document-features", "egui", "log", - "puffin", + "profiling", "raw-window-handle 0.6.2", "serde", "smithay-clipboard", @@ -1273,7 +1307,7 @@ dependencies = [ [[package]] name = "egui_demo_app" -version = "0.29.0" +version = "0.30.0" dependencies = [ "bytemuck", "chrono", @@ -1286,6 +1320,7 @@ dependencies = [ "image", "log", "poll-promise", + "profiling", "puffin", "puffin_http", "rfd", @@ -1298,20 +1333,22 @@ dependencies = [ [[package]] name = "egui_demo_lib" -version = "0.29.0" +version = "0.30.0" dependencies = [ "chrono", "criterion", "document-features", "egui", "egui_extras", + "egui_kittest", "serde", "unicode_names2", + "wgpu", ] [[package]] name = "egui_extras" -version = "0.29.0" +version = "0.30.0" dependencies = [ "ahash", "chrono", @@ -1322,7 +1359,7 @@ dependencies = [ "image", "log", "mime_guess2", - "puffin", + "profiling", "resvg", "serde", "syntect", @@ -1330,24 +1367,38 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.29.0" +version = "0.30.0" dependencies = [ "ahash", "bytemuck", "document-features", "egui", "egui-winit", - "glow 0.14.0", + "glow 0.16.0", "glutin", "glutin-winit", "log", - "memoffset 0.9.0", - "puffin", + "memoffset", + "profiling", "wasm-bindgen", "web-sys", "winit", ] +[[package]] +name = "egui_kittest" +version = "0.30.0" +dependencies = [ + "dify", + "document-features", + "egui", + "egui-wgpu", + "image", + "kittest", + "pollster 0.4.0", + "wgpu", +] + [[package]] name = "ehttp" version = "0.5.0" @@ -1364,13 +1415,13 @@ dependencies = [ [[package]] name = "either" -version = "1.9.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "emath" -version = "0.29.0" +version = "0.30.0" dependencies = [ "bytemuck", "document-features", @@ -1378,11 +1429,17 @@ dependencies = [ "serde", ] +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + [[package]] name = "enum-map" -version = "2.6.3" +version = "2.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c188012f8542dee7b3996e44dd89461d64aa471b0a7c71a1ae2f595d259e96e5" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" dependencies = [ "enum-map-derive", "serde", @@ -1390,20 +1447,20 @@ dependencies = [ [[package]] name = "enum-map-derive" -version = "0.14.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04d0b288e3bb1d861c4403c1774a6f7a798781dfc519b3647df2a3dd4ae95f25" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] name = "enumflags2" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5998b4f30320c9d93aed72f63af821bfdac50465b75428fce77b48ec482c3939" +checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" dependencies = [ "enumflags2_derive", "serde", @@ -1411,31 +1468,41 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f95e2801cd355d4a1a3e3953ce6ee5ae9603a5c833455343a8bfe3f44d418246" +checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] name = "enumn" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ad8cef1d801a4686bfd8919f0b30eac4c8e48968c437a6405ded4fb5272d2b" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", +] + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", ] [[package]] name = "env_logger" -version = "0.10.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" dependencies = [ "humantime", "is-terminal", @@ -1445,7 +1512,7 @@ dependencies = [ [[package]] name = "epaint" -version = "0.29.0" +version = "0.30.0" dependencies = [ "ab_glyph", "ahash", @@ -1459,14 +1526,14 @@ dependencies = [ "log", "nohash-hasher", "parking_lot", - "puffin", + "profiling", "rayon", "serde", ] [[package]] name = "epaint_default_fonts" -version = "0.29.0" +version = "0.30.0" [[package]] name = "equivalent" @@ -1476,42 +1543,25 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ - "cc", "libc", + "windows-sys 0.52.0", ] [[package]] name = "error-code" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "281e452d3bad4005426416cdba5ccfd4f5c1280e10099e21db27f7c1c28347fc" - -[[package]] -name = "event-listener" -version = "2.5.3" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "event-listener" -version = "3.0.0" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29e56284f00d94c1bc7fd3c77027b4623c88c1f53d8d2394c6199f2921dea325" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" dependencies = [ "concurrent-queue", "parking", @@ -1519,23 +1569,12 @@ dependencies = [ ] [[package]] -name = "event-listener" -version = "5.3.1" +name = "event-listener-strategy" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" -dependencies = [ - "event-listener 5.3.1", + "event-listener", "pin-project-lite", ] @@ -1551,24 +1590,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "fastrand" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "fdeflate" -version = "0.3.0" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +checksum = "07c6f4c64c1d33a3111c4466f7365ebdcc37c5bd1ea0d62aae2e3d722aacbedb" dependencies = [ "simd-adler32", ] @@ -1584,9 +1614,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.27" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", "miniz_oxide", @@ -1622,7 +1652,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] @@ -1633,78 +1663,75 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "1.13.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +checksum = "3f1fa2f9765705486b33fd2acf1577f8ec449c2ba1f318ae5447697b7c08d210" dependencies = [ - "fastrand 1.9.0", + "fastrand", "futures-core", "futures-io", - "memchr", "parking", "pin-project-lite", - "waker-fn", -] - -[[package]] -name = "futures-lite" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" -dependencies = [ - "futures-core", - "pin-project-lite", ] [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-io", @@ -1717,36 +1744,6 @@ dependencies = [ "slab", ] -[[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gdk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -1767,11 +1764,20 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -1790,22 +1796,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" - -[[package]] -name = "gio-sys" -version = "0.18.1" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "winapi", -] +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gl_generator" @@ -1818,21 +1811,11 @@ dependencies = [ "xml-rs", ] -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - [[package]] name = "glow" -version = "0.13.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" +checksum = "d51fa363f025f5c111e03f13eda21162faeacb6911fe8caa0c0349f9cf0c4483" dependencies = [ "js-sys", "slotmap", @@ -1842,9 +1825,9 @@ dependencies = [ [[package]] name = "glow" -version = "0.14.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f865cbd94bd355b89611211e49508da98a1fce0ad755c1e8448fb96711b24528" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" dependencies = [ "js-sys", "slotmap", @@ -1854,9 +1837,9 @@ dependencies = [ [[package]] name = "glutin" -version = "0.32.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2491aa3090f682ddd920b184491844440fdd14379c7eef8f5bc10ef7fb3242fd" +checksum = "ec69412a0bf07ea7607e638b415447857a808846c2b685a43c8aa18bc6d5e499" dependencies = [ "bitflags 2.6.0", "cfg_aliases 0.2.1", @@ -1918,17 +1901,6 @@ dependencies = [ "gl_generator", ] -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - [[package]] name = "gpu-alloc" version = "0.6.0" @@ -1948,19 +1920,6 @@ dependencies = [ "bitflags 2.6.0", ] -[[package]] -name = "gpu-allocator" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd4240fc91d3433d5e5b0fc5b67672d771850dc19bbee03c1381e19322803d7" -dependencies = [ - "log", - "presser", - "thiserror", - "winapi", - "windows 0.51.1", -] - [[package]] name = "gpu-descriptor" version = "0.3.0" @@ -1982,60 +1941,36 @@ dependencies = [ ] [[package]] -name = "gtk-sys" -version = "0.18.0" +name = "half" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", + "cfg-if", + "crunchy", ] -[[package]] -name = "half" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" - [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", ] [[package]] -name = "hassle-rs" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" +name = "hello_android" +version = "0.1.0" dependencies = [ - "bitflags 2.6.0", - "com", - "libc", - "libloading", - "thiserror", - "widestring", - "winapi", + "android_logger", + "eframe", + "egui_extras", + "log", + "winit", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "hello_world" version = "0.1.0" @@ -2064,9 +1999,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "hex" @@ -2097,16 +2032,16 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core 0.51.1", + "windows-core 0.52.0", ] [[package]] @@ -2118,24 +2053,153 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "idna" -version = "0.4.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] name = "image" -version = "0.25.0" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9b4f005360d32e9325029b38ba47ebd7a56f3316df09249368939562d518645" +checksum = "bc144d44a31d753b02ce64093d532f55ff8dc4ebf2ffb8a63c0dda691385acae" dependencies = [ "bytemuck", - "byteorder", + "byteorder-lite", "color_quant", "gif", "num-traits", @@ -2162,52 +2226,32 @@ checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" [[package]] name = "immutable-chunkmap" -version = "2.0.5" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4419f022e55cc63d5bbd6b44b71e1d226b9c9480a47824c706e9d54e5c40c5eb" +checksum = "12f97096f508d54f8f8ab8957862eee2ccd628847b6217af1a335e1c44dee578" dependencies = [ "arrayvec", ] [[package]] name = "indexmap" -version = "2.1.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown", ] -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "is-terminal" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ "hermit-abi", - "rustix 0.38.21", - "windows-sys 0.48.0", + "libc", + "windows-sys 0.52.0", ] [[package]] @@ -2221,9 +2265,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jni" @@ -2249,18 +2293,18 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.26" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -2290,6 +2334,17 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "kittest" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f659954571a3c132356bd15c25f0dcf14d270a28ec5c58797adc2f432831bed5" +dependencies = [ + "accesskit", + "accesskit_consumer", + "parking_lot", +] + [[package]] name = "kurbo" version = "0.9.5" @@ -2299,39 +2354,39 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" -version = "0.2.155" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libloading" -version = "0.8.0" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-sys 0.48.0", + "windows-targets 0.52.6", ] [[package]] name = "libredox" -version = "0.0.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.6.0", "libc", - "redox_syscall 0.4.1", + "redox_syscall 0.5.7", ] -[[package]] -name = "line-wrap" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -2340,15 +2395,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] -name = "linux-raw-sys" -version = "0.4.11" +name = "litemap" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "litrs" @@ -2358,9 +2413,9 @@ checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -2368,15 +2423,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lz4_flex" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ea9b256699eda7b0387ffbc776dd625e28bde3918446381781245b7a50349d8" +checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" [[package]] name = "malloc_buf" @@ -2389,33 +2444,24 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.3" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" -version = "0.9.0" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deaba38d7abf1d4cca21cc89e932e542ba2b9258664d2a9ef0e61512039c9375" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" dependencies = [ "libc", ] [[package]] name = "memoffset" -version = "0.7.1" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - -[[package]] -name = "memoffset" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] @@ -2451,19 +2497,13 @@ dependencies = [ "unicase", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", "simd-adler32", ] @@ -2483,12 +2523,12 @@ dependencies = [ [[package]] name = "naga" -version = "22.0.0" +version = "23.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09eeccb9b50f4f7839b214aa3e08be467159506a986c18e0702170ccf720a453" +checksum = "3d5941e45a15b53aad4375eedf02033adb7a28931eedc31117faffa52e6a857e" dependencies = [ "arrayvec", - "bit-set 0.6.0", + "bit-set 0.8.0", "bitflags 2.6.0", "cfg_aliases 0.1.1", "codespan-reporting", @@ -2543,14 +2583,15 @@ dependencies = [ [[package]] name = "nix" -version = "0.26.4" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "cfg-if", + "cfg_aliases 0.2.1", "libc", - "memoffset 0.7.1", + "memoffset", ] [[package]] @@ -2560,43 +2601,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" [[package]] -name = "nom" -version = "7.1.3" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_enum" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683751d591e6d81200c39fb0d1032608b77724f34114db54f571ff1317b337c0" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c11e44798ad209ccdd91fc192f0526a369a01234f7373e1b141c96d7cee4f0e" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] @@ -2608,17 +2645,6 @@ dependencies = [ "malloc_buf", ] -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - [[package]] name = "objc-sys" version = "0.3.5" @@ -2822,35 +2848,26 @@ dependencies = [ "objc2-foundation", ] -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - [[package]] name = "object" -version = "0.32.1" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "oorandom" -version = "11.1.3" +version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "option-ext" @@ -2860,9 +2877,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orbclient" -version = "0.3.47" +version = "0.3.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f0d54bde9774d3a51dcf281a5def240c71996bc6ca05d2c847ec8b2b216166" +checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" dependencies = [ "libredox", ] @@ -2879,36 +2896,24 @@ dependencies = [ [[package]] name = "owned_ttf_parser" -version = "0.19.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "706de7e2214113d63a8238d1910463cfce781129a6f263d13fdb09ff64355ba4" +checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" dependencies = [ "ttf-parser", ] -[[package]] -name = "pango-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - [[package]] name = "parking" -version = "2.1.1" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -2916,28 +2921,28 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.8" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", + "redox_syscall 0.5.7", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pico-args" @@ -2947,29 +2952,29 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -2979,40 +2984,39 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.1" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", - "fastrand 2.0.1", + "fastrand", "futures-io", ] [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "plist" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9" +checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ - "base64", + "base64 0.22.1", "indexmap", - "line-wrap", - "quick-xml 0.31.0", + "quick-xml 0.32.0", "serde", "time", ] [[package]] name = "png" -version = "0.17.10" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -3033,32 +3037,17 @@ dependencies = [ [[package]] name = "polling" -version = "2.8.0" +version = "3.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - -[[package]] -name = "polling" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e53b6af1f60f36f8c2ac2aad5459d75a5a9b4be1e8cdd40264f315d78193e531" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" dependencies = [ "cfg-if", "concurrent-queue", + "hermit-abi", "pin-project-lite", - "rustix 0.38.21", + "rustix", "tracing", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -3067,9 +3056,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + [[package]] name = "popups" -version = "0.29.0" +version = "0.30.0" dependencies = [ "eframe", "env_logger", @@ -3083,51 +3078,62 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "presser" -version = "0.3.1" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "proc-macro-crate" -version = "1.3.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "once_cell", "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.76" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] [[package]] name = "profiling" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0f7f43585c34e4fdd7497d746bc32e14458cf11c69341cc0587b1d825dde42" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +dependencies = [ + "profiling-procmacros", + "puffin", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +dependencies = [ + "quote", + "syn", +] [[package]] name = "puffin" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f76ad4bb049fded4e572df72cbb6381ff5d1f41f85c3a04b56e4eca287a02f" +checksum = "fa9dae7b05c02ec1a6bc9bcf20d8bc64a7dcbf57934107902a872014899b741f" dependencies = [ "anyhow", "bincode", "byteorder", "cfg-if", + "itertools", "lz4_flex", "once_cell", "parking_lot", @@ -3136,9 +3142,9 @@ dependencies = [ [[package]] name = "puffin_http" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4936c085e48efc86f6d96609dc5086d1d236afe3ec4676f09b157a4f4be83ff6" +checksum = "739a3c7f56604713b553d7addd7718c226e88d598979ae3450320800bd0e9810" dependencies = [ "anyhow", "crossbeam-channel", @@ -3154,33 +3160,44 @@ dependencies = [ "eframe", "env_logger", "log", + "profiling", "puffin", "puffin_http", ] [[package]] name = "quick-xml" -version = "0.31.0" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quick-xml" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" dependencies = [ "memchr", ] [[package]] name = "quick-xml" -version = "0.34.0" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -3229,9 +3246,9 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] name = "rayon" -version = "1.8.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -3255,47 +3272,38 @@ checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" [[package]] name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.3.5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", ] [[package]] name = "redox_users" -version = "0.4.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", - "redox_syscall 0.2.16", + "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.9.5" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -3305,9 +3313,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.8" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -3316,9 +3324,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.5" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "renderdoc-sys" @@ -3342,21 +3350,20 @@ dependencies = [ [[package]] name = "rfd" -version = "0.13.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d8ab342bcc5436e04d3a4c1e09e17d74958bfaddf8d5fad6f85607df0f994f" +checksum = "46f6f80a9b882647d9014673ca9925d30ffc9750f2eed2b4490e189eaebd01e8" dependencies = [ - "block", - "dispatch", - "glib-sys", - "gobject-sys", - "gtk-sys", + "ashpd", + "block2", "js-sys", "log", - "objc", - "objc-foundation", - "objc_id", - "raw-window-handle 0.5.2", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "pollster 0.3.0", + "raw-window-handle 0.6.2", + "urlencoding", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -3365,9 +3372,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.36" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" dependencies = [ "bytemuck", ] @@ -3393,7 +3400,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ - "base64", + "base64 0.21.7", "bitflags 2.6.0", "serde", "serde_derive", @@ -3407,9 +3414,9 @@ checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -3419,58 +3426,54 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.37.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4eb579851244c2c03e7c24f501c3432bed80b8f720af1d6e5b0e0f01555a035" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustix" -version = "0.38.21" +version = "0.38.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" dependencies = [ "bitflags 2.6.0", "errno", "libc", - "linux-raw-sys 0.4.11", - "windows-sys 0.48.0", + "linux-raw-sys", + "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.21.11" +version = "0.23.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4" +checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f" dependencies = [ "log", + "once_cell", "ring", + "rustls-pki-types", "rustls-webpki", - "sct", + "subtle", + "zeroize", ] +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring", + "rustls-pki-types", "untrusted", ] [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -3502,16 +3505,6 @@ dependencies = [ "image", ] -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sctk-adwaita" version = "0.10.1" @@ -3527,53 +3520,45 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.188" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] [[package]] name = "serde_repr" -version = "0.1.16" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", -] - -[[package]] -name = "serde_spanned" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" -dependencies = [ - "serde", + "syn", ] [[package]] @@ -3596,11 +3581,17 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -3637,18 +3628,18 @@ dependencies = [ [[package]] name = "slotmap" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" dependencies = [ "version_check", ] [[package]] name = "smallvec" -version = "1.11.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smithay-client-toolkit" @@ -3663,7 +3654,7 @@ dependencies = [ "libc", "log", "memmap2", - "rustix 0.38.21", + "rustix", "thiserror", "wayland-backend", "wayland-client", @@ -3688,23 +3679,13 @@ dependencies = [ [[package]] name = "smol_str" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74212e6bbe9a4352329b2f68ba3130c15a3f26fe88ff22dbdc6cdd58fa85e99c" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" dependencies = [ "serde", ] -[[package]] -name = "socket2" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "spin" version = "0.9.8" @@ -3720,6 +3701,12 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -3735,6 +3722,12 @@ dependencies = [ "float-cmp", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "svgtypes" version = "0.13.0" @@ -3747,9 +3740,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.109" +version = "2.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" dependencies = [ "proc-macro2", "quote", @@ -3757,21 +3750,21 @@ dependencies = [ ] [[package]] -name = "syn" -version = "2.0.48" +name = "synstructure" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn", ] [[package]] name = "syntect" -version = "5.1.0" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02b4b303bf8d08bfeb0445cba5068a3d306b6baece1d5582171a9bf49188f91" +checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" dependencies = [ "bincode", "bitflags 1.3.2", @@ -3782,42 +3775,24 @@ dependencies = [ "plist", "regex-syntax", "serde", + "serde_derive", "serde_json", "thiserror", "walkdir", "yaml-rust", ] -[[package]] -name = "system-deps" -version = "6.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.12.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" - [[package]] name = "tempfile" -version = "3.8.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" dependencies = [ "cfg-if", - "fastrand 2.0.1", - "redox_syscall 0.3.5", - "rustix 0.38.21", - "windows-sys 0.48.0", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", ] [[package]] @@ -3872,32 +3847,33 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] name = "time" -version = "0.3.30" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -3912,18 +3888,19 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ + "num-conv", "time-core", ] [[package]] name = "tiny-skia" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6a067b809476893fce6a254cf285850ff69c847e6cfbade6a20b655b6c7e80d" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" dependencies = [ "arrayref", "arrayvec", @@ -3936,9 +3913,9 @@ dependencies = [ [[package]] name = "tiny-skia-path" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de35e8a90052baaaf61f171680ac2f8e925a1e43ea9d2e3a00514772250e541" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" dependencies = [ "arrayref", "bytemuck", @@ -3946,60 +3923,38 @@ dependencies = [ ] [[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "tinyvec" -version = "1.6.0" +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ - "tinyvec_macros", + "displaydoc", + "zerovec", ] [[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "toml" -version = "0.7.8" +name = "tinytemplate" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", + "serde_json", ] [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" -dependencies = [ - "serde", -] +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" -version = "0.19.15" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", - "serde", - "serde_spanned", "toml_datetime", "winnow", ] @@ -4023,7 +3978,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] @@ -4037,9 +3992,9 @@ dependencies = [ [[package]] name = "ttf-parser" -version = "0.19.1" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a464a4b34948a5f67fddd2b823c62d9d92e44be75058b99939eae6c5b6960b33" +checksum = "5902c5d130972a0000f60860bfbf46f7ca3db5391eddfedd1b8728bd9dc96c0e" [[package]] name = "type-map" @@ -4058,61 +4013,44 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uds_windows" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ + "memoffset", "tempfile", "winapi", ] [[package]] name = "unicase" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.13" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" [[package]] name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-normalization" -version = "0.1.22" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unicode_names2" @@ -4128,31 +4066,38 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.8.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ccd538d4a604753ebc2f17cd9946e89b77bf87f6a8e2309667c6f2e87855e3" +checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" dependencies = [ - "base64", + "base64 0.22.1", "flate2", "log", "once_cell", "rustls", - "rustls-webpki", + "rustls-pki-types", "url", "webpki-roots", ] [[package]] name = "url" -version = "2.4.1" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "user_attention" version = "0.1.0" @@ -4167,7 +4112,7 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b0a51b72ab80ca511d126b77feeeb4fb1e972764653e61feac30adc161a756" dependencies = [ - "base64", + "base64 0.21.7", "log", "pico-args", "usvg-parser", @@ -4206,28 +4151,28 @@ dependencies = [ ] [[package]] -name = "version-compare" -version = "0.1.1" +name = "utf16_iter" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" [[package]] -name = "version_check" -version = "0.9.4" +name = "utf8_iter" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "waker-fn" -version = "1.1.1" +name = "version_check" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -4241,9 +4186,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", "once_cell", @@ -4252,24 +4197,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.48", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ "cfg-if", "js-sys", @@ -4279,9 +4224,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4289,32 +4234,32 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "wayland-backend" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993" +checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" dependencies = [ "cc", "downcast-rs", - "rustix 0.38.21", + "rustix", "scoped-tls", "smallvec", "wayland-sys", @@ -4322,12 +4267,12 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.5" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943" +checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" dependencies = [ "bitflags 2.6.0", - "rustix 0.38.21", + "rustix", "wayland-backend", "wayland-scanner", ] @@ -4345,20 +4290,20 @@ dependencies = [ [[package]] name = "wayland-cursor" -version = "0.31.5" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef9489a8df197ebf3a8ce8a7a7f0a2320035c3743f3c1bd0bdbccf07ce64f95" +checksum = "32b08bc3aafdb0035e7fe0fdf17ba0c09c268732707dca4ae098f60cb28c9e4c" dependencies = [ - "rustix 0.38.21", + "rustix", "wayland-client", "xcursor", ] [[package]] name = "wayland-protocols" -version = "0.32.3" +version = "0.32.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa" +checksum = "7cd0ade57c4e6e9a8952741325c30bf82f4246885dca8bf561898b86d0c1f58e" dependencies = [ "bitflags 2.6.0", "wayland-backend", @@ -4368,9 +4313,9 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79f2d57c7fcc6ab4d602adba364bf59a5c24de57bd194486bf9b8360e06bfc4" +checksum = "9b31cab548ee68c7eb155517f2212049dc151f7cd7910c2b66abfd31c3ee12bd" dependencies = [ "bitflags 2.6.0", "wayland-backend", @@ -4381,9 +4326,9 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953" +checksum = "782e12f6cd923c3c316130d56205ebab53f55d6666b7faddfad36cecaeeb4022" dependencies = [ "bitflags 2.6.0", "wayland-backend", @@ -4394,20 +4339,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.4" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" +checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" dependencies = [ "proc-macro2", - "quick-xml 0.34.0", + "quick-xml 0.36.2", "quote", ] [[package]] name = "wayland-sys" -version = "0.31.4" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148" +checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" dependencies = [ "dlib", "log", @@ -4417,9 +4362,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ "js-sys", "wasm-bindgen", @@ -4454,9 +4399,12 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.2" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +dependencies = [ + "rustls-pki-types", +] [[package]] name = "weezl" @@ -4466,9 +4414,9 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "wgpu" -version = "22.1.0" +version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d1c4ba43f80542cf63a0a6ed3134629ae73e8ab51e4b765a67f3aa062eb433" +checksum = "80f70000db37c469ea9d67defdc13024ddf9a5f1b89cb2941b812ad7cde1735a" dependencies = [ "arrayvec", "cfg_aliases 0.1.1", @@ -4491,12 +4439,12 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "22.1.0" +version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348c840d1051b8e86c3bcd31206080c5e71e5933dabd79be1ce732b0b2f089a" +checksum = "d63c3c478de8e7e01786479919c8769f62a22eec16788d8c2ac77ce2c132778a" dependencies = [ "arrayvec", - "bit-vec 0.7.0", + "bit-vec 0.8.0", "bitflags 2.6.0", "cfg_aliases 0.1.1", "document-features", @@ -4516,23 +4464,22 @@ dependencies = [ [[package]] name = "wgpu-hal" -version = "22.0.0" +version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6bbf4b4de8b2a83c0401d9e5ae0080a2792055f25859a02bf9be97952bbed4f" +checksum = "89364b8a0b211adc7b16aeaf1bd5ad4a919c1154b44c9ce27838213ba05fd821" dependencies = [ "android_system_properties", "arrayvec", "ash", "bitflags 2.6.0", "block", + "bytemuck", "cfg_aliases 0.1.1", "core-graphics-types", - "glow 0.13.1", + "glow 0.14.2", "glutin_wgl_sys", "gpu-alloc", - "gpu-allocator", "gpu-descriptor", - "hassle-rs", "js-sys", "khronos-egl", "libc", @@ -4553,26 +4500,20 @@ dependencies = [ "wasm-bindgen", "web-sys", "wgpu-types", - "winapi", + "windows", ] [[package]] name = "wgpu-types" -version = "22.0.0" +version = "23.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9d91f0e2c4b51434dfa6db77846f2793149d8e73f800fa2e41f52b8eac3c5d" +checksum = "610f6ff27778148c31093f3b03abc4840f9636d58d597ca2f5977433acfe0068" dependencies = [ "bitflags 2.6.0", "js-sys", "web-sys", ] -[[package]] -name = "widestring" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" - [[package]] name = "winapi" version = "0.3.9" @@ -4591,11 +4532,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -4606,74 +4547,75 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.51.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" -dependencies = [ - "windows-core 0.51.1", - "windows-targets 0.48.5", -] - -[[package]] -name = "windows" -version = "0.54.0" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ - "windows-core 0.54.0", - "windows-implement", - "windows-interface", - "windows-targets 0.52.5", + "windows-core 0.58.0", + "windows-targets 0.52.6", ] [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] name = "windows-core" -version = "0.54.0" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ + "windows-implement", + "windows-interface", "windows-result", - "windows-targets 0.52.5", + "windows-strings", + "windows-targets 0.52.6", ] [[package]] name = "windows-implement" -version = "0.53.0" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] name = "windows-interface" -version = "0.53.0" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] name = "windows-result" -version = "0.1.2" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-targets 0.52.5", + "windows-result", + "windows-targets 0.52.6", ] [[package]] @@ -4700,7 +4642,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -4735,18 +4686,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -4763,9 +4714,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -4781,9 +4732,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -4799,15 +4750,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -4823,9 +4774,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -4841,9 +4792,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -4859,9 +4810,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -4877,9 +4828,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" @@ -4913,7 +4864,7 @@ dependencies = [ "pin-project", "raw-window-handle 0.6.2", "redox_syscall 0.4.1", - "rustix 0.38.21", + "rustix", "sctk-adwaita", "smithay-client-toolkit", "smol_str", @@ -4935,13 +4886,25 @@ dependencies = [ [[package]] name = "winnow" -version = "0.5.15" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "x11-dl" version = "2.21.0" @@ -4955,42 +4918,39 @@ dependencies = [ [[package]] name = "x11rb" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "as-raw-xcb-connection", "gethostname", "libc", "libloading", "once_cell", - "rustix 0.38.21", + "rustix", "x11rb-protocol", ] [[package]] name = "x11rb-protocol" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "xcursor" -version = "0.3.4" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7" -dependencies = [ - "nom", -] +checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" [[package]] name = "xdg-home" -version = "1.0.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" dependencies = [ - "nix", - "winapi", + "libc", + "windows-sys 0.59.0", ] [[package]] @@ -5008,15 +4968,15 @@ dependencies = [ [[package]] name = "xkeysym" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xml-rs" -version = "0.8.19" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" +checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" [[package]] name = "xmlwriter" @@ -5026,7 +4986,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xtask" -version = "0.29.0" +version = "0.30.0" [[package]] name = "yaml-rust" @@ -5037,11 +4997,35 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zbus" -version = "3.14.1" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31de390a2d872e4cd04edd71b425e29853f786dc99317ed72d73d6fcf5ebb948" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" dependencies = [ "async-broadcast", "async-executor", @@ -5053,16 +5037,13 @@ dependencies = [ "async-task", "async-trait", "blocking", - "byteorder", - "derivative", "enumflags2", - "event-listener 2.5.3", + "event-listener", "futures-core", "futures-sink", "futures-util", "hex", "nix", - "once_cell", "ordered-stream", "rand", "serde", @@ -5071,56 +5052,205 @@ dependencies = [ "static_assertions", "tracing", "uds_windows", - "winapi", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros 4.4.0", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1162094dc63b1629fcc44150bcceeaa80798cd28bcbe7fa987b65a034c258608" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-util", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow", "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", + "zbus_macros 5.1.1", + "zbus_names 4.1.0", + "zvariant 5.1.0", +] + +[[package]] +name = "zbus-lockstep" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca2c5dceb099bddaade154055c926bb8ae507a18756ba1d8963fd7b51d8ed1d" +dependencies = [ + "zbus_xml", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709ab20fc57cb22af85be7b360239563209258430bccf38d8b979c5a2ae3ecce" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "zbus-lockstep", + "zbus_xml", + "zvariant 4.2.0", ] [[package]] name = "zbus_macros" -version = "3.14.1" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d1794a946878c0e807f55a397187c11fc7a038ba5d868e7db4f3bd7760bc9d" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "regex", - "syn 1.0.109", - "zvariant_utils", + "syn", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zbus_macros" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd2dcdce3e2727f7d74b7e33b5a89539b3cc31049562137faf7ae4eb86cd16d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names 4.1.0", + "zvariant 5.1.0", + "zvariant_utils 3.0.2", ] [[package]] name = "zbus_names" -version = "2.6.0" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus_names" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "856b7a38811f71846fd47856ceee8bccaec8399ff53fb370247e66081ace647b" +dependencies = [ + "serde", + "static_assertions", + "winnow", + "zvariant 5.1.0", +] + +[[package]] +name = "zbus_xml" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb80bb776dbda6e23d705cf0123c3b95df99c4ebeaec6c2599d4a5419902b4a9" +checksum = "ab3f374552b954f6abb4bd6ce979e6c9b38fb9d0cd7cc68a7d796e70c9f3a233" dependencies = [ + "quick-xml 0.30.0", "serde", "static_assertions", - "zvariant", + "zbus_names 3.0.0", + "zvariant 4.2.0", ] [[package]] name = "zerocopy" -version = "0.7.31" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.31" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] @@ -5131,47 +5261,89 @@ checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] name = "zune-jpeg" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448" +checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" dependencies = [ "zune-core", ] [[package]] name = "zvariant" -version = "3.15.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" dependencies = [ - "byteorder", + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive 4.2.0", +] + +[[package]] +name = "zvariant" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1200ee6ac32f1e5a312e455a949a4794855515d34f9909f4a3e082d14e1a56f" +dependencies = [ + "endi", "enumflags2", - "libc", "serde", "static_assertions", - "zvariant_derive", + "url", + "winnow", + "zvariant_derive 5.1.0", + "zvariant_utils 3.0.2", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils 2.1.0", ] [[package]] name = "zvariant_derive" -version = "3.15.0" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd" +checksum = "687e3b97fae6c9104fbbd36c73d27d149abf04fb874e2efbd84838763daa8916" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 1.0.109", - "zvariant_utils", + "syn", + "zvariant_utils 3.0.2", ] [[package]] name = "zvariant_utils" -version = "1.0.1" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zvariant_utils" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" +checksum = "20d1d011a38f12360e5fcccceeff5e2c42a8eb7f27f0dcba97a0862ede05c9c6" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "serde", + "static_assertions", + "syn", + "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index ecec4fcca3c..b551f9720de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/egui_demo_lib", "crates/egui_extras", "crates/egui_glow", + "crates/egui_kittest", "crates/egui-wgpu", "crates/egui-winit", "crates/egui", @@ -22,8 +23,8 @@ members = [ [workspace.package] edition = "2021" license = "MIT OR Apache-2.0" -rust-version = "1.76" -version = "0.29.0" +rust-version = "1.80" +version = "0.30.0" [profile.release] @@ -54,17 +55,18 @@ opt-level = 2 [workspace.dependencies] -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 } +emath = { version = "0.30.0", path = "crates/emath", default-features = false } +ecolor = { version = "0.30.0", path = "crates/ecolor", default-features = false } +epaint = { version = "0.30.0", path = "crates/epaint", default-features = false } +epaint_default_fonts = { version = "0.30.0", path = "crates/epaint_default_fonts" } +egui = { version = "0.30.0", path = "crates/egui", default-features = false } +egui-winit = { version = "0.30.0", path = "crates/egui-winit", default-features = false } +egui_extras = { version = "0.30.0", path = "crates/egui_extras", default-features = false } +egui-wgpu = { version = "0.30.0", path = "crates/egui-wgpu", default-features = false } +egui_demo_lib = { version = "0.30.0", path = "crates/egui_demo_lib", default-features = false } +egui_glow = { version = "0.30.0", path = "crates/egui_glow", default-features = false } +egui_kittest = { version = "0.30.0", path = "crates/egui_kittest", default-features = false } +eframe = { version = "0.30.0", path = "crates/eframe", default-features = false } ahash = { version = "0.8.11", default-features = false, features = [ "no-rng", # we don't need DOS-protection, so we let users opt-in to it instead @@ -73,40 +75,45 @@ ahash = { version = "0.8.11", default-features = false, features = [ backtrace = "0.3" bytemuck = "1.7.2" criterion = { version = "0.5.1", default-features = false } -document-features = " 0.2.8" -glow = "0.14" -glutin = "0.32.0" -glutin-winit = "0.5.0" +dify = { version = "0.7", default-features = false } +document-features = "0.2.10" +glow = "0.16" +glutin = { version = "0.32.0", default-features = false } +glutin-winit = { version = "0.5.0", default-features = false } home = "0.5.9" image = { version = "0.25", default-features = false } +kittest = { version = "0.1" } log = { version = "0.4", features = ["std"] } nohash-hasher = "0.2" parking_lot = "0.12" +pollster = "0.4" +profiling = { version = "1.0.16", default-features = false } puffin = "0.19" puffin_http = "0.16" -ron = "0.8" raw-window-handle = "0.6.0" +ron = "0.8" serde = { version = "1", features = ["derive"] } thiserror = "1.0.37" -web-time = "1.1.0" # Timekeeping for native and web +type-map = "0.5.0" wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" web-sys = "0.3.70" -wgpu = { version = "22.1.0", default-features = false } -windows-sys = "0.52" +web-time = "1.1.0" # Timekeeping for native and web +wgpu = { version = "23.0.0", default-features = false } +windows-sys = "0.59" winit = { version = "0.30.5", default-features = false } [workspace.lints.rust] unsafe_code = "deny" elided_lifetimes_in_paths = "warn" -future_incompatible = "warn" -nonstandard_style = "warn" -rust_2018_idioms = "warn" +future_incompatible = { level = "warn", priority = -1 } +nonstandard_style = { level = "warn", priority = -1 } +rust_2018_idioms = { level = "warn", priority = -1 } rust_2021_prelude_collisions = "warn" semicolon_in_expressions_from_macros = "warn" trivial_numeric_casts = "warn" -unsafe_op_in_unsafe_fn = "warn" # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668 +unsafe_op_in_unsafe_fn = "warn" # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668 unused_extern_crates = "warn" unused_import_braces = "warn" unused_lifetimes = "warn" @@ -139,6 +146,7 @@ disallowed_types = "warn" # See clippy.toml doc_link_with_quotes = "warn" doc_markdown = "warn" empty_enum = "warn" +empty_enum_variants_with_brackets = "warn" enum_glob_use = "warn" equatable_if_let = "warn" exit = "warn" @@ -163,6 +171,8 @@ inefficient_to_string = "warn" infinite_loop = "warn" into_iter_without_iter = "warn" invalid_upcast_comparisons = "warn" +iter_filter_is_ok = "warn" +iter_filter_is_some = "warn" iter_not_returning_iterator = "warn" iter_on_empty_collections = "warn" iter_on_single_items = "warn" @@ -179,6 +189,7 @@ macro_use_imports = "warn" manual_assert = "warn" manual_clamp = "warn" manual_instant_elapsed = "warn" +manual_is_variant_and = "warn" manual_let_else = "warn" manual_ok_or = "warn" manual_string_new = "warn" @@ -196,6 +207,7 @@ mismatching_type_param_order = "warn" missing_enforced_import_renames = "warn" missing_errors_doc = "warn" missing_safety_doc = "warn" +mixed_attributes_style = "warn" mut_mut = "warn" mutex_integer = "warn" needless_borrow = "warn" @@ -205,21 +217,26 @@ needless_pass_by_ref_mut = "warn" needless_pass_by_value = "warn" negative_feature_names = "warn" nonstandard_macro_braces = "warn" +option_as_ref_cloned = "warn" option_option = "warn" path_buf_push_overwrite = "warn" print_stderr = "warn" ptr_as_ptr = "warn" ptr_cast_constness = "warn" +pub_underscore_fields = "warn" pub_without_shorthand = "warn" rc_mutex = "warn" readonly_write_lock = "warn" redundant_type_annotations = "warn" +ref_as_ptr = "warn" ref_option_ref = "warn" ref_patterns = "warn" rest_pat_in_fully_bound_structs = "warn" same_functions_in_if_condition = "warn" semicolon_if_nothing_returned = "warn" +single_char_pattern = "warn" single_match_else = "warn" +str_split_at_newline = "warn" str_to_string = "warn" string_add = "warn" string_add_assign = "warn" @@ -255,12 +272,15 @@ 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" should_panic_without_expect = "allow" too_many_lines = "allow" unwrap_used = "allow" # TODO(emilk): We really wanna warn on this one +# These are meh: +assigning_clones = "allow" # No please +let_underscore_must_use = "allow" +let_underscore_untyped = "allow" manual_range_contains = "allow" # this one is just worse imho self_named_module_files = "allow" # Disabled waiting on https://github.com/rust-lang/rust-clippy/issues/9602 significant_drop_tightening = "allow" # Too many false positives diff --git a/README.md b/README.md index bdf4229125c..247ad32d90c 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,9 @@ Still, egui can be used to create professional looking applications, like [the R * Label text selection * And more! +Check out the [3rd party egui crates wiki](https://github.com/emilk/egui/wiki/3rd-party-egui-crates) for even more +widgets and features, maintained by the community. + Light Theme: @@ -187,30 +190,8 @@ These are the official egui integrations: ### 3rd party integrations -* [`egui-ash`](https://github.com/MatchaChoco010/egui-ash) for [`ash`](https://github.com/ash-rs/ash) (a very lightweight wrapper around Vulkan) -* [`bevy_egui`](https://github.com/mvlabat/bevy_egui) for [the Bevy game engine](https://bevyengine.org/) -* [`egui_gl_glfw`](https://github.com/mrclean71774/egui_gl_glfw) for [GLFW](https://crates.io/crates/glfw) -* [`egui_glium`](https://github.com/fayalalebrun/egui_glium) for compiling native apps with [Glium](https://github.com/glium/glium) -* [`egui-glutin-gl`](https://github.com/h3r2tic/egui-glutin-gl/) for [glutin](https://crates.io/crates/glutin) -* [`egui_sdl2_gl`](https://crates.io/crates/egui_sdl2_gl) for [SDL2](https://crates.io/crates/sdl2) -* [`egui_sdl2_platform`](https://github.com/ComLarsic/egui_sdl2_platform) for [SDL2](https://crates.io/crates/sdl2) -* [`egui_vulkano`](https://github.com/derivator/egui_vulkano) for [Vulkano](https://github.com/vulkano-rs/vulkano) -* [`egui_winit_vulkano`](https://github.com/hakolao/egui_winit_vulkano) for [Vulkano](https://github.com/vulkano-rs/vulkano) -* [`egui-macroquad`](https://github.com/optozorax/egui-macroquad) for [macroquad](https://github.com/not-fl3/macroquad) -* [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad) for [Miniquad](https://github.com/not-fl3/miniquad) -* [`egui_speedy2d`](https://github.com/heretik31/egui_speedy2d) for [Speedy2d](https://github.com/QuantumBadger/Speedy2D) -* [`egui-tetra2`](https://crates.io/crates/egui-tetra2) for [Tetra](https://crates.io/crates/tetra), a 2D game framework -* [`egui-winit-ash-integration`](https://github.com/MatchaChoco010/egui-winit-ash-integration) for [winit](https://github.com/rust-windowing/winit) and [ash](https://github.com/MaikKlein/ash) -* [`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) -* [`egui_skia`](https://github.com/lucasmerlin/egui_skia) for [skia](https://github.com/rust-skia/rust-skia/tree/master/skia-safe) -* [`smithay-egui`](https://github.com/Smithay/smithay-egui) for [smithay](https://github.com/Smithay/smithay/) -* [`tauri-egui`](https://github.com/tauri-apps/tauri-egui) for [tauri](https://github.com/tauri-apps/tauri) +Check the wiki to find [3rd party integrations](https://github.com/emilk/egui/wiki/3rd-party-integrations) +and [egui crates](https://github.com/emilk/egui/wiki/3rd-party-egui-crates). ### Writing your own egui integration Missing an integration for the thing you're working on? Create one, it's easy! @@ -296,7 +277,7 @@ If you call `.await` in your GUI code, the UI will freeze, which is very bad UX. * [`eventuals::Eventual`](https://docs.rs/eventuals/latest/eventuals/struct.Eventual.html) * [`tokio::sync::watch::channel`](https://docs.rs/tokio/latest/tokio/sync/watch/fn.channel.html) -### How to I create a file dialog? +### How do I create a file dialog? The async version of [rfd](https://docs.rs/rfd/latest/rfd/) supports both native and Wasm. See example app here https://github.com/woelper/egui_pick_file which also has a demo available via [gitub pages](https://woelper.github.io/egui_pick_file/). diff --git a/RELEASES.md b/RELEASES.md index b3777a3aedc..79bf23bc614 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -53,8 +53,7 @@ We don't update the MSRV in a patch release, unless we really, really need to. * [ ] run `scripts/generate_example_screenshots.sh` if needed * [ ] write a short release note that fits in a tweet * [ ] record gif for `CHANGELOG.md` release note (and later twitter post) -* [ ] update changelogs using `scripts/generate_changelog.py --write` - - For major releases, always diff to the latest MAJOR release, e.g. `--commit-range 0.29.0..HEAD` +* [ ] update changelogs using `scripts/generate_changelog.py --version 0.x.0 --write` * [ ] bump version numbers in workspace `Cargo.toml` ## Actual release @@ -79,8 +78,9 @@ I usually do this all on the `master` branch, but doing it in a release branch i (cd crates/epaint && cargo publish --quiet) && echo "✅ epaint" (cd crates/egui && cargo publish --quiet) && echo "✅ egui" (cd crates/egui-winit && cargo publish --quiet) && echo "✅ egui-winit" -(cd crates/egui_extras && cargo publish --quiet) && echo "✅ egui_extras" (cd crates/egui-wgpu && cargo publish --quiet) && echo "✅ egui-wgpu" +(cd crates/egui_kittest && cargo publish --quiet) && echo "✅ egui_kittest" +(cd crates/egui_extras && cargo publish --quiet) && echo "✅ egui_extras" (cd crates/egui_demo_lib && cargo publish --quiet) && echo "✅ egui_demo_lib" (cd crates/egui_glow && cargo publish --quiet) && echo "✅ egui_glow" (cd crates/eframe && cargo publish --quiet) && echo "✅ eframe" diff --git a/clippy.toml b/clippy.toml index f480ca93d7e..9e5fdd1e593 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,77 +1,80 @@ -# There is also a scripts/clippy_wasm/clippy.toml which forbids some methods that are not available in wasm. - -# ----------------------------------------------------------------------------- -# Section identical to scripts/clippy_wasm/clippy.toml: - -msrv = "1.76" - -allow-unwrap-in-tests = true - -# https://doc.rust-lang.org/nightly/clippy/lint_configuration.html#avoid-breaking-exported-api -# We want suggestions, even if it changes public API. -avoid-breaking-exported-api = false - -max-fn-params-bools = 2 # TODO(emilk): decrease this to 1 - -# https://rust-lang.github.io/rust-clippy/master/index.html#/large_include_file -max-include-file-size = 1000000 - -# https://rust-lang.github.io/rust-clippy/master/index.html#/type_complexity -type-complexity-threshold = 350 - -# ----------------------------------------------------------------------------- - -# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_macros -disallowed-macros = [ - 'dbg', - 'std::unimplemented', - - # TODO(emilk): consider forbidding these to encourage the use of proper log stream, and then explicitly allow legitimate uses - # 'std::eprint', - # 'std::eprintln', - # 'std::print', - # 'std::println', -] - -# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_methods -disallowed-methods = [ - "std::env::temp_dir", # Use the tempdir crate instead - - # There are many things that aren't allowed on wasm, - # but we cannot disable them all here (because of e.g. https://github.com/rust-lang/rust-clippy/issues/10406) - # so we do that in `clipppy_wasm.toml` instead. - - "std::thread::spawn", # Use `std::thread::Builder` and name the thread - - "sha1::Digest::new", # SHA1 is cryptographically broken - - "std::panic::catch_unwind", # We compile with `panic = "abort"` -] - -# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_names -disallowed-names = [] - -# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_types -disallowed-types = [ - # Use the faster & simpler non-poisonable primitives in `parking_lot` instead - "std::sync::Mutex", - "std::sync::RwLock", - "std::sync::Condvar", - # "std::sync::Once", # enabled for now as the `log_once` macro uses it internally - - "ring::digest::SHA1_FOR_LEGACY_USE_ONLY", # SHA1 is cryptographically broken - - "winit::dpi::LogicalSize", # We do our own pixels<->point conversion, taking `egui_ctx.zoom_factor` into account - "winit::dpi::LogicalPosition", # We do our own pixels<->point conversion, taking `egui_ctx.zoom_factor` into account -] - -# ----------------------------------------------------------------------------- - -# Allow-list of words for markdown in docstrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown -doc-valid-idents = [ - # You must also update the same list in the root `clippy.toml`! - "AccessKit", - "WebGL", - "WebGPU", - "..", -] +# There is also a scripts/clippy_wasm/clippy.toml which forbids some methods that are not available in wasm. + +# ----------------------------------------------------------------------------- +# Section identical to scripts/clippy_wasm/clippy.toml: + +msrv = "1.80" + +allow-unwrap-in-tests = true + +# https://doc.rust-lang.org/nightly/clippy/lint_configuration.html#avoid-breaking-exported-api +# We want suggestions, even if it changes public API. +avoid-breaking-exported-api = false + +max-fn-params-bools = 2 # TODO(emilk): decrease this to 1 + +# https://rust-lang.github.io/rust-clippy/master/index.html#/large_include_file +max-include-file-size = 1000000 + +# https://rust-lang.github.io/rust-clippy/master/index.html#/type_complexity +type-complexity-threshold = 350 + +# ----------------------------------------------------------------------------- + +# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_macros +disallowed-macros = [ + 'dbg', + 'std::unimplemented', + + # TODO(emilk): consider forbidding these to encourage the use of proper log stream, and then explicitly allow legitimate uses + # 'std::eprint', + # 'std::eprintln', + # 'std::print', + # 'std::println', +] + +# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_methods +disallowed-methods = [ + "std::env::temp_dir", # Use the tempdir crate instead + + # There are many things that aren't allowed on wasm, + # but we cannot disable them all here (because of e.g. https://github.com/rust-lang/rust-clippy/issues/10406) + # so we do that in `clipppy_wasm.toml` instead. + + "std::thread::spawn", # Use `std::thread::Builder` and name the thread + + "sha1::Digest::new", # SHA1 is cryptographically broken + + "std::panic::catch_unwind", # We compile with `panic = "abort"` +] + +# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_names +disallowed-names = [] + +# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_types +disallowed-types = [ + # Use the faster & simpler non-poisonable primitives in `parking_lot` instead + "std::sync::Mutex", + "std::sync::RwLock", + "std::sync::Condvar", + # "std::sync::Once", # enabled for now as the `log_once` macro uses it internally + + "ring::digest::SHA1_FOR_LEGACY_USE_ONLY", # SHA1 is cryptographically broken + + "winit::dpi::LogicalSize", # We do our own pixels<->point conversion, taking `egui_ctx.zoom_factor` into account + "winit::dpi::LogicalPosition", # We do our own pixels<->point conversion, taking `egui_ctx.zoom_factor` into account +] + +# ----------------------------------------------------------------------------- + +# Allow-list of words for markdown in docstrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown +doc-valid-idents = [ + # You must also update the same list in `scripts/clippy_wasm/clippy.toml`! + "AccessKit", + "WebGL", + "WebGL1", + "WebGL2", + "WebGPU", + "VirtualBox", + "..", +] diff --git a/crates/ecolor/CHANGELOG.md b/crates/ecolor/CHANGELOG.md index 27280c70bfb..37b5555b60a 100644 --- a/crates/ecolor/CHANGELOG.md +++ b/crates/ecolor/CHANGELOG.md @@ -6,6 +6,15 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.30.0 - 2024-12-16 +* Use boxed slice for lookup table to avoid stack overflow [#5212](https://github.com/emilk/egui/pull/5212) by [@YgorSouza](https://github.com/YgorSouza) +* Add `Color32::mul` [#5437](https://github.com/emilk/egui/pull/5437) by [@emilk](https://github.com/emilk) + + +## 0.29.1 - 2024-10-01 +Nothing new + + ## 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) diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index 80c876d192d..c38a8d6b9f8 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -43,8 +43,11 @@ impl Color32 { pub const TRANSPARENT: Self = Self::from_rgba_premultiplied(0, 0, 0, 0); pub const BLACK: Self = Self::from_rgb(0, 0, 0); + #[doc(alias = "DARK_GREY")] pub const DARK_GRAY: Self = Self::from_rgb(96, 96, 96); + #[doc(alias = "GREY")] pub const GRAY: Self = Self::from_rgb(160, 160, 160); + #[doc(alias = "LIGHT_GREY")] pub const LIGHT_GRAY: Self = Self::from_rgb(220, 220, 220); pub const WHITE: Self = Self::from_rgb(255, 255, 255); @@ -108,15 +111,17 @@ impl Color32 { // common-case optimization 255 => Self::from_rgb(r, g, b), a => { - static LOOKUP_TABLE: OnceLock<[u8; 256 * 256]> = OnceLock::new(); + static LOOKUP_TABLE: OnceLock> = 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) - }) + (0..=u16::MAX) + .map(|i| { + let [value, alpha] = i.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) + }) + .collect() }); let [r, g, b] = @@ -126,6 +131,7 @@ impl Color32 { } } + #[doc(alias = "from_grey")] #[inline] pub const fn from_gray(l: u8) -> Self { Self([l, l, l, 255]) @@ -263,3 +269,18 @@ impl Color32 { ) } } + +impl std::ops::Mul for Color32 { + type Output = Self; + + /// Fast gamma-space multiplication. + #[inline] + fn mul(self, other: Self) -> Self { + Self([ + fast_round(self[0] as f32 * other[0] as f32 / 255.0), + fast_round(self[1] as f32 * other[1] as f32 / 255.0), + fast_round(self[2] as f32 * other[2] as f32 / 255.0), + fast_round(self[3] as f32 * other[3] as f32 / 255.0), + ]) + } +} diff --git a/crates/ecolor/src/hex_color_macro.rs b/crates/ecolor/src/hex_color_macro.rs index fd1075dc639..e95bbc465f5 100644 --- a/crates/ecolor/src/hex_color_macro.rs +++ b/crates/ecolor/src/hex_color_macro.rs @@ -40,11 +40,10 @@ macro_rules! hex_color { ($s:literal) => {{ let array = $crate::color_hex::color_from_hex!($s); - if array.len() == 3 { - $crate::Color32::from_rgb(array[0], array[1], array[2]) - } else { - #[allow(unconditional_panic)] - $crate::Color32::from_rgba_unmultiplied(array[0], array[1], array[2], array[3]) + match array.as_slice() { + [r, g, b] => $crate::Color32::from_rgb(*r, *g, *b), + [r, g, b, a] => $crate::Color32::from_rgba_unmultiplied(*r, *g, *b, *a), + _ => panic!("Invalid hex color length: expected 3 (RGB) or 4 (RGBA) bytes"), } }}; } diff --git a/crates/ecolor/src/rgba.rs b/crates/ecolor/src/rgba.rs index 900286cda43..93cb41d8186 100644 --- a/crates/ecolor/src/rgba.rs +++ b/crates/ecolor/src/rgba.rs @@ -91,6 +91,7 @@ impl Rgba { Self([r, g, b, 1.0]) } + #[doc(alias = "from_grey")] #[inline] pub const fn from_gray(l: f32) -> Self { Self([l, l, l, 1.0]) diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index ff54dc2e83d..cbb6cf411ca 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -7,6 +7,29 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.30.0 - 2024-12-16 - Android support +NOTE: you now need to enable the `wayland` or `x11` features to get Linux support, including getting it to work on most CI systems. + +### ⭐ Added +* Support `ViewportCommand::Screenshot` on web [#5438](https://github.com/emilk/egui/pull/5438) by [@lucasmerlin](https://github.com/lucasmerlin) + +### 🔧 Changed +* Android support [#5318](https://github.com/emilk/egui/pull/5318) by [@parasyte](https://github.com/parasyte) +* Update MSRV to 1.80 [#5457](https://github.com/emilk/egui/pull/5457) by [@emilk](https://github.com/emilk) +* Use `profiling` crate to support more profiler backends [#5150](https://github.com/emilk/egui/pull/5150) by [@teddemunnik](https://github.com/teddemunnik) +* Update glow to 0.16 [#5395](https://github.com/emilk/egui/pull/5395) by [@sagudev](https://github.com/sagudev) +* Forward `x11` and `wayland` features to `glutin` [#5391](https://github.com/emilk/egui/pull/5391) by [@e00E](https://github.com/e00E) + +### 🐛 Fixed +* iOS: Support putting UI next to the dynamic island [#5211](https://github.com/emilk/egui/pull/5211) by [@frederik-uni](https://github.com/frederik-uni) +* Prevent panic when copying text outside of a secure context [#5326](https://github.com/emilk/egui/pull/5326) by [@YgorSouza](https://github.com/YgorSouza) +* Fix accidental change of `FallbackEgl` to `PreferEgl` [#5408](https://github.com/emilk/egui/pull/5408) by [@e00E](https://github.com/e00E) + + +## 0.29.1 - 2024-10-01 - Fix backspace/arrow keys on X11 +* Linux: Disable IME to fix backspace/arrow keys [#5188](https://github.com/emilk/egui/pull/5188) by [@emilk](https://github.com/emilk) + + ## 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)) diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 060e857e941..eb4766dfaab 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -35,7 +35,7 @@ default = [ "accesskit", "default_fonts", "glow", - "wayland", + "wayland", # Required for Linux support (including CI!) "web_screen_reader", "winit/default", "x11", @@ -71,22 +71,17 @@ persistence = [ "serde", ] -## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -## -## `eframe` will call `puffin::GlobalProfiler::lock().new_frame()` for you +## Enables wayland support and fixes clipboard issue. ## -## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. -puffin = [ - "dep:puffin", - "egui/puffin", - "egui_glow?/puffin", - "egui-wgpu?/puffin", - "egui-winit/puffin", +## If you are compiling for Linux (or want to test on a CI system using Linux), you should enable this feature. +wayland = [ + "egui-winit/wayland", + "egui-wgpu?/wayland", + "egui_glow?/wayland", + "glutin?/wayland", + "glutin-winit?/wayland", ] -## Enables wayland support and fixes clipboard issue. -wayland = ["egui-winit/wayland", "egui-wgpu?/wayland", "egui_glow?/wayland"] - ## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`) on web. ## ## For other platforms, use the `accesskit` feature instead. @@ -111,7 +106,15 @@ web_screen_reader = [ wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"] ## Enables compiling for x11. -x11 = ["egui-winit/x11", "egui-wgpu?/x11", "egui_glow?/x11"] +x11 = [ + "egui-winit/x11", + "egui-wgpu?/x11", + "egui_glow?/x11", + "glutin?/x11", + "glutin?/glx", + "glutin-winit?/x11", + "glutin-winit?/glx", +] ## If set, eframe will look for the env-var `EFRAME_SCREENSHOT_TO` and write a screenshot to that location, and then quit. ## This is used to generate images for examples. @@ -127,6 +130,7 @@ ahash.workspace = true document-features.workspace = true log.workspace = true parking_lot.workspace = true +profiling.workspace = true raw-window-handle.workspace = true static_assertions = "1.1.0" web-time.workspace = true @@ -152,14 +156,17 @@ winit = { workspace = true, default-features = false, features = ["rwh_06"] } egui-wgpu = { workspace = true, optional = true, features = [ "winit", ] } # if wgpu is used, use it with winit -pollster = { version = "0.3", optional = true } # needed for wgpu +pollster = { workspace = true, optional = true } # needed for wgpu -# we can expose these to user so that they can select which backends they want to enable to avoid compiling useless deps. -# this can be done at the same time we expose x11/wayland features of winit crate. -glutin = { workspace = true, optional = true } -glutin-winit = { workspace = true, optional = true } +glutin = { workspace = true, optional = true, default-features = false, features = [ + "egl", + "wgl", +] } +glutin-winit = { workspace = true, optional = true, default-features = false, features = [ + "egl", + "wgl", +] } home = { workspace = true, optional = true } -puffin = { workspace = true, optional = true } wgpu = { workspace = true, optional = true, features = [ # Let's enable some backends so that users can use `eframe` out-of-the-box # without having to explicitly opt-in to backends diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index f567507c4da..45d8335018e 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -364,6 +364,16 @@ pub struct NativeOptions { /// /// Defaults to true. pub dithering: bool, + + /// Android application for `winit`'s event loop. + /// + /// This value is required on Android to correctly create the event loop. See + /// [`EventLoopBuilder::build`] and [`with_android_app`] for details. + /// + /// [`EventLoopBuilder::build`]: winit::event_loop::EventLoopBuilder::build + /// [`with_android_app`]: winit::platform::android::EventLoopBuilderExtAndroid::with_android_app + #[cfg(target_os = "android")] + pub android_app: Option, } #[cfg(not(target_arch = "wasm32"))] @@ -383,6 +393,9 @@ impl Clone for NativeOptions { persistence_path: self.persistence_path.clone(), + #[cfg(target_os = "android")] + android_app: self.android_app.clone(), + ..*self } } @@ -424,6 +437,9 @@ impl Default for NativeOptions { persistence_path: None, dithering: true, + + #[cfg(target_os = "android")] + android_app: None, } } } @@ -439,7 +455,7 @@ pub struct WebOptions { /// Unused by webgl context as of writing. pub depth_buffer: u8, - /// Which version of WebGl context to select + /// Which version of WebGL context to select /// /// Default: [`WebGlContextOption::BestFirst`]. #[cfg(feature = "glow")] @@ -772,8 +788,7 @@ pub struct IntegrationInfo { /// /// This includes [`App::update`] as well as rendering (except for vsync waiting). /// - /// For a more detailed view of cpu usage, use the [`puffin`](https://crates.io/crates/puffin) - /// profiler together with the `puffin` feature of `eframe`. + /// For a more detailed view of cpu usage, connect your preferred profiler by enabling it's feature in [`profiling`](https://crates.io/crates/profiling). /// /// `None` if this is the first frame. pub cpu_usage: Option, @@ -815,7 +830,7 @@ impl Storage for DummyStorage { /// Get and deserialize the [RON](https://github.com/ron-rs/ron) stored at the given key. #[cfg(feature = "ron")] pub fn get_value(storage: &dyn Storage, key: &str) -> Option { - crate::profile_function!(key); + profiling::function_scope!(key); storage .get_string(key) .and_then(|value| match ron::from_str(&value) { @@ -831,7 +846,7 @@ pub fn get_value(storage: &dyn Storage, key: &st /// Serialize the given value as [RON](https://github.com/ron-rs/ron) and store with the given key. #[cfg(feature = "ron")] pub fn set_value(storage: &mut dyn Storage, key: &str, value: &T) { - crate::profile_function!(key); + profiling::function_scope!(key); match ron::ser::to_string(value) { Ok(string) => storage.set_string(key, string), Err(err) => log::error!("eframe failed to encode data using ron: {}", err), diff --git a/crates/eframe/src/icon_data.rs b/crates/eframe/src/icon_data.rs index ed514d00e1f..4851bee64b3 100644 --- a/crates/eframe/src/icon_data.rs +++ b/crates/eframe/src/icon_data.rs @@ -22,7 +22,7 @@ pub trait IconDataExt { /// # Errors /// If this is not a valid png. pub fn from_png_bytes(png_bytes: &[u8]) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let image = image::load_from_memory(png_bytes)?; Ok(from_image(image)) } @@ -38,7 +38,7 @@ fn from_image(image: image::DynamicImage) -> IconData { impl IconDataExt for IconData { fn to_image(&self) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let Self { rgba, width, @@ -48,7 +48,7 @@ impl IconDataExt for IconData { } fn to_png_bytes(&self) -> Result, String> { - crate::profile_function!(); + profiling::function_scope!(); let image = self.to_image()?; let mut png_bytes: Vec = Vec::new(); image diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 80da6235792..7b342a4c21f 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -129,6 +129,17 @@ //! ## Feature flags #![doc = document_features::document_features!()] //! +//! ## Instrumentation +//! This crate supports using the [profiling](https://crates.io/crates/profiling) crate for instrumentation. +//! You can enable features on the profiling crates in your application to add instrumentation for all +//! crates that support it, including egui. See the profiling crate docs for more information. +//! ```toml +//! [dependencies] +//! profiling = "1.0" +//! [features] +//! profile-with-puffin = ["profiling/profile-with-puffin"] +//! ``` +//! #![warn(missing_docs)] // let's keep eframe well-documented #![allow(clippy::needless_doctest_main)] @@ -445,33 +456,3 @@ impl std::fmt::Display for Error { /// Short for `Result`. pub type Result = std::result::Result; - -// --------------------------------------------------------------------------- - -mod profiling_scopes { - #![allow(unused_macros)] - #![allow(unused_imports)] - - /// Profiling macro for feature "puffin" - macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} - -#[allow(unused_imports)] -pub(crate) use profiling_scopes::{profile_function, profile_scope}; diff --git a/crates/eframe/src/native/app_icon.rs b/crates/eframe/src/native/app_icon.rs index 840bf367b27..8591ba2a8cc 100644 --- a/crates/eframe/src/native/app_icon.rs +++ b/crates/eframe/src/native/app_icon.rs @@ -59,7 +59,7 @@ enum AppIconStatus { /// Since window creation can be lazy, call this every frame until it's either successfully or gave up. /// (See [`AppIconStatus`]) fn set_title_and_icon(_title: &str, _icon_data: Option<&IconData>) -> AppIconStatus { - crate::profile_function!(); + profiling::function_scope!(); #[cfg(target_os = "windows")] { @@ -201,7 +201,7 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus { #[allow(unsafe_code)] fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus { use crate::icon_data::IconDataExt as _; - crate::profile_function!(); + profiling::function_scope!(); use objc2::ClassType; use objc2_app_kit::{NSApplication, NSImage}; @@ -237,7 +237,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS log::trace!("NSImage::initWithData…"); let app_icon = NSImage::initWithData(NSImage::alloc(), &data); - crate::profile_scope!("setApplicationIconImage_"); + profiling::scope!("setApplicationIconImage_"); log::trace!("setApplicationIconImage…"); app.setApplicationIconImage(app_icon.as_deref()); } @@ -246,7 +246,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS if let Some(main_menu) = app.mainMenu() { if let Some(item) = main_menu.itemAtIndex(0) { if let Some(app_menu) = item.submenu() { - crate::profile_scope!("setTitle_"); + profiling::scope!("setTitle_"); app_menu.setTitle(&NSString::from_str(title)); } } diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 5f9d555e3a5..03b5f2dcd46 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -19,7 +19,7 @@ pub fn viewport_builder( native_options: &mut epi::NativeOptions, window_settings: Option, ) -> ViewportBuilder { - crate::profile_function!(); + profiling::function_scope!(); let mut viewport_builder = native_options.viewport.clone(); @@ -67,7 +67,7 @@ pub fn viewport_builder( #[cfg(not(target_os = "ios"))] if native_options.centered { - crate::profile_scope!("center"); + profiling::scope!("center"); if let Some(monitor) = event_loop .primary_monitor() .or_else(|| event_loop.available_monitors().next()) @@ -94,8 +94,7 @@ pub fn apply_window_settings( window: &winit::window::Window, window_settings: Option, ) { - crate::profile_function!(); - + profiling::function_scope!(); if let Some(window_settings) = window_settings { window_settings.initialize_window(window); } @@ -103,12 +102,11 @@ pub fn apply_window_settings( #[cfg(not(target_os = "ios"))] fn largest_monitor_point_size(egui_zoom_factor: f32, event_loop: &ActiveEventLoop) -> egui::Vec2 { - crate::profile_function!(); - + profiling::function_scope!(); let mut max_size = egui::Vec2::ZERO; let available_monitors = { - crate::profile_scope!("available_monitors"); + profiling::scope!("available_monitors"); event_loop.available_monitors() }; @@ -238,7 +236,7 @@ impl EpiIntegration { egui_winit: &mut egui_winit::State, event: &winit::event::WindowEvent, ) -> EventResponse { - crate::profile_function!(egui_winit::short_window_event_description(event)); + profiling::function_scope!(egui_winit::short_window_event_description(event)); use winit::event::{ElementState, MouseButton, WindowEvent}; @@ -276,10 +274,10 @@ impl EpiIntegration { let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { if let Some(viewport_ui_cb) = viewport_ui_cb { // Child viewport - crate::profile_scope!("viewport_callback"); + profiling::scope!("viewport_callback"); viewport_ui_cb(egui_ctx); } else { - crate::profile_scope!("App::update"); + profiling::scope!("App::update"); app.update(egui_ctx, &mut self.frame); } }); @@ -306,7 +304,7 @@ impl EpiIntegration { } pub fn post_rendering(&mut self, window: &winit::window::Window) { - crate::profile_function!(); + profiling::function_scope!(); if std::mem::take(&mut self.is_first_frame) { // We keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279 window.set_visible(true); @@ -332,11 +330,11 @@ impl EpiIntegration { pub fn save(&mut self, _app: &mut dyn epi::App, _window: Option<&winit::window::Window>) { #[cfg(feature = "persistence")] if let Some(storage) = self.frame.storage_mut() { - crate::profile_function!(); + profiling::function_scope!(); if let Some(window) = _window { if self.persist_window { - crate::profile_scope!("native_window"); + profiling::scope!("native_window"); epi::set_value( storage, STORAGE_WINDOW_KEY, @@ -345,23 +343,23 @@ impl EpiIntegration { } } if _app.persist_egui_memory() { - crate::profile_scope!("egui_memory"); + profiling::scope!("egui_memory"); self.egui_ctx .memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem)); } { - crate::profile_scope!("App::save"); + profiling::scope!("App::save"); _app.save(storage); } - crate::profile_scope!("Storage::flush"); + profiling::scope!("Storage::flush"); storage.flush(); } } } fn load_default_egui_icon() -> egui::IconData { - crate::profile_function!(); + profiling::function_scope!(); crate::icon_data::from_png_bytes(&include_bytes!("../../data/icon.png")[..]).unwrap() } @@ -372,7 +370,7 @@ const STORAGE_EGUI_MEMORY_KEY: &str = "egui"; const STORAGE_WINDOW_KEY: &str = "window"; pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option { - crate::profile_function!(); + profiling::function_scope!(); #[cfg(feature = "persistence")] { epi::get_value(_storage?, STORAGE_WINDOW_KEY) @@ -382,7 +380,7 @@ pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option) -> Option { - crate::profile_function!(); + profiling::function_scope!(); #[cfg(feature = "persistence")] { epi::get_value(_storage?, STORAGE_EGUI_MEMORY_KEY) diff --git a/crates/eframe/src/native/event_loop_context.rs b/crates/eframe/src/native/event_loop_context.rs index 2bb9e42555a..f1262f85358 100644 --- a/crates/eframe/src/native/event_loop_context.rs +++ b/crates/eframe/src/native/event_loop_context.rs @@ -2,7 +2,7 @@ use std::cell::Cell; use winit::event_loop::ActiveEventLoop; thread_local! { - static CURRENT_EVENT_LOOP: Cell> = Cell::new(None); + static CURRENT_EVENT_LOOP: Cell> = const { Cell::new(None) }; } struct EventLoopGuard; @@ -14,7 +14,7 @@ impl EventLoopGuard { cell.get().is_none(), "Attempted to set a new event loop while one is already set" ); - cell.set(Some(event_loop as *const ActiveEventLoop)); + cell.set(Some(std::ptr::from_ref::(event_loop))); }); Self } diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index 439fc5463c6..346c46b4254 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -63,7 +63,7 @@ fn roaming_appdata() -> Option { match SHGetKnownFolderPath( &FOLDERID_RoamingAppData, KF_FLAG_DONT_VERIFY as u32, - 0, + std::ptr::null_mut(), &mut path, ) { S_OK => { @@ -100,7 +100,7 @@ pub struct FileStorage { impl Drop for FileStorage { fn drop(&mut self) { if let Some(join_handle) = self.last_save_join_handle.take() { - crate::profile_scope!("wait_for_save"); + profiling::scope!("wait_for_save"); join_handle.join().ok(); } } @@ -109,7 +109,7 @@ impl Drop for FileStorage { impl FileStorage { /// Store the state in this .ron file. pub(crate) fn from_ron_filepath(ron_filepath: impl Into) -> Self { - crate::profile_function!(); + profiling::function_scope!(); let ron_filepath: PathBuf = ron_filepath.into(); log::debug!("Loading app state from {:?}…", ron_filepath); Self { @@ -122,7 +122,7 @@ impl FileStorage { /// Find a good place to put the files that the OS likes. pub fn from_app_id(app_id: &str) -> Option { - crate::profile_function!(app_id); + profiling::function_scope!(); if let Some(data_dir) = storage_dir(app_id) { if let Err(err) = std::fs::create_dir_all(&data_dir) { log::warn!( @@ -155,7 +155,7 @@ impl crate::Storage for FileStorage { fn flush(&mut self) { if self.dirty { - crate::profile_function!(); + profiling::scope!("FileStorage::flush"); self.dirty = false; let file_path = self.ron_filepath.clone(); @@ -184,7 +184,7 @@ impl crate::Storage for FileStorage { } fn save_to_disk(file_path: &PathBuf, kv: &HashMap) { - crate::profile_function!(); + profiling::function_scope!(); if let Some(parent_dir) = file_path.parent() { if !parent_dir.exists() { @@ -199,7 +199,7 @@ fn save_to_disk(file_path: &PathBuf, kv: &HashMap) { let mut writer = std::io::BufWriter::new(file); let config = Default::default(); - crate::profile_scope!("ron::serialize"); + profiling::scope!("ron::serialize"); if let Err(err) = ron::ser::to_writer_pretty(&mut writer, &kv, config) .and_then(|_| writer.flush().map_err(|err| err.into())) { @@ -220,7 +220,7 @@ fn read_ron(ron_path: impl AsRef) -> Option where T: serde::de::DeserializeOwned, { - crate::profile_function!(); + profiling::function_scope!(); match std::fs::File::open(ron_path) { Ok(file) => { let reader = std::io::BufReader::new(file); diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 476b1bdd466..f17c6ad5091 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -129,7 +129,7 @@ impl<'app> GlowWinitApp<'app> { native_options: NativeOptions, app_creator: AppCreator<'app>, ) -> Self { - crate::profile_function!(); + profiling::function_scope!(); Self { repaint_proxy: Arc::new(egui::mutex::Mutex::new(event_loop.create_proxy())), app_name: app_name.to_owned(), @@ -146,8 +146,7 @@ impl<'app> GlowWinitApp<'app> { storage: Option<&dyn Storage>, native_options: &mut NativeOptions, ) -> Result<(GlutinWindowContext, egui_glow::Painter)> { - crate::profile_function!(); - + profiling::function_scope!(); let window_settings = epi_integration::load_window_settings(storage); let winit_window_builder = epi_integration::viewport_builder( @@ -172,7 +171,7 @@ impl<'app> GlowWinitApp<'app> { } let gl = unsafe { - crate::profile_scope!("glow::Context::from_loader_function"); + profiling::scope!("glow::Context::from_loader_function"); Arc::new(glow::Context::from_loader_function(|s| { let s = std::ffi::CString::new(s) .expect("failed to construct C string from string for gl proc address"); @@ -195,7 +194,7 @@ impl<'app> GlowWinitApp<'app> { &mut self, event_loop: &ActiveEventLoop, ) -> Result<&mut GlowWinitRunning<'app>> { - crate::profile_function!(); + profiling::function_scope!(); let storage = if let Some(file) = &self.native_options.persistence_path { epi_integration::create_storage_with_file(file) @@ -308,7 +307,7 @@ impl<'app> GlowWinitApp<'app> { raw_display_handle: window.display_handle().map(|h| h.as_raw()), raw_window_handle: window.window_handle().map(|h| h.as_raw()), }; - crate::profile_scope!("app_creator"); + profiling::scope!("app_creator"); app_creator(&cc).map_err(crate::Error::AppCreation)? }; @@ -369,7 +368,7 @@ impl<'app> WinitApp for GlowWinitApp<'app> { fn save_and_destroy(&mut self) { if let Some(mut running) = self.running.take() { - crate::profile_function!(); + profiling::function_scope!(); running.integration.save( running.app.as_mut(), @@ -486,7 +485,7 @@ impl<'app> GlowWinitRunning<'app> { event_loop: &ActiveEventLoop, window_id: WindowId, ) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let Some(viewport_id) = self .glutin @@ -498,8 +497,7 @@ impl<'app> GlowWinitRunning<'app> { return Ok(EventResult::Wait); }; - #[cfg(feature = "puffin")] - puffin::GlobalProfiler::lock().new_frame(); + profiling::finish_frame!(); let mut frame_timer = crate::stopwatch::Stopwatch::new(); frame_timer.start(); @@ -661,13 +659,14 @@ impl<'app> GlowWinitRunning<'app> { { for action in viewport.actions_requested.drain() { match action { - ActionRequested::Screenshot => { + ActionRequested::Screenshot(user_data) => { let screenshot = painter.read_screen_rgba(screen_size_in_pixels); egui_winit .egui_input_mut() .events .push(egui::Event::Screenshot { viewport_id, + user_data, image: screenshot.into(), }); } @@ -697,7 +696,7 @@ impl<'app> GlowWinitRunning<'app> { { // vsync - don't count as frame-time: frame_timer.pause(); - crate::profile_scope!("swap_buffers"); + profiling::scope!("swap_buffers"); let context = current_gl_context .as_ref() .ok_or(egui_glow::PainterError::from( @@ -725,7 +724,7 @@ impl<'app> GlowWinitRunning<'app> { if window.is_minimized() == Some(true) { // On Mac, a minimized Window uses up all CPU: // https://github.com/emilk/egui/issues/325 - crate::profile_scope!("minimized_sleep"); + profiling::scope!("minimized_sleep"); std::thread::sleep(std::time::Duration::from_millis(10)); } @@ -856,7 +855,7 @@ fn change_gl_context( not_current_gl_context: &mut Option, gl_surface: &glutin::surface::Surface, ) { - crate::profile_function!(); + profiling::function_scope!(); if !cfg!(target_os = "windows") { // According to https://github.com/emilk/egui/issues/4289 @@ -865,7 +864,7 @@ fn change_gl_context( // See https://github.com/emilk/egui/issues/4173 if let Some(current_gl_context) = current_gl_context { - crate::profile_scope!("is_current"); + profiling::scope!("is_current"); if gl_surface.is_current(current_gl_context) { return; // Early-out to save a lot of time. } @@ -875,7 +874,7 @@ fn change_gl_context( let not_current = if let Some(not_current_context) = not_current_gl_context.take() { not_current_context } else { - crate::profile_scope!("make_not_current"); + profiling::scope!("make_not_current"); current_gl_context .take() .unwrap() @@ -883,7 +882,7 @@ fn change_gl_context( .unwrap() }; - crate::profile_scope!("make_current"); + profiling::scope!("make_current"); *current_gl_context = Some(not_current.make_current(gl_surface).unwrap()); } @@ -895,7 +894,7 @@ impl GlutinWindowContext { native_options: &NativeOptions, event_loop: &ActiveEventLoop, ) -> Result { - crate::profile_function!(); + profiling::function_scope!(); // There is a lot of complexity with opengl creation, // so prefer extensive logging to get all the help we can to debug issues. @@ -941,7 +940,9 @@ impl GlutinWindowContext { // Create GL display. This may probably create a window too on most platforms. Definitely on `MS windows`. Never on Android. let display_builder = glutin_winit::DisplayBuilder::new() // we might want to expose this option to users in the future. maybe using an env var or using native_options. - .with_preference(glutin_winit::ApiPreference::FallbackEgl) // https://github.com/emilk/egui/issues/2520#issuecomment-1367841150 + // + // The justification for FallbackEgl over PreferEgl is at https://github.com/emilk/egui/pull/2526#issuecomment-1400229576 . + .with_preference(glutin_winit::ApiPreference::FallbackEgl) .with_window_attributes(Some(egui_winit::create_winit_window_attributes( egui_ctx, event_loop, @@ -949,7 +950,7 @@ impl GlutinWindowContext { ))); let (window, gl_config) = { - crate::profile_scope!("DisplayBuilder::build"); + profiling::scope!("DisplayBuilder::build"); display_builder .build( @@ -992,7 +993,7 @@ impl GlutinWindowContext { .build(glutin_raw_window_handle); let gl_context_result = unsafe { - crate::profile_scope!("create_context"); + profiling::scope!("create_context"); gl_config .display() .create_context(&gl_config, &context_attributes) @@ -1067,7 +1068,7 @@ impl GlutinWindowContext { /// /// Errors will be logged. fn initialize_all_windows(&mut self, event_loop: &ActiveEventLoop) { - crate::profile_function!(); + profiling::function_scope!(); let viewports: Vec = self.viewports.keys().copied().collect(); @@ -1085,7 +1086,7 @@ impl GlutinWindowContext { viewport_id: ViewportId, event_loop: &ActiveEventLoop, ) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let viewport = self .viewports @@ -1265,7 +1266,7 @@ impl GlutinWindowContext { egui_ctx: &egui::Context, viewport_output: &ViewportIdMap, ) { - crate::profile_function!(); + profiling::function_scope!(); for ( viewport_id, @@ -1326,7 +1327,7 @@ fn initialize_or_update_viewport( mut builder: ViewportBuilder, viewport_ui_cb: Option>, ) -> &mut Viewport { - crate::profile_function!(); + profiling::function_scope!(); if builder.icon.is_none() { // Inherit icon from parent @@ -1390,7 +1391,7 @@ fn render_immediate_viewport( beginning: Instant, immediate_viewport: ImmediateViewport<'_>, ) { - crate::profile_function!(); + profiling::function_scope!(); let ImmediateViewport { ids, @@ -1513,7 +1514,7 @@ fn render_immediate_viewport( ); { - crate::profile_scope!("swap_buffers"); + profiling::scope!("swap_buffers"); if let Err(err) = gl_surface.swap_buffers(current_gl_context) { log::error!("swap_buffers failed: {err}"); } diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 7964a1e9ad4..e328877a4be 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -17,14 +17,25 @@ use crate::{ // ---------------------------------------------------------------------------- fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result> { - crate::profile_function!(); + #[cfg(target_os = "android")] + use winit::platform::android::EventLoopBuilderExtAndroid as _; + + profiling::function_scope!(); let mut builder = winit::event_loop::EventLoop::with_user_event(); + #[cfg(target_os = "android")] + let mut builder = + builder.with_android_app(native_options.android_app.take().ok_or_else(|| { + crate::Error::AppCreation(Box::from( + "`NativeOptions` is missing required `android_app`", + )) + })?); + if let Some(hook) = std::mem::take(&mut native_options.event_loop_builder) { hook(&mut builder); } - crate::profile_scope!("EventLoopBuilder::build"); + profiling::scope!("EventLoopBuilder::build"); Ok(builder.build()?) } @@ -37,7 +48,7 @@ fn with_event_loop( mut native_options: epi::NativeOptions, f: impl FnOnce(&mut EventLoop, epi::NativeOptions) -> R, ) -> Result { - thread_local!(static EVENT_LOOP: std::cell::RefCell>> = std::cell::RefCell::new(None)); + thread_local!(static EVENT_LOOP: std::cell::RefCell>> = const { std::cell::RefCell::new(None) }); EVENT_LOOP.with(|event_loop| { // Since we want to reference NativeOptions when creating the EventLoop we can't @@ -175,7 +186,7 @@ impl WinitAppWrapper { impl ApplicationHandler for WinitAppWrapper { fn suspended(&mut self, event_loop: &ActiveEventLoop) { - crate::profile_function!("Event::Suspended"); + profiling::scope!("Event::Suspended"); event_loop_context::with_event_loop_context(event_loop, move || { let event_result = self.winit_app.suspended(event_loop); @@ -184,7 +195,7 @@ impl ApplicationHandler for WinitAppWrapper { } fn resumed(&mut self, event_loop: &ActiveEventLoop) { - crate::profile_function!("Event::Resumed"); + profiling::scope!("Event::Resumed"); // Nb: Make sure this guard is dropped after this function returns. event_loop_context::with_event_loop_context(event_loop, move || { @@ -208,7 +219,7 @@ impl ApplicationHandler for WinitAppWrapper { device_id: winit::event::DeviceId, event: winit::event::DeviceEvent, ) { - crate::profile_function!(egui_winit::short_device_event_description(&event)); + profiling::function_scope!(egui_winit::short_device_event_description(&event)); // Nb: Make sure this guard is dropped after this function returns. event_loop_context::with_event_loop_context(event_loop, move || { @@ -218,7 +229,7 @@ impl ApplicationHandler for WinitAppWrapper { } fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) { - crate::profile_function!(match &event { + profiling::function_scope!(match &event { UserEvent::RequestRepaint { .. } => "UserEvent::RequestRepaint", #[cfg(feature = "accesskit")] UserEvent::AccessKitActionRequest(_) => "UserEvent::AccessKitActionRequest", @@ -274,7 +285,7 @@ impl ApplicationHandler for WinitAppWrapper { window_id: WindowId, event: winit::event::WindowEvent, ) { - crate::profile_function!(egui_winit::short_window_event_description(&event)); + profiling::function_scope!(egui_winit::short_window_event_description(&event)); // Nb: Make sure this guard is dropped after this function returns. event_loop_context::with_event_loop_context(event_loop, move || { diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index 997383f85c3..93643c2230e 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -102,7 +102,7 @@ impl<'app> WgpuWinitApp<'app> { native_options: NativeOptions, app_creator: AppCreator<'app>, ) -> Self { - crate::profile_function!(); + profiling::function_scope!(); #[cfg(feature = "__screenshot")] assert!( @@ -181,10 +181,10 @@ impl<'app> WgpuWinitApp<'app> { window: Window, builder: ViewportBuilder, ) -> crate::Result<&mut WgpuWinitRunning<'app>> { - crate::profile_function!(); - + profiling::function_scope!(); #[allow(unsafe_code, unused_mut, unused_unsafe)] let mut painter = egui_wgpu::winit::Painter::new( + egui_ctx.clone(), self.native_options.wgpu_options.clone(), self.native_options.multisampling.max(1) as _, egui_wgpu::depth_format_from_bits( @@ -198,7 +198,7 @@ impl<'app> WgpuWinitApp<'app> { let window = Arc::new(window); { - crate::profile_scope!("set_window"); + profiling::scope!("set_window"); pollster::block_on(painter.set_window(ViewportId::ROOT, Some(window.clone())))?; } @@ -267,7 +267,7 @@ impl<'app> WgpuWinitApp<'app> { raw_window_handle: window.window_handle().map(|h| h.as_raw()), }; let app = { - crate::profile_scope!("user_app_creator"); + profiling::scope!("user_app_creator"); app_creator(&cc).map_err(crate::Error::AppCreation)? }; @@ -489,7 +489,7 @@ impl<'app> WinitApp for WgpuWinitApp<'app> { impl<'app> WgpuWinitRunning<'app> { fn save_and_destroy(&mut self) { - crate::profile_function!(); + profiling::function_scope!(); let mut shared = self.shared.borrow_mut(); if let Some(Viewport { window, .. }) = shared.viewports.get(&ViewportId::ROOT) { @@ -507,7 +507,7 @@ impl<'app> WgpuWinitRunning<'app> { /// This is called both for the root viewport, and all deferred viewports fn run_ui_and_paint(&mut self, window_id: WindowId) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let Some(viewport_id) = self .shared @@ -519,8 +519,7 @@ impl<'app> WgpuWinitRunning<'app> { return Ok(EventResult::Wait); }; - #[cfg(feature = "puffin")] - puffin::GlobalProfiler::lock().new_frame(); + profiling::finish_frame!(); let Self { app, @@ -532,7 +531,7 @@ impl<'app> WgpuWinitRunning<'app> { frame_timer.start(); let (viewport_ui_cb, raw_input) = { - crate::profile_scope!("Prepare"); + profiling::scope!("Prepare"); let mut shared_lock = shared.borrow_mut(); let SharedState { @@ -576,7 +575,7 @@ impl<'app> WgpuWinitRunning<'app> { egui_winit::update_viewport_info(info, &integration.egui_ctx, window, false); { - crate::profile_scope!("set_window"); + profiling::scope!("set_window"); pollster::block_on(painter.set_window(viewport_id, Some(window.clone())))?; } @@ -593,6 +592,8 @@ impl<'app> WgpuWinitRunning<'app> { .map(|(id, viewport)| (*id, viewport.info.clone())) .collect(); + painter.handle_screenshots(&mut raw_input.events); + (viewport_ui_cb, raw_input) }; @@ -643,31 +644,27 @@ impl<'app> WgpuWinitRunning<'app> { let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); - let screenshot_requested = viewport - .actions_requested - .take(&ActionRequested::Screenshot) - .is_some(); - let (vsync_secs, screenshot) = painter.paint_and_update_textures( + let mut screenshot_commands = vec![]; + viewport.actions_requested.retain(|cmd| { + if let ActionRequested::Screenshot(info) = cmd { + screenshot_commands.push(info.clone()); + false + } else { + true + } + }); + let vsync_secs = painter.paint_and_update_textures( viewport_id, pixels_per_point, app.clear_color(&egui_ctx.style().visuals), &clipped_primitives, &textures_delta, - screenshot_requested, + screenshot_commands, ); - if let Some(screenshot) = screenshot { - egui_winit - .egui_input_mut() - .events - .push(egui::Event::Screenshot { - viewport_id, - image: screenshot.into(), - }); - } for action in viewport.actions_requested.drain() { match action { - ActionRequested::Screenshot => { + ActionRequested::Screenshot { .. } => { // already handled above } ActionRequested::Cut => { @@ -720,7 +717,7 @@ impl<'app> WgpuWinitRunning<'app> { if window.is_minimized() == Some(true) { // On Mac, a minimized Window uses up all CPU: // https://github.com/emilk/egui/issues/325 - crate::profile_scope!("minimized_sleep"); + profiling::scope!("minimized_sleep"); std::thread::sleep(std::time::Duration::from_millis(10)); } } @@ -847,7 +844,7 @@ impl Viewport { return; // we already have one } - crate::profile_function!(); + profiling::function_scope!(); let viewport_id = self.ids.this; @@ -888,7 +885,7 @@ fn create_window( storage: Option<&dyn Storage>, native_options: &mut NativeOptions, ) -> Result<(Window, ViewportBuilder), winit::error::OsError> { - crate::profile_function!(); + profiling::function_scope!(); let window_settings = epi_integration::load_window_settings(storage); let viewport_builder = epi_integration::viewport_builder( @@ -909,7 +906,7 @@ fn render_immediate_viewport( shared: &RefCell, immediate_viewport: ImmediateViewport<'_>, ) { - crate::profile_function!(); + profiling::function_scope!(); let ImmediateViewport { ids, @@ -989,7 +986,7 @@ fn render_immediate_viewport( }; { - crate::profile_scope!("set_window"); + profiling::scope!("set_window"); if let Err(err) = pollster::block_on(painter.set_window(ids.this, Some(window.clone()))) { log::error!( "when rendering viewport_id={:?}, set_window Error {err}", @@ -1005,7 +1002,7 @@ fn render_immediate_viewport( [0.0, 0.0, 0.0, 0.0], &clipped_primitives, &textures_delta, - false, + vec![], ); egui_winit.handle_platform_output(window, platform_output); @@ -1097,7 +1094,7 @@ fn initialize_or_update_viewport<'a>( viewport_ui_cb: Option>, painter: &mut egui_wgpu::winit::Painter, ) -> &'a mut Viewport { - crate::profile_function!(); + profiling::function_scope!(); if builder.icon.is_none() { // Inherit icon from parent diff --git a/crates/eframe/src/native/winit_integration.rs b/crates/eframe/src/native/winit_integration.rs index e9d214103b8..2b6c54a67f9 100644 --- a/crates/eframe/src/native/winit_integration.rs +++ b/crates/eframe/src/native/winit_integration.rs @@ -11,7 +11,7 @@ use egui_winit::accesskit_winit; /// Create an egui context, restoring it from storage if possible. pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context { - crate::profile_function!(); + profiling::function_scope!(); pub const IS_DESKTOP: bool = cfg!(any( target_os = "freebsd", diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 00cc8f0c182..13ad762874a 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -1,4 +1,4 @@ -use egui::TexturesDelta; +use egui::{TexturesDelta, UserData, ViewportCommand}; use crate::{epi, App}; @@ -16,6 +16,10 @@ pub struct AppRunner { last_save_time: f64, pub(crate) text_agent: TextAgent, + // If not empty, the painter should capture n frames from now. + // zero means capture the exact next frame. + screenshot_commands_with_frame_delay: Vec<(UserData, usize)>, + // Output for the last run: textures_delta: TexturesDelta, clipped_primitives: Option>, @@ -36,7 +40,8 @@ impl AppRunner { app_creator: epi::AppCreator<'static>, text_agent: TextAgent, ) -> Result { - let painter = super::ActiveWebPainter::new(canvas, &web_options).await?; + let egui_ctx = egui::Context::default(); + let painter = super::ActiveWebPainter::new(egui_ctx.clone(), canvas, &web_options).await?; let info = epi::IntegrationInfo { web_info: epi::WebInfo { @@ -47,7 +52,6 @@ impl AppRunner { }; let storage = LocalStorage::default(); - let egui_ctx = egui::Context::default(); egui_ctx.set_os(egui::os::OperatingSystem::from_user_agent( &super::user_agent().unwrap_or_default(), )); @@ -110,6 +114,7 @@ impl AppRunner { needs_repaint, last_save_time: now_sec(), text_agent, + screenshot_commands_with_frame_delay: vec![], textures_delta: Default::default(), clipped_primitives: None, }; @@ -205,6 +210,8 @@ impl AppRunner { pub fn logic(&mut self) { // We sometimes miss blur/focus events due to the text agent, so let's just poll each frame: self.update_focus(); + // We might have received a screenshot + self.painter.handle_screenshots(&mut self.input.raw.events); let canvas_size = super::canvas_size_in_points(self.canvas(), self.egui_ctx()); let mut raw_input = self.input.new_frame(canvas_size); @@ -225,12 +232,20 @@ impl AppRunner { if viewport_output.len() > 1 { log::warn!("Multiple viewports not yet supported on the web"); } - for viewport_output in viewport_output.values() { - for command in &viewport_output.commands { - // TODO(emilk): handle some of the commands - log::warn!( - "Unhandled egui viewport command: {command:?} - not implemented in web backend" - ); + for (_viewport_id, viewport_output) in viewport_output { + for command in viewport_output.commands { + match command { + ViewportCommand::Screenshot(user_data) => { + self.screenshot_commands_with_frame_delay + .push((user_data, 1)); + } + _ => { + // TODO(emilk): handle some of the commands + log::warn!( + "Unhandled egui viewport command: {command:?} - not implemented in web backend" + ); + } + } } } @@ -245,11 +260,27 @@ impl AppRunner { let clipped_primitives = std::mem::take(&mut self.clipped_primitives); if let Some(clipped_primitives) = clipped_primitives { + let mut screenshot_commands = vec![]; + self.screenshot_commands_with_frame_delay + .retain_mut(|(user_data, frame_delay)| { + if *frame_delay == 0 { + screenshot_commands.push(user_data.clone()); + false + } else { + *frame_delay -= 1; + true + } + }); + if !self.screenshot_commands_with_frame_delay.is_empty() { + self.egui_ctx().request_repaint(); + } + if let Err(err) = self.painter.paint_and_update_textures( self.app.clear_color(&self.egui_ctx.style().visuals), &clipped_primitives, self.egui_ctx.pixels_per_point(), &textures_delta, + screenshot_commands, ) { log::error!("Failed to paint: {}", super::string_from_js_value(&err)); } @@ -260,7 +291,7 @@ impl AppRunner { self.frame.info.cpu_usage = Some(cpu_usage_seconds); } - fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) { + fn handle_platform_output(&self, platform_output: egui::PlatformOutput) { #[cfg(feature = "web_screen_reader")] if self.egui_ctx.options(|o| o.screen_reader) { super::screen_reader::speak(&platform_output.events_description()); diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index cdecf3b701e..414e5be2383 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -6,7 +6,7 @@ use super::{ }; use web_sys::EventTarget; -// TODO(emilk): there are more calls to `prevent_default` and `stop_propagaton` +// TODO(emilk): there are more calls to `prevent_default` and `stop_propagation` // than what is probably needed. // ------------------------------------------------------------------------ diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index 746b619fb30..827feb9244f 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -174,6 +174,13 @@ fn set_cursor_icon(cursor: egui::CursorIcon) -> Option<()> { /// Set the clipboard text. fn set_clipboard_text(s: &str) { if let Some(window) = web_sys::window() { + if !window.is_secure_context() { + log::error!( + "Clipboard is not available because we are not in a secure context. \ + See https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts" + ); + return; + } let promise = window.navigator().clipboard().write_text(s); let future = wasm_bindgen_futures::JsFuture::from(promise); let future = async move { diff --git a/crates/eframe/src/web/web_painter.rs b/crates/eframe/src/web/web_painter.rs index b5164b915c0..fe751bf169f 100644 --- a/crates/eframe/src/web/web_painter.rs +++ b/crates/eframe/src/web/web_painter.rs @@ -1,3 +1,4 @@ +use egui::{Event, UserData}; use wasm_bindgen::JsValue; /// Renderer for a browser canvas. @@ -16,14 +17,19 @@ pub(crate) trait WebPainter { fn max_texture_side(&self) -> usize; /// Update all internal textures and paint gui. + /// When `capture` isn't empty, the rendered screen should be captured. + /// Once the screenshot is ready, the screenshot should be returned via [`Self::handle_screenshots`]. fn paint_and_update_textures( &mut self, clear_color: [f32; 4], clipped_primitives: &[egui::ClippedPrimitive], pixels_per_point: f32, textures_delta: &egui::TexturesDelta, + capture: Vec, ) -> Result<(), JsValue>; + fn handle_screenshots(&mut self, events: &mut Vec); + /// Destroy all resources. fn destroy(&mut self); } diff --git a/crates/eframe/src/web/web_painter_glow.rs b/crates/eframe/src/web/web_painter_glow.rs index e13cb0018cd..876a6d78e2d 100644 --- a/crates/eframe/src/web/web_painter_glow.rs +++ b/crates/eframe/src/web/web_painter_glow.rs @@ -1,9 +1,10 @@ +use egui::{Event, UserData, ViewportId}; +use egui_glow::glow; +use std::sync::Arc; use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; use web_sys::HtmlCanvasElement; -use egui_glow::glow; - use crate::{WebGlContextOption, WebOptions}; use super::web_painter::WebPainter; @@ -11,6 +12,7 @@ use super::web_painter::WebPainter; pub(crate) struct WebPainterGlow { canvas: HtmlCanvasElement, painter: egui_glow::Painter, + screenshots: Vec<(egui::ColorImage, Vec)>, } impl WebPainterGlow { @@ -18,7 +20,11 @@ impl WebPainterGlow { self.painter.gl() } - pub async fn new(canvas: HtmlCanvasElement, options: &WebOptions) -> Result { + pub async fn new( + _ctx: egui::Context, + canvas: HtmlCanvasElement, + options: &WebOptions, + ) -> Result { let (gl, shader_prefix) = init_glow_context_from_canvas(&canvas, options.webgl_context_option)?; #[allow(clippy::arc_with_non_send_sync)] @@ -27,7 +33,11 @@ impl WebPainterGlow { let painter = egui_glow::Painter::new(gl, shader_prefix, None, options.dithering) .map_err(|err| format!("Error starting glow painter: {err}"))?; - Ok(Self { canvas, painter }) + Ok(Self { + canvas, + painter, + screenshots: Vec::new(), + }) } } @@ -46,6 +56,7 @@ impl WebPainter for WebPainterGlow { clipped_primitives: &[egui::ClippedPrimitive], pixels_per_point: f32, textures_delta: &egui::TexturesDelta, + capture: Vec, ) -> Result<(), JsValue> { let canvas_dimension = [self.canvas.width(), self.canvas.height()]; @@ -57,6 +68,11 @@ impl WebPainter for WebPainterGlow { self.painter .paint_primitives(canvas_dimension, pixels_per_point, clipped_primitives); + if !capture.is_empty() { + let image = self.painter.read_screen_rgba(canvas_dimension); + self.screenshots.push((image, capture)); + } + for &id in &textures_delta.free { self.painter.free_texture(id); } @@ -67,6 +83,19 @@ impl WebPainter for WebPainterGlow { fn destroy(&mut self) { self.painter.destroy(); } + + fn handle_screenshots(&mut self, events: &mut Vec) { + for (image, data) in self.screenshots.drain(..) { + let image = Arc::new(image); + for data in data { + events.push(Event::Screenshot { + viewport_id: ViewportId::default(), + image: image.clone(), + user_data: data, + }); + } + } + } } /// Returns glow context and shader prefix. diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index bbf38e06a15..591d4224d3b 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -1,44 +1,13 @@ use std::sync::Arc; -use raw_window_handle::{ - DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, - RawWindowHandle, WebDisplayHandle, WebWindowHandle, WindowHandle, -}; +use super::web_painter::WebPainter; +use crate::WebOptions; +use egui::{Event, UserData, ViewportId}; +use egui_wgpu::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState}; +use egui_wgpu::{RenderState, SurfaceErrorAction, WgpuSetup}; use wasm_bindgen::JsValue; use web_sys::HtmlCanvasElement; -use egui_wgpu::{RenderState, SurfaceErrorAction}; - -use crate::WebOptions; - -use super::web_painter::WebPainter; - -struct EguiWebWindow(u32); - -#[allow(unsafe_code)] -impl HasWindowHandle for EguiWebWindow { - fn window_handle(&self) -> Result, HandleError> { - // SAFETY: there is no lifetime here. - unsafe { - Ok(WindowHandle::borrow_raw(RawWindowHandle::Web( - WebWindowHandle::new(self.0), - ))) - } - } -} - -#[allow(unsafe_code)] -impl HasDisplayHandle for EguiWebWindow { - fn display_handle(&self) -> Result, HandleError> { - // SAFETY: there is no lifetime here. - unsafe { - Ok(DisplayHandle::borrow_raw(RawDisplayHandle::Web( - WebDisplayHandle::new(), - ))) - } - } -} - pub(crate) struct WebPainterWgpu { canvas: HtmlCanvasElement, surface: wgpu::Surface<'static>, @@ -47,6 +16,10 @@ pub(crate) struct WebPainterWgpu { on_surface_error: Arc SurfaceErrorAction>, depth_format: Option, depth_texture_view: Option, + screen_capture_state: Option, + capture_tx: CaptureSender, + capture_rx: CaptureReceiver, + ctx: egui::Context, } impl WebPainterWgpu { @@ -84,86 +57,54 @@ impl WebPainterWgpu { #[allow(unused)] // only used if `wgpu` is the only active feature. pub async fn new( + ctx: egui::Context, canvas: web_sys::HtmlCanvasElement, options: &WebOptions, ) -> Result { log::debug!("Creating wgpu painter"); - let mut backends = options.wgpu_options.supported_backends; - - // Don't try WebGPU if we're not in a secure context. - if backends.contains(wgpu::Backends::BROWSER_WEBGPU) { - let is_secure_context = web_sys::window().map_or(false, |w| w.is_secure_context()); - if !is_secure_context { - log::info!( - "WebGPU is only available in secure contexts, i.e. on HTTPS and on localhost." - ); - - // Don't try WebGPU since we established now that it will fail. - backends.remove(wgpu::Backends::BROWSER_WEBGPU); - - if backends.is_empty() { - return Err("No available supported graphics backends.".to_owned()); + let instance = match &options.wgpu_options.wgpu_setup { + WgpuSetup::CreateNew { + supported_backends: backends, + power_preference, + .. + } => { + let mut backends = *backends; + + // Don't try WebGPU if we're not in a secure context. + if backends.contains(wgpu::Backends::BROWSER_WEBGPU) { + let is_secure_context = + web_sys::window().map_or(false, |w| w.is_secure_context()); + if !is_secure_context { + log::info!( + "WebGPU is only available in secure contexts, i.e. on HTTPS and on localhost." + ); + + // Don't try WebGPU since we established now that it will fail. + backends.remove(wgpu::Backends::BROWSER_WEBGPU); + + if backends.is_empty() { + return Err("No available supported graphics backends.".to_owned()); + } + } } - } - } - log::debug!("Creating wgpu instance with backends {:?}", backends); - let mut instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends, - ..Default::default() - }); - - // It can happen that a browser advertises WebGPU support, but then fails to create a - // suitable adapter. As of writing this happens for example on Linux with Chrome 121. - // - // Since WebGPU is handled in a special way in wgpu, we have to recreate the instance - // if we instead want to try with WebGL. - // - // To make matters worse, once a canvas has been used with either WebGL or WebGPU, - // we can't go back and change that without replacing the canvas (which is hard to do from here). - // Therefore, we have to create the surface *after* requesting the adapter. - // However, wgpu offers to pass in a surface on adapter creation to ensure it is actually compatible with the chosen backend. - // This in turn isn't all that important on the web, but it still makes sense for the design of - // `egui::RenderState`! - // Therefore, we have to first check if it's possible to create a WebGPU adapter, - // and if it is not, start over with a WebGL instance. - // - // Note that we also might needlessly try this here if wgpu already determined that there's no - // WebGPU support in the first place. This is not a huge problem since it fails very fast, but - // it would be nice to avoid this. See https://github.com/gfx-rs/wgpu/issues/5142 - if backends.contains(wgpu::Backends::BROWSER_WEBGPU) { - log::debug!("Attempting to create WebGPU adapter to check for support."); - if let Some(adapter) = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: options.wgpu_options.power_preference, - compatible_surface: None, - force_fallback_adapter: false, - }) - .await - { - // WebGPU doesn't spec yet a destroy on the adapter, only on the device. - //adapter.destroy(); - log::debug!( - "Successfully created WebGPU adapter, WebGPU confirmed to be supported!" - ); - } else { - log::debug!("Failed to create WebGPU adapter."); + log::debug!("Creating wgpu instance with backends {:?}", backends); - if backends.contains(wgpu::Backends::GL) { - log::debug!("Recreating wgpu instance with WebGL backend only."); - backends = wgpu::Backends::GL; - instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + let instance = + wgpu::util::new_instance_with_webgpu_detection(wgpu::InstanceDescriptor { backends, ..Default::default() - }); - } else { - return Err( - "Failed to create WebGPU adapter and WebGL was not enabled.".to_owned() - ); - } + }) + .await; + + // 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)] + Arc::new(instance) } - } + WgpuSetup::Existing { instance, .. } => instance.clone(), + }; let surface = instance .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone())) @@ -182,17 +123,21 @@ impl WebPainterWgpu { .await .map_err(|err| err.to_string())?; + let default_configuration = surface + .get_default_config(&render_state.adapter, 0, 0) // Width/height is set later. + .ok_or("The surface isn't supported by this adapter")?; + let surface_configuration = wgpu::SurfaceConfiguration { format: render_state.target_format, present_mode: options.wgpu_options.present_mode, view_formats: vec![render_state.target_format], - ..surface - .get_default_config(&render_state.adapter, 0, 0) // Width/height is set later. - .ok_or("The surface isn't supported by this adapter")? + ..default_configuration }; log::debug!("wgpu painter initialized."); + let (capture_tx, capture_rx) = capture_channel(); + Ok(Self { canvas, render_state: Some(render_state), @@ -201,6 +146,10 @@ impl WebPainterWgpu { depth_format, depth_texture_view: None, on_surface_error: options.wgpu_options.on_surface_error.clone(), + screen_capture_state: None, + capture_tx, + capture_rx, + ctx, }) } } @@ -222,7 +171,10 @@ impl WebPainter for WebPainterWgpu { clipped_primitives: &[egui::ClippedPrimitive], pixels_per_point: f32, textures_delta: &egui::TexturesDelta, + capture_data: Vec, ) -> Result<(), JsValue> { + let capture = !capture_data.is_empty(); + let size_in_pixels = [self.canvas.width(), self.canvas.height()]; let Some(render_state) = &self.render_state else { @@ -266,7 +218,7 @@ impl WebPainter for WebPainterWgpu { // Resize surface if needed let is_zero_sized_surface = size_in_pixels[0] == 0 || size_in_pixels[1] == 0; - let frame = if is_zero_sized_surface { + let frame_and_capture_buffer = if is_zero_sized_surface { None } else { if size_in_pixels[0] != self.surface_configuration.width @@ -283,7 +235,7 @@ impl WebPainter for WebPainterWgpu { ); } - let frame = match self.surface.get_current_texture() { + let output_frame = match self.surface.get_current_texture() { Ok(frame) => frame, Err(err) => match (*self.on_surface_error)(err) { SurfaceErrorAction::RecreateSurface => { @@ -299,12 +251,23 @@ impl WebPainter for WebPainterWgpu { { let renderer = render_state.renderer.read(); - let frame_view = frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); + + let target_texture = if capture { + let capture_state = self.screen_capture_state.get_or_insert_with(|| { + CaptureState::new(&render_state.device, &output_frame.texture) + }); + capture_state.update(&render_state.device, &output_frame.texture); + + &capture_state.texture + } else { + &output_frame.texture + }; + let target_view = + target_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &frame_view, + view: &target_view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { @@ -343,7 +306,19 @@ impl WebPainter for WebPainterWgpu { ); } - Some(frame) + let mut capture_buffer = None; + + if capture { + if let Some(capture_state) = &mut self.screen_capture_state { + capture_buffer = Some(capture_state.copy_textures( + &render_state.device, + &output_frame, + &mut encoder, + )); + } + }; + + Some((output_frame, capture_buffer)) }; { @@ -358,13 +333,38 @@ impl WebPainter for WebPainterWgpu { .queue .submit(user_cmd_bufs.into_iter().chain([encoder.finish()])); - if let Some(frame) = frame { + if let Some((frame, capture_buffer)) = frame_and_capture_buffer { + if let Some(capture_buffer) = capture_buffer { + if let Some(capture_state) = &self.screen_capture_state { + capture_state.read_screen_rgba( + self.ctx.clone(), + capture_buffer, + capture_data, + self.capture_tx.clone(), + ViewportId::ROOT, + ); + } + } + frame.present(); } Ok(()) } + fn handle_screenshots(&mut self, events: &mut Vec) { + for (viewport_id, user_data, screenshot) in self.capture_rx.try_iter() { + let screenshot = Arc::new(screenshot); + for data in user_data { + events.push(Event::Screenshot { + viewport_id, + user_data: data, + image: screenshot.clone(), + }); + } + } + } + fn destroy(&mut self) { self.render_state = None; } diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs index a2793a13aeb..6cbc371f34c 100644 --- a/crates/eframe/src/web/web_runner.rs +++ b/crates/eframe/src/web/web_runner.rs @@ -6,7 +6,7 @@ use crate::{epi, App}; use super::{events, text_agent::TextAgent, AppRunner, PanicHandler}; -/// This is how `eframe` runs your wepp application +/// This is how `eframe` runs your web application /// /// This is cheap to clone. /// @@ -16,7 +16,7 @@ pub struct WebRunner { /// Have we ever panicked? panic_handler: PanicHandler, - /// If we ever panic during running, this RefCell is poisoned. + /// If we ever panic during running, this `RefCell` is poisoned. /// So before we use it, we need to check [`Self::panic_handler`]. runner: Rc>>, diff --git a/crates/egui-wgpu/CHANGELOG.md b/crates/egui-wgpu/CHANGELOG.md index e254517b1c9..d92e867a77e 100644 --- a/crates/egui-wgpu/CHANGELOG.md +++ b/crates/egui-wgpu/CHANGELOG.md @@ -6,6 +6,18 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.30.0 - 2024-12-16 +* Fix docs.rs build [#5204](https://github.com/emilk/egui/pull/5204) by [@lucasmerlin](https://github.com/lucasmerlin) +* Free textures after submitting queue instead of before with wgpu renderer [#5226](https://github.com/emilk/egui/pull/5226) by [@Rusty-Cube](https://github.com/Rusty-Cube) +* Add option to initialize with existing wgpu instance/adapter/device/queue [#5319](https://github.com/emilk/egui/pull/5319) by [@ArthurBrussee](https://github.com/ArthurBrussee) +* Updare to `wgpu` 23.0.0 and `wasm-bindgen` to 0.2.95 [#5330](https://github.com/emilk/egui/pull/5330) by [@torokati44](https://github.com/torokati44) +* Support wgpu-tracing with same mechanism as wgpu examples [#5450](https://github.com/emilk/egui/pull/5450) by [@EriKWDev](https://github.com/EriKWDev) + + +## 0.29.1 - 2024-10-01 +Nothing new + + ## 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) diff --git a/crates/egui-wgpu/Cargo.toml b/crates/egui-wgpu/Cargo.toml index 2cc9852921f..2f4330236d0 100644 --- a/crates/egui-wgpu/Cargo.toml +++ b/crates/egui-wgpu/Cargo.toml @@ -33,11 +33,8 @@ rustdoc-args = ["--generate-link-to-definition"] [features] default = ["fragile-send-sync-non-atomic-wasm"] -## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -puffin = ["dep:puffin"] - ## Enable [`winit`](https://docs.rs/winit) integration. On Linux, requires either `wayland` or `x11` -winit = ["dep:winit"] +winit = ["dep:winit", "winit/rwh_06"] ## Enables Wayland support for winit. wayland = ["winit?/wayland"] @@ -60,15 +57,12 @@ ahash.workspace = true bytemuck.workspace = true document-features.workspace = true log.workspace = true +profiling.workspace = true thiserror.workspace = true -type-map = "0.5.0" +type-map.workspace = true web-time.workspace = true wgpu = { workspace = true, features = ["wgsl"] } # Optional dependencies: winit = { workspace = true, optional = true, default-features = false } - -# Native: -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -puffin = { workspace = true, optional = true } diff --git a/crates/egui-wgpu/src/capture.rs b/crates/egui-wgpu/src/capture.rs new file mode 100644 index 00000000000..1ce780d054f --- /dev/null +++ b/crates/egui-wgpu/src/capture.rs @@ -0,0 +1,257 @@ +use egui::{UserData, ViewportId}; +use epaint::ColorImage; +use std::sync::{mpsc, Arc}; +use wgpu::{BindGroupLayout, MultisampleState, StoreOp}; + +/// A texture and a buffer for reading the rendered frame back to the cpu. +/// The texture is required since [`wgpu::TextureUsages::COPY_SRC`] is not an allowed +/// flag for the surface texture on all platforms. This means that anytime we want to +/// capture the frame, we first render it to this texture, and then we can copy it to +/// both the surface texture (via a render pass) and the buffer (via a texture to buffer copy), +/// from where we can pull it back +/// to the cpu. +pub struct CaptureState { + padding: BufferPadding, + pub texture: wgpu::Texture, + pipeline: wgpu::RenderPipeline, + bind_group: wgpu::BindGroup, +} + +pub type CaptureReceiver = mpsc::Receiver<(ViewportId, Vec, ColorImage)>; +pub type CaptureSender = mpsc::Sender<(ViewportId, Vec, ColorImage)>; +pub use mpsc::channel as capture_channel; + +impl CaptureState { + pub fn new(device: &wgpu::Device, surface_texture: &wgpu::Texture) -> Self { + let shader = device.create_shader_module(wgpu::include_wgsl!("texture_copy.wgsl")); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("texture_copy"), + layout: None, + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + compilation_options: Default::default(), + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + compilation_options: Default::default(), + targets: &[Some(surface_texture.format().into())], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: None, + multisample: MultisampleState::default(), + multiview: None, + cache: None, + }); + + let bind_group_layout = pipeline.get_bind_group_layout(0); + + let (texture, padding, bind_group) = + Self::create_texture(device, surface_texture, &bind_group_layout); + + Self { + padding, + texture, + pipeline, + bind_group, + } + } + + fn create_texture( + device: &wgpu::Device, + surface_texture: &wgpu::Texture, + layout: &BindGroupLayout, + ) -> (wgpu::Texture, BufferPadding, wgpu::BindGroup) { + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("egui_screen_capture_texture"), + size: surface_texture.size(), + mip_level_count: surface_texture.mip_level_count(), + sample_count: surface_texture.sample_count(), + dimension: surface_texture.dimension(), + format: surface_texture.format(), + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + + let padding = BufferPadding::new(surface_texture.width()); + + let view = texture.create_view(&Default::default()); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&view), + }], + label: None, + }); + + (texture, padding, bind_group) + } + + /// Updates the [`CaptureState`] if the size of the surface texture has changed + pub fn update(&mut self, device: &wgpu::Device, texture: &wgpu::Texture) { + if self.texture.size() != texture.size() { + let (new_texture, padding, bind_group) = + Self::create_texture(device, texture, &self.pipeline.get_bind_group_layout(0)); + self.texture = new_texture; + self.padding = padding; + self.bind_group = bind_group; + } + } + + /// Handles copying from the [`CaptureState`] texture to the surface texture and the buffer. + /// Pass the returned buffer to [`CaptureState::read_screen_rgba`] to read the data back to the cpu. + pub fn copy_textures( + &mut self, + device: &wgpu::Device, + output_frame: &wgpu::SurfaceTexture, + encoder: &mut wgpu::CommandEncoder, + ) -> wgpu::Buffer { + debug_assert_eq!( + self.texture.size(), + output_frame.texture.size(), + "Texture sizes must match, `CaptureState::update` was probably not called" + ); + + // It would be more efficient to reuse the Buffer, e.g. via some kind of ring buffer, but + // for most screenshot use cases this should be fine. When taking many screenshots (e.g. for a video) + // it might make sense to revisit this and implement a more efficient solution. + #[allow(clippy::arc_with_non_send_sync)] + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("egui_screen_capture_buffer"), + size: (self.padding.padded_bytes_per_row * self.texture.height()) as u64, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + let padding = self.padding; + let tex = &mut self.texture; + + let tex_extent = tex.size(); + + encoder.copy_texture_to_buffer( + tex.as_image_copy(), + wgpu::ImageCopyBuffer { + buffer: &buffer, + layout: wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(padding.padded_bytes_per_row), + rows_per_image: None, + }, + }, + tex_extent, + ); + + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("texture_copy"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &output_frame.texture.create_view(&Default::default()), + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, &self.bind_group, &[]); + pass.draw(0..3, 0..1); + + buffer + } + + /// Handles copying from the [`CaptureState`] texture to the surface texture and the cpu + /// This function is non-blocking and will send the data to the given sender when it's ready. + /// Pass in the buffer returned from [`CaptureState::copy_textures`]. + /// Make sure to call this after the encoder has been submitted. + pub fn read_screen_rgba( + &self, + ctx: egui::Context, + buffer: wgpu::Buffer, + data: Vec, + tx: CaptureSender, + viewport_id: ViewportId, + ) { + #[allow(clippy::arc_with_non_send_sync)] + let buffer = Arc::new(buffer); + let buffer_clone = buffer.clone(); + let buffer_slice = buffer_clone.slice(..); + let format = self.texture.format(); + let tex_extent = self.texture.size(); + let padding = self.padding; + let to_rgba = match format { + wgpu::TextureFormat::Rgba8Unorm => [0, 1, 2, 3], + wgpu::TextureFormat::Bgra8Unorm => [2, 1, 0, 3], + _ => { + log::error!("Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {:?}", format); + return; + } + }; + buffer_slice.map_async(wgpu::MapMode::Read, move |result| { + if let Err(err) = result { + log::error!("Failed to map buffer for reading: {:?}", err); + return; + } + let buffer_slice = buffer.slice(..); + + let mut pixels = Vec::with_capacity((tex_extent.width * tex_extent.height) as usize); + for padded_row in buffer_slice + .get_mapped_range() + .chunks(padding.padded_bytes_per_row as usize) + { + let row = &padded_row[..padding.unpadded_bytes_per_row as usize]; + for color in row.chunks(4) { + pixels.push(epaint::Color32::from_rgba_premultiplied( + color[to_rgba[0]], + color[to_rgba[1]], + color[to_rgba[2]], + color[to_rgba[3]], + )); + } + } + buffer.unmap(); + + tx.send(( + viewport_id, + data, + ColorImage { + size: [tex_extent.width as usize, tex_extent.height as usize], + pixels, + }, + )) + .ok(); + ctx.request_repaint(); + }); + } +} + +#[derive(Copy, Clone)] +struct BufferPadding { + unpadded_bytes_per_row: u32, + padded_bytes_per_row: u32, +} + +impl BufferPadding { + fn new(width: u32) -> Self { + let bytes_per_pixel = std::mem::size_of::() as u32; + let unpadded_bytes_per_row = width * bytes_per_pixel; + let padded_bytes_per_row = + wgpu::util::align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT); + Self { + unpadded_bytes_per_row, + padded_bytes_per_row, + } + } +} diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index d5c2d309ba3..a91662f45b1 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -8,8 +8,8 @@ //! wgpu = { version = "*", features = ["webgpu", "webgl"] } //! ``` //! -//! You can control whether WebGL or WebGPU will be picked at runtime by setting -//! [`WgpuConfiguration::supported_backends`]. +//! You can control whether WebGL or WebGPU will be picked at runtime by configuring +//! [`WgpuConfiguration::wgpu_setup`]. //! The default is to prefer WebGPU and fall back on WebGL. //! //! ## Feature flags @@ -24,6 +24,10 @@ pub use wgpu; mod renderer; pub use renderer::*; +use wgpu::{Adapter, Device, Instance, Queue}; + +/// Helpers for capturing screenshots of the UI. +pub mod capture; /// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`]. #[cfg(feature = "winit")] @@ -92,79 +96,102 @@ impl RenderState { msaa_samples: u32, dithering: bool, ) -> Result { - crate::profile_scope!("RenderState::create"); // async yield give bad names using `profile_function` + profiling::scope!("RenderState::create"); // async yield give bad names using `profile_function` // This is always an empty list on web. #[cfg(not(target_arch = "wasm32"))] let available_adapters = instance.enumerate_adapters(wgpu::Backends::all()); - let adapter = { - crate::profile_scope!("request_adapter"); - instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: config.power_preference, - compatible_surface: Some(surface), - force_fallback_adapter: false, - }) - .await - .ok_or_else(|| { - #[cfg(not(target_arch = "wasm32"))] - if available_adapters.is_empty() { - log::info!("No wgpu adapters found"); - } else if available_adapters.len() == 1 { - log::info!( - "The only available wgpu adapter was not suitable: {}", - adapter_info_summary(&available_adapters[0].get_info()) - ); - } else { - log::info!( - "No suitable wgpu adapter found out of the {} available ones: {}", - available_adapters.len(), - describe_adapters(&available_adapters) - ); - } + let (adapter, device, queue) = match config.wgpu_setup.clone() { + WgpuSetup::CreateNew { + supported_backends: _, + power_preference, + device_descriptor, + } => { + let adapter = { + profiling::scope!("request_adapter"); + instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference, + compatible_surface: Some(surface), + force_fallback_adapter: false, + }) + .await + .ok_or_else(|| { + #[cfg(not(target_arch = "wasm32"))] + if available_adapters.is_empty() { + log::info!("No wgpu adapters found"); + } else if available_adapters.len() == 1 { + log::info!( + "The only available wgpu adapter was not suitable: {}", + adapter_info_summary(&available_adapters[0].get_info()) + ); + } else { + log::info!( + "No suitable wgpu adapter found out of the {} available ones: {}", + available_adapters.len(), + describe_adapters(&available_adapters) + ); + } + + WgpuError::NoSuitableAdapterFound + })? + }; - WgpuError::NoSuitableAdapterFound - })? - }; + #[cfg(target_arch = "wasm32")] + log::debug!( + "Picked wgpu adapter: {}", + adapter_info_summary(&adapter.get_info()) + ); + + #[cfg(not(target_arch = "wasm32"))] + if available_adapters.len() == 1 { + log::debug!( + "Picked the only available wgpu adapter: {}", + adapter_info_summary(&adapter.get_info()) + ); + } else { + log::info!( + "There were {} available wgpu adapters: {}", + available_adapters.len(), + describe_adapters(&available_adapters) + ); + log::debug!( + "Picked wgpu adapter: {}", + adapter_info_summary(&adapter.get_info()) + ); + } - #[cfg(target_arch = "wasm32")] - log::debug!( - "Picked wgpu adapter: {}", - adapter_info_summary(&adapter.get_info()) - ); + let trace_path = std::env::var("WGPU_TRACE"); + let (device, queue) = { + profiling::scope!("request_device"); + adapter + .request_device( + &(*device_descriptor)(&adapter), + trace_path.ok().as_ref().map(std::path::Path::new), + ) + .await? + }; - #[cfg(not(target_arch = "wasm32"))] - if available_adapters.len() == 1 { - log::debug!( - "Picked the only available wgpu adapter: {}", - adapter_info_summary(&adapter.get_info()) - ); - } else { - log::info!( - "There were {} available wgpu adapters: {}", - available_adapters.len(), - describe_adapters(&available_adapters) - ); - log::debug!( - "Picked wgpu adapter: {}", - adapter_info_summary(&adapter.get_info()) - ); - } + // 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)] + (Arc::new(adapter), Arc::new(device), Arc::new(queue)) + } + WgpuSetup::Existing { + instance: _, + adapter, + device, + queue, + } => (adapter, device, queue), + }; let capabilities = { - crate::profile_scope!("get_capabilities"); + profiling::scope!("get_capabilities"); surface.get_capabilities(&adapter).formats }; let target_format = crate::preferred_framebuffer_format(&capabilities)?; - let (device, queue) = { - crate::profile_scope!("request_device"); - adapter - .request_device(&(*config.device_descriptor)(&adapter), None) - .await? - }; - let renderer = Renderer::new( &device, target_format, @@ -177,11 +204,11 @@ impl RenderState { // 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), + adapter, #[cfg(not(target_arch = "wasm32"))] available_adapters: available_adapters.into(), - device: Arc::new(device), - queue: Arc::new(queue), + device, + queue, target_format, renderer: Arc::new(RwLock::new(renderer)), }) @@ -215,27 +242,65 @@ pub enum SurfaceErrorAction { RecreateSurface, } -/// Configuration for using wgpu with eframe or the egui-wgpu winit feature. -/// -/// This can also be configured with the environment variables: -/// * `WGPU_BACKEND`: `vulkan`, `dx11`, `dx12`, `metal`, `opengl`, `webgpu` -/// * `WGPU_POWER_PREF`: `low`, `high` or `none` #[derive(Clone)] -pub struct WgpuConfiguration { - /// Backends that should be supported (wgpu will pick one of these). +pub enum WgpuSetup { + /// Construct a wgpu setup using some predefined settings & heuristics. + /// This is the default option. You can customize most behaviours overriding the + /// supported backends, power preferences, and device description. /// - /// For instance, if you only want to support WebGL (and not WebGPU), - /// you can set this to [`wgpu::Backends::GL`]. - /// - /// By default on web, WebGPU will be used if available. - /// WebGL will only be used as a fallback, - /// and only if you have enabled the `webgl` feature of crate `wgpu`. - pub supported_backends: wgpu::Backends, + /// This can also be configured with the environment variables: + /// * `WGPU_BACKEND`: `vulkan`, `dx11`, `dx12`, `metal`, `opengl`, `webgpu` + /// * `WGPU_POWER_PREF`: `low`, `high` or `none` + CreateNew { + /// Backends that should be supported (wgpu will pick one of these). + /// + /// For instance, if you only want to support WebGL (and not WebGPU), + /// you can set this to [`wgpu::Backends::GL`]. + /// + /// By default on web, WebGPU will be used if available. + /// WebGL will only be used as a fallback, + /// and only if you have enabled the `webgl` feature of crate `wgpu`. + supported_backends: wgpu::Backends, + + /// Power preference for the adapter. + power_preference: wgpu::PowerPreference, + + /// Configuration passed on device request, given an adapter + device_descriptor: + Arc wgpu::DeviceDescriptor<'static> + Send + Sync>, + }, + + /// Run on an existing wgpu setup. + Existing { + instance: Arc, + adapter: Arc, + device: Arc, + queue: Arc, + }, +} - /// Configuration passed on device request, given an adapter - pub device_descriptor: - Arc wgpu::DeviceDescriptor<'static> + Send + Sync>, +impl std::fmt::Debug for WgpuSetup { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::CreateNew { + supported_backends, + power_preference, + device_descriptor: _, + } => f + .debug_struct("AdapterSelection::Standard") + .field("supported_backends", &supported_backends) + .field("power_preference", &power_preference) + .finish(), + Self::Existing { .. } => f + .debug_struct("AdapterSelection::Existing") + .finish_non_exhaustive(), + } + } +} +/// Configuration for using wgpu with eframe or the egui-wgpu winit feature. +#[derive(Clone)] +pub struct WgpuConfiguration { /// Present mode used for the primary surface. pub present_mode: wgpu::PresentMode, @@ -248,8 +313,8 @@ pub struct WgpuConfiguration { /// `None` = `wgpu` default. pub desired_maximum_frame_latency: Option, - /// Power preference for the adapter. - pub power_preference: wgpu::PowerPreference, + /// How to create the wgpu adapter & device + pub wgpu_setup: WgpuSetup, /// Callback for surface errors. pub on_surface_error: Arc SurfaceErrorAction + Send + Sync>, @@ -264,21 +329,18 @@ fn wgpu_config_impl_send_sync() { impl std::fmt::Debug for WgpuConfiguration { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let Self { - supported_backends, - device_descriptor: _, present_mode, desired_maximum_frame_latency, - power_preference, + wgpu_setup, on_surface_error: _, } = self; f.debug_struct("WgpuConfiguration") - .field("supported_backends", &supported_backends) .field("present_mode", &present_mode) .field( "desired_maximum_frame_latency", &desired_maximum_frame_latency, ) - .field("power_preference", &power_preference) + .field("wgpu_setup", &wgpu_setup) .finish_non_exhaustive() } } @@ -286,37 +348,42 @@ impl std::fmt::Debug for WgpuConfiguration { impl Default for WgpuConfiguration { fn default() -> Self { Self { - // Add GL backend, primarily because WebGPU is not stable enough yet. - // (note however, that the GL backend needs to be opted-in via the wgpu feature flag "webgl") - supported_backends: wgpu::util::backend_bits_from_env() - .unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL), - - device_descriptor: Arc::new(|adapter| { - let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl { - wgpu::Limits::downlevel_webgl2_defaults() - } else { - wgpu::Limits::default() - }; - - wgpu::DeviceDescriptor { - label: Some("egui wgpu device"), - required_features: wgpu::Features::default(), - required_limits: wgpu::Limits { - // When using a depth buffer, we have to be able to create a texture - // large enough for the entire surface, and we want to support 4k+ displays. - max_texture_dimension_2d: 8192, - ..base_limits - }, - memory_hints: wgpu::MemoryHints::default(), - } - }), - present_mode: wgpu::PresentMode::AutoVsync, desired_maximum_frame_latency: None, - power_preference: wgpu::util::power_preference_from_env() - .unwrap_or(wgpu::PowerPreference::HighPerformance), + // By default, create a new wgpu setup. This will create a new instance, adapter, device and queue. + // This will create an instance for the supported backends (which can be configured by + // `WGPU_BACKEND`), and will pick an adapter by iterating adapters based on their power preference. The power + // preference can also be configured by `WGPU_POWER_PREF`. + wgpu_setup: WgpuSetup::CreateNew { + // Add GL backend, primarily because WebGPU is not stable enough yet. + // (note however, that the GL backend needs to be opted-in via the wgpu feature flag "webgl") + supported_backends: wgpu::util::backend_bits_from_env() + .unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL), + + power_preference: wgpu::util::power_preference_from_env() + .unwrap_or(wgpu::PowerPreference::HighPerformance), + device_descriptor: Arc::new(|adapter| { + let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl { + wgpu::Limits::downlevel_webgl2_defaults() + } else { + wgpu::Limits::default() + }; + + wgpu::DeviceDescriptor { + label: Some("egui wgpu device"), + required_features: wgpu::Features::default(), + required_limits: wgpu::Limits { + // When using a depth buffer, we have to be able to create a texture + // large enough for the entire surface, and we want to support 4k+ displays. + max_texture_dimension_2d: 8192, + ..base_limits + }, + memory_hints: wgpu::MemoryHints::default(), + } + }), + }, on_surface_error: Arc::new(|err| { if err == wgpu::SurfaceError::Outdated { @@ -407,33 +474,3 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String { summary } - -// --------------------------------------------------------------------------- - -mod profiling_scopes { - #![allow(unused_macros)] - #![allow(unused_imports)] - - /// Profiling macro for feature "puffin" - macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} - -#[allow(unused_imports)] -pub(crate) use profiling_scopes::{profile_function, profile_scope}; diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index a3fcd667f5f..2c1fa0428f1 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -125,7 +125,7 @@ pub struct ScreenDescriptor { /// Size of the window in physical pixels. pub size_in_pixels: [u32; 2], - /// HiDPI scale factor (pixels per point). + /// High-DPI scale factor (pixels per point). pub pixels_per_point: f32, } @@ -214,14 +214,14 @@ impl Renderer { msaa_samples: u32, dithering: bool, ) -> Self { - crate::profile_function!(); + profiling::function_scope!(); let shader = wgpu::ShaderModuleDescriptor { label: Some("egui"), source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("egui.wgsl"))), }; let module = { - crate::profile_scope!("create_shader_module"); + profiling::scope!("create_shader_module"); device.create_shader_module(shader) }; @@ -236,7 +236,7 @@ impl Renderer { }); let uniform_bind_group_layout = { - crate::profile_scope!("create_bind_group_layout"); + profiling::scope!("create_bind_group_layout"); device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("egui_uniform_bind_group_layout"), entries: &[wgpu::BindGroupLayoutEntry { @@ -253,7 +253,7 @@ impl Renderer { }; let uniform_bind_group = { - crate::profile_scope!("create_bind_group"); + profiling::scope!("create_bind_group"); device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("egui_uniform_bind_group"), layout: &uniform_bind_group_layout, @@ -269,7 +269,7 @@ impl Renderer { }; let texture_bind_group_layout = { - crate::profile_scope!("create_bind_group_layout"); + profiling::scope!("create_bind_group_layout"); device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("egui_texture_bind_group_layout"), entries: &[ @@ -308,12 +308,12 @@ impl Renderer { }); let pipeline = { - crate::profile_scope!("create_render_pipeline"); + profiling::scope!("create_render_pipeline"); device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("egui_pipeline"), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { - entry_point: "vs_main", + entry_point: Some("vs_main"), module: &module, buffers: &[wgpu::VertexBufferLayout { array_stride: 5 * 4, @@ -343,12 +343,12 @@ impl Renderer { fragment: Some(wgpu::FragmentState { module: &module, - entry_point: if output_color_format.is_srgb() { + entry_point: Some(if output_color_format.is_srgb() { log::warn!("Detected a linear (sRGBA aware) framebuffer {:?}. egui prefers Rgba8Unorm or Bgra8Unorm", output_color_format); "fs_main_linear_framebuffer" } else { "fs_main_gamma_framebuffer" // this is what we prefer - }, + }), targets: &[Some(wgpu::ColorTargetState { format: output_color_format, blend: Some(wgpu::BlendState { @@ -420,7 +420,7 @@ impl Renderer { paint_jobs: &[epaint::ClippedPrimitive], screen_descriptor: &ScreenDescriptor, ) { - crate::profile_function!(); + profiling::function_scope!(); let pixels_per_point = screen_descriptor.pixels_per_point; let size_in_pixels = screen_descriptor.size_in_pixels; @@ -506,7 +506,7 @@ impl Renderer { let viewport_px = info.viewport_in_pixels(); if viewport_px.width_px > 0 && viewport_px.height_px > 0 { - crate::profile_scope!("callback"); + profiling::scope!("callback"); needs_reset = true; @@ -544,7 +544,7 @@ impl Renderer { id: epaint::TextureId, image_delta: &epaint::ImageDelta, ) { - crate::profile_function!(); + profiling::function_scope!(); let width = image_delta.image.width() as u32; let height = image_delta.image.height() as u32; @@ -570,14 +570,14 @@ impl Renderer { image.pixels.len(), "Mismatch between texture size and texel count" ); - crate::profile_scope!("font -> sRGBA"); + profiling::scope!("font -> sRGBA"); Cow::Owned(image.srgba_pixels(None).collect::>()) } }; let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice()); let queue_write_data_to_texture = |texture, origin| { - crate::profile_scope!("write_texture"); + profiling::scope!("write_texture"); queue.write_texture( wgpu::ImageCopyTexture { texture, @@ -631,7 +631,7 @@ impl Renderer { } else { // allocate a new texture let texture = { - crate::profile_scope!("create_texture"); + profiling::scope!("create_texture"); device.create_texture(&wgpu::TextureDescriptor { label, size, @@ -756,7 +756,7 @@ impl Renderer { texture: &wgpu::TextureView, sampler_descriptor: wgpu::SamplerDescriptor<'_>, ) -> epaint::TextureId { - crate::profile_function!(); + profiling::function_scope!(); let sampler = device.create_sampler(&wgpu::SamplerDescriptor { compare: None, @@ -804,7 +804,7 @@ impl Renderer { sampler_descriptor: wgpu::SamplerDescriptor<'_>, id: epaint::TextureId, ) { - crate::profile_function!(); + profiling::function_scope!(); let Texture { bind_group: user_texture_binding, @@ -849,7 +849,7 @@ impl Renderer { paint_jobs: &[epaint::ClippedPrimitive], screen_descriptor: &ScreenDescriptor, ) -> Vec { - crate::profile_function!(); + profiling::function_scope!(); let screen_size_in_points = screen_descriptor.screen_size_in_points(); @@ -859,7 +859,7 @@ impl Renderer { _padding: Default::default(), }; if uniform_buffer_content != self.previous_uniform_buffer_content { - crate::profile_scope!("update uniforms"); + profiling::scope!("update uniforms"); queue.write_buffer( &self.uniform_buffer, 0, @@ -871,7 +871,7 @@ impl Renderer { // Determine how many vertices & indices need to be rendered, and gather prepare callbacks let mut callbacks = Vec::new(); let (vertex_count, index_count) = { - crate::profile_scope!("count_vertices_indices"); + profiling::scope!("count_vertices_indices"); paint_jobs.iter().fold((0, 0), |acc, clipped_primitive| { match &clipped_primitive.primitive { Primitive::Mesh(mesh) => { @@ -890,7 +890,7 @@ impl Renderer { }; if index_count > 0 { - crate::profile_scope!("indices", index_count.to_string()); + profiling::scope!("indices", index_count.to_string().as_str()); self.index_buffer.slices.clear(); @@ -928,7 +928,7 @@ impl Renderer { } } if vertex_count > 0 { - crate::profile_scope!("vertices", vertex_count.to_string()); + profiling::scope!("vertices", vertex_count.to_string().as_str()); self.vertex_buffer.slices.clear(); @@ -969,7 +969,7 @@ impl Renderer { let mut user_cmd_bufs = Vec::new(); { - crate::profile_scope!("prepare callbacks"); + profiling::scope!("prepare callbacks"); for callback in &callbacks { user_cmd_bufs.extend(callback.prepare( device, @@ -981,7 +981,7 @@ impl Renderer { } } { - crate::profile_scope!("finish prepare callbacks"); + profiling::scope!("finish prepare callbacks"); for callback in &callbacks { user_cmd_bufs.extend(callback.finish_prepare( device, @@ -1026,7 +1026,7 @@ fn create_sampler( } fn create_vertex_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer { - crate::profile_function!(); + profiling::function_scope!(); device.create_buffer(&wgpu::BufferDescriptor { label: Some("egui_vertex_buffer"), usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, @@ -1036,7 +1036,7 @@ fn create_vertex_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer { } fn create_index_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer { - crate::profile_function!(); + profiling::function_scope!(); device.create_buffer(&wgpu::BufferDescriptor { label: Some("egui_index_buffer"), usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, diff --git a/crates/egui-wgpu/src/texture_copy.wgsl b/crates/egui-wgpu/src/texture_copy.wgsl new file mode 100644 index 00000000000..4096d164cc3 --- /dev/null +++ b/crates/egui-wgpu/src/texture_copy.wgsl @@ -0,0 +1,43 @@ +struct VertexOutput { + @builtin(position) position: vec4, +}; + +var positions: array = array( + vec2f(-1.0, -3.0), + vec2f(-1.0, 1.0), + vec2f(3.0, 1.0) +); + +// meant to be called with 3 vertex indices: 0, 1, 2 +// draws one large triangle over the clip space like this: +// (the asterisks represent the clip space bounds) +//-1,1 1,1 +// --------------------------------- +// | * . +// | * . +// | * . +// | * . +// | * . +// | * . +// |*************** +// | . 1,-1 +// | . +// | . +// | . +// | . +// |. +@vertex +fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { + var result: VertexOutput; + result.position = vec4f(positions[vertex_index], 0.0, 1.0); + return result; +} + +@group(0) +@binding(0) +var r_color: texture_2d; + +@fragment +fn fs_main(vertex: VertexOutput) -> @location(0) vec4 { + return textureLoad(r_color, vec2i(vertex.position.xy), 0); +} diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 26b3c86ae2c..dea2e7fa329 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -1,77 +1,16 @@ #![allow(clippy::missing_errors_doc)] #![allow(clippy::undocumented_unsafe_blocks)] -use std::{num::NonZeroU32, sync::Arc}; - -use egui::{ViewportId, ViewportIdMap, ViewportIdSet}; - +use crate::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState}; use crate::{renderer, RenderState, SurfaceErrorAction, WgpuConfiguration}; +use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet}; +use std::{num::NonZeroU32, sync::Arc}; struct SurfaceState { surface: wgpu::Surface<'static>, alpha_mode: wgpu::CompositeAlphaMode, width: u32, height: u32, - supports_screenshot: bool, -} - -/// A texture and a buffer for reading the rendered frame back to the cpu. -/// The texture is required since [`wgpu::TextureUsages::COPY_DST`] is not an allowed -/// flag for the surface texture on all platforms. This means that anytime we want to -/// capture the frame, we first render it to this texture, and then we can copy it to -/// both the surface texture and the buffer, from where we can pull it back to the cpu. -struct CaptureState { - texture: wgpu::Texture, - buffer: wgpu::Buffer, - padding: BufferPadding, -} - -impl CaptureState { - fn new(device: &Arc, surface_texture: &wgpu::Texture) -> Self { - let texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("egui_screen_capture_texture"), - size: surface_texture.size(), - mip_level_count: surface_texture.mip_level_count(), - sample_count: surface_texture.sample_count(), - dimension: surface_texture.dimension(), - format: surface_texture.format(), - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, - view_formats: &[], - }); - - let padding = BufferPadding::new(surface_texture.width()); - - let buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("egui_screen_capture_buffer"), - size: (padding.padded_bytes_per_row * texture.height()) as u64, - usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, - mapped_at_creation: false, - }); - - Self { - texture, - buffer, - padding, - } - } -} - -struct BufferPadding { - unpadded_bytes_per_row: u32, - padded_bytes_per_row: u32, -} - -impl BufferPadding { - fn new(width: u32) -> Self { - let bytes_per_pixel = std::mem::size_of::() as u32; - let unpadded_bytes_per_row = width * bytes_per_pixel; - let padded_bytes_per_row = - wgpu::util::align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT); - Self { - unpadded_bytes_per_row, - padded_bytes_per_row, - } - } } /// Everything you need to paint egui with [`wgpu`] on [`winit`]. @@ -80,6 +19,7 @@ impl BufferPadding { /// /// NOTE: all egui viewports share the same painter. pub struct Painter { + context: Context, configuration: WgpuConfiguration, msaa_samples: u32, support_transparent_backbuffer: bool, @@ -87,13 +27,15 @@ pub struct Painter { depth_format: Option, screen_capture_state: Option, - instance: wgpu::Instance, + instance: Arc, render_state: Option, // Per viewport/window: depth_texture_view: ViewportIdMap, msaa_texture_view: ViewportIdMap, surfaces: ViewportIdMap, + capture_tx: CaptureSender, + capture_rx: CaptureReceiver, } impl Painter { @@ -110,18 +52,27 @@ impl Painter { /// a [`winit::window::Window`] with a valid `.raw_window_handle()` /// associated. pub fn new( + context: Context, configuration: WgpuConfiguration, msaa_samples: u32, depth_format: Option, support_transparent_backbuffer: bool, dithering: bool, ) -> Self { - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: configuration.supported_backends, - ..Default::default() - }); + let instance = match &configuration.wgpu_setup { + crate::WgpuSetup::CreateNew { + supported_backends, .. + } => Arc::new(wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: *supported_backends, + ..Default::default() + })), + crate::WgpuSetup::Existing { instance, .. } => instance.clone(), + }; + + let (capture_tx, capture_rx) = capture_channel(); Self { + context, configuration, msaa_samples, support_transparent_backbuffer, @@ -135,6 +86,9 @@ impl Painter { depth_texture_view: Default::default(), surfaces: Default::default(), msaa_texture_view: Default::default(), + + capture_tx, + capture_rx, } } @@ -150,19 +104,13 @@ impl Painter { render_state: &RenderState, config: &WgpuConfiguration, ) { - crate::profile_function!(); - - let usage = if surface_state.supports_screenshot { - wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST - } else { - wgpu::TextureUsages::RENDER_ATTACHMENT - }; + profiling::function_scope!(); let width = surface_state.width; let height = surface_state.height; let mut surf_config = wgpu::SurfaceConfiguration { - usage, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: render_state.target_format, present_mode: config.present_mode, alpha_mode: surface_state.alpha_mode, @@ -208,7 +156,7 @@ impl Painter { viewport_id: ViewportId, window: Option>, ) -> Result<(), crate::WgpuError> { - crate::profile_scope!("Painter::set_window"); // profile_function gives bad names for async functions + profiling::scope!("Painter::set_window"); // profile_function gives bad names for async functions if let Some(window) = window { let size = window.inner_size(); @@ -234,7 +182,7 @@ impl Painter { viewport_id: ViewportId, window: Option<&winit::window::Window>, ) -> Result<(), crate::WgpuError> { - crate::profile_scope!("Painter::set_window_unsafe"); // profile_function gives bad names for async functions + profiling::scope!("Painter::set_window_unsafe"); // profile_function gives bad names for async functions if let Some(window) = window { let size = window.inner_size(); @@ -287,8 +235,6 @@ impl Painter { } else { wgpu::CompositeAlphaMode::Auto }; - let supports_screenshot = - !matches!(render_state.adapter.get_info().backend, wgpu::Backend::Gl); self.surfaces.insert( viewport_id, SurfaceState { @@ -296,7 +242,6 @@ impl Painter { width: size.width, height: size.height, alpha_mode, - supports_screenshot, }, ); let Some(width) = NonZeroU32::new(size.width) else { @@ -328,7 +273,7 @@ impl Painter { width_in_pixels: NonZeroU32, height_in_pixels: NonZeroU32, ) { - crate::profile_function!(); + profiling::function_scope!(); let width = width_in_pixels.get(); let height = height_in_pixels.get(); @@ -399,7 +344,7 @@ impl Painter { width_in_pixels: NonZeroU32, height_in_pixels: NonZeroU32, ) { - crate::profile_function!(); + profiling::function_scope!(); if self.surfaces.contains_key(&viewport_id) { self.resize_and_generate_depth_texture_view_and_msaa_view( @@ -412,109 +357,12 @@ impl Painter { } } - // CaptureState only needs to be updated when the size of the two textures don't match and we want to - // capture a frame - fn update_capture_state( - screen_capture_state: &mut Option, - surface_texture: &wgpu::SurfaceTexture, - render_state: &RenderState, - ) { - let surface_texture = &surface_texture.texture; - match screen_capture_state { - Some(capture_state) => { - if capture_state.texture.size() != surface_texture.size() { - *capture_state = CaptureState::new(&render_state.device, surface_texture); - } - } - None => { - *screen_capture_state = - Some(CaptureState::new(&render_state.device, surface_texture)); - } - } - } - - // Handles copying from the CaptureState texture to the surface texture and the cpu - fn read_screen_rgba( - screen_capture_state: &CaptureState, - render_state: &RenderState, - output_frame: &wgpu::SurfaceTexture, - ) -> Option { - let CaptureState { - texture: tex, - buffer, - padding, - } = screen_capture_state; - - let device = &render_state.device; - let queue = &render_state.queue; - - let tex_extent = tex.size(); - - let mut encoder = device.create_command_encoder(&Default::default()); - encoder.copy_texture_to_buffer( - tex.as_image_copy(), - wgpu::ImageCopyBuffer { - buffer, - layout: wgpu::ImageDataLayout { - offset: 0, - bytes_per_row: Some(padding.padded_bytes_per_row), - rows_per_image: None, - }, - }, - tex_extent, - ); - - encoder.copy_texture_to_texture( - tex.as_image_copy(), - output_frame.texture.as_image_copy(), - tex.size(), - ); - - let id = queue.submit(Some(encoder.finish())); - let buffer_slice = buffer.slice(..); - let (sender, receiver) = std::sync::mpsc::channel(); - buffer_slice.map_async(wgpu::MapMode::Read, move |v| { - drop(sender.send(v)); - }); - device.poll(wgpu::Maintain::WaitForSubmissionIndex(id)); - receiver.recv().ok()?.ok()?; - - let to_rgba = match tex.format() { - wgpu::TextureFormat::Rgba8Unorm => [0, 1, 2, 3], - wgpu::TextureFormat::Bgra8Unorm => [2, 1, 0, 3], - _ => { - log::error!("Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {:?}", tex.format()); - return None; - } - }; - - let mut pixels = Vec::with_capacity((tex.width() * tex.height()) as usize); - for padded_row in buffer_slice - .get_mapped_range() - .chunks(padding.padded_bytes_per_row as usize) - { - let row = &padded_row[..padding.unpadded_bytes_per_row as usize]; - for color in row.chunks(4) { - pixels.push(epaint::Color32::from_rgba_premultiplied( - color[to_rgba[0]], - color[to_rgba[1]], - color[to_rgba[2]], - color[to_rgba[3]], - )); - } - } - buffer.unmap(); - - Some(epaint::ColorImage { - size: [tex.width() as usize, tex.height() as usize], - pixels, - }) - } - /// Returns two things: /// /// The approximate number of seconds spent on vsync-waiting (if any), /// and the captures captured screenshot if it was requested. + /// + /// If `capture_data` isn't empty, a screenshot will be captured. pub fn paint_and_update_textures( &mut self, viewport_id: ViewportId, @@ -522,17 +370,18 @@ impl Painter { clear_color: [f32; 4], clipped_primitives: &[epaint::ClippedPrimitive], textures_delta: &epaint::textures::TexturesDelta, - capture: bool, - ) -> (f32, Option) { - crate::profile_function!(); + capture_data: Vec, + ) -> f32 { + profiling::function_scope!(); + let capture = !capture_data.is_empty(); let mut vsync_sec = 0.0; let Some(render_state) = self.render_state.as_mut() else { - return (vsync_sec, None); + return vsync_sec; }; let Some(surface_state) = self.surfaces.get(&viewport_id) else { - return (vsync_sec, None); + return vsync_sec; }; let mut encoder = @@ -568,17 +417,8 @@ impl Painter { ) }; - let capture = match (capture, surface_state.supports_screenshot) { - (false, _) => false, - (true, true) => true, - (true, false) => { - log::error!("The active render surface doesn't support taking screenshots."); - false - } - }; - let output_frame = { - crate::profile_scope!("get_current_texture"); + profiling::scope!("get_current_texture"); // This is what vsync-waiting happens on my Mac. let start = web_time::Instant::now(); let output_frame = surface_state.surface.get_current_texture(); @@ -591,40 +431,35 @@ impl Painter { Err(err) => match (*self.configuration.on_surface_error)(err) { SurfaceErrorAction::RecreateSurface => { Self::configure_surface(surface_state, render_state, &self.configuration); - return (vsync_sec, None); + return vsync_sec; } SurfaceErrorAction::SkipFrame => { - return (vsync_sec, None); + return vsync_sec; } }, }; + let mut capture_buffer = None; { let renderer = render_state.renderer.read(); - let frame_view = if capture { - Self::update_capture_state( - &mut self.screen_capture_state, - &output_frame, - render_state, - ); - self.screen_capture_state - .as_ref() - .map_or_else( - || &output_frame.texture, - |capture_state| &capture_state.texture, - ) - .create_view(&wgpu::TextureViewDescriptor::default()) + + let target_texture = if capture { + let capture_state = self.screen_capture_state.get_or_insert_with(|| { + CaptureState::new(&render_state.device, &output_frame.texture) + }); + capture_state.update(&render_state.device, &output_frame.texture); + + &capture_state.texture } else { - output_frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()) + &output_frame.texture }; + let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default()); let (view, resolve_target) = (self.msaa_samples > 1) .then_some(self.msaa_texture_view.get(&viewport_id)) .flatten() - .map_or((&frame_view, None), |texture_view| { - (texture_view, Some(&frame_view)) + .map_or((&target_view, None), |texture_view| { + (texture_view, Some(&target_view)) }); let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -666,23 +501,26 @@ impl Painter { clipped_primitives, &screen_descriptor, ); - } - { - let mut renderer = render_state.renderer.write(); - for id in &textures_delta.free { - renderer.free_texture(id); + if capture { + if let Some(capture_state) = &mut self.screen_capture_state { + capture_buffer = Some(capture_state.copy_textures( + &render_state.device, + &output_frame, + &mut encoder, + )); + } } } let encoded = { - crate::profile_scope!("CommandEncoder::finish"); + profiling::scope!("CommandEncoder::finish"); encoder.finish() }; // Submit the commands: both the main buffer and user-defined ones. { - crate::profile_scope!("Queue::submit"); + profiling::scope!("Queue::submit"); // wgpu doesn't document where vsync can happen. Maybe here? let start = web_time::Instant::now(); render_state @@ -691,25 +529,51 @@ impl Painter { vsync_sec += start.elapsed().as_secs_f32(); }; - let screenshot = if capture { - self.screen_capture_state - .as_ref() - .and_then(|screen_capture_state| { - Self::read_screen_rgba(screen_capture_state, render_state, &output_frame) - }) - } else { - None - }; + // Free textures marked for destruction **after** queue submit since they might still be used in the current frame. + // Calling `wgpu::Texture::destroy` on a texture that is still in use would invalidate the command buffer(s) it is used in. + // However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live. + { + let mut renderer = render_state.renderer.write(); + for id in &textures_delta.free { + renderer.free_texture(id); + } + } + + if let Some(capture_buffer) = capture_buffer { + if let Some(screen_capture_state) = &mut self.screen_capture_state { + screen_capture_state.read_screen_rgba( + self.context.clone(), + capture_buffer, + capture_data, + self.capture_tx.clone(), + viewport_id, + ); + } + } { - crate::profile_scope!("present"); + profiling::scope!("present"); // wgpu doesn't document where vsync can happen. Maybe here? let start = web_time::Instant::now(); output_frame.present(); vsync_sec += start.elapsed().as_secs_f32(); } - (vsync_sec, screenshot) + vsync_sec + } + + /// Call this at the beginning of each frame to receive the requested screenshots. + pub fn handle_screenshots(&self, events: &mut Vec) { + for (viewport_id, user_data, screenshot) in self.capture_rx.try_iter() { + let screenshot = Arc::new(screenshot); + for data in user_data { + events.push(Event::Screenshot { + viewport_id, + user_data: data, + image: screenshot.clone(), + }); + } + } } pub fn gc_viewports(&mut self, active_viewports: &ViewportIdSet) { @@ -720,7 +584,7 @@ impl Painter { .retain(|id, _| active_viewports.contains(id)); } - #[allow(clippy::unused_self)] + #[allow(clippy::needless_pass_by_ref_mut, clippy::unused_self)] pub fn destroy(&mut self) { // TODO(emilk): something here? } diff --git a/crates/egui-winit/CHANGELOG.md b/crates/egui-winit/CHANGELOG.md index 785c57b9404..d164464208d 100644 --- a/crates/egui-winit/CHANGELOG.md +++ b/crates/egui-winit/CHANGELOG.md @@ -5,6 +5,15 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.30.0 - 2024-12-16 +* iOS: Support putting UI next to the dynamic island [#5211](https://github.com/emilk/egui/pull/5211) by [@frederik-uni](https://github.com/frederik-uni) +* Remove implicit `accesskit_winit` feature [#5316](https://github.com/emilk/egui/pull/5316) by [@waywardmonkeys](https://github.com/waywardmonkeys) + + +## 0.29.1 - 2024-10-01 - Fix backspace/arrow keys on X11 +* Linux: Disable IME to fix backspace/arrow keys [#5188](https://github.com/emilk/egui/pull/5188) by [@emilk](https://github.com/emilk) + + ## 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) diff --git a/crates/egui-winit/Cargo.toml b/crates/egui-winit/Cargo.toml index f8a11ec7a36..c584db85e70 100644 --- a/crates/egui-winit/Cargo.toml +++ b/crates/egui-winit/Cargo.toml @@ -24,7 +24,7 @@ rustdoc-args = ["--generate-link-to-definition"] default = ["clipboard", "links", "wayland", "winit/default", "x11"] ## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/). -accesskit = ["accesskit_winit", "egui/accesskit"] +accesskit = ["dep:accesskit_winit", "egui/accesskit"] # Allow crates to choose an android-activity backend via Winit # - It's important that most applications should not have to depend on android-activity directly, and can @@ -45,9 +45,6 @@ clipboard = ["arboard", "smithay-clipboard"] ## Enable opening links in a browser when an egui hyperlink is clicked. links = ["webbrowser"] -## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -puffin = ["dep:puffin", "egui/puffin"] - ## Allow serialization of [`WindowSettings`] using [`serde`](https://docs.rs/serde). serde = ["egui/serde", "dep:serde"] @@ -62,6 +59,7 @@ egui = { workspace = true, default-features = false, features = ["log"] } ahash.workspace = true log.workspace = true +profiling.workspace = true raw-window-handle.workspace = true web-time.workspace = true winit = { workspace = true, default-features = false } @@ -69,12 +67,11 @@ winit = { workspace = true, default-features = false } #! ### Optional dependencies # feature accesskit -accesskit_winit = { version = "0.22", optional = true } +accesskit_winit = { version = "0.23", optional = true } ## Enable this when generating docs. document-features = { workspace = true, optional = true } -puffin = { workspace = true, optional = true } serde = { workspace = true, optional = true } webbrowser = { version = "1.0.0", optional = true } diff --git a/crates/egui-winit/src/clipboard.rs b/crates/egui-winit/src/clipboard.rs index 44e3840b64f..c4192f78d55 100644 --- a/crates/egui-winit/src/clipboard.rs +++ b/crates/egui-winit/src/clipboard.rs @@ -112,7 +112,7 @@ impl Clipboard { #[cfg(all(feature = "arboard", not(target_os = "android")))] fn init_arboard() -> Option { - crate::profile_function!(); + profiling::function_scope!(); log::trace!("Initializing arboard clipboard…"); match arboard::Clipboard::new() { @@ -139,7 +139,7 @@ fn init_smithay_clipboard( ) -> Option { #![allow(clippy::undocumented_unsafe_blocks)] - crate::profile_function!(); + profiling::function_scope!(); if let Some(RawDisplayHandle::Wayland(display)) = raw_display_handle { log::trace!("Initializing smithay clipboard…"); diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 7110ef3fe21..50ff2d31b4b 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -25,9 +25,6 @@ pub use window_settings::WindowSettings; use ahash::HashSet; use raw_window_handle::HasDisplayHandle; -#[allow(unused_imports)] -pub(crate) use profiling_scopes::{profile_function, profile_scope}; - use winit::{ dpi::{PhysicalPosition, PhysicalSize}, event::ElementState, @@ -36,7 +33,14 @@ use winit::{ }; pub fn screen_size_in_pixels(window: &Window) -> egui::Vec2 { - let size = window.inner_size(); + let size = if cfg!(target_os = "ios") { + // `outer_size` Includes the area behind the "dynamic island". + // It is up to the eframe user to make sure the dynamic island doesn't cover anything important. + // That will be easier once https://github.com/rust-windowing/winit/pull/3890 lands + window.outer_size() + } else { + window.inner_size() + }; egui::vec2(size.width as f32, size.height as f32) } @@ -114,7 +118,7 @@ impl State { theme: Option, max_texture_side: Option, ) -> Self { - crate::profile_function!(); + profiling::function_scope!(); let egui_input = egui::RawInput { focused: false, // winit will tell us when we have focus @@ -165,7 +169,7 @@ impl State { window: &Window, event_loop_proxy: winit::event_loop::EventLoopProxy, ) { - crate::profile_function!(); + profiling::function_scope!(); self.accesskit = Some(accesskit_winit::Adapter::with_event_loop_proxy( window, @@ -226,7 +230,7 @@ impl State { /// Use [`update_viewport_info`] to update the info for each /// viewport. pub fn take_egui_input(&mut self, window: &Window) -> egui::RawInput { - crate::profile_function!(); + profiling::function_scope!(); self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64()); @@ -261,7 +265,7 @@ impl State { window: &Window, event: &winit::event::WindowEvent, ) -> EventResponse { - crate::profile_function!(short_window_event_description(event)); + profiling::function_scope!(short_window_event_description(event)); #[cfg(feature = "accesskit")] if let Some(accesskit) = self.accesskit.as_mut() { @@ -329,39 +333,43 @@ impl State { } WindowEvent::Ime(ime) => { - // on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit. - // So no need to check is_mac_cmd. - // - // How winit produce `Ime::Enabled` and `Ime::Disabled` differs in MacOS - // and Windows. - // - // - On Windows, before and after each Commit will produce an Enable/Disabled - // event. - // - On MacOS, only when user explicit enable/disable ime. No Disabled - // after Commit. - // - // We use input_method_editor_started to manually insert CompositionStart - // between Commits. - match ime { - winit::event::Ime::Enabled => { - self.ime_event_enable(); - } - winit::event::Ime::Preedit(text, Some(_cursor)) => { - self.ime_event_enable(); - self.egui_input - .events - .push(egui::Event::Ime(egui::ImeEvent::Preedit(text.clone()))); - } - winit::event::Ime::Commit(text) => { - self.egui_input - .events - .push(egui::Event::Ime(egui::ImeEvent::Commit(text.clone()))); - self.ime_event_disable(); - } - winit::event::Ime::Disabled | winit::event::Ime::Preedit(_, None) => { - self.ime_event_disable(); - } - }; + if cfg!(target_os = "linux") { + // We ignore IME events on linux, because of https://github.com/emilk/egui/issues/5008 + } else { + // on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit. + // So no need to check is_mac_cmd. + // + // How winit produce `Ime::Enabled` and `Ime::Disabled` differs in MacOS + // and Windows. + // + // - On Windows, before and after each Commit will produce an Enable/Disabled + // event. + // - On MacOS, only when user explicit enable/disable ime. No Disabled + // after Commit. + // + // We use input_method_editor_started to manually insert CompositionStart + // between Commits. + match ime { + winit::event::Ime::Enabled => { + self.ime_event_enable(); + } + winit::event::Ime::Preedit(text, Some(_cursor)) => { + self.ime_event_enable(); + self.egui_input + .events + .push(egui::Event::Ime(egui::ImeEvent::Preedit(text.clone()))); + } + winit::event::Ime::Commit(text) => { + self.egui_input + .events + .push(egui::Event::Ime(egui::ImeEvent::Commit(text.clone()))); + self.ime_event_disable(); + } + winit::event::Ime::Disabled | winit::event::Ime::Preedit(_, None) => { + self.ime_event_disable(); + } + }; + } EventResponse { repaint: true, @@ -812,7 +820,7 @@ impl State { window: &Window, platform_output: egui::PlatformOutput, ) { - crate::profile_function!(); + profiling::function_scope!(); let egui::PlatformOutput { cursor_icon, @@ -840,7 +848,7 @@ impl State { let allow_ime = ime.is_some(); if self.allow_ime != allow_ime { self.allow_ime = allow_ime; - crate::profile_scope!("set_ime_allowed"); + profiling::scope!("set_ime_allowed"); window.set_ime_allowed(allow_ime); } @@ -851,7 +859,7 @@ impl State { || self.egui_ctx.input(|i| !i.events.is_empty()) { self.ime_rect_px = Some(ime_rect_px); - crate::profile_scope!("set_ime_cursor_area"); + profiling::scope!("set_ime_cursor_area"); window.set_ime_cursor_area( winit::dpi::PhysicalPosition { x: ime_rect_px.min.x, @@ -870,7 +878,7 @@ impl State { #[cfg(feature = "accesskit")] if let Some(accesskit) = self.accesskit.as_mut() { if let Some(update) = accesskit_update { - crate::profile_scope!("accesskit"); + profiling::scope!("accesskit"); accesskit.update_if_active(|| update); } } @@ -942,8 +950,7 @@ pub fn update_viewport_info( window: &Window, is_init: bool, ) { - crate::profile_function!(); - + profiling::function_scope!(); let pixels_per_point = pixels_per_point(egui_ctx, window); let has_a_position = match window.is_minimized() { @@ -964,7 +971,7 @@ pub fn update_viewport_info( }; let monitor_size = { - crate::profile_scope!("monitor_size"); + profiling::scope!("monitor_size"); if let Some(monitor) = window.current_monitor() { let size = monitor.size().to_logical::(pixels_per_point.into()); Some(egui::vec2(size.width, size.height)) @@ -1290,7 +1297,7 @@ fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option, ) { - crate::profile_function!(); + profiling::function_scope!(); use winit::window::ResizeDirection; @@ -1505,8 +1512,8 @@ fn process_viewport_command( log::warn!("{command:?}: {err}"); } } - ViewportCommand::Screenshot => { - actions_requested.insert(ActionRequested::Screenshot); + ViewportCommand::Screenshot(user_data) => { + actions_requested.insert(ActionRequested::Screenshot(user_data)); } ViewportCommand::RequestCut => { actions_requested.insert(ActionRequested::Cut); @@ -1531,7 +1538,7 @@ pub fn create_window( event_loop: &ActiveEventLoop, viewport_builder: &ViewportBuilder, ) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let window_attributes = create_winit_window_attributes(egui_ctx, event_loop, viewport_builder.clone()); @@ -1545,7 +1552,7 @@ pub fn create_winit_window_attributes( event_loop: &ActiveEventLoop, viewport_builder: ViewportBuilder, ) -> winit::window::WindowAttributes { - crate::profile_function!(); + profiling::function_scope!(); // We set sizes and positions in egui:s own ui points, which depends on the egui // zoom_factor and the native pixels per point, so we need to know that here. @@ -1741,7 +1748,7 @@ fn to_winit_icon(icon: &egui::IconData) -> Option { if icon.is_empty() { None } else { - crate::profile_function!(); + profiling::function_scope!(); match winit::window::Icon::from_rgba(icon.rgba.clone(), icon.width, icon.height) { Ok(winit_icon) => Some(winit_icon), Err(err) => { @@ -1856,30 +1863,3 @@ pub fn short_window_event_description(event: &winit::event::WindowEvent) -> &'st WindowEvent::PanGesture { .. } => "WindowEvent::PanGesture", } } - -// --------------------------------------------------------------------------- - -mod profiling_scopes { - #![allow(unused_macros)] - #![allow(unused_imports)] - - /// Profiling macro for feature "puffin" - macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} diff --git a/crates/egui-winit/src/window_settings.rs b/crates/egui-winit/src/window_settings.rs index 627d88158c0..168a086c70f 100644 --- a/crates/egui-winit/src/window_settings.rs +++ b/crates/egui-winit/src/window_settings.rs @@ -56,7 +56,7 @@ impl WindowSettings { event_loop: &winit::event_loop::ActiveEventLoop, mut viewport_builder: ViewportBuilder, ) -> ViewportBuilder { - crate::profile_function!(); + profiling::function_scope!(); // `WindowBuilder::with_position` expects inner position in Macos, and outer position elsewhere // See [`winit::window::WindowBuilder::with_position`] for details. @@ -143,8 +143,7 @@ fn find_active_monitor( window_size_pts: egui::Vec2, position_px: &egui::Pos2, ) -> Option { - crate::profile_function!(); - + profiling::function_scope!(); let monitors = event_loop.available_monitors(); // default to primary monitor, in case the correct monitor was disconnected. @@ -178,7 +177,7 @@ fn clamp_pos_to_monitors( window_size_pts: egui::Vec2, position_px: &mut egui::Pos2, ) { - crate::profile_function!(); + profiling::function_scope!(); let Some(active_monitor) = find_active_monitor(egui_zoom_factor, event_loop, window_size_pts, position_px) diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index 487a4eac6e9..ce7999dd1f4 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -62,10 +62,6 @@ mint = ["epaint/mint"] ## Enable persistence of memory (window positions etc). persistence = ["serde", "epaint/serde", "ron"] -## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -## -## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. -puffin = ["dep:puffin", "epaint/puffin"] ## Enable parallel tessellation using [`rayon`](https://docs.rs/rayon). ## @@ -85,9 +81,10 @@ epaint = { workspace = true, default-features = false } ahash.workspace = true nohash-hasher.workspace = true +profiling.workspace = true #! ### Optional dependencies -accesskit = { version = "0.16", optional = true } +accesskit = { version = "0.17.0", optional = true } backtrace = { workspace = true, optional = true } @@ -95,6 +92,5 @@ backtrace = { workspace = true, optional = true } document-features = { workspace = true, optional = true } log = { workspace = true, optional = true } -puffin = { workspace = true, optional = true } ron = { workspace = true, optional = true } serde = { workspace = true, optional = true, features = ["derive", "rc"] } diff --git a/crates/egui/src/cache/cache_storage.rs b/crates/egui/src/cache/cache_storage.rs new file mode 100644 index 00000000000..d4c3c9aef15 --- /dev/null +++ b/crates/egui/src/cache/cache_storage.rs @@ -0,0 +1,69 @@ +use super::CacheTrait; + +/// A typemap of many caches, all implemented with [`CacheTrait`]. +/// +/// You can access egui's caches via [`crate::Memory::caches`], +/// found with [`crate::Context::memory_mut`]. +/// +/// ``` +/// use egui::cache::{CacheStorage, ComputerMut, FrameCache}; +/// +/// #[derive(Default)] +/// struct CharCounter {} +/// impl ComputerMut<&str, usize> for CharCounter { +/// fn compute(&mut self, s: &str) -> usize { +/// s.chars().count() +/// } +/// } +/// type CharCountCache<'a> = FrameCache; +/// +/// # let mut cache_storage = CacheStorage::default(); +/// let mut cache = cache_storage.cache::>(); +/// assert_eq!(cache.get("hello"), 5); +/// ``` +#[derive(Default)] +pub struct CacheStorage { + caches: ahash::HashMap>, +} + +impl CacheStorage { + pub fn cache(&mut self) -> &mut Cache { + self.caches + .entry(std::any::TypeId::of::()) + .or_insert_with(|| Box::::default()) + .as_any_mut() + .downcast_mut::() + .unwrap() + } + + /// Total number of cached values + fn num_values(&self) -> usize { + self.caches.values().map(|cache| cache.len()).sum() + } + + /// Call once per frame to evict cache. + pub fn update(&mut self) { + self.caches.retain(|_, cache| { + cache.update(); + cache.len() > 0 + }); + } +} + +impl Clone for CacheStorage { + fn clone(&self) -> Self { + // We return an empty cache that can be filled in again. + Self::default() + } +} + +impl std::fmt::Debug for CacheStorage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "FrameCacheStorage[{} caches with {} elements]", + self.caches.len(), + self.num_values() + ) + } +} diff --git a/crates/egui/src/cache/cache_trait.rs b/crates/egui/src/cache/cache_trait.rs new file mode 100644 index 00000000000..73cb61f3865 --- /dev/null +++ b/crates/egui/src/cache/cache_trait.rs @@ -0,0 +1,11 @@ +/// A cache, storing some value for some length of time. +#[allow(clippy::len_without_is_empty)] +pub trait CacheTrait: 'static + Send + Sync { + /// Call once per frame to evict cache. + fn update(&mut self); + + /// Number of values currently in the cache. + fn len(&self) -> usize; + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any; +} diff --git a/crates/egui/src/cache/frame_cache.rs b/crates/egui/src/cache/frame_cache.rs new file mode 100644 index 00000000000..6c74c58dc3b --- /dev/null +++ b/crates/egui/src/cache/frame_cache.rs @@ -0,0 +1,86 @@ +use super::CacheTrait; + +/// Something that does an expensive computation that we want to cache +/// to save us from recomputing it each frame. +pub trait ComputerMut: 'static + Send + Sync { + fn compute(&mut self, key: Key) -> Value; +} + +/// Caches the results of a computation for one frame. +/// If it is still used next frame, it is not recomputed. +/// If it is not used next frame, it is evicted from the cache to save memory. +pub struct FrameCache { + generation: u32, + computer: Computer, + cache: nohash_hasher::IntMap, +} + +impl Default for FrameCache +where + Computer: Default, +{ + fn default() -> Self { + Self::new(Computer::default()) + } +} + +impl FrameCache { + pub fn new(computer: Computer) -> Self { + Self { + generation: 0, + computer, + cache: Default::default(), + } + } + + /// Must be called once per frame to clear the cache. + pub fn evict_cache(&mut self) { + let current_generation = self.generation; + self.cache.retain(|_key, cached| { + cached.0 == current_generation // only keep those that were used this frame + }); + self.generation = self.generation.wrapping_add(1); + } +} + +impl FrameCache { + /// Get from cache (if the same key was used last frame) + /// or recompute and store in the cache. + pub fn get(&mut self, key: Key) -> Value + where + Key: Copy + std::hash::Hash, + Value: Clone, + Computer: ComputerMut, + { + let hash = crate::util::hash(key); + + match self.cache.entry(hash) { + std::collections::hash_map::Entry::Occupied(entry) => { + let cached = entry.into_mut(); + cached.0 = self.generation; + cached.1.clone() + } + std::collections::hash_map::Entry::Vacant(entry) => { + let value = self.computer.compute(key); + entry.insert((self.generation, value.clone())); + value + } + } + } +} + +impl CacheTrait + for FrameCache +{ + fn update(&mut self) { + self.evict_cache(); + } + + fn len(&self) -> usize { + self.cache.len() + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } +} diff --git a/crates/egui/src/cache/frame_publisher.rs b/crates/egui/src/cache/frame_publisher.rs new file mode 100644 index 00000000000..0c2bc81d6c5 --- /dev/null +++ b/crates/egui/src/cache/frame_publisher.rs @@ -0,0 +1,61 @@ +use std::hash::Hash; + +use super::CacheTrait; + +/// Stores a key:value pair for the duration of this frame and the next. +pub struct FramePublisher { + generation: u32, + cache: ahash::HashMap, +} + +impl Default for FramePublisher { + fn default() -> Self { + Self::new() + } +} + +impl FramePublisher { + pub fn new() -> Self { + Self { + generation: 0, + cache: Default::default(), + } + } + + /// Publish the value. It will be available for the duration of this and the next frame. + pub fn set(&mut self, key: Key, value: Value) { + self.cache.insert(key, (self.generation, value)); + } + + /// Retrieve a value if it was published this or the previous frame. + pub fn get(&self, key: &Key) -> Option<&Value> { + self.cache.get(key).map(|(_, value)| value) + } + + /// Must be called once per frame to clear the cache. + pub fn evict_cache(&mut self) { + let current_generation = self.generation; + self.cache.retain(|_key, cached| { + cached.0 == current_generation // only keep those that were published this frame + }); + self.generation = self.generation.wrapping_add(1); + } +} + +impl CacheTrait for FramePublisher +where + Key: 'static + Eq + Hash + Send + Sync, + Value: 'static + Send + Sync, +{ + fn update(&mut self) { + self.evict_cache(); + } + + fn len(&self) -> usize { + self.cache.len() + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } +} diff --git a/crates/egui/src/cache/mod.rs b/crates/egui/src/cache/mod.rs new file mode 100644 index 00000000000..68469ef3907 --- /dev/null +++ b/crates/egui/src/cache/mod.rs @@ -0,0 +1,21 @@ +//! Caches for preventing the same value from being recomputed every frame. +//! +//! Computing the same thing each frame can be expensive, +//! so often you want to save the result from the previous frame and reuse it. +//! +//! Enter [`FrameCache`]: it caches the results of a computation for one frame. +//! If it is still used next frame, it is not recomputed. +//! If it is not used next frame, it is evicted from the cache to save memory. +//! +//! You can access egui's caches via [`crate::Memory::caches`], +//! found with [`crate::Context::memory_mut`]. + +mod cache_storage; +mod cache_trait; +mod frame_cache; +mod frame_publisher; + +pub use cache_storage::CacheStorage; +pub use cache_trait::CacheTrait; +pub use frame_cache::{ComputerMut, FrameCache}; +pub use frame_publisher::FramePublisher; diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index fa8e5fc203b..a2bfbe1905f 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -467,7 +467,7 @@ impl Area { id: interact_id, layer_id, rect: state.rect(), - interact_rect: state.rect(), + interact_rect: state.rect().intersect(constrain_rect), sense, enabled, }, @@ -540,6 +540,7 @@ impl Prepared { let mut ui_builder = UiBuilder::new() .ui_stack_info(UiStackInfo::new(self.kind)) + .layer_id(self.layer_id) .max_rect(max_rect); if !self.enabled { @@ -549,7 +550,7 @@ impl Prepared { ui_builder = ui_builder.sizing_pass().invisible(); } - let mut ui = Ui::new(ctx.clone(), self.layer_id, self.layer_id.id, ui_builder); + let mut ui = Ui::new(ctx.clone(), self.layer_id.id, ui_builder); ui.set_clip_rect(self.constrain_rect); // Don't paint outside our bounds if self.fade_in { @@ -568,6 +569,14 @@ impl Prepared { ui } + pub(crate) fn with_widget_info(&self, make_info: impl Fn() -> crate::WidgetInfo) { + self.move_response.widget_info(make_info); + } + + pub(crate) fn id(&self) -> Id { + self.move_response.id + } + #[allow(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`. pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response { let Self { diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index 298cdb47698..d6a6c7fbacf 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -87,6 +87,14 @@ impl CollapsingState { ) -> Response { let (_id, rect) = ui.allocate_space(button_size); let response = ui.interact(rect, self.id, Sense::click()); + response.widget_info(|| { + WidgetInfo::labeled( + WidgetType::Button, + ui.is_enabled(), + if self.is_open() { "Hide" } else { "Show" }, + ) + }); + if response.clicked() { self.toggle(ui); } diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index 3d2bdd50b53..55157294b79 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -45,6 +45,7 @@ pub struct ComboBox { height: Option, icon: Option, wrap_mode: Option, + close_behavior: Option, } impl ComboBox { @@ -58,6 +59,7 @@ impl ComboBox { height: None, icon: None, wrap_mode: None, + close_behavior: None, } } @@ -72,6 +74,7 @@ impl ComboBox { height: None, icon: None, wrap_mode: None, + close_behavior: None, } } @@ -85,6 +88,7 @@ impl ComboBox { height: None, icon: None, wrap_mode: None, + close_behavior: None, } } @@ -173,7 +177,6 @@ impl ComboBox { #[inline] pub fn wrap(mut self) -> Self { self.wrap_mode = Some(TextWrapMode::Wrap); - self } @@ -184,6 +187,15 @@ impl ComboBox { self } + /// Controls the close behavior for the popup. + /// + /// By default, `PopupCloseBehavior::CloseOnClick` will be used. + #[inline] + pub fn close_behavior(mut self, close_behavior: PopupCloseBehavior) -> Self { + self.close_behavior = Some(close_behavior); + self + } + /// Show the combo box, with the given ui code for the menu contents. /// /// Returns `InnerResponse { inner: None }` if the combo box is closed. @@ -208,6 +220,7 @@ impl ComboBox { height, icon, wrap_mode, + close_behavior, } = self; let button_id = ui.make_persistent_id(id_salt); @@ -220,6 +233,7 @@ impl ComboBox { menu_contents, icon, wrap_mode, + close_behavior, (width, height), ); if let Some(label) = label { @@ -301,6 +315,7 @@ fn combo_box_dyn<'c, R>( menu_contents: Box R + 'c>, icon: Option, wrap_mode: Option, + close_behavior: Option, (width, height): (Option, Option), ) -> InnerResponse> { let popup_id = ComboBox::widget_to_popup_id(button_id); @@ -325,6 +340,8 @@ fn combo_box_dyn<'c, R>( let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode()); + let close_behavior = close_behavior.unwrap_or(PopupCloseBehavior::CloseOnClick); + let margin = ui.spacing().button_padding; let button_response = button_frame(ui, button_id, is_popup_open, Sense::click(), |ui| { let icon_spacing = ui.spacing().icon_spacing; @@ -396,7 +413,7 @@ fn combo_box_dyn<'c, R>( popup_id, &button_response, above_or_below, - PopupCloseBehavior::CloseOnClick, + close_behavior, |ui| { ScrollArea::vertical() .max_height(height) diff --git a/crates/egui/src/containers/mod.rs b/crates/egui/src/containers/mod.rs index 3dd75a4458e..e68e0def1b0 100644 --- a/crates/egui/src/containers/mod.rs +++ b/crates/egui/src/containers/mod.rs @@ -6,6 +6,7 @@ pub(crate) mod area; pub mod collapsing_header; mod combo_box; pub mod frame; +pub mod modal; pub mod panel; pub mod popup; pub(crate) mod resize; @@ -18,6 +19,7 @@ pub use { collapsing_header::{CollapsingHeader, CollapsingResponse}, combo_box::*, frame::Frame, + modal::{Modal, ModalResponse}, panel::{CentralPanel, SidePanel, TopBottomPanel}, popup::*, resize::Resize, diff --git a/crates/egui/src/containers/modal.rs b/crates/egui/src/containers/modal.rs new file mode 100644 index 00000000000..521b9dedba4 --- /dev/null +++ b/crates/egui/src/containers/modal.rs @@ -0,0 +1,165 @@ +use crate::{ + Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiBuilder, UiKind, +}; +use emath::{Align2, Vec2}; + +/// A modal dialog. +/// Similar to a [`crate::Window`] but centered and with a backdrop that +/// blocks input to the rest of the UI. +/// +/// You can show multiple modals on top of each other. The topmost modal will always be +/// the most recently shown one. +pub struct Modal { + pub area: Area, + pub backdrop_color: Color32, + pub frame: Option, +} + +impl Modal { + /// Create a new Modal. The id is passed to the area. + pub fn new(id: Id) -> Self { + Self { + area: Self::default_area(id), + backdrop_color: Color32::from_black_alpha(100), + frame: None, + } + } + + /// Returns an area customized for a modal. + /// Makes these changes to the default area: + /// - sense: hover + /// - anchor: center + /// - order: foreground + pub fn default_area(id: Id) -> Area { + Area::new(id) + .kind(UiKind::Modal) + .sense(Sense::hover()) + .anchor(Align2::CENTER_CENTER, Vec2::ZERO) + .order(Order::Foreground) + .interactable(true) + } + + /// Set the frame of the modal. + /// + /// Default is [`Frame::popup`]. + #[inline] + pub fn frame(mut self, frame: Frame) -> Self { + self.frame = Some(frame); + self + } + + /// Set the backdrop color of the modal. + /// + /// Default is `Color32::from_black_alpha(100)`. + #[inline] + pub fn backdrop_color(mut self, color: Color32) -> Self { + self.backdrop_color = color; + self + } + + /// Set the area of the modal. + /// + /// Default is [`Modal::default_area`]. + #[inline] + pub fn area(mut self, area: Area) -> Self { + self.area = area; + self + } + + /// Show the modal. + pub fn show(self, ctx: &Context, content: impl FnOnce(&mut Ui) -> T) -> ModalResponse { + let Self { + area, + backdrop_color, + frame, + } = self; + + let (is_top_modal, any_popup_open) = ctx.memory_mut(|mem| { + mem.set_modal_layer(area.layer()); + ( + mem.top_modal_layer() == Some(area.layer()), + mem.any_popup_open(), + ) + }); + let InnerResponse { + inner: (inner, backdrop_response), + response, + } = area.show(ctx, |ui| { + let bg_rect = ui.ctx().screen_rect(); + let bg_sense = Sense { + click: true, + drag: true, + focusable: false, + }; + let mut backdrop = ui.new_child(UiBuilder::new().sense(bg_sense).max_rect(bg_rect)); + backdrop.set_min_size(bg_rect.size()); + ui.painter().rect_filled(bg_rect, 0.0, backdrop_color); + let backdrop_response = backdrop.response(); + + let frame = frame.unwrap_or_else(|| Frame::popup(ui.style())); + + // We need the extra scope with the sense since frame can't have a sense and since we + // need to prevent the clicks from passing through to the backdrop. + let inner = ui + .scope_builder( + UiBuilder::new().sense(Sense { + click: true, + drag: true, + focusable: false, + }), + |ui| frame.show(ui, content).inner, + ) + .inner; + + (inner, backdrop_response) + }); + + ModalResponse { + response, + backdrop_response, + inner, + is_top_modal, + any_popup_open, + } + } +} + +/// The response of a modal dialog. +pub struct ModalResponse { + /// The response of the modal contents + pub response: Response, + + /// The response of the modal backdrop. + /// + /// A click on this means the user clicked outside the modal, + /// in which case you might want to close the modal. + pub backdrop_response: Response, + + /// The inner response from the content closure + pub inner: T, + + /// Is this the topmost modal? + pub is_top_modal: bool, + + /// Is there any popup open? + /// We need to check this before the modal contents are shown, so we can know if any popup + /// was open when checking if the escape key was clicked. + pub any_popup_open: bool, +} + +impl ModalResponse { + /// Should the modal be closed? + /// Returns true if: + /// - the backdrop was clicked + /// - this is the topmost modal, no popup is open and the escape key was pressed + pub fn should_close(&self) -> bool { + let ctx = &self.response.ctx; + + // this is a closure so that `Esc` is consumed only if the modal is topmost + let escape_clicked = + || ctx.input_mut(|i| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape)); + + self.backdrop_response.clicked() + || (self.is_top_modal && !self.any_popup_open && escape_clicked()) + } +} diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index f3b6c913cfc..7cf22a693db 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -329,9 +329,6 @@ impl SidePanel { ui.ctx().set_cursor_icon(cursor_icon); } - // Keep this rect snapped so that panel content can be pixel-perfect - let rect = ui.painter().round_rect_to_pixels(rect); - PanelState { rect }.store(ui.ctx(), id); { @@ -375,14 +372,14 @@ impl SidePanel { ctx: &Context, add_contents: Box R + 'c>, ) -> InnerResponse { - let layer_id = LayerId::background(); let side = self.side; let available_rect = ctx.available_rect(); let mut panel_ui = Ui::new( ctx.clone(), - layer_id, self.id, - UiBuilder::new().max_rect(available_rect), + UiBuilder::new() + .layer_id(LayerId::background()) + .max_rect(available_rect), ); panel_ui.set_clip_rect(ctx.screen_rect()); @@ -824,9 +821,6 @@ impl TopBottomPanel { ui.ctx().set_cursor_icon(cursor_icon); } - // Keep this rect snapped so that panel content can be pixel-perfect - let rect = ui.painter().round_rect_to_pixels(rect); - PanelState { rect }.store(ui.ctx(), id); { @@ -842,6 +836,8 @@ impl TopBottomPanel { }; // TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done let resize_y = side.opposite().side_y(rect); + + // This makes it pixel-perfect for odd-sized strokes (width=1.0, width=3.0, etc) let resize_y = ui.painter().round_to_pixel_center(resize_y); // We want the line exactly on the last pixel but rust rounds away from zero so we bring it back a bit for @@ -868,15 +864,15 @@ impl TopBottomPanel { ctx: &Context, add_contents: Box R + 'c>, ) -> InnerResponse { - let layer_id = LayerId::background(); let available_rect = ctx.available_rect(); let side = self.side; let mut panel_ui = Ui::new( ctx.clone(), - layer_id, self.id, - UiBuilder::new().max_rect(available_rect), + UiBuilder::new() + .layer_id(LayerId::background()) + .max_rect(available_rect), ); panel_ui.set_clip_rect(ctx.screen_rect()); @@ -1135,14 +1131,14 @@ impl CentralPanel { add_contents: Box R + 'c>, ) -> InnerResponse { let available_rect = ctx.available_rect(); - let layer_id = LayerId::background(); let id = Id::new((ctx.viewport_id(), "central_panel")); let mut panel_ui = Ui::new( ctx.clone(), - layer_id, id, - UiBuilder::new().max_rect(available_rect), + UiBuilder::new() + .layer_id(LayerId::background()) + .max_rect(available_rect), ); panel_ui.set_clip_rect(ctx.screen_rect()); diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 45304245ca4..81bf84a2f4f 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -87,17 +87,22 @@ pub fn show_tooltip_at_pointer( // Add a small exclusion zone around the pointer to avoid tooltips // covering what we're hovering over. - let mut exclusion_rect = Rect::from_center_size(pointer_pos, Vec2::splat(24.0)); + let mut pointer_rect = Rect::from_center_size(pointer_pos, Vec2::splat(24.0)); // Keep the left edge of the tooltip in line with the cursor: - exclusion_rect.min.x = pointer_pos.x; + pointer_rect.min.x = pointer_pos.x; + + // Transform global coords to layer coords: + if let Some(from_global) = ctx.layer_transform_from_global(parent_layer) { + pointer_rect = from_global * pointer_rect; + } show_tooltip_at_dyn( ctx, parent_layer, widget_id, allow_placing_below, - &exclusion_rect, + &pointer_rect, Box::new(add_contents), ) }) @@ -155,9 +160,10 @@ fn show_tooltip_at_dyn<'c, R>( widget_rect: &Rect, add_contents: Box R + 'c>, ) -> R { + // Transform layer coords to global coords: let mut widget_rect = *widget_rect; - if let Some(transform) = ctx.memory(|m| m.layer_transforms.get(&parent_layer).copied()) { - widget_rect = transform * widget_rect; + if let Some(to_global) = ctx.layer_transform_to_global(parent_layer) { + widget_rect = to_global * widget_rect; } remember_that_tooltip_was_shown(ctx); @@ -398,16 +404,17 @@ pub fn popup_above_or_below_widget( AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM), AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP), }; - if let Some(transform) = parent_ui + + if let Some(to_global) = parent_ui .ctx() - .memory(|m| m.layer_transforms.get(&parent_ui.layer_id()).copied()) + .layer_transform_to_global(parent_ui.layer_id()) { - pos = transform * pos; + pos = to_global * pos; } let frame = Frame::popup(parent_ui.style()); let frame_margin = frame.total_margin(); - let inner_width = widget_response.rect.width() - frame_margin.sum().x; + let inner_width = (widget_response.rect.width() - frame_margin.sum().x).max(0.0); parent_ui.ctx().pass_state_mut(|fs| { fs.layers diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 458bf11229d..84f687783aa 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -205,7 +205,7 @@ struct Prepared { } impl Resize { - fn begin(&mut self, ui: &mut Ui) -> Prepared { + fn begin(&self, ui: &mut Ui) -> Prepared { let position = ui.available_rect_before_wrap().min; let id = self.id.unwrap_or_else(|| { let id_salt = self.id_salt.unwrap_or_else(|| Id::new("resize")); @@ -295,7 +295,7 @@ impl Resize { } } - pub fn show(mut self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R { + pub fn show(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R { let mut prepared = self.begin(ui); let ret = add_contents(&mut prepared.content_ui); self.end(ui, prepared); diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index ab7da8aff29..3c14a02e5e1 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -39,7 +39,7 @@ pub struct State { scroll_start_offset_from_top_left: [Option; 2], /// Is the scroll sticky. This is true while scroll handle is in the end position - /// and remains that way until the user moves the scroll_handle. Once unstuck (false) + /// and remains that way until the user moves the `scroll_handle`. Once unstuck (false) /// it remains false until the scroll touches the end position, which reenables stickiness. scroll_stuck_to_end: Vec2b, @@ -499,6 +499,11 @@ struct Prepared { scrolling_enabled: bool, stick_to_end: Vec2b, + + /// If there was a scroll target before the [`ScrollArea`] was added this frame, it's + /// not for us to handle so we save it and restore it after this [`ScrollArea`] is done. + saved_scroll_target: [Option; 2], + animated: bool, } @@ -621,20 +626,28 @@ impl ScrollArea { .interact_rect .map(|rect| ui.interact(rect, id.with("area"), Sense::drag())); - if content_response_option.map(|response| response.dragged()) == Some(true) { + if content_response_option + .as_ref() + .is_some_and(|response| response.dragged()) + { for d in 0..2 { if scroll_enabled[d] { ui.input(|input| { state.offset[d] -= input.pointer.delta()[d]; - state.vel[d] = input.pointer.velocity()[d]; }); state.scroll_stuck_to_end[d] = false; state.offset_target[d] = None; - } else { - state.vel[d] = 0.0; } } } else { + // Apply the cursor velocity to the scroll area when the user releases the drag. + if content_response_option + .as_ref() + .is_some_and(|response| response.drag_stopped()) + { + state.vel = + scroll_enabled.to_vec2() * ui.input(|input| input.pointer.velocity()); + } for d in 0..2 { // Kinetic scrolling let stop_speed = 20.0; // Pixels per second. @@ -685,6 +698,10 @@ impl ScrollArea { } } + let saved_scroll_target = content_ui + .ctx() + .pass_state_mut(|state| std::mem::take(&mut state.scroll_target)); + Prepared { id, state, @@ -699,6 +716,7 @@ impl ScrollArea { viewport, scrolling_enabled, stick_to_end, + saved_scroll_target, animated, } } @@ -812,6 +830,7 @@ impl Prepared { viewport: _, scrolling_enabled, stick_to_end, + saved_scroll_target, animated, } = self; @@ -845,7 +864,7 @@ impl Prepared { let (start, end) = (range.min, range.max); let clip_start = clip_rect.min[d]; let clip_end = clip_rect.max[d]; - let mut spacing = ui.spacing().item_spacing[d]; + let mut spacing = content_ui.spacing().item_spacing[d]; let delta_update = if let Some(align) = align { let center_factor = align.to_factor(); @@ -894,6 +913,15 @@ impl Prepared { } } + // Restore scroll target meant for ScrollAreas up the stack (if any) + ui.ctx().pass_state_mut(|state| { + for d in 0..2 { + if saved_scroll_target[d].is_some() { + state.scroll_target[d] = saved_scroll_target[d].clone(); + }; + } + }); + let inner_rect = { // At this point this is the available size for the inner rect. let mut inner_size = inner_rect.size(); diff --git a/crates/egui/src/containers/sides.rs b/crates/egui/src/containers/sides.rs index 3bc353380d0..e34ae70eb8c 100644 --- a/crates/egui/src/containers/sides.rs +++ b/crates/egui/src/containers/sides.rs @@ -78,7 +78,7 @@ impl Sides { let height = height.unwrap_or_else(|| ui.spacing().interact_size.y); let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing.x); - let mut top_rect = ui.max_rect(); + let mut top_rect = ui.available_rect_before_wrap(); top_rect.max.y = top_rect.min.y + height; let result_left; diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 438d562ede7..e2d133f39ad 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -5,10 +5,11 @@ use std::sync::Arc; use crate::collapsing_header::CollapsingState; use crate::{ Align, Align2, Context, CursorIcon, Id, InnerResponse, LayerId, NumExt, Order, Response, Sense, - TextStyle, Ui, UiKind, Vec2b, WidgetRect, WidgetText, + TextStyle, Ui, UiKind, Vec2b, WidgetInfo, WidgetRect, WidgetText, WidgetType, }; use epaint::{emath, pos2, vec2, Galley, Pos2, Rect, RectShape, Rounding, Shape, Stroke, Vec2}; +use super::scroll_area::ScrollBarVisibility; use super::{area, resize, Area, Frame, Resize, ScrollArea}; /// Builder for a floating window which can be dragged, closed, collapsed, resized and scrolled (off by default). @@ -287,7 +288,9 @@ impl<'open> Window<'open> { /// Set initial size of the window. #[inline] pub fn default_size(mut self, default_size: impl Into) -> Self { + let default_size: Vec2 = default_size.into(); self.resize = self.resize.default_size(default_size); + self.area = self.area.default_size(default_size); self } @@ -295,6 +298,7 @@ impl<'open> Window<'open> { #[inline] pub fn default_width(mut self, default_width: f32) -> Self { self.resize = self.resize.default_width(default_width); + self.area = self.area.default_width(default_width); self } @@ -302,6 +306,7 @@ impl<'open> Window<'open> { #[inline] pub fn default_height(mut self, default_height: f32) -> Self { self.resize = self.resize.default_height(default_height); + self.area = self.area.default_height(default_height); self } @@ -402,6 +407,13 @@ impl<'open> Window<'open> { self.scroll = self.scroll.drag_to_scroll(drag_to_scroll); self } + + /// Sets the [`ScrollBarVisibility`] of the window. + #[inline] + pub fn scroll_bar_visibility(mut self, visibility: ScrollBarVisibility) -> Self { + self.scroll = self.scroll.scroll_bar_visibility(visibility); + self + } } impl<'open> Window<'open> { @@ -466,6 +478,8 @@ impl<'open> Window<'open> { let on_top = Some(area_layer_id) == ctx.top_layer_id(); let mut area = area.begin(ctx); + area.with_widget_info(|| WidgetInfo::labeled(WidgetType::Window, true, title.text())); + // Calculate roughly how much larger the window size is compared to the inner rect let (title_bar_height, title_content_spacing) = if with_title_bar { let style = ctx.style(); @@ -489,8 +503,9 @@ impl<'open> Window<'open> { // First check for resize to avoid frame delay: let last_frame_outer_rect = area.state().rect(); - let resize_interaction = - resize_interaction(ctx, possible, area_layer_id, last_frame_outer_rect); + let resize_interaction = ctx.with_accessibility_parent(area.id(), || { + resize_interaction(ctx, possible, area_layer_id, last_frame_outer_rect) + }); let margins = window_frame.outer_margin.sum() + window_frame.inner_margin.sum() @@ -514,107 +529,109 @@ impl<'open> Window<'open> { } let content_inner = { - // BEGIN FRAME -------------------------------- - let frame_stroke = window_frame.stroke; - let mut frame = window_frame.begin(&mut area_content_ui); - - let show_close_button = open.is_some(); - - let where_to_put_header_background = &area_content_ui.painter().add(Shape::Noop); - - // Backup item spacing before the title bar - let item_spacing = frame.content_ui.spacing().item_spacing; - // Use title bar spacing as the item spacing before the content - frame.content_ui.spacing_mut().item_spacing.y = title_content_spacing; - - let title_bar = if with_title_bar { - let title_bar = TitleBar::new( - &mut frame.content_ui, - title, - show_close_button, - &mut collapsing, - collapsible, - ); - resize.min_size.x = resize.min_size.x.at_least(title_bar.rect.width()); // Prevent making window smaller than title bar width - Some(title_bar) - } else { - None - }; + ctx.with_accessibility_parent(area.id(), || { + // BEGIN FRAME -------------------------------- + let frame_stroke = window_frame.stroke; + let mut frame = window_frame.begin(&mut area_content_ui); + + let show_close_button = open.is_some(); + + let where_to_put_header_background = &area_content_ui.painter().add(Shape::Noop); + + // Backup item spacing before the title bar + let item_spacing = frame.content_ui.spacing().item_spacing; + // Use title bar spacing as the item spacing before the content + frame.content_ui.spacing_mut().item_spacing.y = title_content_spacing; + + let title_bar = if with_title_bar { + let title_bar = TitleBar::new( + &mut frame.content_ui, + title, + show_close_button, + &mut collapsing, + collapsible, + ); + resize.min_size.x = resize.min_size.x.at_least(title_bar.rect.width()); // Prevent making window smaller than title bar width + Some(title_bar) + } else { + None + }; - // Remove item spacing after the title bar - frame.content_ui.spacing_mut().item_spacing.y = 0.0; + // Remove item spacing after the title bar + frame.content_ui.spacing_mut().item_spacing.y = 0.0; + + let (content_inner, mut content_response) = collapsing + .show_body_unindented(&mut frame.content_ui, |ui| { + // Restore item spacing for the content + ui.spacing_mut().item_spacing.y = item_spacing.y; + + resize.show(ui, |ui| { + if scroll.is_any_scroll_enabled() { + scroll.show(ui, add_contents).inner + } else { + add_contents(ui) + } + }) + }) + .map_or((None, None), |ir| (Some(ir.inner), Some(ir.response))); + + let outer_rect = frame.end(&mut area_content_ui).rect; + paint_resize_corner( + &area_content_ui, + &possible, + outer_rect, + frame_stroke, + window_frame.rounding, + ); - let (content_inner, mut content_response) = collapsing - .show_body_unindented(&mut frame.content_ui, |ui| { - // Restore item spacing for the content - ui.spacing_mut().item_spacing.y = item_spacing.y; + // END FRAME -------------------------------- - resize.show(ui, |ui| { - if scroll.is_any_scroll_enabled() { - scroll.show(ui, add_contents).inner - } else { - add_contents(ui) - } - }) - }) - .map_or((None, None), |ir| (Some(ir.inner), Some(ir.response))); - - let outer_rect = frame.end(&mut area_content_ui).rect; - paint_resize_corner( - &area_content_ui, - &possible, - outer_rect, - frame_stroke, - window_frame.rounding, - ); + if let Some(title_bar) = title_bar { + let mut title_rect = Rect::from_min_size( + outer_rect.min, + Vec2 { + x: outer_rect.size().x, + y: title_bar_height, + }, + ); - // END FRAME -------------------------------- + title_rect = area_content_ui.painter().round_rect_to_pixels(title_rect); - if let Some(title_bar) = title_bar { - let mut title_rect = Rect::from_min_size( - outer_rect.min, - Vec2 { - x: outer_rect.size().x, - y: title_bar_height, - }, - ); + if on_top && area_content_ui.visuals().window_highlight_topmost { + let mut round = window_frame.rounding; - title_rect = area_content_ui.painter().round_rect_to_pixels(title_rect); + if !is_collapsed { + round.se = 0.0; + round.sw = 0.0; + } - if on_top && area_content_ui.visuals().window_highlight_topmost { - let mut round = window_frame.rounding; + area_content_ui.painter().set( + *where_to_put_header_background, + RectShape::filled(title_rect, round, header_color), + ); + }; - if !is_collapsed { - round.se = 0.0; - round.sw = 0.0; + // Fix title bar separator line position + if let Some(response) = &mut content_response { + response.rect.min.y = outer_rect.min.y + title_bar_height; } - area_content_ui.painter().set( - *where_to_put_header_background, - RectShape::filled(title_rect, round, header_color), + title_bar.ui( + &mut area_content_ui, + title_rect, + &content_response, + open, + &mut collapsing, + collapsible, ); - }; - - // Fix title bar separator line position - if let Some(response) = &mut content_response { - response.rect.min.y = outer_rect.min.y + title_bar_height; } - title_bar.ui( - &mut area_content_ui, - title_rect, - &content_response, - open, - &mut collapsing, - collapsible, - ); - } - - collapsing.store(ctx); + collapsing.store(ctx); - paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction); + paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction); - content_inner + content_inner + }) }; let full_response = area.end(ctx, area_content_ui); @@ -713,7 +730,7 @@ struct ResizeInteraction { bottom: SideResponse, } -/// A minitature version of `Response`, for each side of the window. +/// A miniature version of `Response`, for each side of the window. #[derive(Clone, Copy, Debug, Default)] struct SideResponse { hover: bool, @@ -1192,6 +1209,9 @@ impl TitleBar { fn close_button(ui: &mut Ui, rect: Rect) -> Response { let close_id = ui.auto_id_with("window_close_button"); let response = ui.interact(rect, close_id, Sense::click()); + response + .widget_info(|| WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), "Close window")); + ui.expand_to_include_rect(response.rect); let visuals = ui.style().interact(&response); diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index c8875dc3bf0..322007113ba 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -9,7 +9,7 @@ use epaint::{ pos2, stats::PaintStats, tessellator, - text::Fonts, + text::{FontInsert, FontPriority, Fonts}, util::OrderedFloat, vec2, ClippedPrimitive, ClippedShape, Color32, ImageData, ImageDelta, Pos2, Rect, TessellationOptions, TextureAtlas, TextureId, Vec2, @@ -109,13 +109,13 @@ struct Plugins { impl Plugins { fn call(ctx: &Context, _cb_name: &str, callbacks: &[NamedContextCallback]) { - crate::profile_scope!("plugins", _cb_name); + profiling::scope!("plugins", _cb_name); for NamedContextCallback { debug_name: _name, callback, } in callbacks { - crate::profile_scope!("plugin", _name); + profiling::scope!("plugin", _name); (callback)(ctx); } } @@ -498,19 +498,8 @@ impl ContextImpl { viewport.this_pass.begin_pass(screen_rect); { - let area_order = self.memory.areas().order_map(); - let mut layers: Vec = viewport.prev_pass.widgets.layer_ids().collect(); - - layers.sort_by(|a, b| { - if a.order == b.order { - // Maybe both are windows, so respect area order: - area_order.get(a).cmp(&area_order.get(b)) - } else { - // comparing e.g. background to tooltips - a.order.cmp(&b.order) - } - }); + layers.sort_by(|&a, &b| self.memory.areas().compare_order(a, b)); viewport.hits = if let Some(pos) = viewport.input.pointer.interact_pos() { let interact_radius = self.memory.options.style().interaction.interact_radius; @@ -518,7 +507,7 @@ impl ContextImpl { crate::hit_test::hit_test( &viewport.prev_pass.widgets, &layers, - &self.memory.layer_transforms, + &self.memory.to_global, pos, interact_radius, ) @@ -549,16 +538,16 @@ impl ContextImpl { #[cfg(feature = "accesskit")] if self.is_accesskit_enabled { - crate::profile_scope!("accesskit"); + profiling::scope!("accesskit"); use crate::pass_state::AccessKitPassState; let id = crate::accesskit_root_id(); - let mut builder = accesskit::NodeBuilder::new(accesskit::Role::Window); + let mut root_node = accesskit::Node::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); + root_node.set_transform(accesskit::Affine::scale(pixels_per_point.into())); + let mut nodes = IdMap::default(); + nodes.insert(id, root_node); viewport.this_pass.accesskit_state = Some(AccessKitPassState { - node_builders, + nodes, parent_stack: vec![id], }); } @@ -568,8 +557,7 @@ impl ContextImpl { /// Load fonts unless already loaded. fn update_fonts_mut(&mut self) { - crate::profile_function!(); - + profiling::function_scope!(); let input = &self.viewport().input; let pixels_per_point = input.pixels_per_point(); let max_texture_side = input.max_texture_side; @@ -582,6 +570,30 @@ impl ContextImpl { log::trace!("Loading new font definitions"); } + if !self.memory.add_fonts.is_empty() { + let fonts = self.memory.add_fonts.drain(..); + for font in fonts { + self.fonts.clear(); // recreate all the fonts + for family in font.families { + let fam = self + .font_definitions + .families + .entry(family.family) + .or_default(); + match family.priority { + FontPriority::Highest => fam.insert(0, font.name.clone()), + FontPriority::Lowest => fam.push(font.name.clone()), + } + } + self.font_definitions + .font_data + .insert(font.name, Arc::new(font.data)); + } + + #[cfg(feature = "log")] + log::trace!("Adding new fonts"); + } + let mut is_new = false; let fonts = self @@ -592,7 +604,7 @@ impl ContextImpl { log::trace!("Creating new Fonts for pixels_per_point={pixels_per_point}"); is_new = true; - crate::profile_scope!("Fonts::new"); + profiling::scope!("Fonts::new"); Fonts::new( pixels_per_point, max_texture_side, @@ -601,12 +613,12 @@ impl ContextImpl { }); { - crate::profile_scope!("Fonts::begin_pass"); + profiling::scope!("Fonts::begin_pass"); fonts.begin_pass(pixels_per_point, max_texture_side); } if is_new && self.memory.options.preload_font_glyphs { - crate::profile_scope!("preload_font_glyphs"); + profiling::scope!("preload_font_glyphs"); // Preload the most common characters for the most common fonts. // This is not very important to do, but may save a few GPU operations. for font_id in self.memory.options.style().text_styles.values() { @@ -616,9 +628,9 @@ impl ContextImpl { } #[cfg(feature = "accesskit")] - fn accesskit_node_builder(&mut self, id: Id) -> &mut accesskit::NodeBuilder { + fn accesskit_node_builder(&mut self, id: Id) -> &mut accesskit::Node { let state = self.viewport().this_pass.accesskit_state.as_mut().unwrap(); - let builders = &mut state.node_builders; + let builders = &mut state.nodes; if let std::collections::hash_map::Entry::Vacant(entry) = builders.entry(id) { entry.insert(Default::default()); let parent_id = state.parent_stack.last().unwrap(); @@ -788,8 +800,7 @@ impl Context { /// ``` #[must_use] pub fn run(&self, mut new_input: RawInput, mut run_ui: impl FnMut(&Self)) -> FullOutput { - crate::profile_function!(); - + profiling::function_scope!(); let viewport_id = new_input.viewport_id; let max_passes = self.write(|ctx| ctx.memory.options.max_passes.get()); @@ -797,9 +808,13 @@ impl Context { debug_assert_eq!(output.platform_output.num_completed_passes, 0); loop { - crate::profile_scope!( + profiling::scope!( "pass", - output.platform_output.num_completed_passes.to_string() + output + .platform_output + .num_completed_passes + .to_string() + .as_str() ); // We must move the `num_passes` (back) to the viewport output so that [`Self::will_discard`] @@ -862,7 +877,7 @@ impl Context { /// // handle full_output /// ``` pub fn begin_pass(&self, new_input: RawInput) { - crate::profile_function!(); + profiling::function_scope!(); self.write(|ctx| ctx.begin_pass(new_input)); @@ -1136,6 +1151,9 @@ impl Context { /// 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, allow_focus: bool) -> Response { + let interested_in_focus = + w.enabled && w.sense.focusable && self.memory(|mem| mem.allows_interaction(w.layer_id)); + // Remember this widget self.write(|ctx| { let viewport = ctx.viewport(); @@ -1145,12 +1163,12 @@ impl Context { // but also to know when we have reached the widget we are checking for cover. viewport.this_pass.widgets.insert(w.layer_id, w); - if allow_focus && w.sense.focusable { - ctx.memory.interested_in_focus(w.id); + if allow_focus && interested_in_focus { + ctx.memory.interested_in_focus(w.id, w.layer_id); } }); - if allow_focus && (!w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction()) { + if allow_focus && !interested_in_focus { // Not interested or allowed input: self.memory_mut(|mem| mem.surrender_focus(w.id)); } @@ -1255,7 +1273,7 @@ impl Context { #[cfg(feature = "accesskit")] if enabled && sense.click - && input.has_accesskit_action_request(id, accesskit::Action::Default) + && input.has_accesskit_action_request(id, accesskit::Action::Click) { res.fake_primary_click = true; } @@ -1302,11 +1320,11 @@ impl Context { res.is_pointer_button_down_on || res.long_touched || clicked || res.drag_stopped; if is_interacted_with { res.interact_pointer_pos = input.pointer.interact_pos(); - if let (Some(transform), Some(pos)) = ( - memory.layer_transforms.get(&res.layer_id), + if let (Some(to_global), Some(pos)) = ( + memory.to_global.get(&res.layer_id), &mut res.interact_pointer_pos, ) { - *pos = transform.inverse() * *pos; + *pos = to_global.inverse() * *pos; } } @@ -1417,6 +1435,10 @@ impl Context { /// /// Empty strings are ignored. /// + /// Note that in wasm applications, the clipboard is only accessible in secure contexts (e.g., + /// HTTPS or localhost). If this method is used outside of a secure context, it will log an + /// error and do nothing. See . + /// /// Equivalent to: /// ``` /// # let ctx = egui::Context::default(); @@ -1727,8 +1749,9 @@ impl Context { /// 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 pass. + /// This will overwrite the existing fonts. pub fn set_fonts(&self, font_definitions: FontDefinitions) { - crate::profile_function!(); + profiling::function_scope!(); let pixels_per_point = self.pixels_per_point(); @@ -1748,6 +1771,39 @@ impl Context { } } + /// Tell `egui` which fonts to use. + /// + /// 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 font will become active at the start of the next pass. + /// This will keep the existing fonts. + pub fn add_font(&self, new_font: FontInsert) { + profiling::function_scope!(); + + let pixels_per_point = self.pixels_per_point(); + + let mut update_fonts = true; + + self.read(|ctx| { + if let Some(current_fonts) = ctx.fonts.get(&pixels_per_point.into()) { + if current_fonts + .lock() + .fonts + .definitions() + .font_data + .contains_key(&new_font.name) + { + update_fonts = false; // no need to update + } + } + }); + + if update_fonts { + self.memory_mut(|mem| mem.add_fonts.push(new_font)); + } + } + /// 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 { @@ -2087,7 +2143,7 @@ impl Context { /// Call at the end of each frame if you called [`Context::begin_pass`]. #[must_use] pub fn end_pass(&self) -> FullOutput { - crate::profile_function!(); + profiling::function_scope!(); if self.options(|o| o.zoom_with_keyboard) { crate::gui_zoom::zoom_with_keyboard(self); @@ -2181,7 +2237,8 @@ impl Context { for id in contains_pointer { let mut widget_text = format!("{id:?}"); if let Some(rect) = widget_rects.get(id) { - widget_text += &format!(" {:?} {:?}", rect.rect, rect.sense); + widget_text += + &format!(" {:?} {:?} {:?}", rect.layer_id, rect.rect, rect.sense); } if let Some(info) = widget_rects.info(id) { widget_text += &format!(" {info:?}"); @@ -2207,11 +2264,17 @@ impl Context { if self.style().debug.show_widget_hits { let hits = self.write(|ctx| ctx.viewport().hits.clone()); let WidgetHits { + close, contains_pointer, click, drag, } = hits; + if false { + for widget in &close { + paint_widget(widget, "close", Color32::from_gray(70)); + } + } if true { for widget in &contains_pointer { paint_widget(widget, "contains_pointer", Color32::BLUE); @@ -2277,7 +2340,7 @@ impl ContextImpl { // https://github.com/emilk/egui/issues/3664 // at the cost of a lot of performance. // (This will override any smaller delta that was uploaded above.) - crate::profile_scope!("full_font_atlas_update"); + profiling::scope!("full_font_atlas_update"); let full_delta = ImageDelta::full(fonts.image(), TextureAtlas::texture_options()); tex_mngr.set(TextureId::default(), full_delta); } @@ -2291,15 +2354,15 @@ impl ContextImpl { #[cfg(feature = "accesskit")] { - crate::profile_scope!("accesskit"); + profiling::scope!("accesskit"); let state = viewport.this_pass.accesskit_state.take(); if let Some(state) = state { let root_id = crate::accesskit_root_id().accesskit_id(); let nodes = { state - .node_builders + .nodes .into_iter() - .map(|(id, builder)| (id.accesskit_id(), builder.build())) + .map(|(id, node)| (id.accesskit_id(), node)) .collect() }; let focus_id = self @@ -2316,12 +2379,12 @@ impl ContextImpl { let shapes = viewport .graphics - .drain(self.memory.areas().order(), &self.memory.layer_transforms); + .drain(self.memory.areas().order(), &self.memory.to_global); let mut repaint_needed = false; if self.memory.options.repaint_on_widget_change { - crate::profile_function!("compare-widget-rects"); + profiling::scope!("compare-widget-rects"); if viewport.prev_pass.widgets != viewport.this_pass.widgets { repaint_needed = true; // Some widget has moved } @@ -2460,7 +2523,7 @@ impl Context { shapes: Vec, pixels_per_point: f32, ) -> Vec { - crate::profile_function!(); + profiling::function_scope!(); // A tempting optimization is to reuse the tessellation from last frame if the // shapes are the same, but just comparing the shapes takes about 50% of the time @@ -2487,7 +2550,7 @@ impl Context { let paint_stats = PaintStats::from_shapes(&shapes); let clipped_primitives = { - crate::profile_scope!("tessellator::tessellate_shapes"); + profiling::scope!("tessellator::tessellate_shapes"); tessellator::Tessellator::new( pixels_per_point, tessellation_options, @@ -2632,6 +2695,7 @@ impl Context { /// Transform the graphics of the given layer. /// /// This will also affect input. + /// The direction of the given transform is "into the global coordinate system". /// /// This is a sticky setting, remembered from one frame to the next. /// @@ -2641,13 +2705,28 @@ impl Context { pub fn set_transform_layer(&self, layer_id: LayerId, transform: TSTransform) { self.memory_mut(|m| { if transform == TSTransform::IDENTITY { - m.layer_transforms.remove(&layer_id) + m.to_global.remove(&layer_id) } else { - m.layer_transforms.insert(layer_id, transform) + m.to_global.insert(layer_id, transform) } }); } + /// Return how to transform the graphics of the given layer into the global coordinate system. + /// + /// Set this with [`Self::layer_transform_to_global`]. + pub fn layer_transform_to_global(&self, layer_id: LayerId) -> Option { + self.memory(|m| m.to_global.get(&layer_id).copied()) + } + + /// Return how to transform the graphics of the global coordinate system into the local coordinate system of the given layer. + /// + /// This returns the inverse of [`Self::layer_transform_to_global`]. + pub fn layer_transform_from_global(&self, layer_id: LayerId) -> Option { + self.layer_transform_to_global(layer_id) + .map(|t| t.inverse()) + } + /// Move all the graphics at the given layer. /// /// Is used to implement drag-and-drop preview. @@ -2712,12 +2791,11 @@ impl Context { /// /// See also [`Response::contains_pointer`]. pub fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool { - let rect = - if let Some(transform) = self.memory(|m| m.layer_transforms.get(&layer_id).copied()) { - transform * rect - } else { - rect - }; + let rect = if let Some(to_global) = self.layer_transform_to_global(layer_id) { + to_global * rect + } else { + rect + }; if !rect.is_positive() { return false; } @@ -2884,7 +2962,9 @@ impl Context { for (name, data) in &mut font_definitions.font_data { ui.collapsing(name, |ui| { - if data.tweak.ui(ui).changed() { + let mut tweak = data.tweak; + if tweak.ui(ui).changed() { + Arc::make_mut(data).tweak = tweak; changed = true; } }); @@ -3077,28 +3157,26 @@ impl Context { self.memory_mut(|mem| *mem.areas_mut() = Default::default()); } }); - ui.indent("areas", |ui| { - ui.label("Visible areas, ordered back to front."); - ui.label("Hover to highlight"); + ui.indent("layers", |ui| { + ui.label("Layers, ordered back to front."); let layers_ids: Vec = self.memory(|mem| mem.areas().order().to_vec()); for layer_id in layers_ids { - let area = AreaState::load(self, layer_id.id); - if let Some(area) = area { + if let Some(area) = AreaState::load(self, layer_id.id) { let is_visible = self.memory(|mem| mem.areas().is_visible(&layer_id)); if !is_visible { continue; } let text = format!("{} - {:?}", layer_id.short_debug_format(), area.rect(),); // TODO(emilk): `Sense::hover_highlight()` - if ui - .add(Label::new(RichText::new(text).monospace()).sense(Sense::click())) - .hovered - && is_visible - { + let response = + ui.add(Label::new(RichText::new(text).monospace()).sense(Sense::click())); + if response.hovered && is_visible { ui.ctx() .debug_painter() .debug_rect(area.rect(), Color32::RED, ""); } + } else { + ui.monospace(layer_id.short_debug_format()); } } }); @@ -3206,7 +3284,7 @@ impl Context { pub fn accesskit_node_builder( &self, id: Id, - writer: impl FnOnce(&mut accesskit::NodeBuilder) -> R, + writer: impl FnOnce(&mut accesskit::Node) -> R, ) -> Option { self.write(|ctx| { ctx.viewport() @@ -3286,7 +3364,7 @@ impl Context { pub fn forget_image(&self, uri: &str) { use load::BytesLoader as _; - crate::profile_function!(); + profiling::function_scope!(); let loaders = self.loaders(); @@ -3308,7 +3386,7 @@ impl Context { pub fn forget_all_images(&self) { use load::BytesLoader as _; - crate::profile_function!(); + profiling::function_scope!(); let loaders = self.loaders(); @@ -3343,7 +3421,7 @@ impl Context { /// [not_supported]: crate::load::LoadError::NotSupported /// [custom]: crate::load::LoadError::Loading pub fn try_load_bytes(&self, uri: &str) -> load::BytesLoadResult { - crate::profile_function!(uri); + profiling::function_scope!(uri); let loaders = self.loaders(); let bytes_loaders = loaders.bytes.lock(); @@ -3380,7 +3458,7 @@ impl Context { /// [not_supported]: crate::load::LoadError::NotSupported /// [custom]: crate::load::LoadError::Loading pub fn try_load_image(&self, uri: &str, size_hint: load::SizeHint) -> load::ImageLoadResult { - crate::profile_function!(uri); + profiling::function_scope!(uri); let loaders = self.loaders(); let image_loaders = loaders.image.lock(); @@ -3388,15 +3466,23 @@ impl Context { return Err(load::LoadError::NoImageLoaders); } + let mut format = None; + // Try most recently added loaders first (hence `.rev()`) for loader in image_loaders.iter().rev() { match loader.load(self, uri, size_hint) { Err(load::LoadError::NotSupported) => continue, + Err(load::LoadError::FormatNotSupported { detected_format }) => { + format = format.or(detected_format); + continue; + } result => return result, } } - Err(load::LoadError::NoMatchingImageLoader) + Err(load::LoadError::NoMatchingImageLoader { + detected_format: format, + }) } /// Try loading the texture from the given uri using any available texture loaders. @@ -3423,7 +3509,7 @@ impl Context { texture_options: TextureOptions, size_hint: load::SizeHint, ) -> load::TextureLoadResult { - crate::profile_function!(uri); + profiling::function_scope!(uri); let loaders = self.loaders(); let texture_loaders = loaders.texture.lock(); @@ -3441,7 +3527,7 @@ impl Context { /// The loaders of bytes, images, and textures. pub fn loaders(&self) -> Arc { - crate::profile_function!(); + profiling::function_scope!(); self.read(|this| this.loaders.clone()) } } @@ -3573,7 +3659,7 @@ impl Context { viewport_builder: ViewportBuilder, viewport_ui_cb: impl Fn(&Self, ViewportClass) + Send + Sync + 'static, ) { - crate::profile_function!(); + profiling::function_scope!(); if self.embed_viewports() { viewport_ui_cb(self, ViewportClass::Embedded); @@ -3597,7 +3683,7 @@ impl Context { /// /// This is the easier type of viewport to use, but it is less performant /// at it requires both parent and child to repaint if any one of them needs repainting, - /// which efficvely produce double work for two viewports, and triple work for three viewports, etc. + /// which effectively produce double work for two viewports, and triple work for three viewports, etc. /// To avoid this, use [`Self::show_viewport_deferred`] instead. /// /// The given id must be unique for each viewport. @@ -3625,7 +3711,7 @@ impl Context { builder: ViewportBuilder, mut viewport_ui_cb: impl FnMut(&Self, ViewportClass) -> T, ) -> T { - crate::profile_function!(); + profiling::function_scope!(); if self.embed_viewports() { return viewport_ui_cb(self, ViewportClass::Embedded); diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 3601fcae46e..7987ea61225 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -68,8 +68,8 @@ pub struct RawInput { /// Dragged files dropped into egui. /// - /// Note: when using `eframe` on Windows you need to enable - /// drag-and-drop support using `eframe::NativeOptions`. + /// Note: when using `eframe` on Windows, this will always be empty if drag-and-drop support has + /// been disabled in [`crate::viewport::ViewportBuilder`]. pub dropped_files: Vec, /// The native window has the keyboard focus (i.e. is receiving key presses). @@ -529,6 +529,10 @@ pub enum Event { /// The reply of a screenshot requested with [`crate::ViewportCommand::Screenshot`]. Screenshot { viewport_id: crate::ViewportId, + + /// Whatever was passed to [`crate::ViewportCommand::Screenshot`]. + user_data: crate::UserData, + image: std::sync::Arc, }, } diff --git a/crates/egui/src/data/key.rs b/crates/egui/src/data/key.rs index c43d1c5685d..a075b025a2f 100644 --- a/crates/egui/src/data/key.rs +++ b/crates/egui/src/data/key.rs @@ -55,7 +55,7 @@ pub enum Key { // `]` CloseBracket, - /// \`, also known as "backquote" or "grave" + /// Also known as "backquote" or "grave" Backtick, /// `-` diff --git a/crates/egui/src/data/mod.rs b/crates/egui/src/data/mod.rs index bfe1e8a327d..f6f267dd06a 100644 --- a/crates/egui/src/data/mod.rs +++ b/crates/egui/src/data/mod.rs @@ -3,5 +3,7 @@ pub mod input; mod key; pub mod output; +mod user_data; pub use key::Key; +pub use user_data::UserData; diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index 94abc1954c6..a878bd5fd70 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -675,6 +675,7 @@ impl WidgetInfo { WidgetType::ImageButton => "image button", WidgetType::CollapsingHeader => "collapsing header", WidgetType::ProgressIndicator => "progress indicator", + WidgetType::Window => "window", WidgetType::Label | WidgetType::Other => "", }; diff --git a/crates/egui/src/data/user_data.rs b/crates/egui/src/data/user_data.rs new file mode 100644 index 00000000000..20bf5e1a123 --- /dev/null +++ b/crates/egui/src/data/user_data.rs @@ -0,0 +1,74 @@ +use std::{any::Any, sync::Arc}; + +/// A wrapper around `dyn Any`, used for passing custom user data +/// to [`crate::ViewportCommand::Screenshot`]. +#[derive(Clone, Debug, Default)] +pub struct UserData { + /// A user value given to the screenshot command, + /// that will be returned in [`crate::Event::Screenshot`]. + pub data: Option>, +} + +impl UserData { + /// You can also use [`Self::default`]. + pub fn new(user_info: impl Any + Send + Sync) -> Self { + Self { + data: Some(Arc::new(user_info)), + } + } +} + +impl PartialEq for UserData { + fn eq(&self, other: &Self) -> bool { + match (&self.data, &other.data) { + (Some(a), Some(b)) => Arc::ptr_eq(a, b), + (None, None) => true, + _ => false, + } + } +} + +impl Eq for UserData {} + +impl std::hash::Hash for UserData { + fn hash(&self, state: &mut H) { + self.data.as_ref().map(Arc::as_ptr).hash(state); + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for UserData { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_none() // can't serialize an `Any` + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for UserData { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct UserDataVisitor; + + impl<'de> serde::de::Visitor<'de> for UserDataVisitor { + type Value = UserData; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("a None value") + } + + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + Ok(UserData::default()) + } + } + + deserializer.deserialize_option(UserDataVisitor) + } +} diff --git a/crates/egui/src/drag_and_drop.rs b/crates/egui/src/drag_and_drop.rs index 13da9d314c1..fc9f29f5e03 100644 --- a/crates/egui/src/drag_and_drop.rs +++ b/crates/egui/src/drag_and_drop.rs @@ -23,27 +23,48 @@ pub struct DragAndDrop { impl DragAndDrop { pub(crate) fn register(ctx: &Context) { - ctx.on_end_pass("debug_text", std::sync::Arc::new(Self::end_pass)); + ctx.on_begin_pass("drag_and_drop_begin_pass", Arc::new(Self::begin_pass)); + ctx.on_end_pass("drag_and_drop_end_pass", Arc::new(Self::end_pass)); } - fn end_pass(ctx: &Context) { - let abort_dnd = - ctx.input(|i| i.pointer.any_released() || i.key_pressed(crate::Key::Escape)); - - let mut is_dragging = false; + /// Interrupt drag-and-drop if the user presses the escape key. + /// + /// This needs to happen at frame start so we can properly capture the escape key. + fn begin_pass(ctx: &Context) { + let has_any_payload = Self::has_any_payload(ctx); - ctx.data_mut(|data| { - let state = data.get_temp_mut_or_default::(Id::NULL); + if has_any_payload { + let abort_dnd_due_to_escape_key = + ctx.input_mut(|i| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape)); - if abort_dnd { - state.payload = None; + if abort_dnd_due_to_escape_key { + Self::clear_payload(ctx); } + } + } - is_dragging = state.payload.is_some(); - }); + /// Interrupt drag-and-drop if the user releases the mouse button. + /// + /// This is a catch-all safety net in case user code doesn't capture the drag payload itself. + /// This must happen at end-of-frame such that we don't shadow the mouse release event from user + /// code. + fn end_pass(ctx: &Context) { + let has_any_payload = Self::has_any_payload(ctx); + + if has_any_payload { + let abort_dnd_due_to_mouse_release = ctx.input_mut(|i| i.pointer.any_released()); - if is_dragging { - ctx.set_cursor_icon(CursorIcon::Grabbing); + if abort_dnd_due_to_mouse_release { + Self::clear_payload(ctx); + } else { + // We set the cursor icon only if its default, as the user code might have + // explicitly set it already. + ctx.output_mut(|o| { + if o.cursor_icon == CursorIcon::Default { + o.cursor_icon = CursorIcon::Grabbing; + } + }); + } } } diff --git a/crates/egui/src/grid.rs b/crates/egui/src/grid.rs index 3c4986fcad0..0342f6f5227 100644 --- a/crates/egui/src/grid.rs +++ b/crates/egui/src/grid.rs @@ -227,7 +227,7 @@ impl GridLayout { self.col += 1; } - fn paint_row(&mut self, cursor: &Rect, painter: &Painter) { + fn paint_row(&self, cursor: &Rect, painter: &Painter) { // handle row color painting based on color-picker function let Some(color_picker) = self.color_picker.as_ref() else { return; @@ -450,7 +450,7 @@ impl Grid { ui.allocate_new_ui(ui_builder, |ui| { ui.horizontal(|ui| { let is_color = color_picker.is_some(); - let mut grid = GridLayout { + let grid = GridLayout { num_columns, color_picker, min_cell_size: vec2(min_col_width, min_row_height), diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 142645f24e6..8741f5f8b41 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -2,7 +2,7 @@ use ahash::HashMap; use emath::TSTransform; -use crate::{ahash, emath, LayerId, Pos2, WidgetRect, WidgetRects}; +use crate::{ahash, emath, LayerId, Pos2, Rect, WidgetRect, WidgetRects}; /// Result of a hit-test against [`WidgetRects`]. /// @@ -12,11 +12,18 @@ use crate::{ahash, emath, LayerId, Pos2, WidgetRect, WidgetRects}; /// or if we're currently already dragging something. #[derive(Clone, Debug, Default)] pub struct WidgetHits { + /// All widgets close to the pointer, back-to-front. + /// + /// This is a superset of all other widgets in this struct. + pub close: Vec, + /// All widgets that contains the pointer, back-to-front. /// - /// i.e. both a Window and the button in it can contain the pointer. + /// i.e. both a Window and the Button in it can contain the pointer. /// /// Some of these may be widgets in a layer below the top-most layer. + /// + /// This will be used for hovering. pub contains_pointer: Vec, /// If the user would start a clicking now, this is what would be clicked. @@ -35,18 +42,18 @@ pub struct WidgetHits { pub fn hit_test( widgets: &WidgetRects, layer_order: &[LayerId], - layer_transforms: &HashMap, + layer_to_global: &HashMap, pos: Pos2, search_radius: f32, ) -> WidgetHits { - crate::profile_function!(); + profiling::function_scope!(); let search_radius_sq = search_radius * search_radius; // Transform the position into the local coordinate space of each layer: - let pos_in_layers: HashMap = layer_transforms + let pos_in_layers: HashMap = layer_to_global .iter() - .map(|(layer_id, t)| (*layer_id, t.inverse() * pos)) + .map(|(layer_id, to_global)| (*layer_id, to_global.inverse() * pos)) .collect(); let mut closest_dist_sq = f32::INFINITY; @@ -63,6 +70,7 @@ pub fn hit_test( } let pos_in_layer = pos_in_layers.get(&w.layer_id).copied().unwrap_or(pos); + // TODO(emilk): we should probably do the distance testing in global space instead let dist_sq = w.interact_rect.distance_sq_to_pos(pos_in_layer); // In tie, pick last = topmost. @@ -76,51 +84,103 @@ pub fn hit_test( .copied() .collect(); - // We need to pick one single layer for the interaction. - if let Some(closest_hit) = closest_hit { - // Select the top layer, and ignore widgets in any other layer: - let top_layer = closest_hit.layer_id; - close.retain(|w| w.layer_id == top_layer); - - // If the widget is disabled, treat it as if it isn't sensing anything. - // This simplifies the code in `hit_test_on_close` so it doesn't have to check - // the `enabled` flag everywhere: - for w in &mut close { - if !w.enabled { - w.sense.click = false; - w.sense.drag = false; - } + // Transform to global coordinates: + for hit in &mut close { + if let Some(to_global) = layer_to_global.get(&hit.layer_id).copied() { + *hit = hit.transform(to_global); } + } - let pos_in_layer = pos_in_layers.get(&top_layer).copied().unwrap_or(pos); - let hits = hit_test_on_close(&close, pos_in_layer); - - if let Some(drag) = hits.drag { - debug_assert!(drag.sense.drag); - } - if let Some(click) = hits.click { - debug_assert!(click.sense.click); + // When using layer transforms it is common to stack layers close to each other. + // For instance, you may have a resize-separator on a panel, with two + // transform-layers on either side. + // The resize-separator is technically in a layer _behind_ the transform-layers, + // but the user doesn't perceive it as such. + // So how do we handle this case? + // + // If we just allow interactions with ALL close widgets, + // then we might accidentally allow clicks through windows and other bad stuff. + // + // Let's try this: + // * Set up a hit-area (based on search_radius) + // * Iterate over all hits top-to-bottom + // * Stop if any hit covers the whole hit-area, otherwise keep going + // * Collect the layers ids in a set + // * Remove all widgets not in the above layer set + // + // This will most often result in only one layer, + // but if the pointer is at the edge of a layer, we might include widgets in + // a layer behind it. + + let mut included_layers: ahash::HashSet = Default::default(); + for hit in close.iter().rev() { + included_layers.insert(hit.layer_id); + let hit_covers_search_area = contains_circle(hit.interact_rect, pos, search_radius); + if hit_covers_search_area { + break; // nothing behind this layer could ever be interacted with } + } + + close.retain(|hit| included_layers.contains(&hit.layer_id)); - hits - } else { - // No close widgets. - Default::default() + // If a widget is disabled, treat it as if it isn't sensing anything. + // This simplifies the code in `hit_test_on_close` so it doesn't have to check + // the `enabled` flag everywhere: + for w in &mut close { + if !w.enabled { + w.sense.click = false; + w.sense.drag = false; + } } -} -fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { - #![allow(clippy::collapsible_else_if)] + let mut hits = hit_test_on_close(&close, pos); - // Only those widgets directly under the `pos`. - let hits: Vec = close + hits.contains_pointer = close .iter() .filter(|widget| widget.interact_rect.contains(pos)) .copied() .collect(); - let hit_click = hits.iter().copied().filter(|w| w.sense.click).last(); - let hit_drag = hits.iter().copied().filter(|w| w.sense.drag).last(); + hits.close = close; + + { + // Undo the to_global-transform we applied earlier, + // go back to local layer-coordinates: + + let restore_widget_rect = |w: &mut WidgetRect| { + *w = widgets.get(w.id).copied().unwrap_or(*w); + }; + + for wr in &mut hits.close { + restore_widget_rect(wr); + } + for wr in &mut hits.contains_pointer { + restore_widget_rect(wr); + } + if let Some(wr) = &mut hits.drag { + debug_assert!(wr.sense.drag); + restore_widget_rect(wr); + } + if let Some(wr) = &mut hits.click { + debug_assert!(wr.sense.click); + restore_widget_rect(wr); + } + } + + hits +} + +/// Returns true if the rectangle contains the whole circle. +fn contains_circle(interact_rect: emath::Rect, pos: Pos2, radius: f32) -> bool { + interact_rect.shrink(radius).contains(pos) +} + +fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { + #![allow(clippy::collapsible_else_if)] + + // First find the best direct hits: + let hit_click = find_closest_within(close.iter().copied().filter(|w| w.sense.click), pos, 0.0); + let hit_drag = find_closest_within(close.iter().copied().filter(|w| w.sense.drag), pos, 0.0); match (hit_click, hit_drag) { (None, None) => { @@ -136,16 +196,16 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { if let Some(closest) = closest { WidgetHits { - contains_pointer: hits, click: closest.sense.click.then_some(closest), drag: closest.sense.drag.then_some(closest), + ..Default::default() } } else { // Found nothing WidgetHits { - contains_pointer: hits, click: None, drag: None, + ..Default::default() } } } @@ -170,17 +230,17 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { // This is a smaller thing on a big background - help the user hit it, // and ignore the big drag background. WidgetHits { - contains_pointer: hits, click: Some(closest_click), drag: Some(closest_click), + ..Default::default() } } else { - // The drag wiudth is separate from the click wiudth, - // so return only the drag widget + // The drag-widget is separate from the click-widget, + // so return only the drag-widget WidgetHits { - contains_pointer: hits, click: None, drag: Some(hit_drag), + ..Default::default() } } } else { @@ -194,17 +254,17 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { // The drag widget is a big background thing (scroll area), // so returning a separate click widget should not be confusing WidgetHits { - contains_pointer: hits, click: Some(closest_click), drag: Some(hit_drag), + ..Default::default() } } else { // The two widgets are just two normal small widgets close to each other. // Highlighting both would be very confusing. WidgetHits { - contains_pointer: hits, click: None, drag: Some(hit_drag), + ..Default::default() } } } @@ -229,17 +289,17 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { // `hit_drag` is a big background thing and `closest_drag` is something small on top of it. // Be helpful and return the small things: return WidgetHits { - contains_pointer: hits, click: None, drag: Some(closest_drag), + ..Default::default() }; } } WidgetHits { - contains_pointer: hits, click: None, drag: Some(hit_drag), + ..Default::default() } } } @@ -253,57 +313,57 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { // where when hovering directly over a drag-widget (like a big ScrollArea), // we look for close click-widgets (e.g. buttons). // This is because big background drag-widgets (ScrollArea, Window) are common, - // but bit clickable things aren't. + // but big clickable things aren't. // Even if they were, I think it would be confusing for a user if clicking // a drag-only widget would click something _behind_ it. WidgetHits { - contains_pointer: hits, click: Some(hit_click), drag: None, + ..Default::default() } } (Some(hit_click), Some(hit_drag)) => { // We have a perfect hit on both click and drag. Which is the topmost? - let click_idx = hits.iter().position(|w| *w == hit_click).unwrap(); - let drag_idx = hits.iter().position(|w| *w == hit_drag).unwrap(); + let click_idx = close.iter().position(|w| *w == hit_click).unwrap(); + let drag_idx = close.iter().position(|w| *w == hit_drag).unwrap(); let click_is_on_top_of_drag = drag_idx < click_idx; if click_is_on_top_of_drag { if hit_click.sense.drag { // The top thing senses both clicks and drags. WidgetHits { - contains_pointer: hits, click: Some(hit_click), drag: Some(hit_click), + ..Default::default() } } else { // They are interested in different things, // and click is on top. Report both hits, // e.g. the top Button and the ScrollArea behind it. WidgetHits { - contains_pointer: hits, click: Some(hit_click), drag: Some(hit_drag), + ..Default::default() } } } else { if hit_drag.sense.click { // The top thing senses both clicks and drags. WidgetHits { - contains_pointer: hits, click: Some(hit_drag), drag: Some(hit_drag), + ..Default::default() } } else { // The top things senses only drags, // so we ignore the click-widget, because it would be confusing // if clicking a drag-widget would actually click something else below it. WidgetHits { - contains_pointer: hits, click: None, drag: Some(hit_drag), + ..Default::default() } } } @@ -312,8 +372,16 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { } fn find_closest(widgets: impl Iterator, pos: Pos2) -> Option { - let mut closest = None; - let mut closest_dist_sq = f32::INFINITY; + find_closest_within(widgets, pos, f32::INFINITY) +} + +fn find_closest_within( + widgets: impl Iterator, + pos: Pos2, + max_dist: f32, +) -> Option { + let mut closest: Option = None; + let mut closest_dist_sq = max_dist * max_dist; for widget in widgets { if widget.interact_rect.is_negative() { continue; @@ -321,6 +389,16 @@ fn find_closest(widgets: impl Iterator, pos: Pos2) -> Option< let dist_sq = widget.interact_rect.distance_sq_to_pos(pos); + if let Some(closest) = closest { + if dist_sq == closest_dist_sq { + // It's a tie! Pick the thin candidate over the thick one. + // This makes it easier to hit a thin resize-handle, for instance: + if should_prioritizie_hits_on_back(closest.interact_rect, widget.interact_rect) { + continue; + } + } + } + // In case of a tie, take the last one = the one on top. if dist_sq <= closest_dist_sq { closest_dist_sq = dist_sq; @@ -331,6 +409,27 @@ fn find_closest(widgets: impl Iterator, pos: Pos2) -> Option< closest } +/// Should we prioritizie hits on `back` over those on `front`? +/// +/// `back` should be behind the `front` widget. +/// +/// Returns true if `back` is a small hit-target and `front` is not. +fn should_prioritizie_hits_on_back(back: Rect, front: Rect) -> bool { + if front.contains_rect(back) { + return false; // back widget is fully occluded; no way to hit it + } + + // Reduce each rect to its width or height, whichever is smaller: + let back = back.width().min(back.height()); + let front = front.width().min(front.height()); + + // These are hard-coded heuristics that could surely be improved. + let back_is_much_thinner = back <= 0.5 * front; + let back_is_thin = back <= 16.0; + + back_is_much_thinner && back_is_thin +} + #[cfg(test)] mod tests { use emath::{pos2, vec2, Rect}; diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index 7f743ee709a..048e880e3d4 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -269,7 +269,7 @@ impl InputState { pixels_per_point: f32, options: &crate::Options, ) -> Self { - crate::profile_function!(); + profiling::function_scope!(); let time = new.time.unwrap_or(self.time + new.predicted_dt as f64); let unstable_dt = (time - self.time) as f32; @@ -889,9 +889,9 @@ impl Default for PointerState { press_start_time: None, has_moved_too_much_for_a_click: false, started_decidedly_dragging: false, - last_click_time: std::f64::NEG_INFINITY, - last_last_click_time: std::f64::NEG_INFINITY, - last_move_time: std::f64::NEG_INFINITY, + last_click_time: f64::NEG_INFINITY, + last_last_click_time: f64::NEG_INFINITY, + last_move_time: f64::NEG_INFINITY, pointer_events: vec![], input_options: Default::default(), } diff --git a/crates/egui/src/input_state/touch_state.rs b/crates/egui/src/input_state/touch_state.rs index df39d961925..1ff2dc388a3 100644 --- a/crates/egui/src/input_state/touch_state.rs +++ b/crates/egui/src/input_state/touch_state.rs @@ -15,6 +15,9 @@ pub struct MultiTouchInfo { /// Position of the pointer at the time the gesture started. pub start_pos: Pos2, + /// Center position of the current gesture (average of all touch points). + pub center_pos: Pos2, + /// Number of touches (fingers) on the surface. Value is ≥ 2 since for a single touch no /// [`MultiTouchInfo`] is created. pub num_touches: usize, @@ -203,6 +206,8 @@ impl TouchState { PinchType::Proportional => Vec2::splat(zoom_delta), }; + let center_pos = state.current.avg_pos; + MultiTouchInfo { start_time: state.start_time, start_pos: state.start_pointer_pos, @@ -212,6 +217,7 @@ impl TouchState { rotation_delta: normalized_angle(state.current.heading - state_previous.heading), translation_delta: state.current.avg_pos - state_previous.avg_pos, force: state.current.avg_force, + center_pos, } }) } diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index 7cbadbf1193..04f8f7dbf6f 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -113,7 +113,7 @@ pub(crate) fn interact( input: &InputState, interaction: &mut InteractionState, ) -> InteractionSnapshot { - crate::profile_function!(); + profiling::function_scope!(); if let Some(id) = interaction.potential_click_id { if !widgets.contains(id) { @@ -249,7 +249,7 @@ pub(crate) fn interact( .copied() .collect() } else { - // We may be hovering a an interactive widget or two. + // We may be hovering an interactive widget or two. // We must also consider the case where non-interactive widgets // are _on top_ of an interactive widget. // For instance: a label in a draggable window. @@ -264,9 +264,9 @@ pub(crate) fn interact( // but none below it (an interactive widget stops the hover search). // // To know when to stop we need to first know the order of the widgets, - // which luckily we have in the `WidgetRects`. + // which luckily we already have in `hits.close`. - let order = |id| widgets.order(id).map(|(_layer, order)| order); // we ignore the layer, since all widgets at this point is in the same layer + let order = |id| hits.close.iter().position(|w| w.id == id); let click_order = hits.click.and_then(|w| order(w.id)).unwrap_or(0); let drag_order = hits.drag.and_then(|w| order(w.id)).unwrap_or(0); diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index 9105ece2591..81a60812ece 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -11,9 +11,6 @@ pub enum Order { /// Painted behind all floating windows Background, - /// Special layer between panels and windows - PanelResizeLine, - /// Normal moveable windows that you reorder by click Middle, @@ -30,10 +27,9 @@ pub enum Order { } impl Order { - const COUNT: usize = 6; + const COUNT: usize = 5; const ALL: [Self; Self::COUNT] = [ Self::Background, - Self::PanelResizeLine, Self::Middle, Self::Foreground, Self::Tooltip, @@ -44,12 +40,9 @@ impl Order { #[inline(always)] pub fn allow_interaction(&self) -> bool { match self { - Self::Background - | Self::PanelResizeLine - | Self::Middle - | Self::Foreground - | Self::Tooltip - | Self::Debug => true, + Self::Background | Self::Middle | Self::Foreground | Self::Tooltip | Self::Debug => { + true + } } } @@ -57,7 +50,6 @@ impl Order { pub fn short_debug_format(&self) -> &'static str { match self { Self::Background => "backg", - Self::PanelResizeLine => "panel", Self::Middle => "middl", Self::Foreground => "foreg", Self::Tooltip => "toolt", @@ -68,7 +60,7 @@ impl Order { /// An identifier for a paint layer. /// Also acts as an identifier for [`crate::Area`]:s. -#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)] +#[derive(Clone, Copy, Hash, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct LayerId { pub order: Order, @@ -95,6 +87,7 @@ impl LayerId { } #[inline(always)] + #[deprecated = "Use `Memory::allows_interaction` instead"] pub fn allow_interaction(&self) -> bool { self.order.allow_interaction() } @@ -109,6 +102,13 @@ impl LayerId { } } +impl std::fmt::Debug for LayerId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self { order, id } = self; + write!(f, "LayerId {{ {order:?} {id:?} }}") + } +} + /// A unique identifier of a specific [`Shape`] in a [`PaintList`]. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -220,9 +220,9 @@ impl GraphicLayers { pub fn drain( &mut self, area_order: &[LayerId], - transforms: &ahash::HashMap, + to_global: &ahash::HashMap, ) -> Vec { - crate::profile_function!(); + profiling::function_scope!(); let mut all_shapes: Vec<_> = Default::default(); @@ -238,10 +238,10 @@ impl GraphicLayers { for layer_id in area_order { if layer_id.order == order { if let Some(list) = order_map.get_mut(&layer_id.id) { - if let Some(transform) = transforms.get(layer_id) { + if let Some(to_global) = to_global.get(layer_id) { for clipped_shape in &mut list.0 { - clipped_shape.clip_rect = *transform * clipped_shape.clip_rect; - clipped_shape.shape.transform(*transform); + clipped_shape.clip_rect = *to_global * clipped_shape.clip_rect; + clipped_shape.shape.transform(*to_global); } } all_shapes.append(&mut list.0); @@ -253,10 +253,10 @@ impl GraphicLayers { for (id, list) in order_map { let layer_id = LayerId::new(order, *id); - if let Some(transform) = transforms.get(&layer_id) { + if let Some(to_global) = to_global.get(&layer_id) { for clipped_shape in &mut list.0 { - clipped_shape.clip_rect = *transform * clipped_shape.clip_rect; - clipped_shape.shape.transform(*transform); + clipped_shape.clip_rect = *to_global * clipped_shape.clip_rect; + clipped_shape.shape.transform(*to_global); } } diff --git a/crates/egui/src/layout.rs b/crates/egui/src/layout.rs index 0293904ec0a..0c9bb494133 100644 --- a/crates/egui/src/layout.rs +++ b/crates/egui/src/layout.rs @@ -2,7 +2,7 @@ use crate::{ emath::{pos2, vec2, Align2, NumExt, Pos2, Rect, Vec2}, Align, }; -use std::f32::INFINITY; +const INFINITY: f32 = f32::INFINITY; // ---------------------------------------------------------------------------- @@ -53,7 +53,7 @@ impl Region { } /// Ensure we are big enough to contain the given X-coordinate. - /// This is sometimes useful to expand an ui to stretch to a certain place. + /// This is sometimes useful to expand a ui to stretch to a certain place. pub fn expand_to_include_x(&mut self, x: f32) { self.min_rect.extend_with_x(x); self.max_rect.extend_with_x(x); @@ -61,7 +61,7 @@ impl Region { } /// Ensure we are big enough to contain the given Y-coordinate. - /// This is sometimes useful to expand an ui to stretch to a certain place. + /// This is sometimes useful to expand a ui to stretch to a certain place. pub fn expand_to_include_y(&mut self, y: f32) { self.min_rect.extend_with_y(y); self.max_rect.extend_with_y(y); diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 29cd9ddcac8..1afaada95e1 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -3,7 +3,7 @@ //! Try the live web demo: . Read more about egui at . //! //! `egui` is in heavy development, with each new version having breaking changes. -//! You need to have rust 1.76.0 or later to use `egui`. +//! You need to have rust 1.80.0 or later to use `egui`. //! //! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template) //! which uses [`eframe`](https://docs.rs/eframe). @@ -388,11 +388,24 @@ //! ## Installing additional fonts //! The default egui fonts only support latin and cryllic characters, and some emojis. //! To use egui with e.g. asian characters you need to install your own font (`.ttf` or `.otf`) using [`Context::set_fonts`]. +//! +//! ## Instrumentation +//! This crate supports using the [profiling](https://crates.io/crates/profiling) crate for instrumentation. +//! You can enable features on the profiling crates in your application to add instrumentation for all +//! crates that support it, including egui. See the profiling crate docs for more information. +//! ```toml +//! [dependencies] +//! profiling = "1.0" +//! [features] +//! profile-with-puffin = ["profiling/profile-with-puffin"] +//! ``` +//! #![allow(clippy::float_cmp)] #![allow(clippy::manual_range_contains)] mod animation_manager; +pub mod cache; pub mod containers; mod context; mod data; @@ -471,7 +484,7 @@ pub use self::{ output::{ self, CursorIcon, FullOutput, OpenUrl, PlatformOutput, UserAttentionType, WidgetInfo, }, - Key, + Key, UserData, }, drag_and_drop::DragAndDrop, epaint::text::TextWrapMode, @@ -656,6 +669,8 @@ pub enum WidgetType { ProgressIndicator, + Window, + /// If you cannot fit any of the above slots. /// /// If this is something you think should be added, file an issue. @@ -688,33 +703,3 @@ pub fn __run_test_ui(add_contents: impl Fn(&mut Ui)) { pub fn accesskit_root_id() -> Id { Id::new("accesskit_root") } - -// --------------------------------------------------------------------------- - -mod profiling_scopes { - #![allow(unused_macros)] - #![allow(unused_imports)] - - /// Profiling macro for feature "puffin" - macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} - -#[allow(unused_imports)] -pub(crate) use profiling_scopes::{profile_function, profile_scope}; diff --git a/crates/egui/src/load.rs b/crates/egui/src/load.rs index b6711de3c52..26950eb2add 100644 --- a/crates/egui/src/load.rs +++ b/crates/egui/src/load.rs @@ -77,16 +77,19 @@ pub enum LoadError { /// Programmer error: There are no image loaders installed. NoImageLoaders, - /// A specific loader does not support this scheme, protocol or image format. + /// A specific loader does not support this scheme or protocol. NotSupported, + /// A specific loader does not support the format of the image. + FormatNotSupported { detected_format: Option }, + /// Programmer error: Failed to find the bytes for this image because /// there was no [`BytesLoader`] supporting the scheme. NoMatchingBytesLoader, /// Programmer error: Failed to parse the bytes as an image because - /// there was no [`ImageLoader`] supporting the scheme. - NoMatchingImageLoader, + /// there was no [`ImageLoader`] supporting the format. + NoMatchingImageLoader { detected_format: Option }, /// Programmer error: no matching [`TextureLoader`]. /// Because of the [`DefaultTextureLoader`], this error should never happen. @@ -96,6 +99,20 @@ pub enum LoadError { Loading(String), } +impl LoadError { + /// Returns the (approximate) size of the error message in bytes. + pub fn byte_size(&self) -> usize { + match self { + Self::FormatNotSupported { detected_format } + | Self::NoMatchingImageLoader { detected_format } => { + detected_format.as_ref().map_or(0, |s| s.len()) + } + Self::Loading(message) => message.len(), + _ => std::mem::size_of::(), + } + } +} + impl Display for LoadError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -105,12 +122,15 @@ impl Display for LoadError { Self::NoMatchingBytesLoader => f.write_str("No matching BytesLoader. Either you need to call Context::include_bytes, or install some more bytes loaders, e.g. using egui_extras."), - Self::NoMatchingImageLoader => f.write_str("No matching ImageLoader. Either you need to call Context::include_bytes, or install some more bytes loaders, e.g. using egui_extras."), + Self::NoMatchingImageLoader { detected_format: None } => f.write_str("No matching ImageLoader. Either no ImageLoader is installed or the image is corrupted / has an unsupported format."), + Self::NoMatchingImageLoader { detected_format: Some(detected_format) } => write!(f, "No matching ImageLoader for format: {detected_format:?}. Make sure you enabled the necessary features on the image crate."), Self::NoMatchingTextureLoader => f.write_str("No matching TextureLoader. Did you remove the default one?"), Self::NotSupported => f.write_str("Image scheme or URI not supported by this loader"), + Self::FormatNotSupported { detected_format } => write!(f, "Image format not supported by this loader: {detected_format:?}"), + Self::Loading(message) => f.write_str(message), } } diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 45b10dc8eba..976ad2d95ef 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -54,7 +54,7 @@ pub struct Memory { /// so as not to lock the UI thread. /// /// ``` - /// use egui::util::cache::{ComputerMut, FrameCache}; + /// use egui::cache::{ComputerMut, FrameCache}; /// /// #[derive(Default)] /// struct CharCounter {} @@ -72,13 +72,17 @@ pub struct Memory { /// }); /// ``` #[cfg_attr(feature = "persistence", serde(skip))] - pub caches: crate::util::cache::CacheStorage, + pub caches: crate::cache::CacheStorage, // ------------------------------------------ /// new fonts that will be applied at the start of the next frame #[cfg_attr(feature = "persistence", serde(skip))] pub(crate) new_font_definitions: Option, + /// add new font that will be applied at the start of the next frame + #[cfg_attr(feature = "persistence", serde(skip))] + pub(crate) add_fonts: Vec, + // Current active viewport #[cfg_attr(feature = "persistence", serde(skip))] pub(crate) viewport_id: ViewportId, @@ -91,8 +95,13 @@ pub struct Memory { #[cfg_attr(feature = "persistence", serde(skip))] everything_is_visible: bool, - /// Transforms per layer - pub layer_transforms: HashMap, + /// Transforms per layer. + /// + /// Instead of using this directly, use: + /// * [`crate::Context::set_transform_layer`] + /// * [`crate::Context::layer_transform_to_global`] + /// * [`crate::Context::layer_transform_from_global`] + pub to_global: HashMap, // ------------------------------------------------- // Per-viewport: @@ -116,9 +125,10 @@ impl Default for Memory { focus: Default::default(), viewport_id: Default::default(), areas: Default::default(), - layer_transforms: Default::default(), + to_global: Default::default(), popup: Default::default(), everything_is_visible: Default::default(), + add_fonts: Default::default(), }; slf.interactions.entry(slf.viewport_id).or_default(); slf.areas.entry(slf.viewport_id).or_default(); @@ -508,6 +518,12 @@ pub(crate) struct Focus { /// Set when looking for widget with navigational keys like arrows, tab, shift+tab. focus_direction: FocusDirection, + /// The top-most modal layer from the previous frame. + top_modal_layer: Option, + + /// The top-most modal layer from the current frame. + top_modal_layer_current_frame: Option, + /// A cache of widget IDs that are interested in focus with their corresponding rectangles. focus_widgets_cache: IdMap, } @@ -618,6 +634,8 @@ impl Focus { self.focused_widget = None; } } + + self.top_modal_layer = self.top_modal_layer_current_frame.take(); } pub(crate) fn had_focus_last_frame(&self, id: Id) -> bool { @@ -671,6 +689,14 @@ impl Focus { self.last_interested = Some(id); } + fn set_modal_layer(&mut self, layer_id: LayerId) { + self.top_modal_layer_current_frame = Some(layer_id); + } + + pub(crate) fn top_modal_layer(&self) -> Option { + self.top_modal_layer + } + fn reset_focus(&mut self) { self.focus_direction = FocusDirection::None; } @@ -715,7 +741,7 @@ impl Focus { let current_rect = self.focus_widgets_cache.get(¤t_focused.id)?; - let mut best_score = std::f32::INFINITY; + let mut best_score = f32::INFINITY; let mut best_id = None; for (candidate_id, candidate_rect) in &self.focus_widgets_cache { @@ -753,7 +779,7 @@ impl Focus { impl Memory { pub(crate) fn begin_pass(&mut self, new_raw_input: &RawInput, viewports: &ViewportIdSet) { - crate::profile_function!(); + profiling::function_scope!(); self.viewport_id = new_raw_input.viewport_id; @@ -797,7 +823,21 @@ impl Memory { /// Top-most layer at the given position. pub fn layer_id_at(&self, pos: Pos2) -> Option { - self.areas().layer_id_at(pos, &self.layer_transforms) + self.areas() + .layer_id_at(pos, &self.to_global) + .and_then(|layer_id| { + if self.is_above_modal_layer(layer_id) { + Some(layer_id) + } else { + self.top_modal_layer() + } + }) + } + + /// The currently set transform of a layer. + #[deprecated = "Use `Context::layer_transform_to_global` instead"] + pub fn layer_transforms(&self, layer_id: LayerId) -> Option { + self.to_global.get(&layer_id).copied() } /// An iterator over all layers. Back-to-front, top is last. @@ -872,6 +912,30 @@ impl Memory { } } + /// Returns true if + /// - this layer is the top-most modal layer or above it + /// - there is no modal layer + pub fn is_above_modal_layer(&self, layer_id: LayerId) -> bool { + if let Some(modal_layer) = self.focus().and_then(|f| f.top_modal_layer) { + matches!( + self.areas().compare_order(layer_id, modal_layer), + std::cmp::Ordering::Equal | std::cmp::Ordering::Greater + ) + } else { + true + } + } + + /// Does this layer allow interaction? + /// Returns true if + /// - the layer is not behind a modal layer + /// - the [`Order`] allows interaction + pub fn allows_interaction(&self, layer_id: LayerId) -> bool { + let is_above_modal_layer = self.is_above_modal_layer(layer_id); + let ordering_allows_interaction = layer_id.order.allow_interaction(); + is_above_modal_layer && ordering_allows_interaction + } + /// Register this widget as being interested in getting keyboard focus. /// This will allow the user to select it with tab and shift-tab. /// This is normally done automatically when handling interactions, @@ -879,11 +943,36 @@ impl Memory { /// e.g. before deciding which type of underlying widget to use, /// as in the [`crate::DragValue`] widget, so a widget can be focused /// and rendered correctly in a single frame. + /// + /// Pass in the `layer_id` of the layer that the widget is in. #[inline(always)] - pub fn interested_in_focus(&mut self, id: Id) { + pub fn interested_in_focus(&mut self, id: Id, layer_id: LayerId) { + if !self.allows_interaction(layer_id) { + return; + } self.focus_mut().interested_in_focus(id); } + /// Limit focus to widgets on the given layer and above. + /// If this is called multiple times per frame, the top layer wins. + pub fn set_modal_layer(&mut self, layer_id: LayerId) { + if let Some(current) = self.focus().and_then(|f| f.top_modal_layer_current_frame) { + if matches!( + self.areas().compare_order(layer_id, current), + std::cmp::Ordering::Less + ) { + return; + } + } + + self.focus_mut().set_modal_layer(layer_id); + } + + /// Get the top modal layer (from the previous frame). + pub fn top_modal_layer(&self) -> Option { + self.focus()?.top_modal_layer() + } + /// Stop editing the active [`TextEdit`](crate::TextEdit) (if any). #[inline(always)] pub fn stop_text_input(&mut self) { @@ -1032,6 +1121,9 @@ impl Memory { // ---------------------------------------------------------------------------- +/// Map containing the index of each layer in the order list, for quick lookups. +type OrderMap = HashMap; + /// Keeps track of [`Area`](crate::containers::area::Area)s, which are free-floating [`Ui`](crate::Ui)s. /// These [`Area`](crate::containers::area::Area)s can be in any [`Order`]. #[derive(Clone, Debug, Default)] @@ -1040,11 +1132,17 @@ impl Memory { pub struct Areas { areas: IdMap, + visible_areas_last_frame: ahash::HashSet, + visible_areas_current_frame: ahash::HashSet, + + // ---------------------------- + // Everything below this is general to all layers, not just areas. + // TODO(emilk): move this to a separate struct. /// Back-to-front, top is last. order: Vec, - visible_last_frame: ahash::HashSet, - visible_current_frame: ahash::HashSet, + /// Inverse of [`Self::order`], calculated at the end of the frame. + order_map: OrderMap, /// When an area wants to be on top, it is assigned here. /// This is used to reorder the layers at the end of the frame. @@ -1053,9 +1151,9 @@ pub struct Areas { /// results in them being sent to the top and keeping their previous internal order. wants_to_be_on_top: ahash::HashSet, - /// List of sublayers for each layer. + /// The sublayers that each layer has. /// - /// When a layer has sublayers, they are moved directly above it in the ordering. + /// The parent sublayer is moved directly above the child sublayers in the ordering. sublayers: ahash::HashMap>, } @@ -1068,22 +1166,24 @@ impl Areas { self.areas.get(&id) } - /// Back-to-front, top is last. + /// All layers back-to-front, top is last. pub(crate) fn order(&self) -> &[LayerId] { &self.order } - /// For each layer, which [`Self::order`] is it in? - pub(crate) fn order_map(&self) -> HashMap { - self.order - .iter() - .enumerate() - .map(|(i, id)| (*id, i)) - .collect() + /// Compare the order of two layers, based on the order list from last frame. + /// + /// May return [`std::cmp::Ordering::Equal`] if the layers are not in the order list. + pub(crate) fn compare_order(&self, a: LayerId, b: LayerId) -> std::cmp::Ordering { + if let (Some(a), Some(b)) = (self.order_map.get(&a), self.order_map.get(&b)) { + a.cmp(b) + } else { + a.order.cmp(&b.order) + } } pub(crate) fn set_state(&mut self, layer_id: LayerId, state: area::AreaState) { - self.visible_current_frame.insert(layer_id); + self.visible_areas_current_frame.insert(layer_id); self.areas.insert(layer_id.id, state); if !self.order.iter().any(|x| *x == layer_id) { self.order.push(layer_id); @@ -1094,15 +1194,15 @@ impl Areas { pub fn layer_id_at( &self, pos: Pos2, - layer_transforms: &HashMap, + layer_to_global: &HashMap, ) -> Option { for layer in self.order.iter().rev() { if self.is_visible(layer) { if let Some(state) = self.areas.get(&layer.id) { let mut rect = state.rect(); if state.interactable { - if let Some(transform) = layer_transforms.get(layer) { - rect = *transform * rect; + if let Some(to_global) = layer_to_global.get(layer) { + rect = *to_global * rect; } if rect.contains(pos) { @@ -1116,18 +1216,19 @@ impl Areas { } pub fn visible_last_frame(&self, layer_id: &LayerId) -> bool { - self.visible_last_frame.contains(layer_id) + self.visible_areas_last_frame.contains(layer_id) } pub fn is_visible(&self, layer_id: &LayerId) -> bool { - self.visible_last_frame.contains(layer_id) || self.visible_current_frame.contains(layer_id) + self.visible_areas_last_frame.contains(layer_id) + || self.visible_areas_current_frame.contains(layer_id) } pub fn visible_layer_ids(&self) -> ahash::HashSet { - self.visible_last_frame + self.visible_areas_last_frame .iter() .copied() - .chain(self.visible_current_frame.iter().copied()) + .chain(self.visible_areas_current_frame.iter().copied()) .collect() } @@ -1140,7 +1241,7 @@ impl Areas { } pub fn move_to_top(&mut self, layer_id: LayerId) { - self.visible_current_frame.insert(layer_id); + self.visible_areas_current_frame.insert(layer_id); self.wants_to_be_on_top.insert(layer_id); if !self.order.iter().any(|x| *x == layer_id) { @@ -1155,8 +1256,21 @@ impl Areas { /// /// This currently only supports one level of nesting. If `parent` is a sublayer of another /// layer, the behavior is unspecified. + /// + /// The two layers must have the same [`LayerId::order`]. pub fn set_sublayer(&mut self, parent: LayerId, child: LayerId) { + debug_assert_eq!(parent.order, child.order, + "DEBUG ASSERT: Trying to set sublayers across layers of different order ({:?}, {:?}), which is currently undefined behavior in egui", parent.order, child.order); + self.sublayers.entry(parent).or_default().insert(child); + + // Make sure the layers are in the order list: + if !self.order.iter().any(|x| *x == parent) { + self.order.push(parent); + } + if !self.order.iter().any(|x| *x == child) { + self.order.push(child); + } } pub fn top_layer_id(&self, order: Order) -> Option { @@ -1167,26 +1281,42 @@ impl Areas { .copied() } + /// If this layer is the sublayer of another layer, return the parent. + pub fn parent_layer(&self, layer_id: LayerId) -> Option { + self.sublayers.iter().find_map(|(parent, children)| { + if children.contains(&layer_id) { + Some(*parent) + } else { + None + } + }) + } + + /// All the child layers of this layer. + pub fn child_layers(&self, layer_id: LayerId) -> impl Iterator + '_ { + self.sublayers.get(&layer_id).into_iter().flatten().copied() + } + pub(crate) fn is_sublayer(&self, layer: &LayerId) -> bool { - self.sublayers - .iter() - .any(|(_, children)| children.contains(layer)) + self.parent_layer(*layer).is_some() } pub(crate) fn end_pass(&mut self) { let Self { - visible_last_frame, - visible_current_frame, + visible_areas_last_frame, + visible_areas_current_frame, order, wants_to_be_on_top, sublayers, .. } = self; - std::mem::swap(visible_last_frame, visible_current_frame); - visible_current_frame.clear(); + std::mem::swap(visible_areas_last_frame, visible_areas_current_frame); + visible_areas_current_frame.clear(); + order.sort_by_key(|layer| (layer.order, wants_to_be_on_top.contains(layer))); wants_to_be_on_top.clear(); + // For all layers with sublayers, put the sublayers directly after the parent layer: let sublayers = std::mem::take(sublayers); for (parent, children) in sublayers { @@ -1204,6 +1334,13 @@ impl Areas { }; order.splice(parent_pos..=parent_pos, moved_layers); } + + self.order_map = self + .order + .iter() + .enumerate() + .map(|(i, id)| (*id, i)) + .collect(); } } diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 8faf8e77ceb..166e18da04b 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -406,11 +406,8 @@ impl MenuRoot { } } - if let Some(transform) = button - .ctx - .memory(|m| m.layer_transforms.get(&button.layer_id).copied()) - { - pos = transform * pos; + if let Some(to_global) = button.ctx.layer_transform_to_global(button.layer_id) { + pos = to_global * pos; } return MenuResponse::Create(pos, id); diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index feefe47db43..232450e3862 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -74,6 +74,9 @@ impl Painter { } /// Redirect where you are painting. + /// + /// It is undefined behavior to change the [`LayerId`] + /// of [`crate::Ui::painter`]. pub fn set_layer_id(&mut self, layer_id: LayerId) { self.layer_id = layer_id; } @@ -341,6 +344,12 @@ impl Painter { }) } + /// Paints a line connecting the points. + /// NOTE: all coordinates are screen coordinates! + pub fn line(&self, points: Vec, stroke: impl Into) -> ShapeIdx { + self.add(Shape::line(points, stroke)) + } + /// Paints a horizontal line. pub fn hline(&self, x: impl Into, y: f32, stroke: impl Into) -> ShapeIdx { self.add(Shape::hline(x, y, stroke.into())) diff --git a/crates/egui/src/pass_state.rs b/crates/egui/src/pass_state.rs index bbeaca9b3c6..5501220f769 100644 --- a/crates/egui/src/pass_state.rs +++ b/crates/egui/src/pass_state.rs @@ -70,7 +70,7 @@ impl ScrollTarget { #[cfg(feature = "accesskit")] #[derive(Clone)] pub struct AccessKitPassState { - pub node_builders: IdMap, + pub nodes: IdMap, pub parent_stack: Vec, } @@ -248,7 +248,7 @@ impl Default for PassState { impl PassState { pub(crate) fn begin_pass(&mut self, screen_rect: Rect) { - crate::profile_function!(); + profiling::function_scope!(); let Self { used_ids, widgets, diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 6940a587589..18ddf793cc6 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -392,11 +392,8 @@ impl Response { pub fn drag_delta(&self) -> Vec2 { if self.dragged() { let mut delta = self.ctx.input(|i| i.pointer.delta()); - if let Some(scaling) = self - .ctx - .memory(|m| m.layer_transforms.get(&self.layer_id).map(|t| t.scaling)) - { - delta /= scaling; + if let Some(from_global) = self.ctx.layer_transform_from_global(self.layer_id) { + delta *= from_global.scaling; } delta } else { @@ -478,11 +475,8 @@ impl Response { pub fn hover_pos(&self) -> Option { if self.hovered() { let mut pos = self.ctx.input(|i| i.pointer.hover_pos())?; - if let Some(transform) = self - .ctx - .memory(|m| m.layer_transforms.get(&self.layer_id).copied()) - { - pos = transform.inverse() * pos; + if let Some(from_global) = self.ctx.layer_transform_from_global(self.layer_id) { + pos = from_global * pos; } Some(pos) } else { @@ -988,7 +982,7 @@ impl Response { } #[cfg(feature = "accesskit")] - pub(crate) fn fill_accesskit_node_common(&self, builder: &mut accesskit::NodeBuilder) { + pub(crate) fn fill_accesskit_node_common(&self, builder: &mut accesskit::Node) { if !self.enabled { builder.set_disabled(); } @@ -1001,15 +995,15 @@ impl Response { if self.sense.focusable { builder.add_action(accesskit::Action::Focus); } - if self.sense.click && builder.default_action_verb().is_none() { - builder.set_default_action_verb(accesskit::DefaultActionVerb::Click); + if self.sense.click { + builder.add_action(accesskit::Action::Click); } } #[cfg(feature = "accesskit")] fn fill_accesskit_node_from_widget_info( &self, - builder: &mut accesskit::NodeBuilder, + builder: &mut accesskit::Node, info: crate::WidgetInfo, ) { use crate::WidgetType; @@ -1032,13 +1026,18 @@ impl Response { WidgetType::DragValue => Role::SpinButton, WidgetType::ColorButton => Role::ColorWell, WidgetType::ProgressIndicator => Role::ProgressIndicator, + WidgetType::Window => Role::Window, WidgetType::Other => Role::Unknown, }); if !info.enabled { builder.set_disabled(); } if let Some(label) = info.label { - builder.set_name(label); + if matches!(builder.role(), Role::Label) { + builder.set_value(label); + } else { + builder.set_label(label); + } } if let Some(value) = info.current_text_value { builder.set_value(value); diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index a0ee81fa139..14b5aecd6ee 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -288,7 +288,7 @@ pub struct Style { /// If true and scrolling is enabled for only one direction, allow horizontal scrolling without pressing shift pub always_scroll_the_only_direction: bool, - /// The animation that should be used when scrolling a [`crate::ScrollArea`] using e.g. [Ui::scroll_to_rect]. + /// The animation that should be used when scrolling a [`crate::ScrollArea`] using e.g. [`Ui::scroll_to_rect`]. pub scroll_animation: ScrollAnimation, } @@ -1015,6 +1015,7 @@ impl Visuals { } /// Returned a "grayed out" version of the given color. + #[doc(alias = "grey_out")] #[inline(always)] pub fn gray_out(&self, color: Color32) -> Color32 { crate::ecolor::tint_color_towards(color, self.fade_out_to_color()) diff --git a/crates/egui/src/text_selection/accesskit_text.rs b/crates/egui/src/text_selection/accesskit_text.rs index f56ab9eddb1..d0c3869038d 100644 --- a/crates/egui/src/text_selection/accesskit_text.rs +++ b/crates/egui/src/text_selection/accesskit_text.rs @@ -29,8 +29,6 @@ pub fn update_accesskit_for_text_widget( }); } - builder.set_default_action_verb(accesskit::DefaultActionVerb::Focus); - builder.set_role(role); parent_id @@ -44,7 +42,7 @@ pub fn update_accesskit_for_text_widget( for (row_index, row) in galley.rows.iter().enumerate() { let row_id = parent_id.with(row_index); ctx.accesskit_node_builder(row_id, |builder| { - builder.set_role(accesskit::Role::InlineTextBox); + builder.set_role(accesskit::Role::TextRun); let rect = row.rect.translate(galley_pos.to_vec2()); builder.set_bounds(accesskit::Rect { x0: rect.min.x.into(), diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index d86f9dc56c2..2a31d1e1270 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -96,8 +96,19 @@ pub fn paint_text_selection( pub fn paint_cursor_end(painter: &Painter, visuals: &Visuals, cursor_rect: Rect) { let stroke = visuals.text_cursor.stroke; - let top = cursor_rect.center_top(); - let bottom = cursor_rect.center_bottom(); + // Ensure the cursor is aligned to the pixel grid for whole number widths. + // See https://github.com/emilk/egui/issues/5164 + let (top, bottom) = if (stroke.width as usize) % 2 == 0 { + ( + painter.round_pos_to_pixels(cursor_rect.center_top()), + painter.round_pos_to_pixels(cursor_rect.center_bottom()), + ) + } else { + ( + painter.round_pos_to_pixel_center(cursor_rect.center_top()), + painter.round_pos_to_pixel_center(cursor_rect.center_bottom()), + ) + }; painter.line_segment([top, bottom], (stroke.width, stroke.color)); @@ -121,14 +132,14 @@ pub fn paint_text_cursor( ui: &Ui, painter: &Painter, primary_cursor_rect: Rect, - time_since_last_edit: f64, + time_since_last_interaction: f64, ) { if ui.visuals().text_cursor.blink { let on_duration = ui.visuals().text_cursor.on_duration; let off_duration = ui.visuals().text_cursor.off_duration; let total_duration = on_duration + off_duration; - let time_in_cycle = (time_since_last_edit % (total_duration as f64)) as f32; + let time_in_cycle = (time_since_last_interaction % (total_duration as f64)) as f32; let wake_in = if time_in_cycle < on_duration { // Cursor is visible diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 0b6a16b9ad9..6cc474e141b 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -49,15 +49,27 @@ use crate::Stroke; /// # }); /// ``` pub struct Ui { - /// ID of this ui. + /// Generated based on id of parent ui together with an optional id salt. /// - /// Generated based on id of parent ui together with - /// another source of child identity (e.g. window title). - /// Acts like a namespace for child uis. - /// Should be unique and persist predictably from one frame to next - /// so it can be used as a source for storing state (e.g. window position, or if a collapsing header is open). + /// This should be stable from one frame to next + /// so it can be used as a source for storing state + /// (e.g. window position, or if a collapsing header is open). + /// + /// However, it is not necessarily globally unique. + /// For instance, sibling `Ui`s share the same [`Self::id`] + /// unless they where explicitly given different id salts using + /// [`UiBuilder::id_salt`]. id: Id, + /// This is a globally unique ID of this `Ui`, + /// based on where in the hierarchy of widgets this Ui is in. + /// + /// This means it is not _stable_, as it can change if new widgets + /// are added or removed prior to this one. + /// It should therefore only be used for transient interactions (clicks etc), + /// not for storing state over time. + unique_id: Id, + /// This is used to create a unique interact ID for some widgets. /// /// This value is based on where in the hierarchy of widgets this Ui is in, @@ -108,10 +120,11 @@ impl Ui { /// /// Normally you would not use this directly, but instead use /// [`crate::SidePanel`], [`crate::TopBottomPanel`], [`crate::CentralPanel`], [`crate::Window`] or [`crate::Area`]. - pub fn new(ctx: Context, layer_id: LayerId, id: Id, ui_builder: UiBuilder) -> Self { + pub fn new(ctx: Context, id: Id, ui_builder: UiBuilder) -> Self { let UiBuilder { id_salt, ui_stack_info, + layer_id, max_rect, layout, disabled, @@ -121,6 +134,8 @@ impl Ui { sense, } = ui_builder; + let layer_id = layer_id.unwrap_or(LayerId::background()); + debug_assert!( id_salt.is_none(), "Top-level Ui:s should not have an id_salt" @@ -144,6 +159,7 @@ impl Ui { }; let mut ui = Ui { id, + unique_id: id, next_auto_id_salt: id.with("auto").value(), painter: Painter::new(ctx, layer_id, clip_rect), style, @@ -157,10 +173,10 @@ impl Ui { }; // Register in the widget stack early, to ensure we are behind all widgets we contain: - let start_rect = Rect::NOTHING; // This will be overwritten when/if `interact_bg` is called + let start_rect = Rect::NOTHING; // This will be overwritten when `remember_min_rect` is called ui.ctx().create_widget( WidgetRect { - id: ui.id, + id: ui.unique_id, layer_id: ui.layer_id(), rect: start_rect, interact_rect: start_rect, @@ -234,6 +250,7 @@ impl Ui { let UiBuilder { id_salt, ui_stack_info, + layer_id, max_rect, layout, disabled, @@ -249,6 +266,9 @@ impl Ui { let max_rect = max_rect.unwrap_or_else(|| self.available_rect_before_wrap()); let mut layout = layout.unwrap_or(*self.layout()); let enabled = self.enabled && !disabled && !invisible; + if let Some(layer_id) = layer_id { + painter.set_layer_id(layer_id); + } if invisible { painter.set_invisible(); } @@ -256,7 +276,7 @@ impl Ui { let style = style.unwrap_or_else(|| self.style.clone()); let sense = sense.unwrap_or(Sense::hover()); - if self.sizing_pass { + if sizing_pass { // During the sizing pass we want widgets to use up as little space as possible, // so that we measure the only the space we _need_. layout.cross_justify = false; @@ -266,22 +286,24 @@ impl Ui { } debug_assert!(!max_rect.any_nan()); - let new_id = self.id.with(id_salt); - let next_auto_id_salt = new_id.with(self.next_auto_id_salt).value(); + let stable_id = self.id.with(id_salt); + let unique_id = stable_id.with(self.next_auto_id_salt); + let next_auto_id_salt = unique_id.value().wrapping_add(1); self.next_auto_id_salt = self.next_auto_id_salt.wrapping_add(1); let placer = Placer::new(max_rect, layout); let ui_stack = UiStack { - id: new_id, + id: unique_id, layout_direction: layout.main_dir, info: ui_stack_info, parent: Some(self.stack.clone()), min_rect: placer.min_rect(), max_rect: placer.max_rect(), }; - let child_ui = Ui { - id: new_id, + let mut child_ui = Ui { + id: stable_id, + unique_id, next_auto_id_salt, painter, style, @@ -294,11 +316,15 @@ impl Ui { min_rect_already_remembered: false, }; + if disabled { + child_ui.disable(); + } + // Register in the widget stack early, to ensure we are behind all widgets we contain: - let start_rect = Rect::NOTHING; // This will be overwritten when/if `interact_bg` is called + let start_rect = Rect::NOTHING; // This will be overwritten when `remember_min_rect` is called child_ui.ctx().create_widget( WidgetRect { - id: child_ui.id, + id: child_ui.unique_id, layer_id: child_ui.layer_id(), rect: start_rect, interact_rect: start_rect, @@ -334,12 +360,33 @@ impl Ui { // ------------------------------------------------- - /// A unique identity of this [`Ui`]. + /// Generated based on id of parent ui together with an optional id salt. + /// + /// This should be stable from one frame to next + /// so it can be used as a source for storing state + /// (e.g. window position, or if a collapsing header is open). + /// + /// However, it is not necessarily globally unique. + /// For instance, sibling `Ui`s share the same [`Self::id`] + /// unless they where explicitly given different id salts using + /// [`UiBuilder::id_salt`]. #[inline] pub fn id(&self) -> Id { self.id } + /// This is a globally unique ID of this `Ui`, + /// based on where in the hierarchy of widgets this Ui is in. + /// + /// This means it is not _stable_, as it can change if new widgets + /// are added or removed prior to this one. + /// It should therefore only be used for transient interactions (clicks etc), + /// not for storing state over time. + #[inline] + pub fn unique_id(&self) -> Id { + self.unique_id + } + /// Style options for this [`Ui`] and its children. /// /// Note that this may be a different [`Style`] than that of [`Context::style`]. @@ -916,13 +963,13 @@ impl Ui { } /// Ensure we are big enough to contain the given x-coordinate. - /// This is sometimes useful to expand an ui to stretch to a certain place. + /// This is sometimes useful to expand a ui to stretch to a certain place. pub fn expand_to_include_x(&mut self, x: f32) { self.placer.expand_to_include_x(x); } /// Ensure we are big enough to contain the given y-coordinate. - /// This is sometimes useful to expand an ui to stretch to a certain place. + /// This is sometimes useful to expand a ui to stretch to a certain place. pub fn expand_to_include_y(&mut self, y: f32) { self.placer.expand_to_include_y(y); } @@ -1044,8 +1091,8 @@ impl Ui { viewport .prev_pass .widgets - .get(self.id) - .or_else(|| viewport.this_pass.widgets.get(self.id)) + .get(self.unique_id) + .or_else(|| viewport.this_pass.widgets.get(self.unique_id)) .copied() }) .map(|widget_rect| self.ctx().get_response(widget_rect)) @@ -1062,12 +1109,12 @@ impl Ui { // when the ui was created with `UiBuilder::sense`. // This is a bit hacky, is there a better way? self.ctx().pass_state_mut(|fs| { - fs.used_ids.remove(&self.id); + fs.used_ids.remove(&self.unique_id); }); // This will update the WidgetRect that was first created in `Ui::new`. self.ctx().create_widget( WidgetRect { - id: self.id, + id: self.unique_id, layer_id: self.layer_id(), rect: self.min_rect(), interact_rect: self.clip_rect().intersect(self.min_rect()), @@ -1085,7 +1132,7 @@ impl Ui { #[deprecated = "Use UiBuilder::sense with Ui::response instead"] pub fn interact_bg(&self, sense: Sense) -> Response { // This will update the WidgetRect that was first created in `Ui::new`. - self.interact(self.min_rect(), self.id, sense) + self.interact(self.min_rect(), self.unique_id, sense) } /// Is the pointer (mouse/touch) above this rectangle in this [`Ui`]? @@ -1372,7 +1419,7 @@ impl Ui { let item_spacing = self.spacing().item_spacing; self.placer.advance_after_rects(rect, rect, item_spacing); register_rect(self, rect); - let response = self.interact(rect, child_ui.id, Sense::hover()); + let response = self.interact(rect, child_ui.unique_id, Sense::hover()); InnerResponse::new(inner, response) } @@ -2260,15 +2307,23 @@ impl Ui { } /// Redirect shapes to another paint layer. + /// + /// ``` + /// # use egui::{LayerId, Order, Id}; + /// # egui::__run_test_ui(|ui| { + /// let layer_id = LayerId::new(Order::Tooltip, Id::new("my_floating_ui")); + /// ui.with_layer_id(layer_id, |ui| { + /// ui.label("This is now in a different layer"); + /// }); + /// # }); + /// ``` + #[deprecated = "Use ui.scope_builder(UiBuilder::new().layer_id(…), …) instead"] pub fn with_layer_id( &mut self, layer_id: LayerId, add_contents: impl FnOnce(&mut Self) -> R, ) -> InnerResponse { - self.scope(|ui| { - ui.painter.set_layer_id(layer_id); - add_contents(ui) - }) + self.scope_builder(UiBuilder::new().layer_id(layer_id), add_contents) } /// A [`CollapsingHeader`] that starts out collapsed. @@ -2714,7 +2769,8 @@ impl Ui { // Paint the body to a new layer: let layer_id = LayerId::new(Order::Tooltip, id); - let InnerResponse { inner, response } = self.with_layer_id(layer_id, add_contents); + let InnerResponse { inner, response } = + self.scope_builder(UiBuilder::new().layer_id(layer_id), add_contents); // Now we move the visuals of the body to where the mouse is. // Normally you need to decide a location for a widget first, diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index 33a48dbd553..d13748543ab 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -1,11 +1,11 @@ use std::{hash::Hash, sync::Arc}; -use crate::{Id, Layout, Rect, Sense, Style, UiStackInfo}; +use crate::{Id, LayerId, Layout, Rect, Sense, Style, UiStackInfo}; #[allow(unused_imports)] // Used for doclinks use crate::Ui; -/// Build a [`Ui`] as the chlild of another [`Ui`]. +/// Build a [`Ui`] as the child of another [`Ui`]. /// /// By default, everything is inherited from the parent, /// except for `max_rect` which by default is set to @@ -15,6 +15,7 @@ use crate::Ui; pub struct UiBuilder { pub id_salt: Option, pub ui_stack_info: UiStackInfo, + pub layer_id: Option, pub max_rect: Option, pub layout: Option, pub disabled: bool, @@ -48,6 +49,13 @@ impl UiBuilder { self } + /// Show the [`Ui`] in a different [`LayerId`] from its parent. + #[inline] + pub fn layer_id(mut self, layer_id: LayerId) -> Self { + self.layer_id = Some(layer_id); + self + } + /// Set the max rectangle, within which widgets will go. /// /// New widgets will *try* to fit within this rectangle. diff --git a/crates/egui/src/ui_stack.rs b/crates/egui/src/ui_stack.rs index 7aae3f2fbeb..7aa131bec7a 100644 --- a/crates/egui/src/ui_stack.rs +++ b/crates/egui/src/ui_stack.rs @@ -24,6 +24,9 @@ pub enum UiKind { /// A bottom [`crate::TopBottomPanel`]. BottomPanel, + /// A modal [`crate::Modal`]. + Modal, + /// A [`crate::Frame`]. Frame, @@ -82,6 +85,7 @@ impl UiKind { Self::Window | Self::Menu + | Self::Modal | Self::Popup | Self::Tooltip | Self::Picker @@ -228,6 +232,12 @@ impl UiStack { self.kind().map_or(false, |kind| kind.is_panel()) } + /// Is this [`crate::Ui`] an [`crate::Area`]? + #[inline] + pub fn is_area_ui(&self) -> bool { + self.kind().map_or(false, |kind| kind.is_area()) + } + /// Is this a root [`crate::Ui`], i.e. created with [`crate::Ui::new()`]? #[inline] pub fn is_root_ui(&self) -> bool { diff --git a/crates/egui/src/util/cache.rs b/crates/egui/src/util/cache.rs deleted file mode 100644 index 52b3bd57357..00000000000 --- a/crates/egui/src/util/cache.rs +++ /dev/null @@ -1,164 +0,0 @@ -//! Computing the same thing each frame can be expensive, -//! so often you want to save the result from the previous frame and reuse it. -//! -//! Enter [`FrameCache`]: it caches the results of a computation for one frame. -//! If it is still used next frame, it is not recomputed. -//! If it is not used next frame, it is evicted from the cache to save memory. - -/// Something that does an expensive computation that we want to cache -/// to save us from recomputing it each frame. -pub trait ComputerMut: 'static + Send + Sync { - fn compute(&mut self, key: Key) -> Value; -} - -/// Caches the results of a computation for one frame. -/// If it is still used next frame, it is not recomputed. -/// If it is not used next frame, it is evicted from the cache to save memory. -pub struct FrameCache { - generation: u32, - computer: Computer, - cache: nohash_hasher::IntMap, -} - -impl Default for FrameCache -where - Computer: Default, -{ - fn default() -> Self { - Self::new(Computer::default()) - } -} - -impl FrameCache { - pub fn new(computer: Computer) -> Self { - Self { - generation: 0, - computer, - cache: Default::default(), - } - } - - /// Must be called once per frame to clear the cache. - pub fn evice_cache(&mut self) { - let current_generation = self.generation; - self.cache.retain(|_key, cached| { - cached.0 == current_generation // only keep those that were used this frame - }); - self.generation = self.generation.wrapping_add(1); - } -} - -impl FrameCache { - /// Get from cache (if the same key was used last frame) - /// or recompute and store in the cache. - pub fn get(&mut self, key: Key) -> Value - where - Key: Copy + std::hash::Hash, - Value: Clone, - Computer: ComputerMut, - { - let hash = crate::util::hash(key); - - match self.cache.entry(hash) { - std::collections::hash_map::Entry::Occupied(entry) => { - let cached = entry.into_mut(); - cached.0 = self.generation; - cached.1.clone() - } - std::collections::hash_map::Entry::Vacant(entry) => { - let value = self.computer.compute(key); - entry.insert((self.generation, value.clone())); - value - } - } - } -} - -#[allow(clippy::len_without_is_empty)] -pub trait CacheTrait: 'static + Send + Sync { - /// Call once per frame to evict cache. - fn update(&mut self); - - /// Number of values currently in the cache. - fn len(&self) -> usize; - - fn as_any_mut(&mut self) -> &mut dyn std::any::Any; -} - -impl CacheTrait - for FrameCache -{ - fn update(&mut self) { - self.evice_cache(); - } - - fn len(&self) -> usize { - self.cache.len() - } - - fn as_any_mut(&mut self) -> &mut dyn std::any::Any { - self - } -} - -/// ``` -/// use egui::util::cache::{CacheStorage, ComputerMut, FrameCache}; -/// -/// #[derive(Default)] -/// struct CharCounter {} -/// impl ComputerMut<&str, usize> for CharCounter { -/// fn compute(&mut self, s: &str) -> usize { -/// s.chars().count() -/// } -/// } -/// type CharCountCache<'a> = FrameCache; -/// -/// # let mut cache_storage = CacheStorage::default(); -/// let mut cache = cache_storage.cache::>(); -/// assert_eq!(cache.get("hello"), 5); -/// ``` -#[derive(Default)] -pub struct CacheStorage { - caches: ahash::HashMap>, -} - -impl CacheStorage { - pub fn cache(&mut self) -> &mut FrameCache { - self.caches - .entry(std::any::TypeId::of::()) - .or_insert_with(|| Box::::default()) - .as_any_mut() - .downcast_mut::() - .unwrap() - } - - /// Total number of cached values - fn num_values(&self) -> usize { - self.caches.values().map(|cache| cache.len()).sum() - } - - /// Call once per frame to evict cache. - pub fn update(&mut self) { - for cache in self.caches.values_mut() { - cache.update(); - } - } -} - -impl Clone for CacheStorage { - fn clone(&self) -> Self { - // We return an empty cache that can be filled in again. - Self::default() - } -} - -impl std::fmt::Debug for CacheStorage { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "FrameCacheStorage[{} caches with {} elements]", - self.caches.len(), - self.num_values() - ) - } -} diff --git a/crates/egui/src/util/id_type_map.rs b/crates/egui/src/util/id_type_map.rs index 7e812ae5572..a7b8f18c9c3 100644 --- a/crates/egui/src/util/id_type_map.rs +++ b/crates/egui/src/util/id_type_map.rs @@ -308,7 +308,7 @@ fn from_ron_str(ron: &str) -> Option { use crate::Id; // TODO(emilk): make IdTypeMap generic over the key (`Id`), and make a library of IdTypeMap. -/// Stores values identified by an [`Id`] AND a the [`std::any::TypeId`] of the value. +/// Stores values identified by an [`Id`] AND the [`std::any::TypeId`] of the value. /// /// In other words, it maps `(Id, TypeId)` to any value you want. /// @@ -574,7 +574,7 @@ struct PersistedMap(Vec<(u64, SerializedElement)>); #[cfg(feature = "persistence")] impl PersistedMap { fn from_map(map: &IdTypeMap) -> Self { - crate::profile_function!(); + profiling::function_scope!(); use std::collections::BTreeMap; @@ -593,7 +593,7 @@ impl PersistedMap { let max_bytes_per_type = map.max_bytes_per_type; { - crate::profile_scope!("gather"); + profiling::scope!("gather"); for (hash, element) in &map.map { if let Some(element) = element.to_serialize() { let stats = types_map.entry(element.type_id).or_default(); @@ -610,7 +610,7 @@ impl PersistedMap { let mut persisted = vec![]; { - crate::profile_scope!("gc"); + profiling::scope!("gc"); for stats in types_map.values() { let mut bytes_written = 0; @@ -634,7 +634,7 @@ impl PersistedMap { } fn into_map(self) -> IdTypeMap { - crate::profile_function!(); + profiling::function_scope!(); let map = self .0 .into_iter() @@ -671,7 +671,7 @@ impl serde::Serialize for IdTypeMap { where S: serde::Serializer, { - crate::profile_scope!("IdTypeMap::serialize"); + profiling::scope!("IdTypeMap::serialize"); PersistedMap::from_map(self).serialize(serializer) } } @@ -682,7 +682,7 @@ impl<'de> serde::Deserialize<'de> for IdTypeMap { where D: serde::Deserializer<'de>, { - crate::profile_scope!("IdTypeMap::deserialize"); + profiling::scope!("IdTypeMap::deserialize"); ::deserialize(deserializer).map(PersistedMap::into_map) } } diff --git a/crates/egui/src/util/mod.rs b/crates/egui/src/util/mod.rs index 55e93eb0400..de62b961822 100644 --- a/crates/egui/src/util/mod.rs +++ b/crates/egui/src/util/mod.rs @@ -1,6 +1,5 @@ //! Miscellaneous tools used by the rest of egui. -pub mod cache; pub(crate) mod fixed_cache; pub mod id_type_map; pub mod undoer; @@ -9,3 +8,7 @@ pub use id_type_map::IdTypeMap; pub use epaint::emath::History; pub use epaint::util::{hash, hash_with}; + +/// Deprecated alias for [`crate::cache`]. +#[deprecated = "Use egui::cache instead"] +pub use crate::cache; diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index e685d3ee6b7..91cd12e2b60 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -19,7 +19,7 @@ //! ### Deferred viewports //! These are created with [`Context::show_viewport_deferred`]. //! Deferred viewports take a closure that is called by the integration at a later time, perhaps multiple times. -//! Deferred viewports are repainted independenantly of the parent viewport. +//! Deferred viewports are repainted independently of the parent viewport. //! This means communication with them needs to be done via channels, or `Arc/Mutex`. //! //! This is the most performant type of child viewport, though a bit more cumbersome to work with compared to immediate viewports. @@ -188,7 +188,7 @@ impl std::fmt::Debug for IconData { impl From for epaint::ColorImage { fn from(icon: IconData) -> Self { - crate::profile_function!(); + profiling::function_scope!(); let IconData { rgba, width, @@ -200,7 +200,7 @@ impl From for epaint::ColorImage { impl From<&IconData> for epaint::ColorImage { fn from(icon: &IconData) -> Self { - crate::profile_function!(); + profiling::function_scope!(); let IconData { rgba, width, @@ -534,7 +534,7 @@ impl ViewportBuilder { /// See [winit's documentation][drag_and_drop] for information on why you /// might want to disable this on windows. /// - /// [drag_and_drop]: https://docs.rs/winit/latest/x86_64-pc-windows-msvc/winit/platform/windows/trait.WindowBuilderExtWindows.html#tymethod.with_drag_and_drop + /// [drag_and_drop]: https://docs.rs/winit/latest/x86_64-pc-windows-msvc/winit/platform/windows/trait.WindowAttributesExtWindows.html#tymethod.with_drag_and_drop #[inline] pub fn with_drag_and_drop(mut self, value: bool) -> Self { self.drag_and_drop = Some(value); @@ -728,7 +728,7 @@ impl ViewportBuilder { } if let Some(new_visible) = new_visible { - if Some(new_visible) != self.active { + if Some(new_visible) != self.visible { self.visible = Some(new_visible); commands.push(ViewportCommand::Visible(new_visible)); } @@ -1056,10 +1056,10 @@ pub enum ViewportCommand { /// Enable mouse pass-through: mouse clicks pass through the window, used for non-interactable overlays. MousePassthrough(bool), - /// Take a screenshot. + /// Take a screenshot of the next frame after this. /// - /// The results are returned in `crate::Event::Screenshot`. - Screenshot, + /// The results are returned in [`crate::Event::Screenshot`]. + Screenshot(crate::UserData), /// Request cut of the current selection /// @@ -1100,6 +1100,8 @@ impl ViewportCommand { } } +// ---------------------------------------------------------------------------- + /// Describes a viewport, i.e. a native window. /// /// This is returned by [`crate::Context::run`] on each frame, and should be applied diff --git a/crates/egui/src/widget_rect.rs b/crates/egui/src/widget_rect.rs index e69badb8701..3725d62a7b0 100644 --- a/crates/egui/src/widget_rect.rs +++ b/crates/egui/src/widget_rect.rs @@ -20,10 +20,10 @@ pub struct WidgetRect { /// What layer the widget is on. pub layer_id: LayerId, - /// The full widget rectangle. + /// The full widget rectangle, in local layer coordinates. pub rect: Rect, - /// Where the widget is. + /// Where the widget is, in local layer coordinates. /// /// This is after clipping with the parent ui clip rect. pub interact_rect: Rect, @@ -42,6 +42,27 @@ pub struct WidgetRect { pub enabled: bool, } +impl WidgetRect { + pub fn transform(self, transform: emath::TSTransform) -> Self { + let Self { + id, + layer_id, + rect, + interact_rect, + sense, + enabled, + } = self; + Self { + id, + layer_id, + rect: transform * rect, + interact_rect: transform * interact_rect, + sense, + enabled, + } + } +} + /// Stores the [`WidgetRect`]s of all widgets generated during a single egui update/frame. /// /// All [`crate::Ui`]s have a [`WidgetRect`]. It is created in [`crate::Ui::new`] with [`Rect::NOTHING`] @@ -141,7 +162,10 @@ impl WidgetRects { debug_assert!( existing.layer_id == widget_rect.layer_id, - "Widget changed layer_id during the frame" + "Widget {:?} changed layer_id during the frame from {:?} to {:?}", + widget_rect.id, + existing.layer_id, + widget_rect.layer_id ); // Update it: diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index 03b8cbb9751..011a4adcbb0 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -67,6 +67,27 @@ impl From for RichText { } } +impl From<&Box> for RichText { + #[inline] + fn from(text: &Box) -> Self { + Self::new(text.clone()) + } +} + +impl From<&mut Box> for RichText { + #[inline] + fn from(text: &mut Box) -> Self { + Self::new(text.clone()) + } +} + +impl From> for RichText { + #[inline] + fn from(text: Box) -> Self { + Self::new(text) + } +} + impl From> for RichText { #[inline] fn from(text: Cow<'_, str>) -> Self { @@ -359,6 +380,9 @@ impl RichText { || fallback_font.resolve(style), |text_style| text_style.resolve(style), ); + if let Some(fid) = style.override_font_id.clone() { + font_id = fid; + } if let Some(size) = size { font_id.size = size; } @@ -701,6 +725,20 @@ impl From for WidgetText { } } +impl From<&Box> for WidgetText { + #[inline] + fn from(text: &Box) -> Self { + Self::RichText(RichText::new(text.clone())) + } +} + +impl From> for WidgetText { + #[inline] + fn from(text: Box) -> Self { + Self::RichText(RichText::new(text)) + } +} + impl From> for WidgetText { #[inline] fn from(text: Cow<'_, str>) -> Self { diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index f5425980c6c..e4355b49e8d 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -21,7 +21,7 @@ use crate::{ /// } /// # }); /// ``` -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +#[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct Button<'a> { image: Option>, text: Option, @@ -37,6 +37,7 @@ pub struct Button<'a> { min_size: Vec2, rounding: Option, selected: bool, + image_tint_follows_text_color: bool, } impl<'a> Button<'a> { @@ -70,6 +71,7 @@ impl<'a> Button<'a> { min_size: Vec2::ZERO, rounding: None, selected: false, + image_tint_follows_text_color: false, } } @@ -156,6 +158,18 @@ impl<'a> Button<'a> { self } + /// If true, the tint of the image is multiplied by the widget text color. + /// + /// This makes sense for images that are white, that should have the same color as the text color. + /// This will also make the icon color depend on hover state. + /// + /// Default: `false`. + #[inline] + pub fn image_tint_follows_text_color(mut self, image_tint_follows_text_color: bool) -> Self { + self.image_tint_follows_text_color = image_tint_follows_text_color; + self + } + /// Show some text on the right side of the button, in weak color. /// /// Designed for menu buttons, for setting a keyboard shortcut text (e.g. `Ctrl+S`). @@ -190,6 +204,7 @@ impl Widget for Button<'_> { min_size, rounding, selected, + image_tint_follows_text_color, } = self; let frame = frame.unwrap_or_else(|| ui.visuals().button_frame); @@ -319,12 +334,16 @@ impl Widget for Button<'_> { let image_rect = Rect::from_min_size(image_pos, image_size); cursor_x += image_size.x; let tlr = image.load_for_size(ui.ctx(), image_size); + let mut image_options = image.image_options().clone(); + if image_tint_follows_text_color { + image_options.tint = image_options.tint * visuals.text_color(); + } widgets::image::paint_texture_load_result( ui, &tlr, image_rect, image.show_loading_spinner, - image.image_options(), + &image_options, ); response = widgets::image::texture_load_result_response( &image.source(ui.ctx()), diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index 66e9df01f8c..f8b31430e15 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -16,7 +16,7 @@ use crate::{ /// ui.add(egui::Checkbox::new(&mut my_bool, "Checked")); /// # }); /// ``` -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +#[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct Checkbox<'a> { checked: &'a mut bool, text: WidgetText, diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs index a00d6f9ac3b..9a605905968 100644 --- a/crates/egui/src/widgets/color_picker.rs +++ b/crates/egui/src/widgets/color_picker.rs @@ -165,8 +165,11 @@ fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color /// * `x_value` - X axis, either saturation or value (0.0-1.0). /// * `y_value` - Y axis, either saturation or value (0.0-1.0). /// * `color_at` - A function that dictates how the mix of saturation and value will be displayed in the 2d slider. -/// E.g.: `|x_value, y_value| HsvaGamma { h: 1.0, s: x_value, v: y_value, a: 1.0 }.into()` displays the colors as follows: top-left: white \[s: 0.0, v: 1.0], top-right: fully saturated color \[s: 1.0, v: 1.0], bottom-right: black \[s: 0.0, v: 1.0]. /// +/// e.g.: `|x_value, y_value| HsvaGamma { h: 1.0, s: x_value, v: y_value, a: 1.0 }.into()` displays the colors as follows: +/// * top-left: white `[s: 0.0, v: 1.0]` +/// * top-right: fully saturated color `[s: 1.0, v: 1.0]` +/// * bottom-right: black `[s: 0.0, v: 1.0].` fn color_slider_2d( ui: &mut Ui, x_value: &mut f32, diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index 14d1d8c45b4..a5b8c25b2f8 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -34,7 +34,7 @@ fn set(get_set_value: &mut GetSetValue<'_>, value: f64) { /// ui.add(egui::DragValue::new(&mut my_f32).speed(0.1)); /// # }); /// ``` -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +#[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct DragValue<'a> { get_set_value: GetSetValue<'a>, speed: f64, @@ -452,7 +452,7 @@ impl<'a> Widget for DragValue<'a> { // in button mode for just one frame. This is important for // screen readers. let is_kb_editing = ui.memory_mut(|mem| { - mem.interested_in_focus(id); + mem.interested_in_focus(id, ui.layer_id()); mem.has_focus(id) }); @@ -686,7 +686,7 @@ impl<'a> Widget for DragValue<'a> { } // The name field is set to the current value by the button, // but we don't want it set that way on this widget type. - builder.clear_name(); + builder.clear_label(); // Always expose the value as a string. This makes the widget // more stable to accessibility users as it switches // between edit and button modes. This is particularly important diff --git a/crates/egui/src/widgets/hyperlink.rs b/crates/egui/src/widgets/hyperlink.rs index 55a7b08b8e7..7d5129b14de 100644 --- a/crates/egui/src/widgets/hyperlink.rs +++ b/crates/egui/src/widgets/hyperlink.rs @@ -23,7 +23,7 @@ use self::text_selection::LabelSelectionState; /// } /// # }); /// ``` -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +#[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct Link { text: WidgetText, } @@ -88,7 +88,7 @@ impl Widget for Link { /// ui.add(egui::Hyperlink::from_label_and_url("My favorite repo", "https://github.com/emilk/egui")); /// # }); /// ``` -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +#[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct Hyperlink { url: String, text: WidgetText, diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index 2e1e49671be..4cdfc5bf749 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -42,7 +42,7 @@ use crate::{ /// # }); /// ``` /// -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +#[must_use = "You should put this widget in a ui with `ui.add(widget);`"] #[derive(Debug, Clone)] pub struct Image<'a> { source: ImageSource<'a>, diff --git a/crates/egui/src/widgets/image_button.rs b/crates/egui/src/widgets/image_button.rs index 4e910eb75dd..bcae9a991d5 100644 --- a/crates/egui/src/widgets/image_button.rs +++ b/crates/egui/src/widgets/image_button.rs @@ -4,7 +4,7 @@ use crate::{ }; /// A clickable image within a frame. -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +#[must_use = "You should put this widget in a ui with `ui.add(widget);`"] #[derive(Clone, Debug)] pub struct ImageButton<'a> { pub(crate) image: Image<'a>, diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index 1119bc14629..b6ade45ae30 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -23,7 +23,7 @@ use self::text_selection::LabelSelectionState; /// /// For full control of the text you can use [`crate::text::LayoutJob`] /// as argument to [`Self::new`]. -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +#[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct Label { text: WidgetText, wrap_mode: Option, diff --git a/crates/egui/src/widgets/mod.rs b/crates/egui/src/widgets/mod.rs index e75a232cfd3..78e095aefdb 100644 --- a/crates/egui/src/widgets/mod.rs +++ b/crates/egui/src/widgets/mod.rs @@ -57,7 +57,7 @@ pub use self::{ /// Tip: you can `impl Widget for &mut YourThing { }`. /// /// `|ui: &mut Ui| -> Response { … }` also implements [`Widget`]. -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +#[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub trait Widget { /// Allocate space, interact, paint, and return a [`Response`]. /// diff --git a/crates/egui/src/widgets/progress_bar.rs b/crates/egui/src/widgets/progress_bar.rs index 5912700dfea..5e099f6a604 100644 --- a/crates/egui/src/widgets/progress_bar.rs +++ b/crates/egui/src/widgets/progress_bar.rs @@ -11,7 +11,7 @@ enum ProgressBarText { /// A simple progress bar. /// /// See also: [`crate::Spinner`]. -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +#[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct ProgressBar { progress: f32, desired_width: Option, diff --git a/crates/egui/src/widgets/radio_button.rs b/crates/egui/src/widgets/radio_button.rs index 35640f8885c..fabf565b57f 100644 --- a/crates/egui/src/widgets/radio_button.rs +++ b/crates/egui/src/widgets/radio_button.rs @@ -22,7 +22,7 @@ use crate::{ /// } /// # }); /// ``` -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +#[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct RadioButton { checked: bool, text: WidgetText, diff --git a/crates/egui/src/widgets/selected_label.rs b/crates/egui/src/widgets/selected_label.rs index 21339a73175..193fee74dea 100644 --- a/crates/egui/src/widgets/selected_label.rs +++ b/crates/egui/src/widgets/selected_label.rs @@ -21,7 +21,7 @@ use crate::{NumExt, Response, Sense, TextStyle, Ui, Widget, WidgetInfo, WidgetTe /// } /// # }); /// ``` -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +#[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct SelectableLabel { selected: bool, text: WidgetText, diff --git a/crates/egui/src/widgets/separator.rs b/crates/egui/src/widgets/separator.rs index e421de9cf89..2cc3a857ce6 100644 --- a/crates/egui/src/widgets/separator.rs +++ b/crates/egui/src/widgets/separator.rs @@ -11,7 +11,7 @@ use crate::{vec2, Response, Sense, Ui, Vec2, Widget}; /// ui.add(egui::Separator::default()); /// # }); /// ``` -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +#[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct Separator { spacing: f32, grow: f32, diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index 7b0cb074ea5..7dc3a5cd568 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -94,7 +94,7 @@ pub enum SliderClamping { /// ``` /// /// The default [`Slider`] size is set by [`crate::style::Spacing::slider_width`]. -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +#[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct Slider<'a> { get_set_value: GetSetValue<'a>, range: RangeInclusive, @@ -1030,7 +1030,7 @@ impl<'a> Widget for Slider<'a> { // Logarithmic sliders are allowed to include zero and infinity, // even though mathematically it doesn't make sense. -use std::f64::INFINITY; +const INFINITY: f64 = f64::INFINITY; /// When the user asks for an infinitely large range (e.g. logarithmic from zero), /// give a scale that this many orders of magnitude in size. diff --git a/crates/egui/src/widgets/spinner.rs b/crates/egui/src/widgets/spinner.rs index f1884284418..97cf2496646 100644 --- a/crates/egui/src/widgets/spinner.rs +++ b/crates/egui/src/widgets/spinner.rs @@ -5,7 +5,7 @@ use crate::{Response, Sense, Ui, Widget, WidgetInfo, WidgetType}; /// A spinner widget used to indicate loading. /// /// See also: [`crate::ProgressBar`]. -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +#[must_use = "You should put this widget in a ui with `ui.add(widget);`"] #[derive(Default)] pub struct Spinner { /// Uses the style's `interact_size` if `None`. diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 3ca36608c47..587f498b0a2 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use emath::Rect; use epaint::text::{cursor::CCursor, Galley, LayoutJob}; use crate::{ @@ -59,8 +60,8 @@ use super::{TextEditOutput, TextEditState}; /// See [`TextEdit::show`]. /// /// ## Other -/// The background color of a [`crate::TextEdit`] is [`crate::Visuals::extreme_bg_color`]. -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +/// The background color of a [`crate::TextEdit`] is [`crate::Visuals::extreme_bg_color`] or can be set with [`crate::TextEdit::background_color`]. +#[must_use = "You should put this widget in a ui with `ui.add(widget);`"] pub struct TextEdit<'t> { text: &'t mut dyn TextBuffer, hint_text: WidgetText, @@ -84,6 +85,7 @@ pub struct TextEdit<'t> { clip_text: bool, char_limit: usize, return_key: Option, + background_color: Option, } impl<'t> WidgetWithState for TextEdit<'t> { @@ -142,6 +144,7 @@ impl<'t> TextEdit<'t> { clip_text: false, char_limit: usize::MAX, return_key: Some(KeyboardShortcut::new(Modifiers::NONE, Key::Enter)), + background_color: None, } } @@ -201,6 +204,14 @@ impl<'t> TextEdit<'t> { self } + /// Set the background color of the [`TextEdit`]. The default is [`crate::Visuals::extreme_bg_color`]. + // TODO(bircni): remove this once #3284 is implemented + #[inline] + pub fn background_color(mut self, color: Color32) -> Self { + self.background_color = Some(color); + self + } + /// Set a specific style for the hint text. #[inline] pub fn hint_text_font(mut self, hint_text_font: impl Into) -> Self { @@ -409,7 +420,9 @@ impl<'t> TextEdit<'t> { let is_mutable = self.text.is_mutable(); let frame = self.frame; let where_to_put_background = ui.painter().add(Shape::Noop); - + let background_color = self + .background_color + .unwrap_or(ui.visuals().extreme_bg_color); let margin = self.margin; let mut output = self.show_content(ui); @@ -427,14 +440,14 @@ impl<'t> TextEdit<'t> { epaint::RectShape::new( frame_rect, visuals.rounding, - ui.visuals().extreme_bg_color, + background_color, ui.visuals().selection.stroke, ) } else { epaint::RectShape::new( frame_rect, visuals.rounding, - ui.visuals().extreme_bg_color, + background_color, visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop". ) } @@ -477,6 +490,7 @@ impl<'t> TextEdit<'t> { clip_text, char_limit, return_key, + background_color: _, } = self; let text_color = text_color @@ -512,13 +526,13 @@ impl<'t> TextEdit<'t> { let mut galley = layouter(ui, text.as_str(), wrap_width); - let desired_width = if clip_text { + let desired_inner_width = if clip_text { wrap_width // visual clipping with scroll in singleline input. } else { galley.size().x.max(wrap_width) }; let desired_height = (desired_height_rows.at_least(1) as f32) * row_height; - let desired_inner_size = vec2(desired_width, galley.size().y.max(desired_height)); + let desired_inner_size = vec2(desired_inner_width, galley.size().y.max(desired_height)); let desired_outer_size = (desired_inner_size + margin.sum()).at_least(min_size); let (auto_id, outer_rect) = ui.allocate_space(desired_outer_size); let rect = outer_rect - margin; // inner rect (excluding frame/margin). @@ -549,6 +563,7 @@ impl<'t> TextEdit<'t> { Sense::hover() }; let mut response = ui.interact(outer_rect, id, sense); + response.intrinsic_size = Some(Vec2::new(desired_width, desired_outer_size.y)); response.fake_primary_click = false; // Don't sent `OutputEvent::Clicked` when a user presses the space bar @@ -588,6 +603,8 @@ impl<'t> TextEdit<'t> { if did_interact || response.clicked() { ui.memory_mut(|mem| mem.request_focus(response.id)); + + state.last_interaction_time = ui.ctx().input(|i| i.time); } } } @@ -706,6 +723,16 @@ impl<'t> TextEdit<'t> { } } + // Allocate additional space if edits were made this frame that changed the size. This is important so that, + // if there's a ScrollArea, it can properly scroll to the cursor. + let extra_size = galley.size() - rect.size(); + if extra_size.x > 0.0 || extra_size.y > 0.0 { + ui.allocate_rect( + Rect::from_min_size(outer_rect.max, extra_size), + Sense::hover(), + ); + } + painter.galley(galley_pos, galley.clone(), text_color); if has_focus { @@ -713,16 +740,15 @@ impl<'t> TextEdit<'t> { let primary_cursor_rect = cursor_rect(galley_pos, &galley, &cursor_range.primary, row_height); - let is_fully_visible = ui.clip_rect().contains_rect(rect); // TODO(emilk): remove this HACK workaround for https://github.com/emilk/egui/issues/1531 - if (response.changed || selection_changed) && !is_fully_visible { + if response.changed || selection_changed { // Scroll to keep primary cursor in view: - ui.scroll_to_rect(primary_cursor_rect, None); + ui.scroll_to_rect(primary_cursor_rect + margin, None); } if text.is_mutable() && interactive { let now = ui.ctx().input(|i| i.time); if response.changed || selection_changed { - state.last_edit_time = now; + state.last_interaction_time = now; } // Only show (and blink) cursor if the egui viewport has focus. @@ -735,19 +761,20 @@ impl<'t> TextEdit<'t> { ui, &painter, primary_cursor_rect, - now - state.last_edit_time, + now - state.last_interaction_time, ); } // Set IME output (in screen coords) when text is editable and visible - let transform = ui - .memory(|m| m.layer_transforms.get(&ui.layer_id()).copied()) + let to_global = ui + .ctx() + .layer_transform_to_global(ui.layer_id()) .unwrap_or_default(); ui.ctx().output_mut(|o| { o.ime = Some(crate::output::IMEOutput { - rect: transform * rect, - cursor_rect: transform * primary_cursor_rect, + rect: to_global * rect, + cursor_rect: to_global * primary_cursor_rect, }); }); } @@ -964,39 +991,41 @@ fn events( break; } } + Event::Key { - key: Key::Z, + key, pressed: true, modifiers, .. - } if modifiers.matches_logically(Modifiers::COMMAND) => { - if let Some((undo_ccursor_range, undo_txt)) = state + } if (modifiers.matches_logically(Modifiers::COMMAND) && *key == Key::Y) + || (modifiers.matches_logically(Modifiers::SHIFT | Modifiers::COMMAND) + && *key == Key::Z) => + { + if let Some((redo_ccursor_range, redo_txt)) = state .undoer .lock() - .undo(&(cursor_range.as_ccursor_range(), text.as_str().to_owned())) + .redo(&(cursor_range.as_ccursor_range(), text.as_str().to_owned())) { - text.replace_with(undo_txt); - Some(*undo_ccursor_range) + text.replace_with(redo_txt); + Some(*redo_ccursor_range) } else { None } } + Event::Key { - key, + key: Key::Z, pressed: true, modifiers, .. - } if (modifiers.matches_logically(Modifiers::COMMAND) && *key == Key::Y) - || (modifiers.matches_logically(Modifiers::SHIFT | Modifiers::COMMAND) - && *key == Key::Z) => - { - if let Some((redo_ccursor_range, redo_txt)) = state + } if modifiers.matches_logically(Modifiers::COMMAND) => { + if let Some((undo_ccursor_range, undo_txt)) = state .undoer .lock() - .redo(&(cursor_range.as_ccursor_range(), text.as_str().to_owned())) + .undo(&(cursor_range.as_ccursor_range(), text.as_str().to_owned())) { - text.replace_with(redo_txt); - Some(*redo_ccursor_range) + text.replace_with(undo_txt); + Some(*undo_ccursor_range) } else { None } diff --git a/crates/egui/src/widgets/text_edit/state.rs b/crates/egui/src/widgets/text_edit/state.rs index e73664a1293..c10a88274ef 100644 --- a/crates/egui/src/widgets/text_edit/state.rs +++ b/crates/egui/src/widgets/text_edit/state.rs @@ -53,10 +53,10 @@ pub struct TextEditState { #[cfg_attr(feature = "serde", serde(skip))] pub(crate) singleline_offset: f32, - /// When did the user last press a key? + /// When did the user last press a key or click on the `TextEdit`. /// Used to pause the cursor animation when typing. #[cfg_attr(feature = "serde", serde(skip))] - pub(crate) last_edit_time: f64, + pub(crate) last_interaction_time: f64, } impl TextEditState { @@ -89,6 +89,7 @@ impl TextEditState { self.undoer.lock().clone() } + #[allow(clippy::needless_pass_by_ref_mut)] // Intentionally hide interiority of mutability pub fn set_undoer(&mut self, undoer: TextEditUndoer) { *self.undoer.lock() = undoer; } diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index b1905356b2e..c3083d11caf 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -8,6 +8,9 @@ rust-version.workspace = true publish = false default-run = "egui_demo_app" +[package.metadata.cargo-machete] +ignored = ["profiling"] + [lints] workspace = true @@ -25,10 +28,15 @@ default = ["glow", "persistence"] # image_viewer adds about 0.9 MB of WASM web_app = ["http", "persistence"] -http = ["ehttp", "image", "poll-promise", "egui_extras/image"] -image_viewer = ["image", "egui_extras/all_loaders", "rfd"] -persistence = ["eframe/persistence", "egui/persistence", "serde", "egui_extras/serde"] -puffin = ["eframe/puffin", "dep:puffin", "dep:puffin_http"] +http = ["ehttp", "image/jpeg", "poll-promise", "egui_extras/image"] +image_viewer = ["image/jpeg", "egui_extras/all_loaders", "rfd"] +persistence = [ + "eframe/persistence", + "egui_extras/serde", + "egui/persistence", + "serde", +] +puffin = ["dep:puffin", "dep:puffin_http", "profiling/profile-with-puffin"] serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"] syntect = ["egui_demo_lib/syntect"] @@ -48,7 +56,12 @@ eframe = { workspace = true, default-features = false, features = [ egui = { workspace = true, features = ["callstack", "default", "log"] } egui_demo_lib = { workspace = true, features = ["default", "chrono"] } egui_extras = { workspace = true, features = ["default", "image"] } +image = { workspace = true, default-features = false, features = [ + # Ensure we can display the test images + "png", +] } log.workspace = true +profiling.workspace = true # Optional dependencies: @@ -61,7 +74,6 @@ wgpu = { workspace = true, features = ["webgpu", "webgl"], optional = true } # feature "http": ehttp = { version = "0.5", optional = true } -image = { workspace = true, optional = true, features = ["jpeg", "png"] } poll-promise = { version = "0.3", optional = true, default-features = false } # feature "persistence": @@ -74,10 +86,10 @@ env_logger = { version = "0.10", default-features = false, features = [ "auto-color", "humantime", ] } -rfd = { version = "0.13", optional = true } +rfd = { version = "0.15", optional = true } # web: [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "=0.2.93" +wasm-bindgen = "=0.2.95" wasm-bindgen-futures.workspace = true web-sys.workspace = true diff --git a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs index 11030a6727c..2e3a34f7df7 100644 --- a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs +++ b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs @@ -47,13 +47,13 @@ impl Custom3d { layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &shader, - entry_point: "vs_main", + entry_point: None, buffers: &[], compilation_options: wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, - entry_point: "fs_main", + entry_point: Some("fs_main"), targets: &[Some(wgpu_render_state.target_format.into())], compilation_options: wgpu::PipelineCompilationOptions::default(), }), diff --git a/crates/egui_demo_app/src/frame_history.rs b/crates/egui_demo_app/src/frame_history.rs index 535d6d9f95f..a6a3fbeeb3f 100644 --- a/crates/egui_demo_app/src/frame_history.rs +++ b/crates/egui_demo_app/src/frame_history.rs @@ -32,7 +32,7 @@ impl FrameHistory { 1.0 / self.frame_times.mean_time_interval().unwrap_or_default() } - pub fn ui(&mut self, ui: &mut egui::Ui) { + pub fn ui(&self, ui: &mut egui::Ui) { ui.label(format!( "Mean CPU usage: {:.2} ms / frame", 1e3 * self.mean_frame_time() diff --git a/crates/egui_demo_app/src/main.rs b/crates/egui_demo_app/src/main.rs index 9f42b422d04..6a2bb2da796 100644 --- a/crates/egui_demo_app/src/main.rs +++ b/crates/egui_demo_app/src/main.rs @@ -51,6 +51,7 @@ fn main() -> eframe::Result { ..Default::default() }; + eframe::run_native( "egui demo app", options, diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index bd313fb0c08..cbed988fa68 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -111,11 +111,19 @@ impl Anchor { Self::Rendering, ] } + + #[cfg(target_arch = "wasm32")] + fn from_str_case_insensitive(anchor: &str) -> Option { + let anchor = anchor.to_lowercase(); + Self::all().into_iter().find(|x| x.to_string() == anchor) + } } impl std::fmt::Display for Anchor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{self:?}") + let mut name = format!("{self:?}"); + name.make_ascii_lowercase(); + f.write_str(&name) } } @@ -263,11 +271,15 @@ impl eframe::App for WrapApp { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { #[cfg(target_arch = "wasm32")] - if let Some(anchor) = frame.info().web_info.location.hash.strip_prefix('#') { - let anchor = Anchor::all().into_iter().find(|x| x.to_string() == anchor); - if let Some(v) = anchor { - self.state.selected_anchor = v; - } + if let Some(anchor) = frame + .info() + .web_info + .location + .hash + .strip_prefix('#') + .and_then(Anchor::from_str_case_insensitive) + { + self.state.selected_anchor = anchor; } #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index 28b0592c228..b494e18a99b 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -56,7 +56,9 @@ serde = { workspace = true, optional = true } [dev-dependencies] criterion.workspace = true - +egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } +wgpu = { workspace = true, features = ["metal"] } +egui = { workspace = true, features = ["default_fonts"] } [[bench]] name = "benchmark" diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index d55cc6aff3f..2cfcdfaeeba 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -33,10 +33,12 @@ impl Default for Demos { Box::::default(), Box::::default(), Box::::default(), + Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), + Box::::default(), Box::::default(), Box::::default(), Box::::default(), @@ -377,3 +379,58 @@ fn file_menu_button(ui: &mut Ui) { } }); } + +#[cfg(test)] +mod tests { + use crate::demo::demo_app_windows::Demos; + use egui::Vec2; + use egui_kittest::kittest::Queryable; + use egui_kittest::{Harness, SnapshotOptions}; + + #[test] + fn demos_should_match_snapshot() { + let demos = Demos::default(); + + let mut errors = Vec::new(); + + for mut demo in demos.demos { + // Remove the emoji from the demo name + let name = demo + .name() + .split_once(' ') + .map_or(demo.name(), |(_, name)| name); + + // Widget Gallery needs to be customized (to set a specific date) and has its own test + if name == "Widget Gallery" { + continue; + } + + let mut harness = Harness::new(|ctx| { + demo.show(ctx, &mut true); + }); + + let window = harness.node().children().next().unwrap(); + // TODO(lucasmerlin): Windows should probably have a label? + //let window = harness.get_by_label(name); + + let size = window.raw_bounds().expect("window bounds").size(); + harness.set_size(Vec2::new(size.width as f32, size.height as f32)); + + // Run the app for some more frames... + harness.run(); + + let mut options = SnapshotOptions::default(); + // The Bézier Curve demo needs a threshold of 2.1 to pass on linux + if name == "Bézier Curve" { + options.threshold = 2.1; + } + + let result = harness.try_wgpu_snapshot_options(&format!("demos/{name}"), &options); + if let Err(err) = result { + errors.push(err.to_string()); + } + } + + assert!(errors.is_empty(), "Errors: {errors:#?}"); + } +} diff --git a/crates/egui_demo_lib/src/demo/mod.rs b/crates/egui_demo_lib/src/demo/mod.rs index 828bdd896a3..c00725fbd59 100644 --- a/crates/egui_demo_lib/src/demo/mod.rs +++ b/crates/egui_demo_lib/src/demo/mod.rs @@ -17,12 +17,14 @@ pub mod frame_demo; pub mod highlighting; pub mod interactive_container; pub mod misc_demo_window; +pub mod modals; pub mod multi_touch; pub mod paint_bezier; pub mod painting; pub mod pan_zoom; pub mod panels; pub mod password; +pub mod screenshot; pub mod scrolling; pub mod sliders; pub mod strip_demo; diff --git a/crates/egui_demo_lib/src/demo/modals.rs b/crates/egui_demo_lib/src/demo/modals.rs new file mode 100644 index 00000000000..989101b4d75 --- /dev/null +++ b/crates/egui_demo_lib/src/demo/modals.rs @@ -0,0 +1,287 @@ +use egui::{ComboBox, Context, Id, Modal, ProgressBar, Ui, Widget, Window}; + +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +pub struct Modals { + user_modal_open: bool, + save_modal_open: bool, + save_progress: Option, + + role: &'static str, + name: String, +} + +impl Default for Modals { + fn default() -> Self { + Self { + user_modal_open: false, + save_modal_open: false, + save_progress: None, + role: Self::ROLES[0], + name: "John Doe".to_owned(), + } + } +} + +impl Modals { + const ROLES: [&'static str; 2] = ["user", "admin"]; +} + +impl crate::Demo for Modals { + fn name(&self) -> &'static str { + "🗖 Modals" + } + + fn show(&mut self, ctx: &Context, open: &mut bool) { + use crate::View as _; + Window::new(self.name()) + .open(open) + .vscroll(false) + .resizable(false) + .show(ctx, |ui| self.ui(ui)); + } +} + +impl crate::View for Modals { + fn ui(&mut self, ui: &mut Ui) { + let Self { + user_modal_open, + save_modal_open, + save_progress, + role, + name, + } = self; + + ui.horizontal(|ui| { + if ui.button("Open User Modal").clicked() { + *user_modal_open = true; + } + + if ui.button("Open Save Modal").clicked() { + *save_modal_open = true; + } + }); + + ui.label("Click one of the buttons to open a modal."); + ui.label("Modals have a backdrop and prevent interaction with the rest of the UI."); + ui.label( + "You can show modals on top of each other and close the topmost modal with \ + escape or by clicking outside the modal.", + ); + + if *user_modal_open { + let modal = Modal::new(Id::new("Modal A")).show(ui.ctx(), |ui| { + ui.set_width(250.0); + + ui.heading("Edit User"); + + ui.label("Name:"); + ui.text_edit_singleline(name); + + ComboBox::new("role", "Role") + .selected_text(*role) + .show_ui(ui, |ui| { + for r in Self::ROLES { + ui.selectable_value(role, r, r); + } + }); + + ui.separator(); + + egui::Sides::new().show( + ui, + |_ui| {}, + |ui| { + if ui.button("Save").clicked() { + *save_modal_open = true; + } + if ui.button("Cancel").clicked() { + *user_modal_open = false; + } + }, + ); + }); + + if modal.should_close() { + *user_modal_open = false; + } + } + + if *save_modal_open { + let modal = Modal::new(Id::new("Modal B")).show(ui.ctx(), |ui| { + ui.set_width(200.0); + ui.heading("Save? Are you sure?"); + + ui.add_space(32.0); + + egui::Sides::new().show( + ui, + |_ui| {}, + |ui| { + if ui.button("Yes Please").clicked() { + *save_progress = Some(0.0); + } + + if ui.button("No Thanks").clicked() { + *save_modal_open = false; + } + }, + ); + }); + + if modal.should_close() { + *save_modal_open = false; + } + } + + if let Some(progress) = *save_progress { + Modal::new(Id::new("Modal C")).show(ui.ctx(), |ui| { + ui.set_width(70.0); + ui.heading("Saving…"); + + ProgressBar::new(progress).ui(ui); + + if progress >= 1.0 { + *save_progress = None; + *save_modal_open = false; + *user_modal_open = false; + } else { + *save_progress = Some(progress + 0.003); + ui.ctx().request_repaint(); + } + }); + } + + ui.vertical_centered(|ui| { + ui.add(crate::egui_github_link_file!()); + }); + } +} + +#[cfg(test)] +mod tests { + use crate::demo::modals::Modals; + use crate::Demo; + use egui::accesskit::Role; + use egui::Key; + use egui_kittest::kittest::Queryable; + use egui_kittest::Harness; + + #[test] + fn clicking_escape_when_popup_open_should_not_close_modal() { + let initial_state = Modals { + user_modal_open: true, + ..Modals::default() + }; + + let mut harness = Harness::new_state( + |ctx, modals| { + modals.show(ctx, &mut true); + }, + initial_state, + ); + + harness.get_by_role(Role::ComboBox).click(); + + harness.run(); + assert!(harness.ctx.memory(|mem| mem.any_popup_open())); + assert!(harness.state().user_modal_open); + + harness.press_key(Key::Escape); + harness.run(); + assert!(!harness.ctx.memory(|mem| mem.any_popup_open())); + assert!(harness.state().user_modal_open); + } + + #[test] + fn escape_should_close_top_modal() { + let initial_state = Modals { + user_modal_open: true, + save_modal_open: true, + ..Modals::default() + }; + + let mut harness = Harness::new_state( + |ctx, modals| { + modals.show(ctx, &mut true); + }, + initial_state, + ); + + assert!(harness.state().user_modal_open); + assert!(harness.state().save_modal_open); + + harness.press_key(Key::Escape); + harness.run(); + + assert!(harness.state().user_modal_open); + assert!(!harness.state().save_modal_open); + } + + #[test] + fn should_match_snapshot() { + let initial_state = Modals { + user_modal_open: true, + ..Modals::default() + }; + + let mut harness = Harness::new_state( + |ctx, modals| { + modals.show(ctx, &mut true); + }, + initial_state, + ); + + let mut results = Vec::new(); + + harness.run(); + results.push(harness.try_wgpu_snapshot("modals_1")); + + harness.get_by_label("Save").click(); + // TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests + harness.run(); + harness.run(); + harness.run(); + results.push(harness.try_wgpu_snapshot("modals_2")); + + harness.get_by_label("Yes Please").click(); + // TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests + harness.run(); + harness.run(); + harness.run(); + results.push(harness.try_wgpu_snapshot("modals_3")); + + for result in results { + result.unwrap(); + } + } + + // This tests whether the backdrop actually prevents interaction with lower layers. + #[test] + fn backdrop_should_prevent_focusing_lower_area() { + let initial_state = Modals { + save_modal_open: true, + save_progress: Some(0.0), + ..Modals::default() + }; + + let mut harness = Harness::new_state( + |ctx, modals| { + modals.show(ctx, &mut true); + }, + initial_state, + ); + + // TODO(lucasmerlin): Remove these extra runs once run checks for repaint requests + harness.run(); + harness.run(); + harness.run(); + + harness.get_by_label("Yes Please").simulate_click(); + + harness.run(); + + // This snapshots should show the progress bar modal on top of the save modal. + harness.wgpu_snapshot("modals_backdrop_should_prevent_focusing_lower_area"); + } +} diff --git a/crates/egui_demo_lib/src/demo/pan_zoom.rs b/crates/egui_demo_lib/src/demo/pan_zoom.rs index 6367946faef..f8411a740c0 100644 --- a/crates/egui_demo_lib/src/demo/pan_zoom.rs +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -73,20 +73,29 @@ impl crate::View for PanZoom { for (i, (pos, callback)) in [ ( egui::Pos2::new(0.0, 0.0), - Box::new(|ui: &mut egui::Ui, _: &mut Self| ui.button("top left!")) - as Box egui::Response>, + Box::new(|ui: &mut egui::Ui, _: &mut Self| { + ui.button("top left").on_hover_text("Normal tooltip") + }) as Box egui::Response>, ), ( egui::Pos2::new(0.0, 120.0), - Box::new(|ui: &mut egui::Ui, _| ui.button("bottom left?")), + Box::new(|ui: &mut egui::Ui, _| { + ui.button("bottom left").on_hover_text("Normal tooltip") + }), ), ( egui::Pos2::new(120.0, 120.0), - Box::new(|ui: &mut egui::Ui, _| ui.button("right bottom :D")), + Box::new(|ui: &mut egui::Ui, _| { + ui.button("right bottom") + .on_hover_text_at_pointer("Tooltip at pointer") + }), ), ( egui::Pos2::new(120.0, 0.0), - Box::new(|ui: &mut egui::Ui, _| ui.button("right top ):")), + Box::new(|ui: &mut egui::Ui, _| { + ui.button("right top") + .on_hover_text_at_pointer("Tooltip at pointer") + }), ), ( egui::Pos2::new(60.0, 60.0), diff --git a/crates/egui_demo_lib/src/demo/screenshot.rs b/crates/egui_demo_lib/src/demo/screenshot.rs new file mode 100644 index 00000000000..eb62611c863 --- /dev/null +++ b/crates/egui_demo_lib/src/demo/screenshot.rs @@ -0,0 +1,84 @@ +use egui::{Image, UserData, ViewportCommand, Widget}; +use std::sync::Arc; + +/// Showcase [`ViewportCommand::Screenshot`]. +#[derive(PartialEq, Eq, Default)] +pub struct Screenshot { + image: Option<(Arc, egui::TextureHandle)>, + continuous: bool, +} + +impl crate::Demo for Screenshot { + fn name(&self) -> &'static str { + "📷 Screenshot" + } + + fn show(&mut self, ctx: &egui::Context, open: &mut bool) { + egui::Window::new(self.name()) + .open(open) + .resizable(false) + .default_width(250.0) + .show(ctx, |ui| { + use crate::View as _; + self.ui(ui); + }); + } +} + +impl crate::View for Screenshot { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.set_width(300.0); + ui.vertical_centered(|ui| { + ui.add(crate::egui_github_link_file!()); + }); + + ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("This demo showcases how to take screenshots via "); + ui.code("ViewportCommand::Screenshot"); + ui.label("."); + }); + + ui.horizontal_top(|ui| { + let capture = ui.button("📷 Take Screenshot").clicked(); + ui.checkbox(&mut self.continuous, "Capture continuously"); + if capture || self.continuous { + ui.ctx() + .send_viewport_cmd(ViewportCommand::Screenshot(UserData::default())); + } + }); + + let image = ui.ctx().input(|i| { + i.events + .iter() + .filter_map(|e| { + if let egui::Event::Screenshot { image, .. } = e { + Some(image.clone()) + } else { + None + } + }) + .last() + }); + + if let Some(image) = image { + self.image = Some(( + image.clone(), + ui.ctx() + .load_texture("screenshot_demo", image, Default::default()), + )); + } + + if let Some((_, texture)) = &self.image { + Image::new(texture).shrink_to_fit().ui(ui); + } else { + ui.group(|ui| { + ui.set_width(ui.available_width()); + ui.set_height(100.0); + ui.centered_and_justified(|ui| { + ui.label("No screenshot taken yet."); + }); + }); + } + } +} diff --git a/crates/egui_demo_lib/src/demo/sliders.rs b/crates/egui_demo_lib/src/demo/sliders.rs index d15d9aa3100..ef8bdb0cd11 100644 --- a/crates/egui_demo_lib/src/demo/sliders.rs +++ b/crates/egui_demo_lib/src/demo/sliders.rs @@ -1,5 +1,4 @@ use egui::{style::HandleShape, Slider, SliderClamping, SliderOrientation, Ui}; -use std::f64::INFINITY; /// Showcase sliders #[derive(PartialEq)] @@ -77,7 +76,7 @@ impl crate::View for Sliders { let (type_min, type_max) = if *integer { ((i32::MIN as f64), (i32::MAX as f64)) } else if *logarithmic { - (-INFINITY, INFINITY) + (-f64::INFINITY, f64::INFINITY) } else { (-1e5, 1e5) // linear sliders make little sense with huge numbers }; diff --git a/crates/egui_demo_lib/src/demo/text_edit.rs b/crates/egui_demo_lib/src/demo/text_edit.rs index 9743306c746..5ce5c93be04 100644 --- a/crates/egui_demo_lib/src/demo/text_edit.rs +++ b/crates/egui_demo_lib/src/demo/text_edit.rs @@ -112,3 +112,41 @@ impl crate::View for TextEditDemo { }); } } + +#[cfg(test)] +mod tests { + use egui::{accesskit, CentralPanel}; + use egui_kittest::kittest::{Key, Queryable}; + use egui_kittest::Harness; + + #[test] + pub fn should_type() { + let text = "Hello, world!".to_owned(); + let mut harness = Harness::new_state( + move |ctx, text| { + CentralPanel::default().show(ctx, |ui| { + ui.text_edit_singleline(text); + }); + }, + text, + ); + + harness.run(); + + let text_edit = harness.get_by_role(accesskit::Role::TextInput); + assert_eq!(text_edit.value().as_deref(), Some("Hello, world!")); + + text_edit.key_combination(&[Key::Command, Key::A]); + text_edit.type_text("Hi "); + + harness.run(); + harness + .get_by_role(accesskit::Role::TextInput) + .type_text("there!"); + + harness.run(); + let text_edit = harness.get_by_role(accesskit::Role::TextInput); + assert_eq!(text_edit.value().as_deref(), Some("Hi there!")); + assert_eq!(harness.state(), "Hi there!"); + } +} diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index 7be6e20c040..b69d0f1c8d2 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -285,3 +285,29 @@ fn doc_link_label_with_crate<'a>( }) } } + +#[cfg(feature = "chrono")] +#[cfg(test)] +mod tests { + use super::*; + use crate::View; + use egui::Vec2; + use egui_kittest::Harness; + + #[test] + pub fn should_match_screenshot() { + let mut demo = WidgetGallery { + // If we don't set a fixed date, the snapshot test will fail. + date: Some(chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()), + ..Default::default() + }; + let mut harness = Harness::builder() + .with_pixels_per_point(2.0) + .with_size(Vec2::new(380.0, 550.0)) + .build_ui(|ui| demo.ui(ui)); + + harness.fit_contents(); + + harness.wgpu_snapshot("widget_gallery"); + } +} diff --git a/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs b/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs index 75d8135dff5..ed3ebe7f90f 100644 --- a/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs +++ b/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs @@ -13,7 +13,7 @@ pub enum Item<'a> { // TODO(emilk): add Style here so empty heading still uses up the right amount of space. Newline, - /// + /// Text Text(Style, &'a str), /// title, url diff --git a/crates/egui_demo_lib/src/rendering_test.rs b/crates/egui_demo_lib/src/rendering_test.rs index 3ae4f111808..f9ead5c584c 100644 --- a/crates/egui_demo_lib/src/rendering_test.rs +++ b/crates/egui_demo_lib/src/rendering_test.rs @@ -435,10 +435,7 @@ fn pixel_test_strokes(ui: &mut Ui) { let thickness_pixels = thickness_pixels as f32; let thickness_points = thickness_pixels / pixels_per_point; let num_squares = (pixels_per_point * 10.0).round().max(10.0) as u32; - let size_pixels = vec2( - ui.available_width(), - num_squares as f32 + thickness_pixels * 2.0, - ); + let size_pixels = vec2(ui.min_size().x, num_squares as f32 + thickness_pixels * 2.0); let size_points = size_pixels / pixels_per_point + Vec2::splat(2.0); let (response, painter) = ui.allocate_painter(size_points, Sense::hover()); @@ -680,3 +677,34 @@ fn mul_color_gamma(left: Color32, right: Color32) -> Color32 { (left.a() as f32 * right.a() as f32 / 255.0).round() as u8, ) } + +#[cfg(test)] +mod tests { + use crate::ColorTest; + use egui::vec2; + + #[test] + pub fn rendering_test() { + let mut errors = vec![]; + for dpi in [1.0, 1.25, 1.5, 1.75, 1.6666667, 2.0] { + let mut color_test = ColorTest::default(); + let mut harness = egui_kittest::Harness::builder() + .with_size(vec2(2000.0, 2000.0)) + .with_pixels_per_point(dpi) + .build_ui(|ui| { + color_test.ui(ui); + }); + + //harness.set_size(harness.ctx.used_size()); + + harness.fit_contents(); + + let result = harness.try_wgpu_snapshot(&format!("rendering_test/dpi_{dpi:.2}")); + if let Err(err) = result { + errors.push(err); + } + } + + assert!(errors.is_empty(), "Errors: {errors:#?}"); + } +} diff --git "a/crates/egui_demo_lib/tests/snapshots/demos/B\303\251zier Curve.png" "b/crates/egui_demo_lib/tests/snapshots/demos/B\303\251zier Curve.png" new file mode 100644 index 00000000000..ad7d9becfaa --- /dev/null +++ "b/crates/egui_demo_lib/tests/snapshots/demos/B\303\251zier Curve.png" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a725aa81433f301fda4ff8a28be869366332964995d1ae4ed996591596eb7e2 +size 31461 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png new file mode 100644 index 00000000000..252c7c8f205 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36028d85f49ee77562250214237def2b676ecc9ed413d2fd8afc473d61289ca1 +size 32761 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png new file mode 100644 index 00000000000..162fc51a1df --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01aaa4ef1a167a94fa1e5163550aabe4fa5e9f3a012b26170fe3088a6ca32d94 +size 81064 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png b/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png new file mode 100644 index 00000000000..c29267377b7 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Context Menus.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:684648bea4ef5ce138fc25dbe7576e3937a797e87f2244cb3656ff8b4c2777f5 +size 11574 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png new file mode 100644 index 00000000000..bdc4739c77f --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad38bff7cc5661be43e730e1b34c444b571b24b9f50791209496a1687610dd3d +size 20543 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png new file mode 100644 index 00000000000..035d4c13f7b --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff78748f2571c49638d8fe8fdc859aaa5181758aad65498b7217551350fb9138 +size 20672 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png b/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png new file mode 100644 index 00000000000..2d48a7e085f --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dee66004cc47f5e27aaac34d137ff005eedf70cbfa3fbe43153dfd5c09d5e18 +size 10610 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png new file mode 100644 index 00000000000..69201f86154 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d1086b789f1fe0a8085c86f5b6a5ae7ecb53020f385b84775d6812ebc9d74a3 +size 132349 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png new file mode 100644 index 00000000000..ff4d08bafe8 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08be378c01e376aab6e99ba3158519bbd7b301e815dc3447b57c9abab558977f +size 24237 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png new file mode 100644 index 00000000000..a3cab2a3097 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53097b2c26ebcba8b8ad657ed8e52ca40261155e96dbbfca1e8eb01fce25d290 +size 17586 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png new file mode 100644 index 00000000000..2bfbf20f91e --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9c8395e6b4287b92d85a52ca2d47750f67abeb0ad88c6b42264bfe2e62fd09d +size 22283 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png b/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png new file mode 100644 index 00000000000..80cb5b5a177 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38d21b6f8c364f86ad759e88ea1068649c23c58ded5d2953ba8ff1c83b46112f +size 63884 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png new file mode 100644 index 00000000000..274b4b57686 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:026723cb5d89b32386a849328c34420ee9e3ae1f97cbf6fa3c4543141123549e +size 32890 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png new file mode 100644 index 00000000000..23bad456c8c --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83162f8c496a55230375dbc4cc636cfacf63049c913904bea9d06bdb56e63da6 +size 36282 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png new file mode 100644 index 00000000000..89a17e67406 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2537c681d1ffceb5cf4bf19d11295891525c96aea0b1422ab28f133021185be0 +size 17451 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Pan Zoom.png b/crates/egui_demo_lib/tests/snapshots/demos/Pan Zoom.png new file mode 100644 index 00000000000..384840b7101 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Pan Zoom.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ccfda16ef7cdf94f7fbbd2c0f8df6f6de7904969e2a66337920c32608a6f9f05 +size 25357 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png new file mode 100644 index 00000000000..585c126360c --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5068df8549ffc91028addfec6f851f12a4de80e208b50b39e4d44b6aa2c7240e +size 261946 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png new file mode 100644 index 00000000000..56978de2001 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:579a7a66f86ade628e9f469b0014e9010aa56312ad5bd1e8de2faaae7e0d1af6 +size 23770 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png new file mode 100644 index 00000000000..440a51f3871 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be2ac005fd5aafa293e21b162c22a09078e46d2d45b6208ce0f7841eeb05314a +size 183934 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png new file mode 100644 index 00000000000..e3a213176e9 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e3436906f7ac459b7f4330a286937722e78ad885ae1e90f75be566e970a8ca7 +size 116899 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Strip.png b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png new file mode 100644 index 00000000000..69ec9e88230 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df7dabf726620ab5205ce153f692d1ba02365848ead7b79c95b873d5121d52a6 +size 25850 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Table.png b/crates/egui_demo_lib/tests/snapshots/demos/Table.png new file mode 100644 index 00000000000..9f3618ba039 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Table.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae6c2e3aad43cfad3322340ff7045ec50ba01d58feb7b8acc5ba062a8a5c9ab8 +size 70230 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png new file mode 100644 index 00000000000..ff972ae484c --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec0c2efff75cb8d621f5a4ea59f9fa8d3076521ca34f4499e07fb9dc8681d7ba +size 65916 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png new file mode 100644 index 00000000000..7af3611b924 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c04aee0a3a77a3691bb601a93871117500be917e0896138fda43251454ec04c2 +size 20988 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png new file mode 100644 index 00000000000..4e7d8a9230b --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:814d863deaa4fa029044da1783db87744f0d82e874edd6cbab16e712ed8715aa +size 59881 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png new file mode 100644 index 00000000000..a635cdfabd0 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e682f5cb9ecb1bdf89281c2ba1612078e70e97f28c76facc64d717e4015ced6a +size 12977 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png new file mode 100644 index 00000000000..f28774030aa --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15acfb041cc53ef9bd966d6edd53a6b692cdb645ae5cf34bc20e70d403371c30 +size 34809 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_1.png b/crates/egui_demo_lib/tests/snapshots/modals_1.png new file mode 100644 index 00000000000..461bef728bc --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/modals_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8348ff582e11fdc9baf008b5434f81f8d77b834479cb3765c87d1f4fd695e30f +size 48212 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_2.png b/crates/egui_demo_lib/tests/snapshots/modals_2.png new file mode 100644 index 00000000000..0f1273f41df --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/modals_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23482b77cbd817c66421a630e409ac3d8c5d24de00aa91e476e8d42b607c24b1 +size 48104 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_3.png b/crates/egui_demo_lib/tests/snapshots/modals_3.png new file mode 100644 index 00000000000..ba8cca6228b --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/modals_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d94aa33d72c32f6f1aafab92c9753dc07bc5224c701003ac7fe8a01ae8c701a +size 44011 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png new file mode 100644 index 00000000000..14d6fb9cbfc --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1a5d265470c36e64340ccceea4ade464b3c4a1177d60630b02ae8287934748f +size 44026 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png new file mode 100644 index 00000000000..ae442978324 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:023eaa363b42ec24ae845dc2ca9ff271a0bd47217e625785d3716044ecfa7a64 +size 278444 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png new file mode 100644 index 00000000000..428160c867d --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d81f618e54176b1c43b710121f249e13ce29827fbea3451827ab62229006677e +size 378603 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png new file mode 100644 index 00000000000..003a08bd6e3 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d8eca6d5555ef779233175615b877fb91318b4a09a37e5cfbe71973d56f4caf +size 465907 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png new file mode 100644 index 00000000000..629edf05c1e --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4768804f57dfc54c5f6b84a2686038b8d630a28c7e928ae044d5b2ce8377e2cd +size 538775 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png new file mode 100644 index 00000000000..98ffb99034b --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fcee0e0302f33d681348d62bee3b548beb494c6dd1fa3454586986e0b699e162 +size 572403 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png new file mode 100644 index 00000000000..04d581799bd --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:254a8dff0b1d4b74971fd3bd4044c4ec0ce49412a95e98419a14dc55b32a4fc9 +size 663272 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png new file mode 100644 index 00000000000..914b3da9ead --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c069ef4f86beeeafd8686f30fc914bedd7e7e7ec38fd96e9a46ac6b31308c43f +size 160883 diff --git a/crates/egui_extras/CHANGELOG.md b/crates/egui_extras/CHANGELOG.md index 27f23ef4ea9..7617b1863b0 100644 --- a/crates/egui_extras/CHANGELOG.md +++ b/crates/egui_extras/CHANGELOG.md @@ -5,6 +5,16 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.30.0 - 2024-12-16 +* Use `Table::id_salt` on `ScrollArea` [#5282](https://github.com/emilk/egui/pull/5282) by [@jwhear](https://github.com/jwhear) +* Use proper `image` crate URI and MIME support detection [#5324](https://github.com/emilk/egui/pull/5324) by [@xangelix](https://github.com/xangelix) +* Support loading images with weird urls and improve error message [#5431](https://github.com/emilk/egui/pull/5431) by [@lucasmerlin](https://github.com/lucasmerlin) + + +## 0.29.1 - 2024-10-01 - Fix table interaction +* Bug fix: click anywhere on a `Table` row to select it [#5193](https://github.com/emilk/egui/pull/5193) by [@emilk](https://github.com/emilk) + + ## 0.29.0 - 2024-09-26 ### ⭐ Added * Add `TableRow::set_hovered` [#4820](https://github.com/emilk/egui/pull/4820) by [@addiswebb](https://github.com/addiswebb) diff --git a/crates/egui_extras/Cargo.toml b/crates/egui_extras/Cargo.toml index b13a518e8d0..41fbcf0a462 100644 --- a/crates/egui_extras/Cargo.toml +++ b/crates/egui_extras/Cargo.toml @@ -53,11 +53,6 @@ http = ["dep:ehttp"] ## ``` image = ["dep:image"] -## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -## -## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. -puffin = ["dep:puffin", "egui/puffin"] - ## Derive serde Serialize/Deserialize on stateful structs serde = ["egui/serde", "dep:serde"] @@ -74,6 +69,7 @@ egui = { workspace = true, default-features = false } ahash.workspace = true enum-map = { version = "2", features = ["serde"] } log.workspace = true +profiling.workspace = true #! ### Optional dependencies @@ -96,7 +92,6 @@ image = { workspace = true, optional = true } # file feature mime_guess2 = { version = "2", optional = true, default-features = false } -puffin = { workspace = true, optional = true } syntect = { version = "5", optional = true, default-features = false, features = [ "default-fancy", diff --git a/crates/egui_extras/src/image.rs b/crates/egui_extras/src/image.rs index f6301aec823..46d9df170a0 100644 --- a/crates/egui_extras/src/image.rs +++ b/crates/egui_extras/src/image.rs @@ -28,7 +28,7 @@ pub struct RetainedImage { } impl RetainedImage { - pub fn from_color_image(debug_name: impl Into, image: ColorImage) -> Self { + pub fn from_color_image(debug_name: impl Into, image: egui::ColorImage) -> Self { Self { debug_name: debug_name.into(), size: image.size, @@ -54,7 +54,7 @@ impl RetainedImage { ) -> Result { Ok(Self::from_color_image( debug_name, - load_image_bytes(image_bytes)?, + load_image_bytes(image_bytes).map_err(|err| err.to_string())?, )) } @@ -154,7 +154,7 @@ impl RetainedImage { self.texture .lock() .get_or_insert_with(|| { - let image: &mut ColorImage = &mut self.image.lock(); + let image: &mut egui::ColorImage = &mut self.image.lock(); let image = std::mem::take(image); ctx.load_texture(&self.debug_name, image, self.options) }) @@ -190,8 +190,6 @@ impl RetainedImage { // ---------------------------------------------------------------------------- -use egui::ColorImage; - /// Load a (non-svg) image. /// /// Requires the "image" feature. You must also opt-in to the image formats you need @@ -200,9 +198,19 @@ use egui::ColorImage; /// # Errors /// On invalid image or unsupported image format. #[cfg(feature = "image")] -pub fn load_image_bytes(image_bytes: &[u8]) -> Result { - crate::profile_function!(); - let image = image::load_from_memory(image_bytes).map_err(|err| err.to_string())?; +pub fn load_image_bytes(image_bytes: &[u8]) -> Result { + profiling::function_scope!(); + let image = image::load_from_memory(image_bytes).map_err(|err| match err { + image::ImageError::Unsupported(err) => match err.kind() { + image::error::UnsupportedErrorKind::Format(format) => { + egui::load::LoadError::FormatNotSupported { + detected_format: Some(format.to_string()), + } + } + _ => egui::load::LoadError::Loading(err.to_string()), + }, + err => egui::load::LoadError::Loading(err.to_string()), + })?; let size = [image.width() as _, image.height() as _]; let image_buffer = image.to_rgba8(); let pixels = image_buffer.as_flat_samples(); @@ -237,7 +245,8 @@ pub fn load_svg_bytes_with_size( use resvg::tiny_skia::{IntSize, Pixmap}; use resvg::usvg::{Options, Tree, TreeParsing}; - crate::profile_function!(); + profiling::function_scope!(); + let opt = Options::default(); let mut rtree = Tree::from_data(svg_bytes, &opt).map_err(|err| err.to_string())?; diff --git a/crates/egui_extras/src/layout.rs b/crates/egui_extras/src/layout.rs index ec4938f00d3..239e7b1d29b 100644 --- a/crates/egui_extras/src/layout.rs +++ b/crates/egui_extras/src/layout.rs @@ -149,10 +149,13 @@ impl<'l> StripLayout<'l> { ); } - let child_ui = self.cell(flags, max_rect, child_ui_id_salt, add_cell_contents); + let mut child_ui = self.cell(flags, max_rect, child_ui_id_salt, add_cell_contents); let used_rect = child_ui.min_rect(); + // Make sure we catch clicks etc on the _whole_ cell: + child_ui.set_min_size(max_rect.size()); + let allocation_rect = if self.ui.is_sizing_pass() { used_rect } else if flags.clip { @@ -165,7 +168,7 @@ impl<'l> StripLayout<'l> { self.ui.advance_cursor_after_rect(allocation_rect); - let response = child_ui.interact(max_rect, child_ui.id(), self.sense); + let response = child_ui.response(); (used_rect, response) } @@ -204,7 +207,8 @@ impl<'l> StripLayout<'l> { .id_salt(child_ui_id_salt) .ui_stack_info(egui::UiStackInfo::new(egui::UiKind::TableCell)) .max_rect(max_rect) - .layout(self.cell_layout); + .layout(self.cell_layout) + .sense(self.sense); if flags.sizing_pass { ui_builder = ui_builder.sizing_pass(); } diff --git a/crates/egui_extras/src/lib.rs b/crates/egui_extras/src/lib.rs index ab2dde735b9..6f339010224 100644 --- a/crates/egui_extras/src/lib.rs +++ b/crates/egui_extras/src/lib.rs @@ -37,36 +37,6 @@ pub use loaders::install_image_loaders; // --------------------------------------------------------------------------- -mod profiling_scopes { - #![allow(unused_macros)] - #![allow(unused_imports)] - - /// Profiling macro for feature "puffin" - macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} - -#[allow(unused_imports)] -pub(crate) use profiling_scopes::profile_function; - -// --------------------------------------------------------------------------- - /// Panic in debug builds, log otherwise. macro_rules! log_or_panic { ($fmt: literal) => {$crate::log_or_panic!($fmt,)}; diff --git a/crates/egui_extras/src/loaders/image_loader.rs b/crates/egui_extras/src/loaders/image_loader.rs index 14086df935b..4c1a846e26b 100644 --- a/crates/egui_extras/src/loaders/image_loader.rs +++ b/crates/egui_extras/src/loaders/image_loader.rs @@ -4,9 +4,10 @@ use egui::{ mutex::Mutex, ColorImage, }; +use image::ImageFormat; use std::{mem::size_of, path::Path, sync::Arc}; -type Entry = Result, String>; +type Entry = Result, LoadError>; #[derive(Default)] pub struct ImageCrateLoader { @@ -18,18 +19,29 @@ impl ImageCrateLoader { } fn is_supported_uri(uri: &str) -> bool { - // TODO(emilk): use https://github.com/image-rs/image/pull/2038 when new `image` crate is released. let Some(ext) = Path::new(uri).extension().and_then(|ext| ext.to_str()) else { // `true` because if there's no extension, assume that we support it return true; }; - ext != "svg" + // Uses only the enabled image crate features + ImageFormat::all() + .filter(ImageFormat::reading_enabled) + .flat_map(ImageFormat::extensions_str) + .any(|format_ext| ext == *format_ext) } -fn is_unsupported_mime(mime: &str) -> bool { - // TODO(emilk): use https://github.com/image-rs/image/pull/2038 when new `image` crate is released. - mime.contains("svg") +fn is_supported_mime(mime: &str) -> bool { + // This is the default mime type for binary files, so this might actually be a valid image, + // let's relay on image's format guessing + if mime == "application/octet-stream" { + return true; + } + // Uses only the enabled image crate features + ImageFormat::all() + .filter(ImageFormat::reading_enabled) + .map(|fmt| fmt.to_mime_type()) + .any(|format_mime| mime == format_mime) } impl ImageLoader for ImageCrateLoader { @@ -39,12 +51,12 @@ impl ImageLoader for ImageCrateLoader { fn load(&self, ctx: &egui::Context, uri: &str, _: SizeHint) -> ImageLoadResult { // three stages of guessing if we support loading the image: - // 1. URI extension + // 1. URI extension (only done for files) // 2. Mime from `BytesPoll::Ready` - // 3. image::guess_format + // 3. image::guess_format (used internally by image::load_from_memory) // (1) - if !is_supported_uri(uri) { + if uri.starts_with("file://") && !is_supported_uri(uri) { return Err(LoadError::NotSupported); } @@ -52,26 +64,32 @@ impl ImageLoader for ImageCrateLoader { if let Some(entry) = cache.get(uri).cloned() { match entry { Ok(image) => Ok(ImagePoll::Ready { image }), - Err(err) => Err(LoadError::Loading(err)), + Err(err) => Err(err), } } else { match ctx.try_load_bytes(uri) { Ok(BytesPoll::Ready { bytes, mime, .. }) => { - // (2 and 3) - if mime.as_deref().is_some_and(is_unsupported_mime) - || image::guess_format(&bytes).is_err() - { - return Err(LoadError::NotSupported); + // (2) + if let Some(mime) = mime { + if !is_supported_mime(&mime) { + return Err(LoadError::FormatNotSupported { + detected_format: Some(mime), + }); + } } + if bytes.starts_with(b"version https://git-lfs") { + return Err(LoadError::FormatNotSupported { + detected_format: Some("git-lfs".to_owned()), + }); + } + + // (3) log::trace!("started loading {uri:?}"); let result = crate::image::load_image_bytes(&bytes).map(Arc::new); log::trace!("finished loading {uri:?}"); cache.insert(uri.into(), result.clone()); - match result { - Ok(image) => Ok(ImagePoll::Ready { image }), - Err(err) => Err(LoadError::Loading(err)), - } + result.map(|image| ImagePoll::Ready { image }) } Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }), Err(err) => Err(err), @@ -93,7 +111,7 @@ impl ImageLoader for ImageCrateLoader { .values() .map(|result| match result { Ok(image) => image.pixels.len() * size_of::(), - Err(err) => err.len(), + Err(err) => err.byte_size(), }) .sum() } @@ -108,7 +126,6 @@ mod tests { assert!(is_supported_uri("https://test.png")); assert!(is_supported_uri("test.jpeg")); assert!(is_supported_uri("http://test.gif")); - assert!(is_supported_uri("test.webp")); assert!(is_supported_uri("file://test")); assert!(!is_supported_uri("test.svg")); } diff --git a/crates/egui_extras/src/syntax_highlighting.rs b/crates/egui_extras/src/syntax_highlighting.rs index 293fbde007f..027ba5ee9da 100644 --- a/crates/egui_extras/src/syntax_highlighting.rs +++ b/crates/egui_extras/src/syntax_highlighting.rs @@ -33,9 +33,7 @@ pub fn highlight( // performing it at a separate thread (ctx, ctx.style()) can be used and when ui is available // (ui.ctx(), ui.style()) can be used - impl egui::util::cache::ComputerMut<(&egui::FontId, &CodeTheme, &str, &str), LayoutJob> - for Highlighter - { + impl egui::cache::ComputerMut<(&egui::FontId, &CodeTheme, &str, &str), LayoutJob> for Highlighter { fn compute( &mut self, (font_id, theme, code, lang): (&egui::FontId, &CodeTheme, &str, &str), @@ -44,7 +42,7 @@ pub fn highlight( } } - type HighlightCache = egui::util::cache::FrameCache; + type HighlightCache = egui::cache::FrameCache; let font_id = style .override_font_id @@ -405,7 +403,7 @@ struct Highlighter { #[cfg(feature = "syntect")] impl Default for Highlighter { fn default() -> Self { - crate::profile_function!(); + profiling::function_scope!(); Self { ps: syntect::parsing::SyntaxSet::load_defaults_newlines(), ts: syntect::highlighting::ThemeSet::load_defaults(), @@ -439,8 +437,7 @@ impl Highlighter { #[cfg(feature = "syntect")] fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option { - crate::profile_function!(); - + profiling::function_scope!(); use syntect::easy::HighlightLines; use syntect::highlighting::FontStyle; use syntect::util::LinesWithEndings; @@ -514,7 +511,7 @@ impl Highlighter { mut text: &str, language: &str, ) -> Option { - crate::profile_function!(); + profiling::function_scope!(); let language = Language::new(language)?; diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index 7f9c81109fa..ec88990305b 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -743,6 +743,7 @@ impl<'a> Table<'a> { let cursor_position = ui.cursor().min; let mut scroll_area = ScrollArea::new([false, vscroll]) + .id_salt(state_id.with("__scroll_area")) .drag_to_scroll(drag_to_scroll) .stick_to_bottom(stick_to_bottom) .min_scrolled_height(min_scrolled_height) @@ -1226,7 +1227,7 @@ impl<'a> TableBody<'a> { // Capture the hover information for the just created row. This is used in the next render // to ensure that the entire row is highlighted. - fn capture_hover_state(&mut self, response: &Option, row_index: usize) { + fn capture_hover_state(&self, response: &Option, row_index: usize) { let is_row_hovered = response.as_ref().map_or(false, |r| r.hovered()); if is_row_hovered { self.layout diff --git a/crates/egui_glow/CHANGELOG.md b/crates/egui_glow/CHANGELOG.md index 6ab4aee2207..dc3d6dfda46 100644 --- a/crates/egui_glow/CHANGELOG.md +++ b/crates/egui_glow/CHANGELOG.md @@ -6,6 +6,14 @@ Changes since the last release can be found at { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} - -#[allow(unused_imports)] -pub(crate) use profiling_scopes::{profile_function, profile_scope}; diff --git a/crates/egui_glow/src/painter.rs b/crates/egui_glow/src/painter.rs index f8f6145c8a0..bec46cf085f 100644 --- a/crates/egui_glow/src/painter.rs +++ b/crates/egui_glow/src/painter.rs @@ -144,7 +144,7 @@ impl Painter { shader_version: Option, dithering: bool, ) -> Result { - crate::profile_function!(); + profiling::function_scope!(); crate::check_for_gl_error_even_in_release!(&gl, "before Painter::new"); // some useful debug info. all three of them are present in gl 1.1. @@ -366,7 +366,7 @@ impl Painter { clipped_primitives: &[egui::ClippedPrimitive], textures_delta: &egui::TexturesDelta, ) { - crate::profile_function!(); + profiling::function_scope!(); for (id, image_delta) in &textures_delta.set { self.set_texture(*id, image_delta); @@ -405,7 +405,7 @@ impl Painter { pixels_per_point: f32, clipped_primitives: &[egui::ClippedPrimitive], ) { - crate::profile_function!(); + profiling::function_scope!(); self.assert_not_destroyed(); unsafe { self.prepare_painting(screen_size_px, pixels_per_point) }; @@ -423,7 +423,7 @@ impl Painter { } Primitive::Callback(callback) => { if callback.rect.is_positive() { - crate::profile_scope!("callback"); + profiling::scope!("callback"); let info = egui::PaintCallbackInfo { viewport: callback.rect, @@ -508,7 +508,7 @@ impl Painter { // ------------------------------------------------------------------------ pub fn set_texture(&mut self, tex_id: egui::TextureId, delta: &egui::epaint::ImageDelta) { - crate::profile_function!(); + profiling::function_scope!(); self.assert_not_destroyed(); @@ -540,7 +540,7 @@ impl Painter { ); let data: Vec = { - crate::profile_scope!("font -> sRGBA"); + profiling::scope!("font -> sRGBA"); image .srgba_pixels(None) .flat_map(|a| a.to_array()) @@ -559,7 +559,7 @@ impl Painter { options: egui::TextureOptions, data: &[u8], ) { - crate::profile_function!(); + profiling::function_scope!(); assert_eq!(data.len(), w * h * 4); assert!( w <= self.max_texture_side && h <= self.max_texture_side, @@ -610,7 +610,7 @@ impl Painter { let level = 0; if let Some([x, y]) = pos { - crate::profile_scope!("gl.tex_sub_image_2d"); + profiling::scope!("gl.tex_sub_image_2d"); self.gl.tex_sub_image_2d( glow::TEXTURE_2D, level, @@ -620,12 +620,12 @@ impl Painter { h as _, src_format, glow::UNSIGNED_BYTE, - glow::PixelUnpackData::Slice(data), + glow::PixelUnpackData::Slice(Some(data)), ); check_for_gl_error!(&self.gl, "tex_sub_image_2d"); } else { let border = 0; - crate::profile_scope!("gl.tex_image_2d"); + profiling::scope!("gl.tex_image_2d"); self.gl.tex_image_2d( glow::TEXTURE_2D, level, @@ -635,7 +635,7 @@ impl Painter { border, src_format, glow::UNSIGNED_BYTE, - Some(data), + glow::PixelUnpackData::Slice(Some(data)), ); check_for_gl_error!(&self.gl, "tex_image_2d"); } @@ -675,7 +675,7 @@ impl Painter { } pub fn read_screen_rgba(&self, [w, h]: [u32; 2]) -> egui::ColorImage { - crate::profile_function!(); + profiling::function_scope!(); let mut pixels = vec![0_u8; (w * h * 4) as usize]; unsafe { @@ -686,7 +686,7 @@ impl Painter { h as _, glow::RGBA, glow::UNSIGNED_BYTE, - glow::PixelPackData::Slice(&mut pixels), + glow::PixelPackData::Slice(Some(&mut pixels)), ); } let mut flipped = Vec::with_capacity((w * h * 4) as usize); @@ -700,8 +700,7 @@ impl Painter { } pub fn read_screen_rgb(&self, [w, h]: [u32; 2]) -> Vec { - crate::profile_function!(); - + profiling::function_scope!(); let mut pixels = vec![0_u8; (w * h * 3) as usize]; unsafe { self.gl.read_pixels( @@ -711,7 +710,7 @@ impl Painter { h as _, glow::RGB, glow::UNSIGNED_BYTE, - glow::PixelPackData::Slice(&mut pixels), + glow::PixelPackData::Slice(Some(&mut pixels)), ); } pixels @@ -748,7 +747,7 @@ impl Painter { } pub fn clear(gl: &glow::Context, screen_size_in_pixels: [u32; 2], clear_color: [f32; 4]) { - crate::profile_function!(); + profiling::function_scope!(); unsafe { gl.disable(glow::SCISSOR_TEST); diff --git a/crates/egui_kittest/CHANGELOG.md b/crates/egui_kittest/CHANGELOG.md new file mode 100644 index 00000000000..aa8b7cac577 --- /dev/null +++ b/crates/egui_kittest/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog for egui_kittest +All notable changes to the `egui_kittest` crate will be noted in this file. + + +This file is updated upon each release. +Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. + + +## 0.30.0 - 2024-12-16 - Initial relrease +* Support for egui 0.30.0 +* Automate clicks and text input +* Automatic screenshot testing with wgpu diff --git a/crates/egui_kittest/Cargo.toml b/crates/egui_kittest/Cargo.toml new file mode 100644 index 00000000000..d93f0348368 --- /dev/null +++ b/crates/egui_kittest/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "egui_kittest" +version.workspace = true +authors = [ + "Lucas Meurer ", + "Emil Ernerfeldt ", +] +description = "Testing library for egui based on kittest and AccessKit" +edition.workspace = true +rust-version.workspace = true +homepage = "https://github.com/emilk/egui" +license.workspace = true +readme = "./README.md" +repository = "https://github.com/emilk/egui" +categories = ["gui", "development-tools::testing", "accessibility"] +keywords = ["gui", "immediate", "egui", "testing", "accesskit"] +include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +# Adds a wgpu-based test renderer. +wgpu = ["dep:egui-wgpu", "dep:pollster", "dep:image"] + +# Adds a dify-based image snapshot utility. +snapshot = ["dep:dify", "dep:image", "image/png"] + + +[dependencies] +kittest.workspace = true +egui = { workspace = true, features = ["accesskit"] } + +# wgpu dependencies +egui-wgpu = { workspace = true, optional = true } +pollster = { workspace = true, optional = true } +image = { workspace = true, optional = true } + +# snapshot dependencies +dify = { workspace = true, optional = true } + +## Enable this when generating docs. +document-features = { workspace = true, optional = true } + +[dev-dependencies] +egui = { workspace = true, features = ["default_fonts"] } +image = { workspace = true, features = ["png"] } +wgpu = { workspace = true, features = ["metal"] } + +[lints] +workspace = true diff --git a/crates/egui_kittest/README.md b/crates/egui_kittest/README.md new file mode 100644 index 00000000000..c124fac3a62 --- /dev/null +++ b/crates/egui_kittest/README.md @@ -0,0 +1,51 @@ +# egui_kittest + +Ui testing library for egui, based on [kittest](https://github.com/rerun-io/kittest) (an [AccessKit](https://github.com/AccessKit/accesskit) based testing library). + +## Example usage +```rust +use egui::accesskit::Toggled; +use egui_kittest::{Harness, kittest::Queryable}; + +fn main() { + let mut checked = false; + let app = |ui: &mut egui::Ui| { + ui.checkbox(&mut checked, "Check me!"); + }; + + let mut harness = Harness::new_ui(app); + + let checkbox = harness.get_by_label("Check me!"); + assert_eq!(checkbox.toggled(), Some(Toggled::False)); + checkbox.click(); + + harness.run(); + + let checkbox = harness.get_by_label("Check me!"); + assert_eq!(checkbox.toggled(), Some(Toggled::True)); + + // Shrink the window size to the smallest size possible + harness.fit_contents(); + + // You can even render the ui and do image snapshot tests + #[cfg(all(feature = "wgpu", feature = "snapshot"))] + harness.wgpu_snapshot("readme_example"); +} +``` + +## Snapshot testing +There is a snapshot testing feature. To create snapshot tests, enable the `snapshot` and `wgpu` features. +Once enabled, you can call `Harness::wgpu_snapshot` to render the ui and save the image to the `tests/snapshots` directory. + +To update the snapshots, run your tests with `UPDATE_SNAPSHOTS=true`, so e.g. `UPDATE_SNAPSHOTS=true cargo test`. +Running with `UPDATE_SNAPSHOTS=true` will still cause the tests to fail, but on the next run, the tests should pass. + +If you want to have multiple snapshots in the same test, it makes sense to collect the results in a `Vec` +([look here](https://github.com/emilk/egui/blob/70a01138b77f9c5724a35a6ef750b9ae1ab9f2dc/crates/egui_demo_lib/src/demo/demo_app_windows.rs#L388-L427) for an example). +This way they can all be updated at the same time. + +You should add the following to your `.gitignore`: +```gitignore +**/tests/snapshots/**/*.diff.png +**/tests/snapshots/**/*.new.png +``` diff --git a/crates/egui_kittest/src/app_kind.rs b/crates/egui_kittest/src/app_kind.rs new file mode 100644 index 00000000000..8a180b3b93b --- /dev/null +++ b/crates/egui_kittest/src/app_kind.rs @@ -0,0 +1,88 @@ +use egui::Frame; + +type AppKindContextState<'a, State> = Box; +type AppKindUiState<'a, State> = Box; +type AppKindContext<'a> = Box; +type AppKindUi<'a> = Box; + +pub(crate) enum AppKind<'a, State> { + Context(AppKindContext<'a>), + Ui(AppKindUi<'a>), + ContextState(AppKindContextState<'a, State>), + UiState(AppKindUiState<'a, State>), +} + +// TODO(lucasmerlin): These aren't working unfortunately :( +// I think they should work though: https://geo-ant.github.io/blog/2021/rust-traits-and-variadic-functions/ +// pub trait IntoAppKind<'a, UiKind> { +// fn into_harness_kind(self) -> AppKind<'a>; +// } +// +// impl<'a, F> IntoAppKind<'a, &egui::Context> for F +// where +// F: FnMut(&egui::Context) + 'a, +// { +// fn into_harness_kind(self) -> AppKind<'a> { +// AppKind::Context(Box::new(self)) +// } +// } +// +// impl<'a, F> IntoAppKind<'a, &mut egui::Ui> for F +// where +// F: FnMut(&mut egui::Ui) + 'a, +// { +// fn into_harness_kind(self) -> AppKind<'a> { +// AppKind::Ui(Box::new(self)) +// } +// } + +impl<'a, State> AppKind<'a, State> { + pub fn run( + &mut self, + ctx: &egui::Context, + state: &mut State, + sizing_pass: bool, + ) -> Option { + match self { + AppKind::Context(f) => { + debug_assert!(!sizing_pass, "Context closures cannot do a sizing pass"); + f(ctx); + None + } + AppKind::ContextState(f) => { + debug_assert!(!sizing_pass, "Context closures cannot do a sizing pass"); + f(ctx, state); + None + } + kind_ui => Some(kind_ui.run_ui(ctx, state, sizing_pass)), + } + } + + fn run_ui( + &mut self, + ctx: &egui::Context, + state: &mut State, + sizing_pass: bool, + ) -> egui::Response { + egui::CentralPanel::default() + .frame(Frame::none()) + .show(ctx, |ui| { + let mut builder = egui::UiBuilder::new(); + if sizing_pass { + builder.sizing_pass = true; + } + ui.scope_builder(builder, |ui| { + Frame::central_panel(ui.style()) + .outer_margin(8.0) + .inner_margin(0.0) + .show(ui, |ui| match self { + AppKind::Ui(f) => f(ui), + AppKind::UiState(f) => f(ui, state), + _ => unreachable!(), + }); + }) + .response + }) + .inner + } +} diff --git a/crates/egui_kittest/src/builder.rs b/crates/egui_kittest/src/builder.rs new file mode 100644 index 00000000000..65c4dfbf0f6 --- /dev/null +++ b/crates/egui_kittest/src/builder.rs @@ -0,0 +1,143 @@ +use crate::app_kind::AppKind; +use crate::Harness; +use egui::{Pos2, Rect, Vec2}; +use std::marker::PhantomData; + +/// Builder for [`Harness`]. +pub struct HarnessBuilder { + pub(crate) screen_rect: Rect, + pub(crate) pixels_per_point: f32, + pub(crate) state: PhantomData, +} + +impl Default for HarnessBuilder { + fn default() -> Self { + Self { + screen_rect: Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0)), + pixels_per_point: 1.0, + state: PhantomData, + } + } +} + +impl HarnessBuilder { + /// Set the size of the window. + #[inline] + pub fn with_size(mut self, size: impl Into) -> Self { + let size = size.into(); + self.screen_rect.set_width(size.x); + self.screen_rect.set_height(size.y); + self + } + + /// Set the `pixels_per_point` of the window. + #[inline] + pub fn with_pixels_per_point(mut self, pixels_per_point: f32) -> Self { + self.pixels_per_point = pixels_per_point; + self + } + + /// Create a new Harness with the given app closure and a state. + /// + /// The app closure will immediately be called once to create the initial ui. + /// + /// If you don't need to create Windows / Panels, you can use [`HarnessBuilder::build_ui`] instead. + /// + /// # Example + /// ```rust + /// # use egui::CentralPanel; + /// # use egui_kittest::{Harness, kittest::Queryable}; + /// let checked = false; + /// let mut harness = Harness::builder() + /// .with_size(egui::Vec2::new(300.0, 200.0)) + /// .build_state(|ctx, checked| { + /// CentralPanel::default().show(ctx, |ui| { + /// ui.checkbox(checked, "Check me!"); + /// }); + /// }, checked); + /// + /// harness.get_by_label("Check me!").click(); + /// harness.run(); + /// + /// assert_eq!(*harness.state(), true); + /// ``` + pub fn build_state<'a>( + self, + app: impl FnMut(&egui::Context, &mut State) + 'a, + state: State, + ) -> Harness<'a, State> { + Harness::from_builder(&self, AppKind::ContextState(Box::new(app)), state) + } + + /// Create a new Harness with the given ui closure and a state. + /// + /// The ui closure will immediately be called once to create the initial ui. + /// + /// If you need to create Windows / Panels, you can use [`HarnessBuilder::build`] instead. + /// + /// # Example + /// ```rust + /// # use egui_kittest::{Harness, kittest::Queryable}; + /// let mut checked = false; + /// let mut harness = Harness::builder() + /// .with_size(egui::Vec2::new(300.0, 200.0)) + /// .build_ui_state(|ui, checked| { + /// ui.checkbox(checked, "Check me!"); + /// }, checked); + /// + /// harness.get_by_label("Check me!").click(); + /// harness.run(); + /// + /// assert_eq!(*harness.state(), true); + /// ``` + pub fn build_ui_state<'a>( + self, + app: impl FnMut(&mut egui::Ui, &mut State) + 'a, + state: State, + ) -> Harness<'a, State> { + Harness::from_builder(&self, AppKind::UiState(Box::new(app)), state) + } +} + +impl HarnessBuilder { + /// Create a new Harness with the given app closure. + /// + /// The app closure will immediately be called once to create the initial ui. + /// + /// If you don't need to create Windows / Panels, you can use [`HarnessBuilder::build_ui`] instead. + /// + /// # Example + /// ```rust + /// # use egui::CentralPanel; + /// # use egui_kittest::{Harness, kittest::Queryable}; + /// let mut harness = Harness::builder() + /// .with_size(egui::Vec2::new(300.0, 200.0)) + /// .build(|ctx| { + /// CentralPanel::default().show(ctx, |ui| { + /// ui.label("Hello, world!"); + /// }); + /// }); + /// ``` + pub fn build<'a>(self, app: impl FnMut(&egui::Context) + 'a) -> Harness<'a> { + Harness::from_builder(&self, AppKind::Context(Box::new(app)), ()) + } + + /// Create a new Harness with the given ui closure. + /// + /// The ui closure will immediately be called once to create the initial ui. + /// + /// If you need to create Windows / Panels, you can use [`HarnessBuilder::build`] instead. + /// + /// # Example + /// ```rust + /// # use egui_kittest::{Harness, kittest::Queryable}; + /// let mut harness = Harness::builder() + /// .with_size(egui::Vec2::new(300.0, 200.0)) + /// .build_ui(|ui| { + /// ui.label("Hello, world!"); + /// }); + /// ``` + pub fn build_ui<'a>(self, app: impl FnMut(&mut egui::Ui) + 'a) -> Harness<'a> { + Harness::from_builder(&self, AppKind::Ui(Box::new(app)), ()) + } +} diff --git a/crates/egui_kittest/src/event.rs b/crates/egui_kittest/src/event.rs new file mode 100644 index 00000000000..5ac07488d0c --- /dev/null +++ b/crates/egui_kittest/src/event.rs @@ -0,0 +1,182 @@ +use egui::Event::PointerButton; +use egui::{Event, Modifiers, Pos2}; +use kittest::{ElementState, MouseButton, SimulatedEvent}; + +#[derive(Default)] +pub(crate) struct EventState { + modifiers: Modifiers, + last_mouse_pos: Pos2, +} + +impl EventState { + pub fn kittest_event_to_egui(&mut self, event: kittest::Event) -> Option { + match event { + kittest::Event::ActionRequest(e) => Some(Event::AccessKitActionRequest(e)), + kittest::Event::Simulated(e) => match e { + SimulatedEvent::CursorMoved { position } => { + self.last_mouse_pos = Pos2::new(position.x as f32, position.y as f32); + Some(Event::PointerMoved(Pos2::new( + position.x as f32, + position.y as f32, + ))) + } + SimulatedEvent::MouseInput { state, button } => { + pointer_button_to_egui(button).map(|button| PointerButton { + button, + modifiers: self.modifiers, + pos: self.last_mouse_pos, + pressed: matches!(state, ElementState::Pressed), + }) + } + SimulatedEvent::Ime(text) => Some(Event::Text(text)), + SimulatedEvent::KeyInput { state, key } => { + match key { + kittest::Key::Alt => { + self.modifiers.alt = matches!(state, ElementState::Pressed); + } + kittest::Key::Command => { + self.modifiers.command = matches!(state, ElementState::Pressed); + } + kittest::Key::Control => { + self.modifiers.ctrl = matches!(state, ElementState::Pressed); + } + kittest::Key::Shift => { + self.modifiers.shift = matches!(state, ElementState::Pressed); + } + _ => {} + } + kittest_key_to_egui(key).map(|key| Event::Key { + key, + modifiers: self.modifiers, + pressed: matches!(state, ElementState::Pressed), + repeat: false, + physical_key: None, + }) + } + }, + } + } +} + +pub fn kittest_key_to_egui(value: kittest::Key) -> Option { + use egui::Key as EKey; + use kittest::Key; + match value { + Key::ArrowDown => Some(EKey::ArrowDown), + Key::ArrowLeft => Some(EKey::ArrowLeft), + Key::ArrowRight => Some(EKey::ArrowRight), + Key::ArrowUp => Some(EKey::ArrowUp), + Key::Escape => Some(EKey::Escape), + Key::Tab => Some(EKey::Tab), + Key::Backspace => Some(EKey::Backspace), + Key::Enter => Some(EKey::Enter), + Key::Space => Some(EKey::Space), + Key::Insert => Some(EKey::Insert), + Key::Delete => Some(EKey::Delete), + Key::Home => Some(EKey::Home), + Key::End => Some(EKey::End), + Key::PageUp => Some(EKey::PageUp), + Key::PageDown => Some(EKey::PageDown), + Key::Copy => Some(EKey::Copy), + Key::Cut => Some(EKey::Cut), + Key::Paste => Some(EKey::Paste), + Key::Colon => Some(EKey::Colon), + Key::Comma => Some(EKey::Comma), + Key::Backslash => Some(EKey::Backslash), + Key::Slash => Some(EKey::Slash), + Key::Pipe => Some(EKey::Pipe), + Key::Questionmark => Some(EKey::Questionmark), + Key::OpenBracket => Some(EKey::OpenBracket), + Key::CloseBracket => Some(EKey::CloseBracket), + Key::Backtick => Some(EKey::Backtick), + Key::Minus => Some(EKey::Minus), + Key::Period => Some(EKey::Period), + Key::Plus => Some(EKey::Plus), + Key::Equals => Some(EKey::Equals), + Key::Semicolon => Some(EKey::Semicolon), + Key::Quote => Some(EKey::Quote), + Key::Num0 => Some(EKey::Num0), + Key::Num1 => Some(EKey::Num1), + Key::Num2 => Some(EKey::Num2), + Key::Num3 => Some(EKey::Num3), + Key::Num4 => Some(EKey::Num4), + Key::Num5 => Some(EKey::Num5), + Key::Num6 => Some(EKey::Num6), + Key::Num7 => Some(EKey::Num7), + Key::Num8 => Some(EKey::Num8), + Key::Num9 => Some(EKey::Num9), + Key::A => Some(EKey::A), + Key::B => Some(EKey::B), + Key::C => Some(EKey::C), + Key::D => Some(EKey::D), + Key::E => Some(EKey::E), + Key::F => Some(EKey::F), + Key::G => Some(EKey::G), + Key::H => Some(EKey::H), + Key::I => Some(EKey::I), + Key::J => Some(EKey::J), + Key::K => Some(EKey::K), + Key::L => Some(EKey::L), + Key::M => Some(EKey::M), + Key::N => Some(EKey::N), + Key::O => Some(EKey::O), + Key::P => Some(EKey::P), + Key::Q => Some(EKey::Q), + Key::R => Some(EKey::R), + Key::S => Some(EKey::S), + Key::T => Some(EKey::T), + Key::U => Some(EKey::U), + Key::V => Some(EKey::V), + Key::W => Some(EKey::W), + Key::X => Some(EKey::X), + Key::Y => Some(EKey::Y), + Key::Z => Some(EKey::Z), + Key::F1 => Some(EKey::F1), + Key::F2 => Some(EKey::F2), + Key::F3 => Some(EKey::F3), + Key::F4 => Some(EKey::F4), + Key::F5 => Some(EKey::F5), + Key::F6 => Some(EKey::F6), + Key::F7 => Some(EKey::F7), + Key::F8 => Some(EKey::F8), + Key::F9 => Some(EKey::F9), + Key::F10 => Some(EKey::F10), + Key::F11 => Some(EKey::F11), + Key::F12 => Some(EKey::F12), + Key::F13 => Some(EKey::F13), + Key::F14 => Some(EKey::F14), + Key::F15 => Some(EKey::F15), + Key::F16 => Some(EKey::F16), + Key::F17 => Some(EKey::F17), + Key::F18 => Some(EKey::F18), + Key::F19 => Some(EKey::F19), + Key::F20 => Some(EKey::F20), + Key::F21 => Some(EKey::F21), + Key::F22 => Some(EKey::F22), + Key::F23 => Some(EKey::F23), + Key::F24 => Some(EKey::F24), + Key::F25 => Some(EKey::F25), + Key::F26 => Some(EKey::F26), + Key::F27 => Some(EKey::F27), + Key::F28 => Some(EKey::F28), + Key::F29 => Some(EKey::F29), + Key::F30 => Some(EKey::F30), + Key::F31 => Some(EKey::F31), + Key::F32 => Some(EKey::F32), + Key::F33 => Some(EKey::F33), + Key::F34 => Some(EKey::F34), + Key::F35 => Some(EKey::F35), + _ => None, + } +} + +pub fn pointer_button_to_egui(value: MouseButton) -> Option { + match value { + MouseButton::Left => Some(egui::PointerButton::Primary), + MouseButton::Right => Some(egui::PointerButton::Secondary), + MouseButton::Middle => Some(egui::PointerButton::Middle), + MouseButton::Back => Some(egui::PointerButton::Extra1), + MouseButton::Forward => Some(egui::PointerButton::Extra2), + MouseButton::Other(_) => None, + } +} diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs new file mode 100644 index 00000000000..03648aa16fe --- /dev/null +++ b/crates/egui_kittest/src/lib.rs @@ -0,0 +1,326 @@ +#![doc = include_str!("../README.md")] +//! +//! ## Feature flags +#![cfg_attr(feature = "document-features", doc = document_features::document_features!())] + +mod builder; +mod event; +#[cfg(feature = "snapshot")] +mod snapshot; + +#[cfg(feature = "snapshot")] +pub use snapshot::*; +use std::fmt::{Debug, Formatter}; +mod app_kind; +#[cfg(feature = "wgpu")] +mod texture_to_image; +#[cfg(feature = "wgpu")] +pub mod wgpu; + +pub use kittest; +use std::mem; + +use crate::app_kind::AppKind; +use crate::event::EventState; +pub use builder::*; +use egui::{Pos2, Rect, TexturesDelta, Vec2, ViewportId}; +use kittest::{Node, Queryable}; + +/// The test Harness. This contains everything needed to run the test. +/// Create a new Harness using [`Harness::new`] or [`Harness::builder`]. +/// +/// The [Harness] has a optional generic state that can be used to pass data to the app / ui closure. +/// In _most cases_ it should be fine to just store the state in the closure itself. +/// The state functions are useful if you need to access the state after the harness has been created. +pub struct Harness<'a, State = ()> { + pub ctx: egui::Context, + input: egui::RawInput, + kittest: kittest::State, + output: egui::FullOutput, + texture_deltas: Vec, + app: AppKind<'a, State>, + event_state: EventState, + response: Option, + state: State, +} + +impl<'a, State> Debug for Harness<'a, State> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.kittest.fmt(f) + } +} + +impl<'a, State> Harness<'a, State> { + pub(crate) fn from_builder( + builder: &HarnessBuilder, + mut app: AppKind<'a, State>, + mut state: State, + ) -> Self { + let ctx = egui::Context::default(); + ctx.enable_accesskit(); + let mut input = egui::RawInput { + screen_rect: Some(builder.screen_rect), + ..Default::default() + }; + let viewport = input.viewports.get_mut(&ViewportId::ROOT).unwrap(); + viewport.native_pixels_per_point = Some(builder.pixels_per_point); + + let mut response = None; + + // We need to run egui for a single frame so that the AccessKit state can be initialized + // and users can immediately start querying for widgets. + let mut output = ctx.run(input.clone(), |ctx| { + response = app.run(ctx, &mut state, false); + }); + + let mut harness = Self { + app, + ctx, + input, + kittest: kittest::State::new( + output + .platform_output + .accesskit_update + .take() + .expect("AccessKit was disabled"), + ), + texture_deltas: vec![mem::take(&mut output.textures_delta)], + output, + response, + event_state: EventState::default(), + state, + }; + // Run the harness until it is stable, ensuring that all Areas are shown and animations are done + harness.run(); + harness + } + + /// Create a [`Harness`] via a [`HarnessBuilder`]. + pub fn builder() -> HarnessBuilder { + HarnessBuilder::default() + } + + /// Create a new Harness with the given app closure and a state. + /// + /// The app closure will immediately be called once to create the initial ui. + /// + /// If you don't need to create Windows / Panels, you can use [`Harness::new_ui`] instead. + /// + /// If you e.g. want to customize the size of the window, you can use [`Harness::builder`]. + /// + /// # Example + /// ```rust + /// # use egui::CentralPanel; + /// # use egui_kittest::{Harness, kittest::Queryable}; + /// let mut checked = false; + /// let mut harness = Harness::new_state(|ctx, checked| { + /// CentralPanel::default().show(ctx, |ui| { + /// ui.checkbox(checked, "Check me!"); + /// }); + /// }, checked); + /// + /// harness.get_by_label("Check me!").click(); + /// harness.run(); + /// + /// assert_eq!(*harness.state(), true); + /// ``` + pub fn new_state(app: impl FnMut(&egui::Context, &mut State) + 'a, state: State) -> Self { + Self::builder().build_state(app, state) + } + + /// Create a new Harness with the given ui closure and a state. + /// + /// The ui closure will immediately be called once to create the initial ui. + /// + /// If you need to create Windows / Panels, you can use [`Harness::new`] instead. + /// + /// If you e.g. want to customize the size of the ui, you can use [`Harness::builder`]. + /// + /// # Example + /// ```rust + /// # use egui_kittest::{Harness, kittest::Queryable}; + /// let mut checked = false; + /// let mut harness = Harness::new_ui_state(|ui, checked| { + /// ui.checkbox(checked, "Check me!"); + /// }, checked); + /// + /// harness.get_by_label("Check me!").click(); + /// harness.run(); + /// + /// assert_eq!(*harness.state(), true); + /// ``` + pub fn new_ui_state(app: impl FnMut(&mut egui::Ui, &mut State) + 'a, state: State) -> Self { + Self::builder().build_ui_state(app, state) + } + + /// Set the size of the window. + /// Note: If you only want to set the size once at the beginning, + /// prefer using [`HarnessBuilder::with_size`]. + #[inline] + pub fn set_size(&mut self, size: Vec2) -> &mut Self { + self.input.screen_rect = Some(Rect::from_min_size(Pos2::ZERO, size)); + self + } + + /// Set the `pixels_per_point` of the window. + /// Note: If you only want to set the `pixels_per_point` once at the beginning, + /// prefer using [`HarnessBuilder::with_pixels_per_point`]. + #[inline] + pub fn set_pixels_per_point(&mut self, pixels_per_point: f32) -> &mut Self { + self.ctx.set_pixels_per_point(pixels_per_point); + self + } + + /// Run a frame. + /// This will call the app closure with the current context and update the Harness. + pub fn step(&mut self) { + self._step(false); + } + + fn _step(&mut self, sizing_pass: bool) { + for event in self.kittest.take_events() { + if let Some(event) = self.event_state.kittest_event_to_egui(event) { + self.input.events.push(event); + } + } + + let mut output = self.ctx.run(self.input.take(), |ctx| { + self.response = self.app.run(ctx, &mut self.state, sizing_pass); + }); + self.kittest.update( + output + .platform_output + .accesskit_update + .take() + .expect("AccessKit was disabled"), + ); + self.texture_deltas + .push(mem::take(&mut output.textures_delta)); + self.output = output; + } + + /// Resize the test harness to fit the contents. This only works when creating the Harness via + /// [`Harness::new_ui`] or [`HarnessBuilder::build_ui`]. + pub fn fit_contents(&mut self) { + self._step(true); + if let Some(response) = &self.response { + self.set_size(response.rect.size()); + } + self.run(); + } + + /// Run a few frames. + /// This will soon be changed to run the app until it is "stable", meaning + /// - all animations are done + /// - no more repaints are requested + pub fn run(&mut self) { + const STEPS: usize = 2; + for _ in 0..STEPS { + self.step(); + } + } + + /// Access the [`egui::RawInput`] for the next frame. + pub fn input(&self) -> &egui::RawInput { + &self.input + } + + /// Access the [`egui::RawInput`] for the next frame mutably. + pub fn input_mut(&mut self) -> &mut egui::RawInput { + &mut self.input + } + + /// Access the [`egui::FullOutput`] for the last frame. + pub fn output(&self) -> &egui::FullOutput { + &self.output + } + + /// Access the [`kittest::State`]. + pub fn kittest_state(&self) -> &kittest::State { + &self.kittest + } + + /// Access the state. + pub fn state(&self) -> &State { + &self.state + } + + /// Access the state mutably. + pub fn state_mut(&mut self) -> &mut State { + &mut self.state + } + + /// Press a key. + /// This will create a key down event and a key up event. + pub fn press_key(&mut self, key: egui::Key) { + self.input.events.push(egui::Event::Key { + key, + pressed: true, + modifiers: Default::default(), + repeat: false, + physical_key: None, + }); + self.input.events.push(egui::Event::Key { + key, + pressed: false, + modifiers: Default::default(), + repeat: false, + physical_key: None, + }); + } +} + +/// Utilities for stateless harnesses. +impl<'a> Harness<'a> { + /// Create a new Harness with the given app closure. + /// Use the [`Harness::run`], [`Harness::step`], etc... methods to run the app. + /// + /// The app closure will immediately be called once to create the initial ui. + /// + /// If you don't need to create Windows / Panels, you can use [`Harness::new_ui`] instead. + /// + /// If you e.g. want to customize the size of the window, you can use [`Harness::builder`]. + /// + /// # Example + /// ```rust + /// # use egui::CentralPanel; + /// # use egui_kittest::Harness; + /// let mut harness = Harness::new(|ctx| { + /// CentralPanel::default().show(ctx, |ui| { + /// ui.label("Hello, world!"); + /// }); + /// }); + /// ``` + pub fn new(app: impl FnMut(&egui::Context) + 'a) -> Self { + Self::builder().build(app) + } + + /// Create a new Harness with the given ui closure. + /// Use the [`Harness::run`], [`Harness::step`], etc... methods to run the app. + /// + /// The ui closure will immediately be called once to create the initial ui. + /// + /// If you need to create Windows / Panels, you can use [`Harness::new`] instead. + /// + /// If you e.g. want to customize the size of the ui, you can use [`Harness::builder`]. + /// + /// # Example + /// ```rust + /// # use egui_kittest::Harness; + /// let mut harness = Harness::new_ui(|ui| { + /// ui.label("Hello, world!"); + /// }); + /// ``` + pub fn new_ui(app: impl FnMut(&mut egui::Ui) + 'a) -> Self { + Self::builder().build_ui(app) + } +} + +impl<'t, 'n, 'h, State> Queryable<'t, 'n> for Harness<'h, State> +where + 'n: 't, +{ + fn node(&'n self) -> Node<'t> { + self.kittest_state().node() + } +} diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs new file mode 100644 index 00000000000..5be6c275419 --- /dev/null +++ b/crates/egui_kittest/src/snapshot.rs @@ -0,0 +1,398 @@ +use crate::Harness; +use image::ImageError; +use std::fmt::Display; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; + +#[non_exhaustive] +pub struct SnapshotOptions { + /// The threshold for the image comparison. + /// The default is `0.6` (which is enough for most egui tests to pass across different + /// wgpu backends). + pub threshold: f32, + + /// The path where the snapshots will be saved. + /// The default is `tests/snapshots`. + pub output_path: PathBuf, +} + +impl Default for SnapshotOptions { + fn default() -> Self { + Self { + threshold: 0.6, + output_path: PathBuf::from("tests/snapshots"), + } + } +} + +impl SnapshotOptions { + /// Create a new [`SnapshotOptions`] with the default values. + pub fn new() -> Self { + Default::default() + } + + /// Change the threshold for the image comparison. + /// The default is `0.6` (which is enough for most egui tests to pass across different + /// wgpu backends). + #[inline] + pub fn threshold(mut self, threshold: f32) -> Self { + self.threshold = threshold; + self + } + + /// Change the path where the snapshots will be saved. + /// The default is `tests/snapshots`. + #[inline] + pub fn output_path(mut self, output_path: impl Into) -> Self { + self.output_path = output_path.into(); + self + } +} + +#[derive(Debug)] +pub enum SnapshotError { + /// Image did not match snapshot + Diff { + /// Name of the test + name: String, + + /// Count of pixels that were different + diff: i32, + + /// Path where the diff image was saved + diff_path: PathBuf, + }, + + /// Error opening the existing snapshot (it probably doesn't exist, check the + /// [`ImageError`] for more information) + OpenSnapshot { + /// Path where the snapshot was expected to be + path: PathBuf, + + /// The error that occurred + err: ImageError, + }, + + /// The size of the image did not match the snapshot + SizeMismatch { + /// Name of the test + name: String, + + /// Expected size + expected: (u32, u32), + + /// Actual size + actual: (u32, u32), + }, + + /// Error writing the snapshot output + WriteSnapshot { + /// Path where a file was expected to be written + path: PathBuf, + + /// The error that occurred + err: ImageError, + }, +} + +const HOW_TO_UPDATE_SCREENSHOTS: &str = + "Run `UPDATE_SNAPSHOTS=1 cargo test` to update the snapshots."; + +impl Display for SnapshotError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Diff { + name, + diff, + diff_path, + } => { + write!( + f, + "'{name}' Image did not match snapshot. Diff: {diff}, {diff_path:?}. {HOW_TO_UPDATE_SCREENSHOTS}" + ) + } + Self::OpenSnapshot { path, err } => match err { + ImageError::IoError(io) => match io.kind() { + ErrorKind::NotFound => { + write!(f, "Missing snapshot: {path:?}. {HOW_TO_UPDATE_SCREENSHOTS}") + } + err => { + write!(f, "Error reading snapshot: {err:?}\nAt: {path:?}. {HOW_TO_UPDATE_SCREENSHOTS}") + } + }, + err => { + write!(f, "Error decoding snapshot: {err:?}\nAt: {path:?}. Make sure git-lfs is setup correctly. Read the instructions here: https://github.com/emilk/egui/blob/master/CONTRIBUTING.md#making-a-pr") + } + }, + Self::SizeMismatch { + name, + expected, + actual, + } => { + write!( + f, + "'{name}' Image size did not match snapshot. Expected: {expected:?}, Actual: {actual:?}. {HOW_TO_UPDATE_SCREENSHOTS}" + ) + } + Self::WriteSnapshot { path, err } => { + write!(f, "Error writing snapshot: {err:?}\nAt: {path:?}") + } + } + } +} + +fn should_update_snapshots() -> bool { + std::env::var("UPDATE_SNAPSHOTS").is_ok() +} + +fn maybe_update_snapshot( + snapshot_path: &Path, + current: &image::RgbaImage, +) -> Result<(), SnapshotError> { + if should_update_snapshots() { + current + .save(snapshot_path) + .map_err(|err| SnapshotError::WriteSnapshot { + err, + path: snapshot_path.into(), + })?; + println!("Updated snapshot: {snapshot_path:?}"); + } + Ok(()) +} + +/// Image snapshot test with custom options. +/// +/// If you want to change the default options for your whole project, it's recommended to create a +/// new `my_image_snapshot` function in your project that calls this function with the desired options. +/// You could additionally use the +/// [disallowed_methods](https://rust-lang.github.io/rust-clippy/master/#disallowed_methods) +/// lint to disable use of the [`image_snapshot`] to prevent accidentally using the wrong defaults. +/// +/// The snapshot files will be saved under [`SnapshotOptions::output_path`]. +/// The snapshot will be saved under `{output_path}/{name}.png`. +/// The new image from the most recent test run will be saved under `{output_path}/{name}.new.png`. +/// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`. +/// +/// # Errors +/// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error +/// reading or writing the snapshot. +pub fn try_image_snapshot_options( + current: &image::RgbaImage, + name: &str, + options: &SnapshotOptions, +) -> Result<(), SnapshotError> { + let SnapshotOptions { + threshold, + output_path, + } = options; + + let path = output_path.join(format!("{name}.png")); + std::fs::create_dir_all(path.parent().expect("Could not get snapshot folder")).ok(); + + let diff_path = output_path.join(format!("{name}.diff.png")); + let current_path = output_path.join(format!("{name}.new.png")); + + current + .save(¤t_path) + .map_err(|err| SnapshotError::WriteSnapshot { + err, + path: current_path, + })?; + + let previous = match image::open(&path) { + Ok(image) => image.to_rgba8(), + Err(err) => { + maybe_update_snapshot(&path, current)?; + return Err(SnapshotError::OpenSnapshot { path, err }); + } + }; + + if previous.dimensions() != current.dimensions() { + maybe_update_snapshot(&path, current)?; + return Err(SnapshotError::SizeMismatch { + name: name.to_owned(), + expected: previous.dimensions(), + actual: current.dimensions(), + }); + } + + let result = dify::diff::get_results( + previous, + current.clone(), + *threshold, + true, + None, + &None, + &None, + ); + + if let Some((diff, result_image)) = result { + result_image + .save(diff_path.clone()) + .map_err(|err| SnapshotError::WriteSnapshot { + path: diff_path.clone(), + err, + })?; + maybe_update_snapshot(&path, current)?; + Err(SnapshotError::Diff { + name: name.to_owned(), + diff, + diff_path, + }) + } else { + // Delete old diff if it exists + std::fs::remove_file(diff_path).ok(); + Ok(()) + } +} + +/// Image snapshot test. +/// +/// This uses the default [`SnapshotOptions`]. Use [`try_image_snapshot_options`] if you want to +/// e.g. change the threshold or output path. +/// +/// The snapshot files will be saved under [`SnapshotOptions::output_path`]. +/// The snapshot will be saved under `{output_path}/{name}.png`. +/// The new image from the most recent test run will be saved under `{output_path}/{name}.new.png`. +/// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`. +/// +/// # Errors +/// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error +/// reading or writing the snapshot. +pub fn try_image_snapshot(current: &image::RgbaImage, name: &str) -> Result<(), SnapshotError> { + try_image_snapshot_options(current, name, &SnapshotOptions::default()) +} + +/// Image snapshot test with custom options. +/// +/// If you want to change the default options for your whole project, it's recommended to create a +/// new `my_image_snapshot` function in your project that calls this function with the desired options. +/// You could additionally use the +/// [disallowed_methods](https://rust-lang.github.io/rust-clippy/master/#disallowed_methods) +/// lint to disable use of the [`image_snapshot`] to prevent accidentally using the wrong defaults. +/// +/// The snapshot files will be saved under [`SnapshotOptions::output_path`]. +/// The snapshot will be saved under `{output_path}/{name}.png`. +/// The new image from the most recent test run will be saved under `{output_path}/{name}.new.png`. +/// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`. +/// +/// # Panics +/// Panics if the image does not match the snapshot or if there was an error reading or writing the +/// snapshot. +#[track_caller] +pub fn image_snapshot_options(current: &image::RgbaImage, name: &str, options: &SnapshotOptions) { + match try_image_snapshot_options(current, name, options) { + Ok(_) => {} + Err(err) => { + panic!("{}", err); + } + } +} + +/// Image snapshot test. +/// The snapshot will be saved under `tests/snapshots/{name}.png`. +/// The new image from the last test run will be saved under `tests/snapshots/{name}.new.png`. +/// If new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`. +/// +/// # Panics +/// Panics if the image does not match the snapshot or if there was an error reading or writing the +/// snapshot. +#[track_caller] +pub fn image_snapshot(current: &image::RgbaImage, name: &str) { + match try_image_snapshot(current, name) { + Ok(_) => {} + Err(err) => { + panic!("{}", err); + } + } +} + +#[cfg(feature = "wgpu")] +impl Harness<'_, State> { + /// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot + /// with custom options. + /// + /// If you want to change the default options for your whole project, you could create an + /// [extension trait](http://xion.io/post/code/rust-extension-traits.html) to create a + /// new `my_image_snapshot` function on the Harness that calls this function with the desired options. + /// You could additionally use the + /// [disallowed_methods](https://rust-lang.github.io/rust-clippy/master/#disallowed_methods) + /// lint to disable use of the [`Harness::wgpu_snapshot`] to prevent accidentally using the wrong defaults. + /// + /// The snapshot files will be saved under [`SnapshotOptions::output_path`]. + /// The snapshot will be saved under `{output_path}/{name}.png`. + /// The new image from the most recent test run will be saved under `{output_path}/{name}.new.png`. + /// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`. + /// + /// # Errors + /// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error + /// reading or writing the snapshot. + pub fn try_wgpu_snapshot_options( + &self, + name: &str, + options: &SnapshotOptions, + ) -> Result<(), SnapshotError> { + let image = crate::wgpu::TestRenderer::new().render(self); + try_image_snapshot_options(&image, name, options) + } + + /// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot. + /// The snapshot will be saved under `tests/snapshots/{name}.png`. + /// The new image from the last test run will be saved under `tests/snapshots/{name}.new.png`. + /// If new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`. + /// + /// # Errors + /// Returns a [`SnapshotError`] if the image does not match the snapshot or if there was an error + /// reading or writing the snapshot. + pub fn try_wgpu_snapshot(&self, name: &str) -> Result<(), SnapshotError> { + let image = crate::wgpu::TestRenderer::new().render(self); + try_image_snapshot(&image, name) + } + + /// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot + /// with custom options. + /// + /// If you want to change the default options for your whole project, you could create an + /// [extension trait](http://xion.io/post/code/rust-extension-traits.html) to create a + /// new `my_image_snapshot` function on the Harness that calls this function with the desired options. + /// You could additionally use the + /// [disallowed_methods](https://rust-lang.github.io/rust-clippy/master/#disallowed_methods) + /// lint to disable use of the [`Harness::wgpu_snapshot`] to prevent accidentally using the wrong defaults. + /// + /// The snapshot files will be saved under [`SnapshotOptions::output_path`]. + /// The snapshot will be saved under `{output_path}/{name}.png`. + /// The new image from the most recent test run will be saved under `{output_path}/{name}.new.png`. + /// If new image didn't match the snapshot, a diff image will be saved under `{output_path}/{name}.diff.png`. + /// + /// # Panics + /// Panics if the image does not match the snapshot or if there was an error reading or writing the + /// snapshot. + #[track_caller] + pub fn wgpu_snapshot_options(&self, name: &str, options: &SnapshotOptions) { + match self.try_wgpu_snapshot_options(name, options) { + Ok(_) => {} + Err(err) => { + panic!("{}", err); + } + } + } + + /// Render a image using a default [`crate::wgpu::TestRenderer`] and compare it to the snapshot. + /// The snapshot will be saved under `tests/snapshots/{name}.png`. + /// The new image from the last test run will be saved under `tests/snapshots/{name}.new.png`. + /// If new image didn't match the snapshot, a diff image will be saved under `tests/snapshots/{name}.diff.png`. + /// + /// # Panics + /// Panics if the image does not match the snapshot or if there was an error reading or writing the + /// snapshot. + #[track_caller] + pub fn wgpu_snapshot(&self, name: &str) { + match self.try_wgpu_snapshot(name) { + Ok(_) => {} + Err(err) => { + panic!("{}", err); + } + } + } +} diff --git a/crates/egui_kittest/src/texture_to_image.rs b/crates/egui_kittest/src/texture_to_image.rs new file mode 100644 index 00000000000..98803ac8a37 --- /dev/null +++ b/crates/egui_kittest/src/texture_to_image.rs @@ -0,0 +1,83 @@ +use egui_wgpu::wgpu; +use egui_wgpu::wgpu::{Device, Extent3d, Queue, Texture}; +use image::RgbaImage; +use std::iter; +use std::mem::size_of; +use std::sync::mpsc::channel; + +pub(crate) fn texture_to_image(device: &Device, queue: &Queue, texture: &Texture) -> RgbaImage { + let buffer_dimensions = + BufferDimensions::new(texture.width() as usize, texture.height() as usize); + + let output_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Texture to bytes output buffer"), + size: (buffer_dimensions.padded_bytes_per_row * buffer_dimensions.height) as u64, + usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Texture to bytes encoder"), + }); + + // Copy the data from the texture to the buffer + encoder.copy_texture_to_buffer( + texture.as_image_copy(), + wgpu::ImageCopyBuffer { + buffer: &output_buffer, + layout: wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(buffer_dimensions.padded_bytes_per_row as u32), + rows_per_image: None, + }, + }, + Extent3d { + width: texture.width(), + height: texture.height(), + depth_or_array_layers: 1, + }, + ); + + let submission_index = queue.submit(iter::once(encoder.finish())); + + // Note that we're not calling `.await` here. + let buffer_slice = output_buffer.slice(..); + // Sets the buffer up for mapping, sending over the result of the mapping back to us when it is finished. + let (sender, receiver) = channel(); + buffer_slice.map_async(wgpu::MapMode::Read, move |v| drop(sender.send(v))); + + // Poll the device in a blocking manner so that our future resolves. + device.poll(wgpu::Maintain::WaitForSubmissionIndex(submission_index)); + + receiver.recv().unwrap().unwrap(); + let buffer_slice = output_buffer.slice(..); + let data = buffer_slice.get_mapped_range(); + let data = data + .chunks_exact(buffer_dimensions.padded_bytes_per_row) + .flat_map(|row| row.iter().take(buffer_dimensions.unpadded_bytes_per_row)) + .copied() + .collect::>(); + + RgbaImage::from_raw(texture.width(), texture.height(), data).expect("Failed to create image") +} + +struct BufferDimensions { + height: usize, + unpadded_bytes_per_row: usize, + padded_bytes_per_row: usize, +} + +impl BufferDimensions { + fn new(width: usize, height: usize) -> Self { + let bytes_per_pixel = size_of::(); + let unpadded_bytes_per_row = width * bytes_per_pixel; + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; + let padded_bytes_per_row_padding = (align - unpadded_bytes_per_row % align) % align; + let padded_bytes_per_row = unpadded_bytes_per_row + padded_bytes_per_row_padding; + Self { + height, + unpadded_bytes_per_row, + padded_bytes_per_row, + } + } +} diff --git a/crates/egui_kittest/src/wgpu.rs b/crates/egui_kittest/src/wgpu.rs new file mode 100644 index 00000000000..4c3001fa72b --- /dev/null +++ b/crates/egui_kittest/src/wgpu.rs @@ -0,0 +1,151 @@ +use crate::texture_to_image::texture_to_image; +use crate::Harness; +use egui_wgpu::wgpu::{Backends, InstanceDescriptor, StoreOp, TextureFormat}; +use egui_wgpu::{wgpu, ScreenDescriptor}; +use image::RgbaImage; +use std::iter::once; +use wgpu::Maintain; + +/// Utility to render snapshots from a [`Harness`] using [`egui_wgpu`]. +pub struct TestRenderer { + device: wgpu::Device, + queue: wgpu::Queue, + dithering: bool, +} + +impl Default for TestRenderer { + fn default() -> Self { + Self::new() + } +} + +impl TestRenderer { + /// Create a new [`TestRenderer`] using a default [`wgpu::Instance`]. + pub fn new() -> Self { + let instance = wgpu::Instance::new(InstanceDescriptor::default()); + + let adapters = instance.enumerate_adapters(Backends::all()); + let adapter = adapters.first().expect("No adapter found"); + + let (device, queue) = pollster::block_on(adapter.request_device( + &wgpu::DeviceDescriptor { + label: Some("Egui Device"), + memory_hints: Default::default(), + required_limits: Default::default(), + required_features: Default::default(), + }, + None, + )) + .expect("Failed to create device"); + + Self::create(device, queue) + } + + /// Create a new [`TestRenderer`] using the provided [`wgpu::Device`] and [`wgpu::Queue`]. + pub fn create(device: wgpu::Device, queue: wgpu::Queue) -> Self { + Self { + device, + queue, + dithering: false, + } + } + + /// Enable or disable dithering. + /// + /// Disabled by default. + #[inline] + pub fn with_dithering(mut self, dithering: bool) -> Self { + self.dithering = dithering; + self + } + + /// Render the [`Harness`] and return the resulting image. + pub fn render(&self, harness: &Harness<'_, State>) -> RgbaImage { + // We need to create a new renderer each time we render, since the renderer stores + // textures related to the Harnesses' egui Context. + // Calling the renderer from different Harnesses would cause problems if we store the renderer. + let mut renderer = egui_wgpu::Renderer::new( + &self.device, + TextureFormat::Rgba8Unorm, + None, + 1, + self.dithering, + ); + + for delta in &harness.texture_deltas { + for (id, image_delta) in &delta.set { + renderer.update_texture(&self.device, &self.queue, *id, image_delta); + } + } + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Egui Command Encoder"), + }); + + let size = harness.ctx.screen_rect().size() * harness.ctx.pixels_per_point(); + let screen = ScreenDescriptor { + pixels_per_point: harness.ctx.pixels_per_point(), + size_in_pixels: [size.x.round() as u32, size.y.round() as u32], + }; + + let tessellated = harness.ctx.tessellate( + harness.output().shapes.clone(), + harness.ctx.pixels_per_point(), + ); + + let user_buffers = renderer.update_buffers( + &self.device, + &self.queue, + &mut encoder, + &tessellated, + &screen, + ); + + let texture = self.device.create_texture(&wgpu::TextureDescriptor { + label: Some("Egui Texture"), + size: wgpu::Extent3d { + width: screen.size_in_pixels[0], + height: screen.size_in_pixels[1], + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + + let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + + { + let mut pass = encoder + .begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Egui Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &texture_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }) + .forget_lifetime(); + + renderer.render(&mut pass, &tessellated, &screen); + } + + self.queue + .submit(user_buffers.into_iter().chain(once(encoder.finish()))); + + self.device.poll(Maintain::Wait); + + texture_to_image(&self.device, &self.queue, &texture) + } +} diff --git a/crates/egui/tests/accesskit.rs b/crates/egui_kittest/tests/accesskit.rs similarity index 61% rename from crates/egui/tests/accesskit.rs rename to crates/egui_kittest/tests/accesskit.rs index bcc26d024b9..02afddefc80 100644 --- a/crates/egui/tests/accesskit.rs +++ b/crates/egui_kittest/tests/accesskit.rs @@ -1,8 +1,9 @@ //! Tests the accesskit accessibility output of egui. -#![cfg(feature = "accesskit")] -use accesskit::{Role, TreeUpdate}; -use egui::{CentralPanel, Context, RawInput}; +use egui::{ + accesskit::{NodeId, Role, TreeUpdate}, + CentralPanel, Context, RawInput, Window, +}; /// Baseline test that asserts there are no spurious nodes in the /// accesskit output when the ui is empty. @@ -46,7 +47,7 @@ fn button_node() { .find(|(_, node)| node.role() == Role::Button) .expect("Button should exist in the accesskit output"); - assert_eq!(button.name(), Some(button_text)); + assert_eq!(button.label(), Some(button_text)); assert!(!button.is_disabled()); } @@ -72,7 +73,7 @@ fn disabled_button_node() { .find(|(_, node)| node.role() == Role::Button) .expect("Button should exist in the accesskit output"); - assert_eq!(button.name(), Some(button_text)); + assert_eq!(button.label(), Some(button_text)); assert!(button.is_disabled()); } @@ -97,7 +98,7 @@ fn toggle_button_node() { .find(|(_, node)| node.role() == Role::Button) .expect("Toggle button should exist in the accesskit output"); - assert_eq!(toggle.name(), Some(button_text)); + assert_eq!(toggle.label(), Some(button_text)); assert!(!toggle.is_disabled()); } @@ -130,8 +131,30 @@ fn multiple_disabled_widgets() { ); } +#[test] +fn window_children() { + let output = accesskit_output_single_egui_frame(|ctx| { + let mut open = true; + Window::new("test window") + .open(&mut open) + .resizable(false) + .show(ctx, |ui| { + let _ = ui.button("A button"); + }); + }); + + let root = output.tree.as_ref().map(|tree| tree.root).unwrap(); + + let window_id = assert_window_exists(&output, "test window", root); + assert_button_exists(&output, "A button", window_id); + assert_button_exists(&output, "Close window", window_id); + assert_button_exists(&output, "Hide", window_id); +} + fn accesskit_output_single_egui_frame(run_ui: impl FnMut(&Context)) -> TreeUpdate { let ctx = Context::default(); + // Disable animations, so we do not need to wait for animations to end to see the result. + ctx.style_mut(|style| style.animation_time = 0.0); ctx.enable_accesskit(); let output = ctx.run(RawInput::default(), run_ui); @@ -141,3 +164,45 @@ fn accesskit_output_single_egui_frame(run_ui: impl FnMut(&Context)) -> TreeUpdat .accesskit_update .expect("Missing accesskit update") } + +#[track_caller] +fn assert_button_exists(tree: &TreeUpdate, label: &str, parent: NodeId) { + let (node_id, _) = tree + .nodes + .iter() + .find(|(_, node)| { + !node.is_hidden() && node.role() == Role::Button && node.label() == Some(label) + }) + .expect("No visible button with that label exists."); + + assert_parent_child(tree, parent, *node_id); +} + +#[track_caller] +fn assert_window_exists(tree: &TreeUpdate, title: &str, parent: NodeId) -> NodeId { + let (node_id, _) = tree + .nodes + .iter() + .find(|(_, node)| { + !node.is_hidden() && node.role() == Role::Window && node.label() == Some(title) + }) + .expect("No visible window with that title exists."); + + assert_parent_child(tree, parent, *node_id); + + *node_id +} + +#[track_caller] +fn assert_parent_child(tree: &TreeUpdate, parent: NodeId, child: NodeId) { + let (_, parent) = tree + .nodes + .iter() + .find(|(id, _)| id == &parent) + .expect("Parent does not exist."); + + assert!( + parent.children().contains(&child), + "Node is not a child of the given parent." + ); +} diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs new file mode 100644 index 00000000000..9493d5443f4 --- /dev/null +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -0,0 +1,29 @@ +use egui::Button; +use egui_kittest::{kittest::Queryable, Harness}; + +#[test] +pub fn focus_should_skip_over_disabled_buttons() { + let mut harness = Harness::new_ui(|ui| { + ui.add(Button::new("Button 1")); + ui.add_enabled(false, Button::new("Button Disabled")); + ui.add(Button::new("Button 3")); + }); + + harness.press_key(egui::Key::Tab); + harness.run(); + + let button_1 = harness.get_by_label("Button 1"); + assert!(button_1.is_focused()); + + harness.press_key(egui::Key::Tab); + harness.run(); + + let button_3 = harness.get_by_label("Button 3"); + assert!(button_3.is_focused()); + + harness.press_key(egui::Key::Tab); + harness.run(); + + let button_1 = harness.get_by_label("Button 1"); + assert!(button_1.is_focused()); +} diff --git a/crates/egui_kittest/tests/snapshots/readme_example.png b/crates/egui_kittest/tests/snapshots/readme_example.png new file mode 100644 index 00000000000..ef0774162da --- /dev/null +++ b/crates/egui_kittest/tests/snapshots/readme_example.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31bd906040fcc356c19dc36036fbfd2a28dfcef54c7a073f584f4a9abddbdb4c +size 1699 diff --git a/crates/egui_kittest/tests/snapshots/test_shrink.png b/crates/egui_kittest/tests/snapshots/test_shrink.png new file mode 100644 index 00000000000..10967a3d52a --- /dev/null +++ b/crates/egui_kittest/tests/snapshots/test_shrink.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7008bdb595a19782c4f724bed363e51bd93121f5211186aa0e8014c8ba1007c2 +size 3005 diff --git a/crates/egui_kittest/tests/tests.rs b/crates/egui_kittest/tests/tests.rs new file mode 100644 index 00000000000..6799b9a3567 --- /dev/null +++ b/crates/egui_kittest/tests/tests.rs @@ -0,0 +1,15 @@ +use egui_kittest::Harness; + +#[test] +fn test_shrink() { + let mut harness = Harness::new_ui(|ui| { + ui.label("Hello, world!"); + ui.separator(); + ui.label("This is a test"); + }); + + harness.fit_contents(); + + #[cfg(all(feature = "snapshot", feature = "wgpu"))] + harness.wgpu_snapshot("test_shrink"); +} diff --git a/crates/emath/src/numeric.rs b/crates/emath/src/numeric.rs index 03d00077129..9a7814b23d2 100644 --- a/crates/emath/src/numeric.rs +++ b/crates/emath/src/numeric.rs @@ -18,8 +18,8 @@ macro_rules! impl_numeric_float { ($t: ident) => { impl Numeric for $t { const INTEGRAL: bool = false; - const MIN: Self = std::$t::MIN; - const MAX: Self = std::$t::MAX; + const MIN: Self = $t::MIN; + const MAX: Self = $t::MAX; #[inline(always)] fn to_f64(self) -> f64 { @@ -44,8 +44,8 @@ macro_rules! impl_numeric_integer { ($t: ident) => { impl Numeric for $t { const INTEGRAL: bool = true; - const MIN: Self = std::$t::MIN; - const MAX: Self = std::$t::MAX; + const MIN: Self = $t::MIN; + const MAX: Self = $t::MAX; #[inline(always)] fn to_f64(self) -> f64 { diff --git a/crates/emath/src/rect.rs b/crates/emath/src/rect.rs index 6c0677ad55e..8b655bd722f 100644 --- a/crates/emath/src/rect.rs +++ b/crates/emath/src/rect.rs @@ -1,4 +1,3 @@ -use std::f32::INFINITY; use std::fmt; use crate::{lerp, pos2, vec2, Div, Mul, Pos2, Rangef, Rot2, Vec2}; @@ -33,8 +32,8 @@ pub struct Rect { impl Rect { /// Infinite rectangle that contains every point. pub const EVERYTHING: Self = Self { - min: pos2(-INFINITY, -INFINITY), - max: pos2(INFINITY, INFINITY), + min: pos2(-f32::INFINITY, -f32::INFINITY), + max: pos2(f32::INFINITY, f32::INFINITY), }; /// The inverse of [`Self::EVERYTHING`]: stretches from positive infinity to negative infinity. @@ -53,8 +52,8 @@ impl Rect { /// assert_eq!(rect, Rect::from_min_max(pos2(0.0, 1.0), pos2(2.0, 3.0))) /// ``` pub const NOTHING: Self = Self { - min: pos2(INFINITY, INFINITY), - max: pos2(-INFINITY, -INFINITY), + min: pos2(f32::INFINITY, f32::INFINITY), + max: pos2(-f32::INFINITY, -f32::INFINITY), }; /// An invalid [`Rect`] filled with [`f32::NAN`]. @@ -650,6 +649,8 @@ impl Rect { /// /// A ray that starts inside the rect will return `true`. pub fn intersects_ray(&self, o: Pos2, d: Vec2) -> bool { + debug_assert!(d.is_normalized(), "expected normalized direction"); + let mut tmin = -f32::INFINITY; let mut tmax = f32::INFINITY; @@ -671,6 +672,32 @@ impl Rect { 0.0 <= tmax && tmin <= tmax } + + /// Where does a ray from the center intersect the rectangle? + /// + /// `d` is the direction of the ray and assumed to be normalized. + pub fn intersects_ray_from_center(&self, d: Vec2) -> Pos2 { + debug_assert!(d.is_normalized(), "expected normalized direction"); + + let mut tmin = f32::NEG_INFINITY; + let mut tmax = f32::INFINITY; + + for i in 0..2 { + let inv_d = 1.0 / -d[i]; + let mut t0 = (self.min[i] - self.center()[i]) * inv_d; + let mut t1 = (self.max[i] - self.center()[i]) * inv_d; + + if inv_d < 0.0 { + std::mem::swap(&mut t0, &mut t1); + } + + tmin = tmin.max(t0); + tmax = tmax.min(t1); + } + + let t = tmax.min(tmin); + self.center() + t * -d + } } impl fmt::Debug for Rect { @@ -793,4 +820,57 @@ mod tests { println!("Leftward ray from right:"); assert!(rect.intersects_ray(pos2(4.0, 2.0), Vec2::LEFT)); } + + #[test] + fn test_ray_from_center_intersection() { + let rect = Rect::from_min_max(pos2(1.0, 1.0), pos2(3.0, 3.0)); + + assert_eq!( + rect.intersects_ray_from_center(Vec2::RIGHT), + pos2(3.0, 2.0), + "rightward ray" + ); + + assert_eq!( + rect.intersects_ray_from_center(Vec2::UP), + pos2(2.0, 1.0), + "upward ray" + ); + + assert_eq!( + rect.intersects_ray_from_center(Vec2::LEFT), + pos2(1.0, 2.0), + "leftward ray" + ); + + assert_eq!( + rect.intersects_ray_from_center(Vec2::DOWN), + pos2(2.0, 3.0), + "downward ray" + ); + + assert_eq!( + rect.intersects_ray_from_center((Vec2::LEFT + Vec2::DOWN).normalized()), + pos2(1.0, 3.0), + "bottom-left corner ray" + ); + + assert_eq!( + rect.intersects_ray_from_center((Vec2::LEFT + Vec2::UP).normalized()), + pos2(1.0, 1.0), + "top-left corner ray" + ); + + assert_eq!( + rect.intersects_ray_from_center((Vec2::RIGHT + Vec2::DOWN).normalized()), + pos2(3.0, 3.0), + "bottom-right corner ray" + ); + + assert_eq!( + rect.intersects_ray_from_center((Vec2::RIGHT + Vec2::UP).normalized()), + pos2(3.0, 1.0), + "top-right corner ray" + ); + } } diff --git a/crates/emath/src/smart_aim.rs b/crates/emath/src/smart_aim.rs index 88b807cf80b..72094706995 100644 --- a/crates/emath/src/smart_aim.rs +++ b/crates/emath/src/smart_aim.rs @@ -138,7 +138,9 @@ fn test_aim() { assert_eq!(best_in_range_f64(99.999, 100.000), 100.0); assert_eq!(best_in_range_f64(10.001, 100.001), 100.0); - use std::f64::{INFINITY, NAN, NEG_INFINITY}; + const NAN: f64 = f64::NAN; + const INFINITY: f64 = f64::INFINITY; + const NEG_INFINITY: f64 = f64::NEG_INFINITY; assert!(best_in_range_f64(NAN, NAN).is_nan()); assert_eq!(best_in_range_f64(NAN, 1.2), 1.2); assert_eq!(best_in_range_f64(NAN, INFINITY), INFINITY); diff --git a/crates/emath/src/vec2.rs b/crates/emath/src/vec2.rs index 03e0715c20a..9a173348b03 100644 --- a/crates/emath/src/vec2.rs +++ b/crates/emath/src/vec2.rs @@ -176,6 +176,12 @@ impl Vec2 { } } + /// Checks if `self` has length `1.0` up to a precision of `1e-6`. + #[inline(always)] + pub fn is_normalized(self) -> bool { + (self.length_sq() - 1.0).abs() < 2e-6 + } + /// Rotates the vector by 90°, i.e positive X to positive Y /// (clockwise in egui coordinates). #[inline(always)] @@ -497,8 +503,10 @@ impl fmt::Display for Vec2 { } } -#[test] -fn test_vec2() { +#[cfg(test)] +mod test { + use super::*; + macro_rules! almost_eq { ($left: expr, $right: expr) => { let left = $left; @@ -506,32 +514,58 @@ fn test_vec2() { assert!((left - right).abs() < 1e-6, "{} != {}", left, right); }; } - use std::f32::consts::TAU; - assert_eq!(Vec2::ZERO.angle(), 0.0); - assert_eq!(Vec2::angled(0.0).angle(), 0.0); - assert_eq!(Vec2::angled(1.0).angle(), 1.0); - assert_eq!(Vec2::X.angle(), 0.0); - assert_eq!(Vec2::Y.angle(), 0.25 * TAU); + #[test] + fn test_vec2() { + use std::f32::consts::TAU; + + assert_eq!(Vec2::ZERO.angle(), 0.0); + assert_eq!(Vec2::angled(0.0).angle(), 0.0); + assert_eq!(Vec2::angled(1.0).angle(), 1.0); + assert_eq!(Vec2::X.angle(), 0.0); + assert_eq!(Vec2::Y.angle(), 0.25 * TAU); + + assert_eq!(Vec2::RIGHT.angle(), 0.0); + assert_eq!(Vec2::DOWN.angle(), 0.25 * TAU); + almost_eq!(Vec2::LEFT.angle(), 0.50 * TAU); + assert_eq!(Vec2::UP.angle(), -0.25 * TAU); - assert_eq!(Vec2::RIGHT.angle(), 0.0); - assert_eq!(Vec2::DOWN.angle(), 0.25 * TAU); - almost_eq!(Vec2::LEFT.angle(), 0.50 * TAU); - assert_eq!(Vec2::UP.angle(), -0.25 * TAU); + let mut assignment = vec2(1.0, 2.0); + assignment += vec2(3.0, 4.0); + assert_eq!(assignment, vec2(4.0, 6.0)); - let mut assignment = vec2(1.0, 2.0); - assignment += vec2(3.0, 4.0); - assert_eq!(assignment, vec2(4.0, 6.0)); + let mut assignment = vec2(4.0, 6.0); + assignment -= vec2(1.0, 2.0); + assert_eq!(assignment, vec2(3.0, 4.0)); - let mut assignment = vec2(4.0, 6.0); - assignment -= vec2(1.0, 2.0); - assert_eq!(assignment, vec2(3.0, 4.0)); + let mut assignment = vec2(1.0, 2.0); + assignment *= 2.0; + assert_eq!(assignment, vec2(2.0, 4.0)); - let mut assignment = vec2(1.0, 2.0); - assignment *= 2.0; - assert_eq!(assignment, vec2(2.0, 4.0)); + let mut assignment = vec2(2.0, 4.0); + assignment /= 2.0; + assert_eq!(assignment, vec2(1.0, 2.0)); + } + + #[test] + fn test_vec2_normalized() { + fn generate_spiral(n: usize, start: Vec2, end: Vec2) -> impl Iterator { + let angle_step = 2.0 * std::f32::consts::PI / n as f32; + let radius_step = (end.length() - start.length()) / n as f32; + + (0..n).map(move |i| { + let angle = i as f32 * angle_step; + let radius = start.length() + i as f32 * radius_step; + let x = radius * angle.cos(); + let y = radius * angle.sin(); + vec2(x, y) + }) + } - let mut assignment = vec2(2.0, 4.0); - assignment /= 2.0; - assert_eq!(assignment, vec2(1.0, 2.0)); + for v in generate_spiral(40, Vec2::splat(0.1), Vec2::splat(2.0)) { + let vn = v.normalized(); + almost_eq!(vn.length(), 1.0); + assert!(vn.is_normalized()); + } + } } diff --git a/crates/emath/src/vec2b.rs b/crates/emath/src/vec2b.rs index f241de64ed5..673f2959e0f 100644 --- a/crates/emath/src/vec2b.rs +++ b/crates/emath/src/vec2b.rs @@ -1,3 +1,5 @@ +use crate::Vec2; + /// Two bools, one for each axis (X and Y). #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -43,6 +45,12 @@ impl Vec2b { y: self.y || other.y, } } + + /// Convert to a float `Vec2` where the components are 1.0 for `true` and 0.0 for `false`. + #[inline] + pub fn to_vec2(self) -> Vec2 { + Vec2::new(self.x.into(), self.y.into()) + } } impl From for Vec2b { diff --git a/crates/epaint/CHANGELOG.md b/crates/epaint/CHANGELOG.md index dda7d6e9edb..742cf0acd78 100644 --- a/crates/epaint/CHANGELOG.md +++ b/crates/epaint/CHANGELOG.md @@ -5,6 +5,17 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.30.0 - 2024-12-16 +* Expand max font atlas size from 8k to 16k [#5257](https://github.com/emilk/egui/pull/5257) by [@rustbasic](https://github.com/rustbasic) +* Put font data into `Arc` to reduce memory consumption [#5276](https://github.com/emilk/egui/pull/5276) by [@StarStarJ](https://github.com/StarStarJ) +* Reduce aliasing when painting thin box outlines [#5484](https://github.com/emilk/egui/pull/5484) by [@emilk](https://github.com/emilk) +* Fix zero-width strokes still affecting the feathering color of boxes [#5485](https://github.com/emilk/egui/pull/5485) by [@emilk](https://github.com/emilk) + + +## 0.29.1 - 2024-10-01 +Nothing new + + ## 0.29.0 - 2024-09-26 ### 🚀 Performance * Optimize `Color32::from_rgba_unmultiplied` with LUT [#5088](https://github.com/emilk/egui/pull/5088) by [@YgorSouza](https://github.com/YgorSouza) diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml index 7815189dc5a..0a4afde26b5 100644 --- a/crates/epaint/Cargo.toml +++ b/crates/epaint/Cargo.toml @@ -55,11 +55,6 @@ log = ["dep:log"] ## [`mint`](https://docs.rs/mint) enables interoperability with other math libraries such as [`glam`](https://docs.rs/glam) and [`nalgebra`](https://docs.rs/nalgebra). mint = ["emath/mint"] -## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -## -## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. -puffin = ["dep:puffin"] - ## Enable parallel tessellation using [`rayon`](https://docs.rs/rayon). ## ## This can help performance for graphics-intense applications. @@ -79,6 +74,7 @@ ab_glyph = "0.2.11" ahash.workspace = true nohash-hasher.workspace = true parking_lot.workspace = true # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios. +profiling = { workspace = true} #! ### Optional dependencies bytemuck = { workspace = true, optional = true, features = ["derive"] } @@ -87,7 +83,6 @@ bytemuck = { workspace = true, optional = true, features = ["derive"] } document-features = { workspace = true, optional = true } log = { workspace = true, optional = true } -puffin = { workspace = true, optional = true } rayon = { version = "1.7", optional = true } ## Allow serialization using [`serde`](https://docs.rs/serde) . diff --git a/crates/epaint/src/bezier.rs b/crates/epaint/src/bezier.rs index 7c55352cfeb..cab0d29f918 100644 --- a/crates/epaint/src/bezier.rs +++ b/crates/epaint/src/bezier.rs @@ -172,7 +172,7 @@ impl CubicBezierShape { } } - // copied from lyon::geom::flattern_cubic.rs + // copied from // Computes the number of quadratic bézier segments to approximate a cubic one. // Derived by Raph Levien from section 10.6 of Sedeberg's CAGD notes // https://scholarsarchive.byu.edu/cgi/viewcontent.cgi?article=1000&context=facpub#section.10.6 @@ -207,17 +207,21 @@ impl CubicBezierShape { /// B.x = (P3.x - 3 * P2.x + 3 * P1.x - P0.x) * t^3 + (3 * P2.x - 6 * P1.x + 3 * P0.x) * t^2 + (3 * P1.x - 3 * P0.x) * t + P0.x /// B.y = (P3.y - 3 * P2.y + 3 * P1.y - P0.y) * t^3 + (3 * P2.y - 6 * P1.y + 3 * P0.y) * t^2 + (3 * P1.y - 3 * P0.y) * t + P0.y /// Combine the above three equations and iliminate B.x and B.y, we get: + /// ```text /// t^3 * ( (P3.x - 3*P2.x + 3*P1.x - P0.x) * (P3.y - P0.y) - (P3.y - 3*P2.y + 3*P1.y - P0.y) * (P3.x - P0.x)) /// + t^2 * ( (3 * P2.x - 6 * P1.x + 3 * P0.x) * (P3.y - P0.y) - (3 * P2.y - 6 * P1.y + 3 * P0.y) * (P3.x - P0.x)) /// + t^1 * ( (3 * P1.x - 3 * P0.x) * (P3.y - P0.y) - (3 * P1.y - 3 * P0.y) * (P3.x - P0.x)) /// + (P0.x * (P3.y - P0.y) - P0.y * (P3.x - P0.x)) + P0.x * (P0.y - P3.y) + P0.y * (P3.x - P0.x) /// = 0 - /// or a * t^3 + b * t^2 + c * t + d = 0 + /// ``` + /// or `a * t^3 + b * t^2 + c * t + d = 0` /// /// let x = t - b / (3 * a), then we have: + /// ```text /// x^3 + p * x + q = 0, where: /// p = (3.0 * a * c - b^2) / (3.0 * a^2) /// q = (2.0 * b^3 - 9.0 * a * b * c + 27.0 * a^2 * d) / (27.0 * a^3) + /// ``` /// /// when p > 0, there will be one real root, two complex roots /// when p = 0, there will be two real roots, when p=q=0, there will be three real roots but all 0. diff --git a/crates/epaint/src/image.rs b/crates/epaint/src/image.rs index 454969d75cb..9a204a12199 100644 --- a/crates/epaint/src/image.rs +++ b/crates/epaint/src/image.rs @@ -124,6 +124,7 @@ impl ColorImage { /// Create a [`ColorImage`] from iterator over flat opaque gray data. /// /// Panics if `size[0] * size[1] != gray_iter.len()`. + #[doc(alias = "from_grey_iter")] pub fn from_gray_iter(size: [usize; 2], gray_iter: impl Iterator) -> Self { let pixels: Vec<_> = gray_iter.map(Color32::from_gray).collect(); assert_eq!(size[0] * size[1], pixels.len()); @@ -153,8 +154,10 @@ impl ColorImage { let max_x = (region.max.x * pixels_per_point) as usize; let min_y = (region.min.y * pixels_per_point) as usize; let max_y = (region.max.y * pixels_per_point) as usize; - assert!(min_x <= max_x); - assert!(min_y <= max_y); + assert!( + min_x <= max_x && min_y <= max_y, + "Screenshot region is invalid: {region:?}" + ); let width = max_x - min_x; let height = max_y - min_y; let mut output = Vec::with_capacity(width * height); @@ -301,7 +304,9 @@ impl FontImage { /// If you are having problems with text looking skinny and pixelated, try using a low gamma, e.g. `0.4`. #[inline] pub fn srgba_pixels(&self, gamma: Option) -> impl ExactSizeIterator + '_ { - let gamma = gamma.unwrap_or(0.55); // TODO(emilk): this default coverage gamma is a magic constant, chosen by eye. I don't even know why we need it. + // TODO(emilk): this default coverage gamma is a magic constant, chosen by eye. I don't even know why we need it. + // Maybe we need to implement the ideas in https://hikogui.org/2022/10/24/the-trouble-with-anti-aliasing.html + let gamma = gamma.unwrap_or(0.55); self.pixels.iter().map(move |coverage| { let alpha = coverage.powf(gamma); // We want to multiply with `vec4(alpha)` in the fragment shader: diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index 4dee42381cd..89664cdb844 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -143,33 +143,3 @@ pub enum Primitive { /// Was epaint compiled with the `rayon` feature? pub const HAS_RAYON: bool = cfg!(feature = "rayon"); - -// --------------------------------------------------------------------------- - -mod profiling_scopes { - #![allow(unused_macros)] - #![allow(unused_imports)] - - /// Profiling macro for feature "puffin" - macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} - -#[allow(unused_imports)] -pub(crate) use profiling_scopes::{profile_function, profile_scope}; diff --git a/crates/epaint/src/mesh.rs b/crates/epaint/src/mesh.rs index ba37e715856..2447cad293e 100644 --- a/crates/epaint/src/mesh.rs +++ b/crates/epaint/src/mesh.rs @@ -85,7 +85,7 @@ impl Mesh { /// Are all indices within the bounds of the contained vertices? pub fn is_valid(&self) -> bool { - crate::profile_function!(); + profiling::function_scope!(); if let Ok(n) = u32::try_from(self.vertices.len()) { self.indices.iter().all(|&i| i < n) @@ -111,7 +111,7 @@ impl Mesh { /// /// Panics when `other` mesh has a different texture. pub fn append(&mut self, other: Self) { - crate::profile_function!(); + profiling::function_scope!(); debug_assert!(other.is_valid()); if self.is_empty() { diff --git a/crates/epaint/src/mutex.rs b/crates/epaint/src/mutex.rs index 157701c2be0..bd984a1d0ec 100644 --- a/crates/epaint/src/mutex.rs +++ b/crates/epaint/src/mutex.rs @@ -75,7 +75,7 @@ mod mutex_impl { // Detect if we are recursively taking out a lock on this mutex. // use a pointer to the inner data as an id for this lock - let ptr = (&self.0 as *const parking_lot::Mutex<_>).cast::<()>(); + let ptr = std::ptr::from_ref::>(&self.0).cast::<()>(); // Store it in thread local storage while we have a lock guard taken out HELD_LOCKS_TLS.with(|held_locks| { diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index d69a832efb4..6f67a2bc6a6 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -1001,7 +1001,7 @@ pub struct TextShape { pub underline: Stroke, /// Any [`Color32::PLACEHOLDER`] in the galley will be replaced by the given color. - /// Affects everything: backgrounds, glyphs, strikethough, underline, etc. + /// Affects everything: backgrounds, glyphs, strikethrough, underline, etc. pub fallback_color: Color32, /// If set, the text color in the galley will be ignored and replaced @@ -1011,7 +1011,7 @@ pub struct TextShape { pub override_text_color: Option, /// If set, the text will be rendered with the given opacity in gamma space - /// Affects everything: backgrounds, glyphs, strikethough, underline, etc. + /// Affects everything: backgrounds, glyphs, strikethrough, underline, etc. pub opacity_factor: f32, /// Rotate text by this many radians clockwise. diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 399a602c259..63412cdc013 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -161,10 +161,15 @@ where impl From for PathStroke { fn from(value: Stroke) -> Self { - Self { - width: value.width, - color: ColorMode::Solid(value.color), - kind: StrokeKind::default(), + if value.is_empty() { + // Important, since we use the stroke color when doing feathering of the fill! + Self::NONE + } else { + Self { + width: value.width, + color: ColorMode::Solid(value.color), + kind: StrokeKind::default(), + } } } } diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 0c15b7dba09..ebd22f55303 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -502,6 +502,8 @@ impl Path { /// Calling this may reverse the vertices in the path if they are wrong winding order. /// /// The preferred winding order is clockwise. + /// + /// The stroke colors is used for color-correct feathering. pub fn fill(&mut self, feathering: f32, color: Color32, stroke: &PathStroke, out: &mut Mesh) { fill_closed_path(feathering, &mut self.0, color, stroke, out); } @@ -918,7 +920,7 @@ fn stroke_path( ) { let n = path.len() as u32; - if stroke.width <= 0.0 || stroke.color == ColorMode::TRANSPARENT || n < 2 { + if stroke.is_empty() || n < 2 { return; } @@ -1277,6 +1279,11 @@ impl Tessellator { ((point * self.pixels_per_point - 0.5).round() + 0.5) / self.pixels_per_point } + #[inline(always)] + pub fn round_pos_to_pixel(&self, pos: Pos2) -> Pos2 { + pos2(self.round_to_pixel(pos.x), self.round_to_pixel(pos.y)) + } + #[inline(always)] pub fn round_pos_to_pixel_center(&self, pos: Pos2) -> Pos2 { pos2( @@ -1363,7 +1370,7 @@ impl Tessellator { self.tessellate_ellipse(ellipse, out); } Shape::Mesh(mesh) => { - crate::profile_scope!("mesh"); + profiling::scope!("mesh"); if self.options.validate_meshes && !mesh.is_valid() { debug_assert!(false, "Invalid Mesh in Shape::Mesh"); @@ -1596,7 +1603,7 @@ impl Tessellator { return; } - crate::profile_function!(); + profiling::function_scope!(); let PathShape { points, @@ -1702,6 +1709,20 @@ impl Tessellator { self.tessellate_line(line, stroke, out); // …and forth } } else { + let rect = if !stroke.is_empty() && stroke.width < self.feathering { + // Very thin rectangle strokes create extreme aliasing when they move around. + // We can fix that by rounding the rectangle corners to pixel centers. + // TODO(#5164): maybe do this for all shapes and stroke sizes + // TODO(emilk): since we use StrokeKind::Outside, we should probably round the + // corners after offsetting them with half the stroke width (see `translate_stroke_point`). + Rect { + min: self.round_pos_to_pixel_center(rect.min), + max: self.round_pos_to_pixel_center(rect.max), + } + } else { + rect + }; + let path = &mut self.scratchpad_path; path.clear(); path::rounded_rectangle(&mut self.scratchpad_points, rect, rounding); @@ -1977,7 +1998,7 @@ impl Tessellator { /// A list of clip rectangles with matching [`Mesh`]. #[allow(unused_mut)] pub fn tessellate_shapes(&mut self, mut shapes: Vec) -> Vec { - crate::profile_function!(); + profiling::function_scope!(); #[cfg(feature = "rayon")] if self.options.parallel_tessellation { @@ -1987,7 +2008,7 @@ impl Tessellator { let mut clipped_primitives: Vec = Vec::default(); { - crate::profile_scope!("tessellate"); + profiling::scope!("tessellate"); for clipped_shape in shapes { self.tessellate_clipped_shape(clipped_shape, &mut clipped_primitives); } @@ -2024,13 +2045,13 @@ impl Tessellator { /// then replace the original shape with their tessellated meshes. #[cfg(feature = "rayon")] fn parallel_tessellation_of_large_shapes(&self, shapes: &mut [ClippedShape]) { - crate::profile_function!(); + profiling::function_scope!(); use rayon::prelude::*; // We only parallelize large/slow stuff, because each tessellation job - // will allocate a new Mesh, and so it creates a lot of extra memory framentation - // and callocations that is only worth it for large shapes. + // will allocate a new Mesh, and so it creates a lot of extra memory fragmentation + // and allocations that is only worth it for large shapes. fn should_parallelize(shape: &Shape) -> bool { match shape { Shape::Vec(shapes) => 4 < shapes.len() || shapes.iter().any(should_parallelize), @@ -2054,7 +2075,7 @@ impl Tessellator { .enumerate() .filter(|(_, clipped_shape)| should_parallelize(&clipped_shape.shape)) .map(|(index, clipped_shape)| { - crate::profile_scope!("tessellate_big_shape"); + profiling::scope!("tessellate_big_shape"); // TODO(emilk): reuse tessellator in a thread local let mut tessellator = (*self).clone(); let mut mesh = Mesh::default(); @@ -2063,7 +2084,7 @@ impl Tessellator { }) .collect(); - crate::profile_scope!("distribute results", tessellated.len().to_string()); + profiling::scope!("distribute results", tessellated.len().to_string()); for (index, mesh) in tessellated { shapes[index].shape = Shape::Mesh(mesh); } diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 42398e5f4db..4be2fdfe8d0 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -224,7 +224,11 @@ fn ab_glyph_font_from_font_data(name: &str, data: &FontData) -> ab_glyph::FontAr /// /// // Install my own font (maybe supporting non-latin characters): /// fonts.font_data.insert("my_font".to_owned(), -/// FontData::from_static(include_bytes!("../../../epaint_default_fonts/fonts/Ubuntu-Light.ttf"))); // .ttf and .otf supported +/// std::sync::Arc::new( +/// // .ttf and .otf supported +/// FontData::from_static(include_bytes!("../../../epaint_default_fonts/fonts/Ubuntu-Light.ttf")) +/// ) +/// ); /// /// // Put my font first (highest priority): /// fonts.families.get_mut(&FontFamily::Proportional).unwrap() @@ -243,7 +247,7 @@ pub struct FontDefinitions { /// List of font names and their definitions. /// /// `epaint` has built-in-default for these, but you can override them if you like. - pub font_data: BTreeMap, + pub font_data: BTreeMap>, /// Which fonts (names) to use for each [`FontFamily`]. /// @@ -254,6 +258,50 @@ pub struct FontDefinitions { pub families: BTreeMap>, } +#[derive(Debug, Clone)] +pub struct FontInsert { + /// Font name + pub name: String, + + /// A `.ttf` or `.otf` file and a font face index. + pub data: FontData, + + /// Sets the font family and priority + pub families: Vec, +} + +#[derive(Debug, Clone)] +pub struct InsertFontFamily { + /// Font family + pub family: FontFamily, + + /// Fallback or Primary font + pub priority: FontPriority, +} + +#[derive(Debug, Clone)] +pub enum FontPriority { + /// Prefer this font before all existing ones. + /// + /// If a desired glyph exists in this font, it will be used. + Highest, + + /// Use this font as a fallback, after all existing ones. + /// + /// This font will only be used if the glyph is not found in any of the previously installed fonts. + Lowest, +} + +impl FontInsert { + pub fn new(name: &str, data: FontData, families: Vec) -> Self { + Self { + name: name.to_owned(), + data, + families, + } + } +} + impl Default for FontDefinitions { /// Specifies the default fonts if the feature `default_fonts` is enabled, /// otherwise this is the same as [`Self::empty`]. @@ -266,33 +314,36 @@ impl Default for FontDefinitions { /// otherwise this is the same as [`Self::empty`]. #[cfg(feature = "default_fonts")] fn default() -> Self { - let mut font_data: BTreeMap = BTreeMap::new(); + let mut font_data: BTreeMap> = BTreeMap::new(); let mut families = BTreeMap::new(); - font_data.insert("Hack".to_owned(), FontData::from_static(HACK_REGULAR)); + font_data.insert( + "Hack".to_owned(), + Arc::new(FontData::from_static(HACK_REGULAR)), + ); // Some good looking emojis. Use as first priority: font_data.insert( "NotoEmoji-Regular".to_owned(), - FontData::from_static(NOTO_EMOJI_REGULAR).tweak(FontTweak { + Arc::new(FontData::from_static(NOTO_EMOJI_REGULAR).tweak(FontTweak { scale: 0.81, // Make smaller ..Default::default() - }), + })), ); font_data.insert( "Ubuntu-Light".to_owned(), - FontData::from_static(UBUNTU_LIGHT), + Arc::new(FontData::from_static(UBUNTU_LIGHT)), ); // Bigger emojis, and more. : font_data.insert( "emoji-icon-font".to_owned(), - FontData::from_static(EMOJI_ICON).tweak(FontTweak { + Arc::new(FontData::from_static(EMOJI_ICON).tweak(FontTweak { scale: 0.90, // Make smaller ..Default::default() - }), + })), ); families.insert( @@ -589,7 +640,7 @@ impl FontsImpl { "pixels_per_point out of range: {pixels_per_point}" ); - let texture_width = max_texture_side.at_most(8 * 1024); + let texture_width = max_texture_side.at_most(16 * 1024); let initial_height = 32; // Keep initial font atlas small, so it is fast to upload to GPU. This will expand as needed anyways. let atlas = TextureAtlas::new([texture_width, initial_height]); @@ -751,7 +802,7 @@ impl FontImplCache { pub fn new( atlas: Arc>, pixels_per_point: f32, - font_data: &BTreeMap, + font_data: &BTreeMap>, ) -> Self { let ab_glyph_fonts = font_data .iter() diff --git a/crates/epaint/src/text/mod.rs b/crates/epaint/src/text/mod.rs index ca1bc314c22..3cb0e98cbc5 100644 --- a/crates/epaint/src/text/mod.rs +++ b/crates/epaint/src/text/mod.rs @@ -10,7 +10,10 @@ mod text_layout_types; pub const TAB_SIZE: usize = 4; pub use { - fonts::{FontData, FontDefinitions, FontFamily, FontId, FontTweak, Fonts, FontsImpl}, + fonts::{ + FontData, FontDefinitions, FontFamily, FontId, FontInsert, FontPriority, FontTweak, Fonts, + FontsImpl, InsertFontFamily, + }, text_layout::layout, text_layout_types::*, }; diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 17826e6afb1..64dd827148c 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -623,10 +623,10 @@ pub struct Glyph { /// The row/line height of this font. pub font_height: f32, - /// The ascent of the sub-font within the font ("FontImpl"). + /// The ascent of the sub-font within the font (`FontImpl`). pub font_impl_ascent: f32, - /// The row/line height of the sub-font within the font ("FontImpl"). + /// The row/line height of the sub-font within the font (`FontImpl`). pub font_impl_height: f32, /// Position and size of the glyph in the font texture, in texels. diff --git a/crates/epaint/src/texture_atlas.rs b/crates/epaint/src/texture_atlas.rs index b0f0ad03881..7ea76f8722d 100644 --- a/crates/epaint/src/texture_atlas.rs +++ b/crates/epaint/src/texture_atlas.rs @@ -159,8 +159,8 @@ impl TextureAtlas { } fn max_height(&self) -> usize { - // the initial width is likely the max texture side size - self.image.width() + // the initial width is set to the max size + self.image.height().max(self.image.width()) } /// When this get high, it might be time to clear and start over! diff --git a/crates/epaint/src/texture_handle.rs b/crates/epaint/src/texture_handle.rs index f4142d91510..1f640a171de 100644 --- a/crates/epaint/src/texture_handle.rs +++ b/crates/epaint/src/texture_handle.rs @@ -66,6 +66,7 @@ impl TextureHandle { } /// Assign a new image to an existing texture. + #[allow(clippy::needless_pass_by_ref_mut)] // Intentionally hide interiority of mutability pub fn set(&mut self, image: impl Into, options: TextureOptions) { self.tex_mngr .write() @@ -73,6 +74,7 @@ impl TextureHandle { } /// Assign a new image to a subregion of the whole texture. + #[allow(clippy::needless_pass_by_ref_mut)] // Intentionally hide interiority of mutability pub fn set_partial( &mut self, pos: [usize; 2], diff --git a/crates/epaint_default_fonts/CHANGELOG.md b/crates/epaint_default_fonts/CHANGELOG.md index b41f316d39f..42cd89ba51a 100644 --- a/crates/epaint_default_fonts/CHANGELOG.md +++ b/crates/epaint_default_fonts/CHANGELOG.md @@ -5,6 +5,14 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.30.0 - 2024-12-16 +Nothing new + + +## 0.29.1 - 2024-10-01 +Nothing new + + ## 0.29.0 - 2024-09-26 * Nothing new diff --git a/deny.toml b/deny.toml index f5291700dfc..ede37dea405 100644 --- a/deny.toml +++ b/deny.toml @@ -31,11 +31,9 @@ all-features = true [advisories] version = 2 ignore = [ - "RUSTSEC-2024-0320", # unmaintaines yaml-rust pulled in by syntect - { name = "async-process" }, # yanked crated pulled in by old accesskit + "RUSTSEC-2024-0320", # unmaintaines yaml-rust pulled in by syntect ] - [bans] multiple-versions = "deny" wildcards = "deny" @@ -46,28 +44,25 @@ deny = [ ] skip = [ - { name = "bit-set" }, # wgpu's naga depends on 0.6, syntect's (used by egui_extras) fancy-regex depends on 0.5 + { name = "base64" }, # Pretty small + { name = "bit-set" }, # wgpu's naga depends on 0.8, syntect's (used by egui_extras) fancy-regex depends on 0.5 { name = "bit-vec" }, # dependency of bit-set in turn, different between 0.6 and 0.5 { name = "bitflags" }, # old 1.0 version via glutin, png, spirv, … { name = "cfg_aliases" }, # old version via wgpu { name = "event-listener" }, # TODO(emilk): rustls pulls in two versions of this 😭 { name = "futures-lite" }, # old version via accesskit_unix and zbus + { name = "glow" }, # old version via wgpu { name = "memoffset" }, # tiny dependency { name = "ndk-sys" }, # old version via wgpu, winit uses newer version { name = "quick-xml" }, # old version via wayland-scanner { name = "redox_syscall" }, # old version via winit { name = "time" }, # old version pulled in by unmaintianed crate 'chrono' - { name = "windows-core" }, # old version via accesskit_windows - { name = "windows" }, # old version via accesskit_windows - { name = "glow" }, # wgpu uses an old `glow`, but realistically no one uses _both_ `egui_wgpu` and `egui_glow`, so we won't get a duplicate dependency - + { name = "windows-core" }, # Chrono pulls in 0.51, accesskit uses 0.58.0 + { name = "windows-sys" }, # glutin pulls in 0.52.0, accesskit pulls in 0.59.0, rfd pulls 0.48, webbrowser pulls 0.45.0 (via jni) ] skip-tree = [ { name = "criterion" }, # dev-dependency - { name = "fastrand" }, # old version via accesskit_unix { name = "foreign-types" }, # small crate. Old version via core-graphics (winit). - { name = "objc2" }, # old version via accesskit_macos - { name = "polling" }, # old version via accesskit_unix { name = "rfd" }, # example dependency ] @@ -90,6 +85,7 @@ allow = [ "MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11. Used by webpki-roots on Linux. "OFL-1.1", # https://spdx.org/licenses/OFL-1.1.html "OpenSSL", # https://www.openssl.org/source/license.html - used on Linux + "Unicode-3.0", # https://www.unicode.org/license.txt "Unicode-DFS-2016", # https://spdx.org/licenses/Unicode-DFS-2016.html "Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib) ] @@ -109,3 +105,7 @@ license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] [sources] unknown-registry = "deny" unknown-git = "deny" + +allow-git = [ + "https://github.com/rerun-io/kittest", # TODO(lucasmerlin): remove this once the kittest crate is published" +] diff --git a/examples/confirm_exit/Cargo.toml b/examples/confirm_exit/Cargo.toml index b1cab21a1bb..62e8bc29c14 100644 --- a/examples/confirm_exit/Cargo.toml +++ b/examples/confirm_exit/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] diff --git a/examples/confirm_exit/screenshot.png b/examples/confirm_exit/screenshot.png index 931f17c9882..854955cf78d 100644 Binary files a/examples/confirm_exit/screenshot.png and b/examples/confirm_exit/screenshot.png differ diff --git a/examples/custom_3d_glow/Cargo.toml b/examples/custom_3d_glow/Cargo.toml index 3b8c9a45383..1ddec02f5f8 100644 --- a/examples/custom_3d_glow/Cargo.toml +++ b/examples/custom_3d_glow/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] diff --git a/examples/custom_3d_glow/screenshot.png b/examples/custom_3d_glow/screenshot.png index cea68ab36c2..c3907a5ec21 100644 Binary files a/examples/custom_3d_glow/screenshot.png and b/examples/custom_3d_glow/screenshot.png differ diff --git a/examples/custom_font/Cargo.toml b/examples/custom_font/Cargo.toml index d6021c12425..5214bdc1f5f 100644 --- a/examples/custom_font/Cargo.toml +++ b/examples/custom_font/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] diff --git a/examples/custom_font/screenshot.png b/examples/custom_font/screenshot.png index 97132d02cd0..7e6edc3dd59 100644 Binary files a/examples/custom_font/screenshot.png and b/examples/custom_font/screenshot.png differ diff --git a/examples/custom_font/src/main.rs b/examples/custom_font/src/main.rs index 5afa72905af..c487b6714c0 100644 --- a/examples/custom_font/src/main.rs +++ b/examples/custom_font/src/main.rs @@ -1,7 +1,10 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![allow(rustdoc::missing_crate_level_docs)] // it's an example -use eframe::egui; +use eframe::{ + egui, + epaint::text::{FontInsert, InsertFontFamily}, +}; fn main() -> eframe::Result { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). @@ -16,7 +19,28 @@ fn main() -> eframe::Result { ) } -fn setup_custom_fonts(ctx: &egui::Context) { +// Demonstrates how to add a font to the existing ones +fn add_font(ctx: &egui::Context) { + ctx.add_font(FontInsert::new( + "my_font", + egui::FontData::from_static(include_bytes!( + "../../../crates/epaint_default_fonts/fonts/Hack-Regular.ttf" + )), + vec![ + InsertFontFamily { + family: egui::FontFamily::Proportional, + priority: egui::epaint::text::FontPriority::Highest, + }, + InsertFontFamily { + family: egui::FontFamily::Monospace, + priority: egui::epaint::text::FontPriority::Lowest, + }, + ], + )); +} + +// Demonstrates how to replace all fonts. +fn replace_fonts(ctx: &egui::Context) { // Start with the default fonts (we will be adding to them rather than replacing them). let mut fonts = egui::FontDefinitions::default(); @@ -24,9 +48,9 @@ fn setup_custom_fonts(ctx: &egui::Context) { // .ttf and .otf files supported. fonts.font_data.insert( "my_font".to_owned(), - egui::FontData::from_static(include_bytes!( + std::sync::Arc::new(egui::FontData::from_static(include_bytes!( "../../../crates/epaint_default_fonts/fonts/Hack-Regular.ttf" - )), + ))), ); // Put my font first (highest priority) for proportional text: @@ -53,7 +77,8 @@ struct MyApp { impl MyApp { fn new(cc: &eframe::CreationContext<'_>) -> Self { - setup_custom_fonts(&cc.egui_ctx); + replace_fonts(&cc.egui_ctx); + add_font(&cc.egui_ctx); Self { text: "Edit this text field if you want".to_owned(), } diff --git a/examples/custom_font_style/Cargo.toml b/examples/custom_font_style/Cargo.toml index 65b5045238c..241b893401d 100644 --- a/examples/custom_font_style/Cargo.toml +++ b/examples/custom_font_style/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["tami5 "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] diff --git a/examples/custom_font_style/screenshot.png b/examples/custom_font_style/screenshot.png index 0a8cdf08e5e..bb7727a1dbe 100644 Binary files a/examples/custom_font_style/screenshot.png and b/examples/custom_font_style/screenshot.png differ diff --git a/examples/custom_keypad/Cargo.toml b/examples/custom_keypad/Cargo.toml index 7d000fd0d46..73c6b0e7aa8 100644 --- a/examples/custom_keypad/Cargo.toml +++ b/examples/custom_keypad/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Varphone Wong "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] diff --git a/examples/custom_keypad/screenshot.png b/examples/custom_keypad/screenshot.png index 632459e51aa..1d860e14046 100644 Binary files a/examples/custom_keypad/screenshot.png and b/examples/custom_keypad/screenshot.png differ diff --git a/examples/custom_style/Cargo.toml b/examples/custom_style/Cargo.toml index c7ae125493a..f87ce0bf82a 100644 --- a/examples/custom_style/Cargo.toml +++ b/examples/custom_style/Cargo.toml @@ -3,7 +3,7 @@ name = "custom_style" version = "0.1.0" license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] diff --git a/examples/custom_style/screenshot.png b/examples/custom_style/screenshot.png index dc051c5d942..4c2ad30b041 100644 Binary files a/examples/custom_style/screenshot.png and b/examples/custom_style/screenshot.png differ diff --git a/examples/custom_window_frame/Cargo.toml b/examples/custom_window_frame/Cargo.toml index 6b800d0a0af..4a53ee48745 100644 --- a/examples/custom_window_frame/Cargo.toml +++ b/examples/custom_window_frame/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] diff --git a/examples/custom_window_frame/screenshot.png b/examples/custom_window_frame/screenshot.png index 040a31ff2db..92e11a695ee 100644 Binary files a/examples/custom_window_frame/screenshot.png and b/examples/custom_window_frame/screenshot.png differ diff --git a/examples/file_dialog/Cargo.toml b/examples/file_dialog/Cargo.toml index 9684a423256..1a9f86c40c0 100644 --- a/examples/file_dialog/Cargo.toml +++ b/examples/file_dialog/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] @@ -20,4 +20,4 @@ env_logger = { version = "0.10", default-features = false, features = [ "auto-color", "humantime", ] } -rfd = "0.13" +rfd = "0.15" diff --git a/examples/file_dialog/screenshot.png b/examples/file_dialog/screenshot.png index 263a8521c72..7a1cc041736 100644 Binary files a/examples/file_dialog/screenshot.png and b/examples/file_dialog/screenshot.png differ diff --git a/examples/hello_android/Cargo.toml b/examples/hello_android/Cargo.toml new file mode 100644 index 00000000000..dcb0a5a5c01 --- /dev/null +++ b/examples/hello_android/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "hello_android" +version = "0.1.0" +authors = ["Emil Ernerfeldt "] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.76" +publish = false + +# `unsafe_code` is required for `#[no_mangle]`, disable workspace lints to workaround lint error. +# [lints] +# workspace = true + +[lib] +crate-type = ["cdylib"] + + +[dependencies] +eframe = { workspace = true, features = [ + "default", + "android-native-activity", +] } + +# For image support: +egui_extras = { workspace = true, features = ["default", "image"] } + +log = { workspace = true } +winit = { workspace = true } +android_logger = "0.14" + +[package.metadata.android] +build_targets = [ "armv7-linux-androideabi", "aarch64-linux-android" ] diff --git a/examples/hello_android/README.md b/examples/hello_android/README.md new file mode 100644 index 00000000000..fe14eb9face --- /dev/null +++ b/examples/hello_android/README.md @@ -0,0 +1,20 @@ +Hello world example for Android. + +Use `cargo-apk` to build and run. Requires a patch to workaround [an upstream bug](https://github.com/rust-mobile/cargo-subcommand/issues/29). + +One-time setup: + +```sh +cargo install \ + --git https://github.com/parasyte/cargo-apk.git \ + --rev 282639508eeed7d73f2e1eaeea042da2716436d5 \ + cargo-apk +``` + +Build and run: + +```sh +cargo apk run -p hello_android +``` + +![](screenshot.png) diff --git a/examples/hello_android/screenshot.png b/examples/hello_android/screenshot.png new file mode 100644 index 00000000000..91179fa2f41 --- /dev/null +++ b/examples/hello_android/screenshot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7add91d7d6b73f48e98f20d84cba3bd3a950cf97aa31f5e9fa93da9af98e876c +size 120019 diff --git a/examples/hello_android/src/lib.rs b/examples/hello_android/src/lib.rs new file mode 100644 index 00000000000..adda66ca5f7 --- /dev/null +++ b/examples/hello_android/src/lib.rs @@ -0,0 +1,65 @@ +#![cfg(target_os = "android")] +#![allow(rustdoc::missing_crate_level_docs)] // it's an example + +use android_logger::Config; +use eframe::egui; +use log::LevelFilter; +use winit::platform::android::activity::AndroidApp; + +#[no_mangle] +fn android_main(app: AndroidApp) { + // Log to android output + android_logger::init_once(Config::default().with_max_level(LevelFilter::Info)); + + let options = eframe::NativeOptions { + android_app: Some(app), + ..Default::default() + }; + eframe::run_native( + "My egui App", + options, + Box::new(|cc| { + // This gives us image support: + egui_extras::install_image_loaders(&cc.egui_ctx); + + Ok(Box::::default()) + }), + ) + .unwrap() +} + +struct MyApp { + name: String, + age: u32, +} + +impl Default for MyApp { + fn default() -> Self { + Self { + name: "Arthur".to_owned(), + age: 42, + } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("My egui Application"); + ui.horizontal(|ui| { + let name_label = ui.label("Your name: "); + ui.text_edit_singleline(&mut self.name) + .labelled_by(name_label.id); + }); + ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age")); + if ui.button("Increment").clicked() { + self.age += 1; + } + ui.label(format!("Hello '{}', age {}", self.name, self.age)); + + ui.image(egui::include_image!( + "../../../crates/egui/assets/ferris.png" + )); + }); + } +} diff --git a/examples/hello_world/Cargo.toml b/examples/hello_world/Cargo.toml index bdd728a61e1..8816b425598 100644 --- a/examples/hello_world/Cargo.toml +++ b/examples/hello_world/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] diff --git a/examples/hello_world/screenshot.png b/examples/hello_world/screenshot.png index 9581b8cf97e..da6ceede9b6 100644 Binary files a/examples/hello_world/screenshot.png and b/examples/hello_world/screenshot.png differ diff --git a/examples/hello_world_par/Cargo.toml b/examples/hello_world_par/Cargo.toml index 458847eba4f..b3e00b20897 100644 --- a/examples/hello_world_par/Cargo.toml +++ b/examples/hello_world_par/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Maxim Osipenko "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] @@ -29,6 +29,4 @@ env_logger = { version = "0.10", default-features = false, features = [ ] } # This is normally enabled by eframe/default, which is not being used here # because of accesskit, as mentioned above -winit = { workspace = true, features = [ - "default" -] } +winit = { workspace = true, features = ["default"] } diff --git a/examples/hello_world_simple/Cargo.toml b/examples/hello_world_simple/Cargo.toml index c88382b9f6d..7197c60337b 100644 --- a/examples/hello_world_simple/Cargo.toml +++ b/examples/hello_world_simple/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] diff --git a/examples/hello_world_simple/README.md b/examples/hello_world_simple/README.md index 08c58a7d229..12aee1fada0 100644 --- a/examples/hello_world_simple/README.md +++ b/examples/hello_world_simple/README.md @@ -1,7 +1,7 @@ Example showing some UI controls like `Label`, `TextEdit`, `Slider`, `Button`. ```sh -cargo run -p hello_world +cargo run -p hello_world_simple ``` ![](screenshot.png) diff --git a/examples/hello_world_simple/screenshot.png b/examples/hello_world_simple/screenshot.png index e7423f00369..8e5d56570f9 100644 Binary files a/examples/hello_world_simple/screenshot.png and b/examples/hello_world_simple/screenshot.png differ diff --git a/examples/images/Cargo.toml b/examples/images/Cargo.toml index fd6ed440b39..f1b4f97ddf2 100644 --- a/examples/images/Cargo.toml +++ b/examples/images/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Jan Procházka "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] diff --git a/examples/images/screenshot.png b/examples/images/screenshot.png index 87d3d53d4c8..7d81312aafd 100644 Binary files a/examples/images/screenshot.png and b/examples/images/screenshot.png differ diff --git a/examples/keyboard_events/Cargo.toml b/examples/keyboard_events/Cargo.toml index f7b12047062..1af09849319 100644 --- a/examples/keyboard_events/Cargo.toml +++ b/examples/keyboard_events/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Jose Palazon "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] diff --git a/examples/keyboard_events/screenshot.png b/examples/keyboard_events/screenshot.png index e144d870f1a..ed6ba6837f1 100644 Binary files a/examples/keyboard_events/screenshot.png and b/examples/keyboard_events/screenshot.png differ diff --git a/examples/multiple_viewports/Cargo.toml b/examples/multiple_viewports/Cargo.toml index 7aad28550fc..1644d6a72a5 100644 --- a/examples/multiple_viewports/Cargo.toml +++ b/examples/multiple_viewports/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] diff --git a/examples/popups/src/main.rs b/examples/popups/src/main.rs index 46762d667c0..baed12c2a26 100644 --- a/examples/popups/src/main.rs +++ b/examples/popups/src/main.rs @@ -14,12 +14,33 @@ fn main() -> Result<(), eframe::Error> { struct MyApp { checkbox: bool, number: u8, + numbers: [bool; 10], } impl eframe::App for MyApp { fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) { CentralPanel::default().show(ctx, |ui| { - ui.label("PopupCloseBehavior::CloseOnClickAway popup"); + ui.label("PopupCloseBehavior::CloseOnClick popup"); + ComboBox::from_label("ComboBox") + .selected_text(format!("{}", self.number)) + .show_ui(ui, |ui| { + for num in 0..10 { + ui.selectable_value(&mut self.number, num, format!("{num}")); + } + }); + + ui.label("PopupCloseBehavior::CloseOnClickOutside popup"); + ComboBox::from_label("Ignore Clicks") + .close_behavior(PopupCloseBehavior::CloseOnClickOutside) + .selected_text("Select Numbers") + .show_ui(ui, |ui| { + ui.label("This popup will be open even if you click the checkboxes"); + for (i, num) in self.numbers.iter_mut().enumerate() { + ui.checkbox(num, format!("Checkbox {}", i + 1)); + } + }); + + ui.label("PopupCloseBehavior::IgnoreClicks popup"); let response = ui.button("Open"); let popup_id = Id::new("popup_id"); @@ -31,22 +52,13 @@ impl eframe::App for MyApp { ui, popup_id, &response, - PopupCloseBehavior::CloseOnClickOutside, + PopupCloseBehavior::IgnoreClicks, |ui| { - ui.set_min_width(300.0); - ui.label("This popup will be open even if you click the checkbox"); + ui.set_min_width(310.0); + ui.label("This popup will be open until you press the button again"); ui.checkbox(&mut self.checkbox, "Checkbox"); }, ); - - ui.label("PopupCloseBehavior::CloseOnClick popup"); - ComboBox::from_label("ComboBox") - .selected_text(format!("{}", self.number)) - .show_ui(ui, |ui| { - for num in 0..10 { - ui.selectable_value(&mut self.number, num, format!("{num}")); - } - }); }); } } diff --git a/examples/puffin_profiler/Cargo.toml b/examples/puffin_profiler/Cargo.toml index 4527d5c269b..d0e9e485a8d 100644 --- a/examples/puffin_profiler/Cargo.toml +++ b/examples/puffin_profiler/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false +[package.metadata.cargo-machete] +ignored = ["profiling"] + [lints] workspace = true @@ -18,7 +21,6 @@ wgpu = ["eframe/wgpu"] [dependencies] eframe = { workspace = true, features = [ "default", - "puffin", "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO ] } env_logger = { version = "0.10", default-features = false, features = [ @@ -28,3 +30,4 @@ env_logger = { version = "0.10", default-features = false, features = [ log = { workspace = true } puffin = "0.19" puffin_http = "0.16" +profiling = {workspace = true, features = ["profile-with-puffin"] } diff --git a/examples/puffin_profiler/screenshot.png b/examples/puffin_profiler/screenshot.png index 1792f0e649d..319a347308d 100644 Binary files a/examples/puffin_profiler/screenshot.png and b/examples/puffin_profiler/screenshot.png differ diff --git a/examples/screenshot/Cargo.toml b/examples/screenshot/Cargo.toml index 84399e4bc24..5963ea3ad71 100644 --- a/examples/screenshot/Cargo.toml +++ b/examples/screenshot/Cargo.toml @@ -7,7 +7,7 @@ authors = [ ] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] diff --git a/examples/screenshot/screenshot.png b/examples/screenshot/screenshot.png index 27bf79908a1..9522383246a 100644 Binary files a/examples/screenshot/screenshot.png and b/examples/screenshot/screenshot.png differ diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 88b57f20e7a..1dd0bbf5076 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -45,7 +45,7 @@ impl eframe::App for MyApp { if ui.button("save to 'top_left.png'").clicked() { self.save_to_file = true; - ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot); + ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot(Default::default())); } ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| { @@ -58,9 +58,13 @@ impl eframe::App for MyApp { } else { ctx.set_theme(egui::Theme::Light); }; - ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot); + ctx.send_viewport_cmd( + egui::ViewportCommand::Screenshot(Default::default()), + ); } else if ui.button("take screenshot!").clicked() { - ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot); + ctx.send_viewport_cmd( + egui::ViewportCommand::Screenshot(Default::default()), + ); } }); }); diff --git a/examples/serial_windows/Cargo.toml b/examples/serial_windows/Cargo.toml index 5d52826e919..1f6d5ea431e 100644 --- a/examples/serial_windows/Cargo.toml +++ b/examples/serial_windows/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] diff --git a/examples/serial_windows/screenshot.png b/examples/serial_windows/screenshot.png index 6926765b04e..b1300a4194f 100644 Binary files a/examples/serial_windows/screenshot.png and b/examples/serial_windows/screenshot.png differ diff --git a/examples/user_attention/Cargo.toml b/examples/user_attention/Cargo.toml index 6c3c91cdc25..25aa473486c 100644 --- a/examples/user_attention/Cargo.toml +++ b/examples/user_attention/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["TicClick "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] diff --git a/examples/user_attention/screenshot.png b/examples/user_attention/screenshot.png index d7ee61cce5f..015bdf7ecdc 100644 Binary files a/examples/user_attention/screenshot.png and b/examples/user_attention/screenshot.png differ diff --git a/media/demo-2020-08-21.png b/media/demo-2020-08-21.png index 00101a6d510..a852ab8309c 100644 Binary files a/media/demo-2020-08-21.png and b/media/demo-2020-08-21.png differ diff --git a/media/demo-2020-10-24.png b/media/demo-2020-10-24.png index 96282a706cd..affeff063cc 100644 Binary files a/media/demo-2020-10-24.png and b/media/demo-2020-10-24.png differ diff --git a/media/demo-2021-01-02.png b/media/demo-2021-01-02.png index d6aff2e68fd..43f524a8b54 100644 Binary files a/media/demo-2021-01-02.png and b/media/demo-2021-01-02.png differ diff --git a/media/demo_light_mode.png b/media/demo_light_mode.png index 993b880abc3..16e33eb724d 100644 Binary files a/media/demo_light_mode.png and b/media/demo_light_mode.png differ diff --git a/media/light_theme.png b/media/light_theme.png index e2026ef259b..e7e991892e6 100644 Binary files a/media/light_theme.png and b/media/light_theme.png differ diff --git a/media/pompodoro-skin.png b/media/pompodoro-skin.png index cbb18ae0e7d..7f7b71fccc7 100644 Binary files a/media/pompodoro-skin.png and b/media/pompodoro-skin.png differ diff --git a/media/rerun_io_logo.png b/media/rerun_io_logo.png index cda0426627d..8e3f89a59ea 100644 Binary files a/media/rerun_io_logo.png and b/media/rerun_io_logo.png differ diff --git a/media/widget_gallery_0.23_light.png b/media/widget_gallery_0.23_light.png index a975f97cdde..4a10a56b707 100644 Binary files a/media/widget_gallery_0.23_light.png and b/media/widget_gallery_0.23_light.png differ diff --git a/rust-toolchain b/rust-toolchain index 871f562485d..38e5e90f3ac 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -5,6 +5,6 @@ # to the user in the error, instead of "error: invalid channel name '[toolchain]'". [toolchain] -channel = "1.76.0" +channel = "1.80.0" components = ["rustfmt", "clippy"] targets = ["wasm32-unknown-unknown"] diff --git a/scripts/build_demo_web.sh b/scripts/build_demo_web.sh index 282d493e6bc..b6eb7197a73 100755 --- a/scripts/build_demo_web.sh +++ b/scripts/build_demo_web.sh @@ -19,7 +19,7 @@ WASM_OPT_FLAGS="-O2 --fast-math" while test $# -gt 0; do case "$1" in -h|--help) - echo "build_demo_web.sh [--release] [--webgpu] [--open]" + echo "build_demo_web.sh [--release] [--wgpu] [--open]" echo "" echo " -g: Keep debug symbols even with --release." echo " These are useful profiling and size trimming." diff --git a/scripts/check.sh b/scripts/check.sh index 6f4aee69ec3..7db1a6aa3e2 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -9,7 +9,7 @@ set -x # Checks all tests, lints etc. # Basically does what the CI does. -cargo +1.76.0 install --quiet typos-cli +cargo +1.80.0 install --quiet typos-cli export RUSTFLAGS="-D warnings" export RUSTDOCFLAGS="-D warnings" # https://github.com/emilk/egui/pull/1454 @@ -29,7 +29,8 @@ cargo check --quiet --all-targets cargo check --quiet --all-targets --all-features cargo check --quiet -p egui_demo_app --lib --target wasm32-unknown-unknown cargo check --quiet -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features -cargo test --quiet --all-targets --all-features +# TODO(#5297) re-enable --all-features once the tests work with the unity feature +cargo test --quiet --all-targets cargo test --quiet --doc # slow - checks all doc-tests cargo check --quiet -p eframe --no-default-features --features "glow" diff --git a/scripts/clippy_wasm/clippy.toml b/scripts/clippy_wasm/clippy.toml index f91ef9dd614..f06033c71b9 100644 --- a/scripts/clippy_wasm/clippy.toml +++ b/scripts/clippy_wasm/clippy.toml @@ -6,7 +6,7 @@ # ----------------------------------------------------------------------------- # Section identical to the root clippy.toml: -msrv = "1.76" +msrv = "1.80" allow-unwrap-in-tests = true @@ -47,6 +47,9 @@ doc-valid-idents = [ # You must also update the same list in the root `clippy.toml`! "AccessKit", "WebGL", + "WebGL1", + "WebGL2", "WebGPU", + "VirtualBox", "..", ] diff --git a/scripts/generate_changelog.py b/scripts/generate_changelog.py index 2f40f995068..1621fd29793 100755 --- a/scripts/generate_changelog.py +++ b/scripts/generate_changelog.py @@ -214,24 +214,49 @@ def add_to_changelog_file(crate: str, content: str, version: str) -> None: file.write(content) +def calc_commit_range(new_version: str) -> str: + parts = new_version.split(".") + assert len(parts) == 3, "Expected version to be on the format X.Y.Z" + major = int(parts[0]) + minor = int(parts[1]) + patch = int(parts[2]) + + if 0 < patch: + # A patch release. + # Include changes since last patch release. + # This assumes we've cherry-picked stuff for this release. + diff_since_version = f"0.{minor}.{patch - 1}" + elif 0 < minor: + # A minor release + # The diff should span everything since the last minor release. + # The script later excludes duplicated automatically, so we don't include stuff that + # was part of intervening patch releases. + diff_since_version = f"{major}.{minor - 1}.0" + else: + # A major release + # The diff should span everything since the last major release. + # The script later excludes duplicated automatically, so we don't include stuff that + # was part of intervening minor/patch releases. + diff_since_version = f"{major - 1}.{minor}.0" + + return f"{diff_since_version}..HEAD" + + def main() -> None: parser = argparse.ArgumentParser(description="Generate a changelog.") - parser.add_argument("--commit-range", help="e.g. 0.24.0..HEAD", required=True) + parser.add_argument("--version", help="What release is this?", required=True) parser.add_argument( "--write", help="Write into the different changelogs?", action="store_true" ) - parser.add_argument("--version", help="What release is this?") args = parser.parse_args() - - if args.write and not args.version: - print("ERROR: --version is required when --write is used") - sys.exit(1) + commit_range = calc_commit_range(args.version) crate_names = [ "ecolor", "eframe", "egui_extras", "egui_glow", + "egui_kittest", "egui-wgpu", "egui-winit", "egui", @@ -250,7 +275,7 @@ def main() -> None: all_changelogs += file.read() repo = Repo(".") - commits = list(repo.iter_commits(args.commit_range)) + commits = list(repo.iter_commits(commit_range)) commits.reverse() # Most recent last commit_infos = list(map(get_commit_info, commits)) @@ -306,7 +331,7 @@ def main() -> None: unsorted_prs.append(pr_summary(pr_info)) print() - print(f"Full diff at https://github.com/emilk/egui/compare/{args.commit_range}") + print(f"Full diff at https://github.com/emilk/egui/compare/{commit_range}") print() for crate in crate_names: if crate in crate_sections: diff --git a/scripts/setup_web.sh b/scripts/setup_web.sh index b295ed11fff..879f0a77e68 100755 --- a/scripts/setup_web.sh +++ b/scripts/setup_web.sh @@ -7,4 +7,4 @@ cd "$script_path/.." rustup target add wasm32-unknown-unknown # For generating JS bindings: -cargo install --quiet wasm-bindgen-cli --version 0.2.93 +cargo install --quiet wasm-bindgen-cli --version 0.2.95 diff --git a/tests/test_egui_extras_compilation/Cargo.toml b/tests/test_egui_extras_compilation/Cargo.toml index aa1ea2c7ec9..f1d67f4bb8b 100644 --- a/tests/test_egui_extras_compilation/Cargo.toml +++ b/tests/test_egui_extras_compilation/Cargo.toml @@ -3,14 +3,17 @@ name = "test_egui_extras_compilation" version = "0.1.0" license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] workspace = true [package.metadata.cargo-machete] -ignored = ["eframe", "egui_extras"] # We don't use them, just check that things compile +ignored = [ + "eframe", + "egui_extras", +] # We don't use them, just check that things compile [dependencies] eframe = { workspace = true, features = ["default", "persistence"] } diff --git a/tests/test_inline_glow_paint/Cargo.toml b/tests/test_inline_glow_paint/Cargo.toml index ae66b6abdd3..bcda5b3ddea 100644 --- a/tests/test_inline_glow_paint/Cargo.toml +++ b/tests/test_inline_glow_paint/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] diff --git a/tests/test_size_pass/Cargo.toml b/tests/test_size_pass/Cargo.toml index 6886c7af2a1..e3819a107e9 100644 --- a/tests/test_size_pass/Cargo.toml +++ b/tests/test_size_pass/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] diff --git a/tests/test_ui_stack/Cargo.toml b/tests/test_ui_stack/Cargo.toml index ebcbf077194..12ba9961be7 100644 --- a/tests/test_ui_stack/Cargo.toml +++ b/tests/test_ui_stack/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Antoine Beyeler "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] diff --git a/tests/test_viewports/Cargo.toml b/tests/test_viewports/Cargo.toml index 142bafe2293..cb877a7107a 100644 --- a/tests/test_viewports/Cargo.toml +++ b/tests/test_viewports/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["konkitoman"] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.76" +rust-version = "1.80" publish = false [lints] diff --git a/tests/test_viewports/src/main.rs b/tests/test_viewports/src/main.rs index 24046e041ac..9d06db431e4 100644 --- a/tests/test_viewports/src/main.rs +++ b/tests/test_viewports/src/main.rs @@ -433,7 +433,7 @@ fn drag_source( // Paint the body to a new layer: let layer_id = egui::LayerId::new(egui::Order::Tooltip, id); - let res = ui.with_layer_id(layer_id, body); + let res = ui.scope_builder(UiBuilder::new().layer_id(layer_id), body); if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { let delta = pointer_pos - res.response.rect.center();