diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c00eac197b4..c3bc239276b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,6 +2,7 @@ Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md) before opening a Pull Request! * Keep your PR:s small and focused. +* The PR title is what ends up in the changelog, so make it descriptive! * If applicable, add a screenshot or gif. * If it is a non-trivial addition, consider adding a demo for it to `egui_demo_lib`, or a new example. * Do NOT open PR:s from your `master` branch, as that makes it hard for maintainers to add commits to your PR. @@ -9,7 +10,7 @@ Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/ * Open the PR as a draft until you have self-reviewed it and run `./scripts/check.sh`. * When you have addressed a PR comment, mark it as resolved. -Please be patient! I will review you PR, but my time is limited! +Please be patient! I will review your PR, but my time is limited! --> -Closes . +* Closes diff --git a/.github/workflows/deploy_web_demo.yml b/.github/workflows/deploy_web_demo.yml new file mode 100644 index 00000000000..ea2956db313 --- /dev/null +++ b/.github/workflows/deploy_web_demo.yml @@ -0,0 +1,66 @@ +name: Deploy web demo + +on: + # We only run this on merges to master + push: + branches: ["master"] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: +# to only run when you do a new github release, comment out above part and uncomment the below trigger. +# on: +# release: +# types: ["published"] + + +permissions: + contents: write # for committing to gh-pages branch + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +env: + # web_sys_unstable_apis is required to enable the web_sys clipboard API which eframe web uses, + # as well as by the wasm32-backend of the wgpu crate. + # https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html + # https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html + RUSTFLAGS: --cfg=web_sys_unstable_apis -D warnings + RUSTDOCFLAGS: -D warnings + +jobs: + # Single deploy job since we're just deploying + deploy: + name: Deploy web demo + + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + target: wasm32-unknown-unknown + toolchain: 1.72.0 + override: true + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: "web-demo-" + + - name: "Install wasmopt / binaryen" + run: | + sudo apt-get update && sudo apt-get install binaryen + + - run: | + scripts/build_demo_web.sh --release + + - name: Deploy + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: web_demo + # this option will not maintain any history of your previous pages deployment + # set to false if you want all page build to be committed to your gh-pages branch history + single-commit: true diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 193ac9134a0..d3f5cd14e5d 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, plot, typo" + labels: "CI, dependencies, docs and examples, ecolor, eframe, egui_extras, egui_glow, egui_plot, egui-wgpu, egui-winit, egui, epaint, exclude from changelog, typo" diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b4920b3bfdb..d641b17b284 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -15,11 +15,11 @@ jobs: name: Format + check + test runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.67.0 + toolchain: 1.72.0 - name: Install packages (Linux) if: runner.os == 'Linux' @@ -49,7 +49,7 @@ jobs: run: cargo check --locked --all-features --all-targets - name: check egui_extras --all-features - run: cargo check --locked --all-features --all-targets -p egui_extras + run: cargo check --locked --all-features -p egui_extras - name: check default features run: cargo check --locked --all-targets @@ -57,11 +57,14 @@ jobs: - name: check --no-default-features run: cargo check --locked --no-default-features --lib --all-targets - - name: check epaint --no-default-features - run: cargo check --locked --no-default-features --lib --all-targets -p epaint - - name: check eframe --no-default-features - run: cargo check --locked --no-default-features --features x11 --lib --all-targets -p eframe + run: cargo check --locked --no-default-features --features x11 --lib -p eframe + + - name: check egui_extras --no-default-features + run: cargo check --locked --no-default-features --lib -p egui_extras + + - name: check epaint --no-default-features + run: cargo check --locked --no-default-features --lib -p epaint - name: Test doc-tests run: cargo test --doc --all-features @@ -78,19 +81,22 @@ jobs: - name: Cranky run: cargo cranky --all-targets --all-features -- -D warnings + - name: Cranky release + run: cargo cranky --all-targets --all-features --release -- -D warnings + # --------------------------------------------------------------------------- check_wasm: name: Check wasm32 + wasm-bindgen runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.67.0 + toolchain: 1.72.0 targets: wasm32-unknown-unknown - - run: sudo apt-get update && sudo apt-get install libgtk-3-dev + - run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev - name: Set up cargo cache uses: Swatinem/rust-cache@v2 @@ -112,7 +118,7 @@ jobs: - name: wasm-bindgen uses: jetli/wasm-bindgen-action@v0.1.0 with: - version: "0.2.87" + version: "0.2.88" - run: ./scripts/wasm_bindgen_check.sh --skip-setup @@ -142,10 +148,10 @@ jobs: name: cargo-deny ${{ matrix.target }} runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: EmbarkStudios/cargo-deny-action@v1 with: - rust-version: "1.67.0" + rust-version: "1.72.0" log-level: error command: check arguments: --target ${{ matrix.target }} @@ -156,11 +162,11 @@ jobs: name: android runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.67.0 + toolchain: 1.72.0 targets: aarch64-linux-android - name: Set up cargo cache @@ -175,13 +181,16 @@ jobs: name: Check Windows runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.67.0 + toolchain: 1.72.0 - name: Set up cargo cache uses: Swatinem/rust-cache@v2 - - name: Check + - name: Check all run: cargo check --all-targets --all-features + + - name: Check hello_world + run: cargo check -p hello_world diff --git a/.github/workflows/spelling_and_links.yml b/.github/workflows/spelling_and_links.yml new file mode 100644 index 00000000000..2b4c8de14d8 --- /dev/null +++ b/.github/workflows/spelling_and_links.yml @@ -0,0 +1,38 @@ +name: Check spelling and links +on: [pull_request] + +jobs: + typos: + # https://github.com/crate-ci/typos + # Add exceptions to _typos.toml + # install and run locally: cargo install typos-cli && typos + name: typos + runs-on: ubuntu-latest + steps: + - name: Checkout Actions Repository + uses: actions/checkout@v4 + + - name: Check spelling of entire workspace + uses: crate-ci/typos@master + # Disabled: too many names of crates and user-names etc + # spellcheck: + # name: Spellcheck + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: streetsidesoftware/cspell-action@v2 + # with: + # files: "**/*.md" + linkinator: + name: linkinator + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: jprochazk/linkinator-action@main + with: + linksToSkip: "https://crates.io/crates/.*, http://localhost:.*" # Avoid crates.io rate-limiting + retry: true + retryErrors: true + retryErrorsCount: 5 + retryErrorsJitter: 2000 + diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml deleted file mode 100644 index 4f3ae1d8b42..00000000000 --- a/.github/workflows/typos.yml +++ /dev/null @@ -1,17 +0,0 @@ -# https://github.com/crate-ci/typos -# Add exceptions to _typos.toml -# install and run locally: cargo install typos-cli && typos - -name: Spell Check -on: [pull_request] - -jobs: - run: - name: Spell Check - runs-on: ubuntu-latest - steps: - - name: Checkout Actions Repository - uses: actions/checkout@v3 - - - name: Check spelling of entire workspace - uses: crate-ci/typos@master diff --git a/.typos.toml b/.typos.toml index 2d9cf89c741..6d856495178 100644 --- a/.typos.toml +++ b/.typos.toml @@ -3,8 +3,7 @@ # run: typos [default.extend-words] -nknown = "nknown" # part of @55nknown username -Prefence = "Prefence" # typo in glutin_winit API +nknown = "nknown" # part of @55nknown username [files] -extend-exclude = ["docs/egui_demo_app.js"] # auto-generated +extend-exclude = ["web_demo/egui_demo_app.js"] # auto-generated diff --git a/.vscode/settings.json b/.vscode/settings.json index 0ac80cfd7b7..8ec1e4aa67f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,14 +20,17 @@ "--workspace", "--message-format=json", "--all-targets", + "--all-features", ], "rust-analyzer.cargo.buildScripts.overrideCommand": [ "cargo", - "check", + "cranky", "--quiet", "--target-dir=target_ra", "--workspace", "--message-format=json", "--all-targets", + "--all-features", ], + "rust-analyzer.showUnlinkedFileNotification": false, } diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 71d2d1ae56c..c9f314f7d60 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -5,7 +5,7 @@ Also see [`CONTRIBUTING.md`](CONTRIBUTING.md) for what to do before opening a PR ## Crate overview -The crates in this repository are: `egui, emath, epaint, egui_extras, egui-winit, egui_glium, egui_glow, egui_demo_lib, egui_demo_app`. +The crates in this repository are: `egui, emath, epaint, egui_extras, egui_plot, egui-winit, egui_glow, egui_demo_lib, egui_demo_app`. ### `egui`: The main GUI library. Example code: `if ui.button("Click me").clicked() { … }` @@ -24,14 +24,14 @@ Depends on `emath`. ### `egui_extras` This adds additional features on top of `egui`. +### `egui_plot` +Plotting for `egui`. + ### `egui-winit` This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [winit](https://crates.io/crates/winit). The library translates winit events to egui, handled copy/paste, updates the cursor, open links clicked in egui, etc. -### `egui_glium` -Puts an egui app inside a native window on your laptop. Paints the triangles that egui outputs using [glium](https://github.com/glium/glium). - ### `egui_glow` Puts an egui app inside a native window on your laptop. Paints the triangles that egui outputs using [glow](https://github.com/grovesNL/glow). diff --git a/CHANGELOG.md b/CHANGELOG.md index fc129dd8e99..0a074c6f11e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,257 @@ # egui changelog All notable changes to the `egui` crate will be documented in this file. -NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG.md), [`egui-winit`](crates/egui-winit/CHANGELOG.md), [`egui_glium`](crates/egui_glium/CHANGELOG.md), [`egui_glow`](crates/egui_glow/CHANGELOG.md) and [`egui-wgpu`](crates/egui-wgpu/CHANGELOG.md) have their own changelogs! +NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`egui_plot`](crates/egui_plot/CHANGELOG.md), [`eframe`](crates/eframe/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 file is updated upon each release. -Changes since the last release can be found by running the `scripts/generate_changelog.py` script. +Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. + + +## Unreleased + +### ⚠️ BREAKING +* `Response::clicked*` and `Response::dragged*` may lock the `Context`, so don't call it from a `Context`-locking closure. +* `Response::clicked_by` will no longer be true if clicked with keyboard. Use `Response::clicked` instead. + + +## 0.26.2 - 2024-02-14 +* Avoid interacting twice when not required [#4041](https://github.com/emilk/egui/pull/4041) (thanks [@abey79](https://github.com/abey79)!) + + +## 0.26.1 - 2024-02-11 +* Fix `Window` title bar incorrect handling spacing [#3995](https://github.com/emilk/egui/pull/3995) (thanks [@varphone](https://github.com/varphone)!) +* Make `on_disabled_hover_ui` respect `tooltip_delay` [#4012](https://github.com/emilk/egui/pull/4012) (thanks [@YgorSouza](https://github.com/YgorSouza)!) +* Fix `TextEdit` being too short whenever there is horizontal margin [#4005](https://github.com/emilk/egui/pull/4005) (thanks [@gweisert](https://github.com/gweisert)!) +* Fix `Response::interact` and `Ui:interact_with_hovered` [#4013](https://github.com/emilk/egui/pull/4013) +* Fix: `Response.interact_pointer_pos` is `Some` on click and drag released [#4014](https://github.com/emilk/egui/pull/4014) +* Fix custom `Window` `Frame`s [#4009](https://github.com/emilk/egui/pull/4009) (thanks [@varphone](https://github.com/varphone)!) + + +## 0.26.0 - 2024-02-05 - Text selection in labels + +### ⚠️ BREAKING +* Always set `response.hovered` to false when dragging another widget [#3860](https://github.com/emilk/egui/pull/3860) +* `InputState::scroll_delta` has been replaced by `InputState::raw_scroll_delta` and `InputState::smooth_scroll_delta` [#3884](https://github.com/emilk/egui/pull/3884) +* Improve `Response.dragged`, `drag_started` and `clicked` [#3888](https://github.com/emilk/egui/pull/3888) + +### ⭐ Added +* Selectable text in Labels [#3814](https://github.com/emilk/egui/pull/3814) [#3870](https://github.com/emilk/egui/pull/3870) +* Add some drag-and-drop-related APIs in `Response` and `Memory` [#3876](https://github.com/emilk/egui/pull/3876) (thanks [@abey79](https://github.com/abey79)!) +* Add drag-and-drop APIs with payloads storage [#3887](https://github.com/emilk/egui/pull/3887) +* `ComboBox`: add builder method for height [#3001](https://github.com/emilk/egui/pull/3001) (thanks [@hinto-janai](https://github.com/hinto-janai)!) +* Add keys `?`, `/`, `|` [#3820](https://github.com/emilk/egui/pull/3820) +* Add `Response::contains_pointer` [#3859](https://github.com/emilk/egui/pull/3859) +* Add `Align2::anchor_size` [#3863](https://github.com/emilk/egui/pull/3863) +* Add `Context::debug_text` [#3864](https://github.com/emilk/egui/pull/3864) +* Allow read access to shapes added to painter this frame [#3866](https://github.com/emilk/egui/pull/3866) (thanks [@brunizzl](https://github.com/brunizzl)!) +* Register callbacks with `Context::on_begin_frame` and `on_end_frame` [#3886](https://github.com/emilk/egui/pull/3886) +* Improve `Frame` API to allow picking color until after adding content [#3889](https://github.com/emilk/egui/pull/3889) +* Add opacity factor to `TextShape` [#3916](https://github.com/emilk/egui/pull/3916) (thanks [@StratusFearMe21](https://github.com/StratusFearMe21)!) +* `Context::repaint_causes`: `file:line` of what caused a repaint [#3949](https://github.com/emilk/egui/pull/3949) +* Add `TextureOptions::wrap_mode` [#3954](https://github.com/emilk/egui/pull/3954) (thanks [@CodedNil](https://github.com/CodedNil)!) +* Add `Spacing::menu_width` [#3973](https://github.com/emilk/egui/pull/3973) + +### 🔧 Changed +* Move text selection logic to own module [#3843](https://github.com/emilk/egui/pull/3843) +* Smooth scrolling [#3884](https://github.com/emilk/egui/pull/3884) +* Turn off text wrapping by default in combo-box popups [#3912](https://github.com/emilk/egui/pull/3912) +* `Response.context_menu` now returns the response of the context menu, if open [#3904](https://github.com/emilk/egui/pull/3904) (thanks [@AufarZakiev](https://github.com/AufarZakiev)!) +* Update to puffin 0.19 [#3940](https://github.com/emilk/egui/pull/3940) +* Wait with showing tooltip until mouse has been still for 300ms [#3977](https://github.com/emilk/egui/pull/3977) + +### 🐛 Fixed +* Fix: dragging to above/below a `TextEdit` or `Label` will select text to begin/end [#3858](https://github.com/emilk/egui/pull/3858) +* Fix clickable widgets blocking scrolling on touch screens [#3815](https://github.com/emilk/egui/pull/3815) (thanks [@lucasmerlin](https://github.com/lucasmerlin)!) +* Fix `stable_dt` [#3832](https://github.com/emilk/egui/pull/3832) +* Bug Fix : `Response::is_pointer_button_down_on` is now false the frame the button is released [#3833](https://github.com/emilk/egui/pull/3833) (thanks [@rustbasic](https://github.com/rustbasic)!) +* Use runtime knowledge of OS for OS-specific text editing [#3840](https://github.com/emilk/egui/pull/3840) +* Fix calling `request_repaint_after` every frame causing immediate repaint [#3978](https://github.com/emilk/egui/pull/3978) + +### 🚀 Performance +* Niche-optimize `Id` so that `Option` is the same size as `Id` [#3932](https://github.com/emilk/egui/pull/3932) +* Parallel tessellation with opt-in `rayon` feature [#3934](https://github.com/emilk/egui/pull/3934) + + + +## 0.25.0 - 2024-01-08 - Better keyboard input + +### ⚠️ BREAKING +* Ignore extra SHIFT and ALT when matching modifiers [#3769](https://github.com/emilk/egui/pull/3769) +* Replace `Key::PlusEquals` with `Key::Plus` and `Key::Equals` [#3769](https://github.com/emilk/egui/pull/3769) +* Removed `WidgetTextGalley`, `WidgetTextJob`, `RichText::into_text_job`, `WidgetText::into_text_job` [#3727](https://github.com/emilk/egui/pull/3727) +* Rename `TextBuffer::replace` to `replace_with` [#3751](https://github.com/emilk/egui/pull/3751) + +### ⭐ Added +* Replace a special `Color32::PLACEHOLDER` with widget fallback color [#3727](https://github.com/emilk/egui/pull/3727) +* Add `Key`s for `Cut` `Copy` `Paste` `[` `]` `,` `\` `:` `.` `;` `+` `=` [#3725](https://github.com/emilk/egui/pull/3725) [#3373](https://github.com/emilk/egui/pull/3373) [#3649](https://github.com/emilk/egui/pull/3649) [#3769](https://github.com/emilk/egui/pull/3769) (thanks [@MarijnS95](https://github.com/MarijnS95) and [@mkrueger](https://github.com/mkrueger)!) +* Add `Key::from_name`, `Key::ALL` [#3649](https://github.com/emilk/egui/pull/3649) +* Add `Event::Key::physical_key` [#3649](https://github.com/emilk/egui/pull/3649) +* Add indeterminate state to checkbox [#3605](https://github.com/emilk/egui/pull/3605) (thanks [@YgorSouza](https://github.com/YgorSouza)!) +* Add `Color32::from_hex` and `Color32::to_hex` [#3570](https://github.com/emilk/egui/pull/3570) [#3777](https://github.com/emilk/egui/pull/3777) (thanks [@YgorSouza](https://github.com/YgorSouza)!) +* Add `DragValue`s for RGB(A) in the color picker [#2734](https://github.com/emilk/egui/pull/2734) (thanks [@IVAN-MK7](https://github.com/IVAN-MK7)!) +* Add option to customize progress bar rounding [#2881](https://github.com/emilk/egui/pull/2881) (thanks [@YgorSouza](https://github.com/YgorSouza)!) +* Add methods to load/store `TextEditState` undoer [#3479](https://github.com/emilk/egui/pull/3479) (thanks [@LoganDark](https://github.com/LoganDark)!) +* `ScrollArea`: Add option to always scroll the only enabled direction [#3710](https://github.com/emilk/egui/pull/3710) (thanks [@untbu](https://github.com/untbu)!) + +### 🔧 Changed +* `Grid` now follows `style.visuals.striped` if not explicitly overwritten [#3723](https://github.com/emilk/egui/pull/3723) (thanks [@Wcubed](https://github.com/Wcubed)!) +* Allow arrow keys to move away focus from a Slider [#3641](https://github.com/emilk/egui/pull/3641) (thanks [@fornwall](https://github.com/fornwall)!) +* Keep submenus open until another one is hovered [#3055](https://github.com/emilk/egui/pull/3055) (thanks [@DannyStoll1](https://github.com/DannyStoll1)!) +* Highlight the header of the topmost `Window`, controlled by `Visuals.window_highlight_topmost` [#3515](https://github.com/emilk/egui/pull/3515) (thanks [@GuillaumeSchmid](https://github.com/GuillaumeSchmid)!) + +### 🐛 Fixed +* Derive `serde` `Serialize` and `Deserialize` for `KeyboardShortcut` [#3694](https://github.com/emilk/egui/pull/3694) (thanks [@zeozeozeo](https://github.com/zeozeozeo)!) +* Fix `Window` positioning bug when bad `pivot` is stored in app data [#3721](https://github.com/emilk/egui/pull/3721) (thanks [@abey79](https://github.com/abey79)!) +* Impl `Clone` for `Fonts` [#3737](https://github.com/emilk/egui/pull/3737) +* Add missing `ResizeDirection::East` [#3749](https://github.com/emilk/egui/pull/3749) (thanks [@dbuch](https://github.com/dbuch)!) +* Fix: don't open context menu on drag [#3767](https://github.com/emilk/egui/pull/3767) +* Fix IME input of `CompositionEnd` without a `CompositionStart` [#3768](https://github.com/emilk/egui/pull/3768) (thanks [@FrankLeeC](https://github.com/FrankLeeC)!) +* Fix: allow using the full Private Use Area for custom fonts [#3509](https://github.com/emilk/egui/pull/3509) (thanks [@varphone](https://github.com/varphone)!) +* Fix: apply edited `DragValue` when it looses focus [#3776](https://github.com/emilk/egui/pull/3776) +* Fix: Non-resizable `Area`s now ignore mouse input outside their bounds [#3039](https://github.com/emilk/egui/pull/3039) (thanks [@fleabitdev](https://github.com/fleabitdev)!) +* Highlight submenu buttons when hovered and open [#3780](https://github.com/emilk/egui/pull/3780) +* Invalidate font atlas on any change to `pixels_per_point`, not matter how small [#3698](https://github.com/emilk/egui/pull/3698) (thanks [@StarStarJ](https://github.com/StarStarJ)!) +* Fix zoom-in shortcut (`Cmd +`) on non-English keyboards [#3769](https://github.com/emilk/egui/pull/3769) + + +## 0.24.1 - 2023-11-30 - Bug fixes +* Fix buggy text with multiple viewports on monitors with different scales [#3666](https://github.com/emilk/egui/pull/3666) + + +## 0.24.0 - 2023-11-23 - Multi-viewport + +### ✨ Highlights +You can now spawn multiple native windows on supported backends (e.g. `eframe`), using [the new `viewport` API](https://docs.rs/egui/latest/egui/viewport/index.html) ([#3172](https://github.com/emilk/egui/pull/3172)). + +You can easily zoom any egui app using Cmd+Plus, Cmd+Minus or Cmd+0, just like in a browser ([#3608](https://github.com/emilk/egui/pull/3608)). + +Scrollbars are now hidden by default until you hover the `ScrollArea` ([#3539](https://github.com/emilk/egui/pull/3539)). + +### ⭐ Added +* Multiple viewports/windows [#3172](https://github.com/emilk/egui/pull/3172) (thanks [@konkitoman](https://github.com/konkitoman)!) +* Introduce global `zoom_factor` [#3608](https://github.com/emilk/egui/pull/3608) +* Floating scroll bars [#3539](https://github.com/emilk/egui/pull/3539) +* Add redo support to `Undoer` [#3478](https://github.com/emilk/egui/pull/3478) (thanks [@LoganDark](https://github.com/LoganDark)!) +* Add `egui::Vec2b` [#3543](https://github.com/emilk/egui/pull/3543) +* Add max `Window` size & other size helpers [#3537](https://github.com/emilk/egui/pull/3537) (thanks [@arduano](https://github.com/arduano)!) +* Allow changing shape of slider handle [#3429](https://github.com/emilk/egui/pull/3429) (thanks [@YgorSouza](https://github.com/YgorSouza)!) +* `RawInput::viewports` contains a list of all viewports. Access the current one with `ctx.input(|i| i.viewport())` + +### 🔧 Changed +* Replace `Id::null()` with `Id::NULL` [#3544](https://github.com/emilk/egui/pull/3544) +* Update MSRV to Rust 1.72 [#3595](https://github.com/emilk/egui/pull/3595) +* Update puffin to 0.18 [#3600](https://github.com/emilk/egui/pull/3600) + +### 🐛 Fixed +* Fix upside down slider in the vertical orientation [#3424](https://github.com/emilk/egui/pull/3424) (thanks [@YgorSouza](https://github.com/YgorSouza)!) +* Make slider step account for range start [#3488](https://github.com/emilk/egui/pull/3488) (thanks [@YgorSouza](https://github.com/YgorSouza)!) +* Fix rounding of `ImageButton` [#3531](https://github.com/emilk/egui/pull/3531) (thanks [@chriscate](https://github.com/chriscate)!) +* Fix naming: `constraint_to` -> `constrain_to` [#3438](https://github.com/emilk/egui/pull/3438) (thanks [@rinde](https://github.com/rinde)!) +* Fix Shift+Tab behavior when no widget is focused [#3498](https://github.com/emilk/egui/pull/3498) (thanks [@DataTriny](https://github.com/DataTriny)!) +* Fix scroll not sticking when scrollbar is hidden [#3434](https://github.com/emilk/egui/pull/3434) (thanks [@LoganDark](https://github.com/LoganDark)!) +* Add `#[inline]` to all builder-pattern functions [#3557](https://github.com/emilk/egui/pull/3557) +* Properly reverse bool animation if value changes before it's finished [#3577](https://github.com/emilk/egui/pull/3577) (thanks [@YgorSouza](https://github.com/YgorSouza)!) + + +### ⚠️ BREAKING +* `egui::gui_zoom::zoom_with_keyboard_shortcuts` is gone, replaced with `Options::zoom_with_keyboard`, which is `true` by default +* `Spacing::scroll_bar_X` has been moved to `Spacing::scroll_bar.X` +* `Context::set_pixels_per_point` now calls `Context::set_zoom_level`, and it may make sense for you to call that directly instead +* If you are using `eframe`, check out the breaking changes in [the `eframe` changelog](crates/eframe/CHANGELOG.md) + +#### For integrations +There are several changes relevant to integrations. + +* Added `crate::RawInput::viewports` with information about all active viewports +* The repaint callback set by `Context::set_request_repaint_callback` now points to which viewport should be repainted +* `Context::run` now returns a list of `ViewportOutput` in `FullOutput` which should result in their own independent windows +* There is a new `Context::set_immediate_viewport_renderer` for setting up the immediate viewport integration +* If you support viewports, you need to call `Context::set_embed_viewports(false)`, or all new viewports will be embedded (the default behavior) + + +## 0.23.0 - 2023-09-27 - New image API +This release contains a simple and powerful image API: + +```rs +// Load from web: +ui.image("https://www.example.com/some_image.png"); + +// Include image in the binary using `include_bytes`: +ui.image(egui::include_image!("../assets/ferris.svg")); + +// With options: +ui.add( + egui::Image::new("file://path/to/image.jpg") + .max_width(200.0) + .rounding(10.0), +); +``` + +The API is based on a plugin-system, where you can tell `egui` how to load the images, and from where. + +`egui_extras` comes with loaders for you, so all you need to do is add the following to your `Cargo.toml`: + +```toml +egui_extras = { version = "0.23", features = ["all_loaders"] } +image = { version = "0.24", features = ["jpeg", "png"] } # Add the types you want support for +``` + +And this to your code: + +```rs +egui_extras::install_image_loaders(egui_ctx); +``` + +### ⚠️ BREAKING +* Update MSRV to Rust 1.70.0 [#3310](https://github.com/emilk/egui/pull/3310) +* Break out plotting to own crate `egui_plot` [#3282](https://github.com/emilk/egui/pull/3282) + +### ⭐ Added +* A new image API [#3297](https://github.com/emilk/egui/pull/3297) [#3315](https://github.com/emilk/egui/pull/3315) [#3328](https://github.com/emilk/egui/pull/3328) [#3338](https://github.com/emilk/egui/pull/3338) [#3342](https://github.com/emilk/egui/pull/3342) [#3343](https://github.com/emilk/egui/pull/3343) [#3402](https://github.com/emilk/egui/pull/3402) (thanks [@jprochazk](https://github.com/jprochazk)!) +* Add option to truncate text at some width [#3244](https://github.com/emilk/egui/pull/3244) +* Add control of line height and letter spacing [#3302](https://github.com/emilk/egui/pull/3302) +* Support images with rounded corners [#3257](https://github.com/emilk/egui/pull/3257) +* Change focused widget with arrow keys [#3272](https://github.com/emilk/egui/pull/3272) (thanks [@TimonPost](https://github.com/TimonPost)!) +* Add opt-in `puffin` feature to egui [#3298](https://github.com/emilk/egui/pull/3298) +* Add debug-option to show a callstack to the widget under the mouse and removed the `trace!` macro as this is more useful [#3391](https://github.com/emilk/egui/pull/3391) +* Add `Context::open_url` and `Context::copy_text` [#3380](https://github.com/emilk/egui/pull/3380) +* Add `Area::constrain_to` and `Window::constrain_to` [#3396](https://github.com/emilk/egui/pull/3396) +* Add `Memory::area_rect` [#3161](https://github.com/emilk/egui/pull/3161) (thanks [@tosti007](https://github.com/tosti007)!) +* Add `Margin::expand_rect` and `shrink_rect` [#3214](https://github.com/emilk/egui/pull/3214) +* Provide `into_inner()` for `egui::mutex::{Mutex, RwLock}` [#3110](https://github.com/emilk/egui/pull/3110) (thanks [@KmolYuan](https://github.com/KmolYuan)!) +* Support multi-threaded Wasm [#3236](https://github.com/emilk/egui/pull/3236) +* Change touch force to be `Option` instead of `f32` [#3240](https://github.com/emilk/egui/pull/3240) (thanks [@lucasmerlin](https://github.com/lucasmerlin)!) +* Add option to always open hyperlink in a new browser tab [#3242](https://github.com/emilk/egui/pull/3242) (thanks [@FreddyFunk](https://github.com/FreddyFunk)!) +* Add `Window::drag_to_scroll` [#3118](https://github.com/emilk/egui/pull/3118) (thanks [@KYovchevski](https://github.com/KYovchevski)!) +* Add `CollapsingState::remove` to clear stored state [#3252](https://github.com/emilk/egui/pull/3252) (thanks [@dmackdev](https://github.com/dmackdev)!) +* Add tooltip_delay option [#3245](https://github.com/emilk/egui/pull/3245) (thanks [@YgorSouza](https://github.com/YgorSouza)!) +* Added `Context::is_context_menu_open()` [#3267](https://github.com/emilk/egui/pull/3267) (thanks [@dmlary](https://github.com/dmlary)!) +* Add `mime` field to `DroppedFile` [#3273](https://github.com/emilk/egui/pull/3273) (thanks [@abey79](https://github.com/abey79)!) +* Allow setting the progress bar height [#3183](https://github.com/emilk/egui/pull/3183) (thanks [@s-nie](https://github.com/s-nie)!) +* Add `scroll_area::State::velocity` [#3300](https://github.com/emilk/egui/pull/3300) (thanks [@Barugon](https://github.com/Barugon)!) +* Add `Visuals::interact_cursor` [#3312](https://github.com/emilk/egui/pull/3312) (thanks [@zkldi](https://github.com/zkldi)!) +* Add method to `RichText` making it easier to construct layout jobs [#3319](https://github.com/emilk/egui/pull/3319) (thanks [@OmegaJak](https://github.com/OmegaJak)!) +* Add `Context::style_mut` [#3359](https://github.com/emilk/egui/pull/3359) +* `std::borrow::Cow<'_, str>` now implements `TextBuffer` [#3164](https://github.com/emilk/egui/pull/3164) (thanks [@burtonageo](https://github.com/burtonageo)!) + +### 🔧 Changed +* Separate text cursor from selection visuals [#3181](https://github.com/emilk/egui/pull/3181) (thanks [@lampsitter](https://github.com/lampsitter)!) +* `DragValue`: update value on each key press by default [#2880](https://github.com/emilk/egui/pull/2880) (thanks [@Barugon](https://github.com/Barugon)!) +* Replace uses of `RangeInclusive` with `emath::Rangef` [#3221](https://github.com/emilk/egui/pull/3221) +* Implement `Send + Sync` for `ColorPickerFn` and `Ui` (#3148) [#3233](https://github.com/emilk/egui/pull/3233) (thanks [@idanarye](https://github.com/idanarye)!) +* Use the minus character instead of "dash" [#3271](https://github.com/emilk/egui/pull/3271) +* Changing `menu_image_button` to use `ImageButton` builder [#3288](https://github.com/emilk/egui/pull/3288) (thanks [@v-kat](https://github.com/v-kat)!) +* Prune old egui memory data when reaching some limit [#3299](https://github.com/emilk/egui/pull/3299) + +### 🐛 Fixed +* Fix TextEdit's character limit [#3173](https://github.com/emilk/egui/pull/3173) (thanks [@Serverator](https://github.com/Serverator)!) +* Set the correct unicode character for "ctrl" shortcuts [#3186](https://github.com/emilk/egui/pull/3186) (thanks [@abey79](https://github.com/abey79)!) +* Fix crash in `DragValue` when only setting `min_decimals` [#3231](https://github.com/emilk/egui/pull/3231) +* Fix clipping issued with `ScrollArea` [#2860](https://github.com/emilk/egui/pull/2860) (thanks [@Barugon](https://github.com/Barugon)!) +* Fix moving slider with arrow keys [#3354](https://github.com/emilk/egui/pull/3354) +* Fix problems with tabs in text [#3355](https://github.com/emilk/egui/pull/3355) +* Fix interaction with moved color-picker [#3395](https://github.com/emilk/egui/pull/3395) + ## 0.22.0 - 2023-05-23 - A plethora of small improvements @@ -244,7 +491,7 @@ Changes since the last release can be found by running the `scripts/generate_cha ### Contributors 🙏 * [4JX](https://github.com/4JX) -* [AlexxxRu](https://github.com/AlexxxRu) +* [a-liashenko](https://github.com/a-liashenko) * [ascclemens](https://github.com/ascclemens) * [awaken1ng](https://github.com/awaken1ng) * [bigfarts](https://github.com/bigfarts) @@ -340,7 +587,7 @@ Changes since the last release can be found by running the `scripts/generate_cha * [4JX](https://github.com/4JX) * [55nknown](https://github.com/55nknown) * [AlanRace](https://github.com/AlanRace) -* [AlexxxRu](https://github.com/AlexxxRu) +* [a-liashenko](https://github.com/a-liashenko) * [awaken1ng](https://github.com/awaken1ng) * [BctfN0HUK7Yg](https://github.com/BctfN0HUK7Yg) * [Bromeon](https://github.com/Bromeon) diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000000..1c8d123d025 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +/crates/egui_plot @Bromeon @EmbersArc diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8d3897457d4..206829e7aa8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,11 +1,13 @@ -# Contributing guidelines +# Contribution Guidelines ## Introduction -`egui` has been an on-and-off weekend project of mine since late 2018. I am grateful to any help I can get, but bare in mind that sometimes I can be slow to respond because I am busy with other things! +`egui` has been an on-and-off weekend project of mine since late 2018. I am grateful to any help I can get, but bear in mind that sometimes I can be slow to respond because I am busy with other things! / Emil +## How to contribute to egui +You want to contribute to egui, but don't know how? First of all: thank you! I created a special issue just for that: . But make sure you still read this file first :) ## Discussion @@ -25,7 +27,7 @@ If you are filing a bug, please provide a way to reproduce it. ## Making a PR -First file an issue (or find an existing one) and announce that you plan to work on something. That way we will avoid having several people doing double work. Please ask for feedback before you start working on something non-trivial! +For small things, just go ahead an open a PR. For bigger things, please file an issue first (or find an existing one) and announce that you plan to work on something. That way we will avoid having several people doing double work, and you might get useful feedback on the issue before you start working. Browse through [`ARCHITECTURE.md`](ARCHITECTURE.md) to get a sense of how all pieces connects. @@ -34,76 +36,115 @@ You can test your code locally by running `./scripts/check.sh`. 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. -Please keep pull requests small and focused. - Don't worry about having many small commits in the PR - they will be squashed to one commit once merged. -Do not include the `.js` and `.wasm` build artifacts generated for building for web. -`git` is not great at storing large files like these, so we only commit a new web demo after a new egui release. +Please keep pull requests small and focused. The smaller it is, the more likely it is to get merged. + +## PR review + +Most PR reviews are done by me, Emil, but I very much appreciate any help I can get reviewing PRs! +It is very easy to add complexity to a project, but remember that each line of code added is code that needs to be maintained in perpetuity, so we have a high bar on what get merged! + +When reviewing, we look for: +* The PR title and description should be helpful +* Breaking changes are documented in the PR description +* The code should be readable +* The code should have helpful docstrings +* The code should follow the [Code Style](CONTRIBUTING.md#code-style) + +Note that each new egui release have some breaking changes, so we don't mind having a few of those in a PR. Of course, we still try to avoid them if we can, and if we can't we try to first deprecate old code using the `#[deprecated]` attribute. ## Creating an integration for egui -If you make an integration for `egui` for some engine or renderer, please share it with the world! -I will add a link to it from the `egui` README.md so others can easily find it. +See for how to write your own egui integration. -Read the section on integrations at . +If you make an integration for `egui` for some engine or renderer, please share it with the world! +Make a PR to add it as a link to [`README.md`](README.md#integrations) so others can easily find it. ## Testing the web viewer -* Install some tools with `scripts/setup_web.sh` * Build with `scripts/build_demo_web.sh` * Host with `scripts/start_server.sh` * Open -## Code Conventions -Conventions unless otherwise specified: - -* angles are in radians and clock-wise -* `Vec2::X` is right and `Vec2::Y` is down. -* `Pos2::ZERO` is left top. - +## Code Style While using an immediate mode gui is simple, implementing one is a lot more tricky. There are many subtle corner-case you need to think through. The `egui` source code is a bit messy, partially because it is still evolving. -* Read some code before writing your own. -* Follow the `egui` code style. -* Add blank lines around all `fn`, `struct`, `enum`, etc. -* `// Comment like this.` and not `//like this`. -* Use `TODO` instead of `FIXME`. -* Add your github handle to the `TODO`:s you write, e.g: `TODO(emilk): clean this up`. -* Write idiomatic rust. -* Avoid `unsafe`. -* Avoid code that can cause panics. -* Use good names for everything. -* Add docstrings to types, `struct` fields and all `pub fn`. -* Add some example code (doc-tests). -* Before making a function longer, consider adding a helper function. -* If you are only using it in one function, put the `use` statement in that function. This improves locality, making it easier to read and move the code. -* When importing a `trait` to use it's trait methods, do this: `use Trait as _;`. That lets the reader know why you imported it, even though it seems unused. -* Follow the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/). -* Break the above rules when it makes sense. +* Read some code before writing your own +* Leave the code cleaner than how you found it +* Write idiomatic rust +* Follow the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) +* Add blank lines around all `fn`, `struct`, `enum`, etc +* `// Comment like this.` and not `//like this` +* Use `TODO` instead of `FIXME` +* Add your github handle to the `TODO`:s you write, e.g: `TODO(emilk): clean this up` +* Avoid `unsafe` +* Avoid `unwrap` and any other code that can cause panics +* Use good names for everything +* Add docstrings to types, `struct` fields and all `pub fn` +* Add some example code (doc-tests) +* Before making a function longer, consider adding a helper function +* If you are only using it in one function, put the `use` statement in that function. This improves locality, making it easier to read and move the code +* When importing a `trait` to use it's trait methods, do this: `use Trait as _;`. That lets the reader know why you imported it, even though it seems unused +* Avoid double negatives +* Flip `if !condition {} else {}` +* Sets of things should be lexicographically sorted (e.g. crate dependencies in `Cargo.toml`) +* Break the above rules when it makes sense ### Good: ``` rust /// The name of the thing. -fn name(&self) -> &str { +pub fn name(&self) -> &str { &self.name } fn foo(&self) { - // TODO(emilk): implement + // TODO(emilk): this can be optimized } ``` ### Bad: ``` rust -//some function -fn get_name(&self) -> &str { +//gets the name +pub fn get_name(&self) -> &str { &self.name } fn foo(&self) { - //FIXME: implement + //FIXME: this can be optimized } ``` + +### Coordinate system +The left-top corner of the screen is `(0.0, 0.0)`, +with `Vec2::X` increasing to the right and `Vec2::Y` increasing downwards. + +`egui` uses logical _points_ as its coordinate system. +Those related to physical _pixels_ by the `pixels_per_point` scale factor. +For example, a high-dpi screen can have `pixels_per_point = 2.0`, +meaning there are two physical screen pixels for each logical point. + +Angles are in radians, and are measured clockwise from the X-axis, which has angle=0. + + +### Avoid `unwrap`, `expect` etc. +The code should never panic or crash, which means that any instance of `unwrap` or `expect` is a potential time-bomb. Even if you structured your code to make them impossible, any reader will have to read the code very carefully to prove to themselves that an `unwrap` won't panic. Often you can instead rewrite your code so as to avoid it. The same goes for indexing into a slice (which will panic on out-of-bounds) - it is often preferable to use `.get()`. + +For instance: + +``` rust +let first = if vec.is_empty() { + return; +} else { + vec[0] +}; +``` +can be better written as: + +``` rust +let Some(first) = vec.first() else { + return; +}; +``` diff --git a/Cargo.lock b/Cargo.lock index e5b0161344c..ba8d5a17a5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,9 +20,9 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" [[package]] name = "accesskit" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c98a5d094590335462354da402d754fe2cb78f0e6ce5024611c28ed539c1de" +checksum = "ca8410747ed85a17c4a1e9ed3f5a74d3e7bdcc876cf9a18ff40ae21d645997b2" dependencies = [ "enumn", "serde", @@ -30,59 +30,61 @@ dependencies = [ [[package]] name = "accesskit_consumer" -version = "0.15.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca541e0fdb600916d196a940228df99b86d804fd2e6ef13894d7814f2799db43" +checksum = "8c17cca53c09fbd7288667b22a201274b9becaa27f0b91bf52a526db95de45e6" dependencies = [ "accesskit", ] [[package]] name = "accesskit_macos" -version = "0.7.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfea17e5bb5dcbfcf5b256ab2f5889a3e6f6582de78b9db9b6689adad3b002f3" +checksum = "cd3b6ae1eabbfbced10e840fd3fce8a93ae84f174b3e4ba892ab7bcb42e477a7" dependencies = [ "accesskit", "accesskit_consumer", - "objc2", + "objc2 0.3.0-beta.3.patch-leaks.3", "once_cell", ] [[package]] name = "accesskit_unix" -version = "0.5.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4d1517421278cc8e67422d0786a18cf4291093ebe49eadf1cf989ff80e57f90" +checksum = "6c8c9b4467d77cacfbc93cee9aa8e7822f6d527c774efdca5f8b3a5280c34847" dependencies = [ "accesskit", "accesskit_consumer", "async-channel", + "async-once-cell", "atspi", "futures-lite", + "once_cell", "serde", "zbus", ] [[package]] name = "accesskit_windows" -version = "0.14.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11c7f177739f23bd19bb856e4a64fdd96eb8638ec0a6a6dde9a7019a9e91c53" +checksum = "afcae27ec0974fc7c3b0b318783be89fd1b2e66dd702179fe600166a38ff4a0b" dependencies = [ "accesskit", "accesskit_consumer", - "arrayvec", "once_cell", "paste", - "windows 0.44.0", + "static_assertions", + "windows 0.48.0", ] [[package]] name = "accesskit_winit" -version = "0.14.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f741b54fba827e49a73d55fdd43e8d3d5133aa7710a48581013c7802f232b83" +checksum = "5284218aca17d9e150164428a0ebc7b955f70e3a9a78b4c20894513aabf98a67" dependencies = [ "accesskit", "accesskit_macos", @@ -93,9 +95,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.19.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -108,45 +110,44 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "ahash" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", + "getrandom", "once_cell", "serde", "version_check", + "zerocopy", ] [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "android-activity" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c77a0045eda8b888c76ea473c2b0515ba6f471d318f8927c5c72240937035a6" +checksum = "052ad56e336bcc615a214bffbeca6c181ee9550acec193f0327e0b103b033a4d" dependencies = [ "android-properties", - "bitflags 1.3.2", + "bitflags 2.4.0", "cc", + "cesu8", + "jni", "jni-sys", "libc", "log", @@ -154,6 +155,7 @@ dependencies = [ "ndk-context", "ndk-sys", "num_enum", + "thiserror", ] [[package]] @@ -162,6 +164,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -177,27 +185,31 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arboard" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6041616acea41d67c4a984709ddab1587fd0b10efe5cc563fee954d2f011854" +checksum = "1faa3c733d9a3dd6fbaf85da5d162a2e03b2e0033a90dceb0e2a90fdd1e5380a" dependencies = [ "clipboard-win", "log", "objc", "objc-foundation", "objc_id", - "once_cell", "parking_lot", "thiserror", - "winapi", "x11rb", ] @@ -209,9 +221,15 @@ checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" [[package]] name = "arrayvec" -version = "0.7.2" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +checksum = "2d5f312b0a56c5cdf967c0aeb67f6289603354951683bc97ddc595ab974ba9aa" [[package]] name = "ash" @@ -228,35 +246,47 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" dependencies = [ - "event-listener", + "event-listener 2.5.3", "futures-core", ] [[package]] name = "async-channel" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ "concurrent-queue", - "event-listener", + "event-listener 2.5.3", "futures-core", ] [[package]] name = "async-executor" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" +checksum = "78f2db9467baa66a700abce2a18c5ad793f6f83310aca1284796fc3921d113fd" dependencies = [ "async-lock", "async-task", "concurrent-queue", - "fastrand", + "fastrand 2.0.1", "futures-lite", "slab", ] +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock", + "autocfg", + "blocking", + "futures-lite", +] + [[package]] name = "async-io" version = "1.13.0" @@ -270,8 +300,8 @@ dependencies = [ "futures-lite", "log", "parking", - "polling", - "rustix", + "polling 2.8.0", + "rustix 0.37.25", "slab", "socket2", "waker-fn", @@ -279,46 +309,88 @@ dependencies = [ [[package]] name = "async-lock" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ - "event-listener", + "event-listener 2.5.3", +] + +[[package]] +name = "async-once-cell" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9338790e78aa95a416786ec8389546c4b6a1dfc3dc36071ed9518a9413a542eb" + +[[package]] +name = "async-process" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf012553ce51eb7aa6dc2143804cc8252bd1cb681a1c5cb7fa94ca88682dee1d" +dependencies = [ + "async-io", + "async-lock", + "async-signal", + "blocking", + "cfg-if", + "event-listener 3.0.0", + "futures-lite", + "rustix 0.38.21", + "windows-sys 0.48.0", ] [[package]] name = "async-recursion" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" +checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.48", +] + +[[package]] +name = "async-signal" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4af361a844928cb7d36590d406709473a1b574f443094422ef166daa3b493208" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "concurrent-queue", + "futures-core", + "futures-io", + "libc", + "signal-hook-registry", + "slab", + "windows-sys 0.48.0", ] [[package]] name = "async-task" -version = "4.4.0" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" +checksum = "b9441c6b2fe128a7c2bf680a44c34d0df31ce09e5b7e401fcca3faa483dbc921" [[package]] name = "async-trait" -version = "0.1.68" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.48", ] [[package]] name = "atk-sys" -version = "0.16.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ad703eb64dc058024f0e57ccfa069e15a413b98dbd50a1a950e743b7f11148" +checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" dependencies = [ "glib-sys", "gobject-sys", @@ -326,42 +398,58 @@ dependencies = [ "system-deps", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atspi" -version = "0.10.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "674e7a3376837b2e7d12d34d58ac47073c491dc3bf6f71a7adaf687d4d817faa" +checksum = "6059f350ab6f593ea00727b334265c4dfc7fd442ee32d264794bd9bdc68e87ca" +dependencies = [ + "atspi-common", + "atspi-connection", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92af95f966d2431f962bc632c2e68eda7777330158bf640c4af4249349b2cdf5" dependencies = [ - "async-recursion", - "async-trait", - "atspi-macros", "enumflags2", - "futures-lite", "serde", - "tracing", + "static_assertions", "zbus", "zbus_names", + "zvariant", ] [[package]] -name = "atspi-macros" -version = "0.2.0" +name = "atspi-connection" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb4870a32c0eaa17e35bca0e6b16020635157121fb7d45593d242c295bc768" +checksum = "a0c65e7d70f86d4c0e3b2d585d9bf3f979f0b19d635a336725a88d279f76b939" dependencies = [ - "quote", - "syn 1.0.109", + "atspi-common", + "atspi-proxies", + "futures-lite", + "zbus", ] [[package]] -name = "atty" -version = "0.2.14" +name = "atspi-proxies" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "6495661273703e7a229356dcbe8c8f38223d697aacfaf0e13590a9ac9977bb52" dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", + "atspi-common", + "serde", + "zbus", ] [[package]] @@ -372,9 +460,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.67" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -387,9 +475,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.13.1" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" [[package]] name = "bincode" @@ -400,29 +488,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.65.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" -dependencies = [ - "bitflags 1.3.2", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.16", - "which", -] - [[package]] name = "bit-set" version = "0.5.3" @@ -446,9 +511,12 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +dependencies = [ + "serde", +] [[package]] name = "block" @@ -471,7 +539,16 @@ version = "0.1.0-beta.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa55741ee90902547802152aaf3f8e5248aab7e21468089560d4c8840561146" dependencies = [ - "objc-sys", + "objc-sys 0.2.0-beta.2", +] + +[[package]] +name = "block-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dd7cf50912cddc06dc5ea7c08c5e81c1b2c842a70d19def1848d54c586fed92" +dependencies = [ + "objc-sys 0.3.1", ] [[package]] @@ -480,34 +557,60 @@ version = "0.2.0-alpha.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8dd9e63c1744f755c2f60332b88de39d341e5e86239014ad839bd71c106dec42" dependencies = [ - "block-sys", - "objc2-encode", + "block-sys 0.1.0-beta.1", + "objc2-encode 2.0.0-pre.2", +] + +[[package]] +name = "block2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b55663a85f33501257357e6421bb33e769d5c9ffb5ba0921c975a123e35e68" +dependencies = [ + "block-sys 0.2.0", + "objc2 0.4.1", +] + +[[package]] +name = "blocking" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94c4ef1f913d78636d78d538eec1f18de81e481f44b1be0a81060090530846e1" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "fastrand 2.0.1", + "futures-io", + "futures-lite", + "piper", + "tracing", ] [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "bytemuck" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdde5c9cd29ebd706ce1b35600920a33550e402fc998a2e53ad3b42c3c47a192" +checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.48", ] [[package]] @@ -518,15 +621,15 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cairo-sys-rs" -version = "0.16.3" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c48f4af05fabdcfa9658178e1326efa061853f040ce7d72e33af6885196f421" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" dependencies = [ "libc", "system-deps", @@ -534,15 +637,28 @@ dependencies = [ [[package]] name = "calloop" -version = "0.10.5" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a59225be45a478d772ce015d9743e49e92798ece9e34eda9a6aa2a6a7f40192" +checksum = "7b50b5a44d59a98c55a9eeb518f39bf7499ba19fd98ee7d22618687f3f10adbf" dependencies = [ + "bitflags 2.4.0", "log", - "nix 0.25.1", - "slotmap", + "polling 3.3.0", + "rustix 0.38.21", + "slab", "thiserror", - "vec_map", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" +dependencies = [ + "calloop", + "rustix 0.38.21", + "wayland-backend", + "wayland-client", ] [[package]] @@ -553,11 +669,12 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -566,20 +683,11 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-expr" -version = "0.15.1" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8790cf1286da485c72cf5fc7aeba308438800036ec67d89425924c4807268c9" +checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" dependencies = [ "smallvec", "target-lexicon", @@ -608,17 +716,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", - "time 0.1.45", "wasm-bindgen", - "winapi", + "windows-targets 0.48.5", ] [[package]] @@ -655,75 +762,65 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a0e87cdf78571d9fbeff16861c37a006cd718d2433dc6d5b80beaae367d899a" [[package]] -name = "clang-sys" -version = "1.6.1" +name = "clap" +version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" dependencies = [ - "glob", - "libc", - "libloading 0.7.4", + "clap_builder", ] [[package]] -name = "clap" -version = "3.2.25" +name = "clap_builder" +version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" dependencies = [ - "bitflags 1.3.2", + "anstyle", "clap_lex", - "indexmap", - "textwrap", ] [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "clipboard-win" -version = "4.5.0" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +checksum = "3ec832972fefb8cf9313b45a0d1945e29c9c251f1d4c6eafc5fe2124c02d2e81" dependencies = [ "error-code", - "str-buf", - "winapi", ] [[package]] name = "cocoa" -version = "0.24.1" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" dependencies = [ "bitflags 1.3.2", "block", "cocoa-foundation", "core-foundation", "core-graphics", - "foreign-types 0.3.2", + "foreign-types", "libc", "objc", ] [[package]] name = "cocoa-foundation" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "931d3837c286f56e3c58423ce4eba12d08db2374461a785c86f672b08b5650d6" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" dependencies = [ "bitflags 1.3.2", "block", "core-foundation", "core-graphics-types", - "foreign-types 0.3.2", "libc", "objc", ] @@ -751,10 +848,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] -name = "com-rs" -version = "0.2.1" +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" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf43edc576402991846b093a7ca18a3477e0ef9c588cde84964b5d3e43016642" +checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] [[package]] name = "combine" @@ -768,9 +890,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" dependencies = [ "crossbeam-utils", ] @@ -801,34 +923,33 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "core-graphics" -version = "0.22.3" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212" dependencies = [ "bitflags 1.3.2", "core-foundation", "core-graphics-types", - "foreign-types 0.3.2", + "foreign-types", "libc", ] [[package]] name = "core-graphics-types" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" +checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33" dependencies = [ "bitflags 1.3.2", "core-foundation", - "foreign-types 0.3.2", "libc", ] [[package]] name = "cpufeatures" -version = "0.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" dependencies = [ "libc", ] @@ -844,19 +965,19 @@ dependencies = [ [[package]] name = "criterion" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" dependencies = [ "anes", - "atty", "cast", "ciborium", "clap", "criterion-plot", + "is-terminal", "itertools", - "lazy_static", "num-traits", + "once_cell", "oorandom", "regex", "serde", @@ -887,14 +1008,30 @@ dependencies = [ ] [[package]] -name = "crossbeam-utils" -version = "0.8.15" +name = "crossbeam-deque" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + [[package]] name = "crypto-common" version = "0.1.6" @@ -905,14 +1042,18 @@ dependencies = [ "typenum", ] +[[package]] +name = "cursor-icon" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" + [[package]] name = "custom_3d_glow" version = "0.1.0" dependencies = [ "eframe", - "egui_glow", "env_logger", - "glow", ] [[package]] @@ -932,64 +1073,45 @@ dependencies = [ ] [[package]] -name = "custom_window_frame" +name = "custom_keypad" version = "0.1.0" dependencies = [ "eframe", + "egui_extras", "env_logger", ] [[package]] -name = "d3d12" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16e44ab292b1dddfdaf7be62cfd8877df52f2f3fde5858d95bab606be259f20" +name = "custom_plot_manipulation" +version = "0.1.0" dependencies = [ - "bitflags 2.3.1", - "libloading 0.8.0", - "winapi", + "eframe", + "egui_plot", + "env_logger", ] [[package]] -name = "darling" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +name = "custom_window_frame" +version = "0.1.0" dependencies = [ - "darling_core", - "darling_macro", + "eframe", + "env_logger", ] [[package]] -name = "darling_core" -version = "0.13.4" +name = "data-url" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 1.0.109", -] +checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" [[package]] -name = "darling_macro" -version = "0.13.4" +name = "deranged" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" dependencies = [ - "darling_core", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "data-url" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5" + "powerfmt", +] [[package]] name = "derivative" @@ -1022,26 +1144,6 @@ dependencies = [ "dirs-sys-next", ] -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -1061,18 +1163,18 @@ checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] name = "dlib" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.7.4", + "libloading 0.8.0", ] [[package]] name = "document-features" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e493c573fce17f00dcab13b6ac057994f3ce17d1af4dc39bfd482b83c6eb6157" +checksum = "ef5282ad69563b5fc40319526ba27e0e7363d552a896f0297d54f767717f9b95" dependencies = [ "litrs", ] @@ -1083,48 +1185,9 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" -[[package]] -name = "download_image" -version = "0.1.0" -dependencies = [ - "eframe", - "egui_extras", - "ehttp", - "env_logger", - "image", - "poll-promise", -] - -[[package]] -name = "dyn-clonable" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e9232f0e607a262ceb9bd5141a3dfb3e4db6994b31989bbfd845878cba59fd4" -dependencies = [ - "dyn-clonable-impl", - "dyn-clone", -] - -[[package]] -name = "dyn-clonable-impl" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558e40ea573c374cf53507fd240b7ee2f5477df7cfebdb97323ec61c719399c5" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "dyn-clone" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" - [[package]] name = "ecolor" -version = "0.22.0" +version = "0.26.2" dependencies = [ "bytemuck", "cint", @@ -1135,7 +1198,7 @@ dependencies = [ [[package]] name = "eframe" -version = "0.22.0" +version = "0.26.2" dependencies = [ "bytemuck", "cocoa", @@ -1152,18 +1215,20 @@ dependencies = [ "js-sys", "log", "objc", + "parking_lot", "percent-encoding", "pollster", "puffin", - "raw-window-handle", + "raw-window-handle 0.5.2", + "raw-window-handle 0.6.0", "ron", "serde", "static_assertions", "thiserror", - "tts", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "web-time", "wgpu", "winapi", "winit", @@ -1171,54 +1236,58 @@ dependencies = [ [[package]] name = "egui" -version = "0.22.0" +version = "0.26.2" dependencies = [ "accesskit", - "ahash 0.8.3", + "ahash", + "backtrace", "document-features", "epaint", "log", "nohash-hasher", + "puffin", "ron", "serde", ] [[package]] name = "egui-wgpu" -version = "0.22.0" +version = "0.26.2" dependencies = [ "bytemuck", "document-features", + "egui", "epaint", "log", "puffin", "thiserror", "type-map", + "web-time", "wgpu", "winit", ] [[package]] name = "egui-winit" -version = "0.22.0" +version = "0.26.2" dependencies = [ "accesskit_winit", "arboard", "document-features", "egui", - "instant", "log", "puffin", - "raw-window-handle", + "raw-window-handle 0.6.0", "serde", "smithay-clipboard", + "web-time", "webbrowser", "winit", ] [[package]] name = "egui_demo_app" -version = "0.22.0" +version = "0.26.2" dependencies = [ "bytemuck", "chrono", @@ -1231,46 +1300,52 @@ dependencies = [ "image", "log", "poll-promise", + "puffin", + "puffin_http", + "rfd", "serde", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "wgpu", ] [[package]] name = "egui_demo_lib" -version = "0.22.0" +version = "0.26.2" dependencies = [ "chrono", "criterion", "document-features", "egui", "egui_extras", - "enum-map", + "egui_plot", "log", "serde", - "syntect", "unicode_names2", ] [[package]] name = "egui_extras" -version = "0.22.0" +version = "0.26.2" dependencies = [ "chrono", "document-features", "egui", + "ehttp", + "enum-map", "image", "log", + "mime_guess2", + "puffin", "resvg", "serde", - "tiny-skia", - "usvg", + "syntect", ] [[package]] name = "egui_glow" -version = "0.22.0" +version = "0.26.2" dependencies = [ "bytemuck", "document-features", @@ -1280,19 +1355,30 @@ dependencies = [ "glutin", "glutin-winit", "log", - "memoffset", + "memoffset 0.9.0", "puffin", - "raw-window-handle", + "raw-window-handle 0.5.2", "wasm-bindgen", "web-sys", + "winit", +] + +[[package]] +name = "egui_plot" +version = "0.26.2" +dependencies = [ + "document-features", + "egui", + "serde", ] [[package]] name = "ehttp" -version = "0.2.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80b69a6f9168b96c0ae04763bec27a8b06b34343c334dd2703a4ec21f0f5e110" +checksum = "59a81c221a1e4dad06cb9c9deb19aea1193a5eea084e8cd42d869068132bf876" dependencies = [ + "document-features", "js-sys", "ureq", "wasm-bindgen", @@ -1302,13 +1388,13 @@ dependencies = [ [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "emath" -version = "0.22.0" +version = "0.26.2" dependencies = [ "bytemuck", "document-features", @@ -1318,9 +1404,9 @@ dependencies = [ [[package]] name = "enum-map" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "988f0d17a0fa38291e5f41f71ea8d46a5d5497b9054d5a759fae2cbb819f2356" +checksum = "c188012f8542dee7b3996e44dd89461d64aa471b0a7c71a1ae2f595d259e96e5" dependencies = [ "enum-map-derive", "serde", @@ -1328,20 +1414,20 @@ dependencies = [ [[package]] name = "enum-map-derive" -version = "0.11.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4da76b3b6116d758c7ba93f7ec6a35d2e2cf24feda76c6e38a375f4d5c59f2" +checksum = "04d0b288e3bb1d861c4403c1774a6f7a798781dfc519b3647df2a3dd4ae95f25" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.48", ] [[package]] name = "enumflags2" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c041f5090df68b32bcd905365fd51769c8b9d553fe87fde0b683534f10c01bd2" +checksum = "5998b4f30320c9d93aed72f63af821bfdac50465b75428fce77b48ec482c3939" dependencies = [ "enumflags2_derive", "serde", @@ -1349,24 +1435,24 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" +checksum = "f95e2801cd355d4a1a3e3953ce6ee5ae9603a5c833455343a8bfe3f44d418246" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.48", ] [[package]] name = "enumn" -version = "0.1.8" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48016319042fb7c87b78d2993084a831793a897a5cd1a2a67cab9d1eeb4b7d76" +checksum = "c2ad8cef1d801a4686bfd8919f0b30eac4c8e48968c437a6405ded4fb5272d2b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.48", ] [[package]] @@ -1378,16 +1464,15 @@ dependencies = [ "humantime", "is-terminal", "log", - "regex", "termcolor", ] [[package]] name = "epaint" -version = "0.22.0" +version = "0.26.2" dependencies = [ "ab_glyph", - "ahash 0.8.3", + "ahash", "backtrace", "bytemuck", "criterion", @@ -1397,14 +1482,22 @@ dependencies = [ "log", "nohash-hasher", "parking_lot", + "puffin", + "rayon", "serde", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ "errno-dragonfly", "libc", @@ -1423,13 +1516,9 @@ dependencies = [ [[package]] name = "error-code" -version = "2.3.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" -dependencies = [ - "libc", - "str-buf", -] +checksum = "281e452d3bad4005426416cdba5ccfd4f5c1280e10099e21db27f7c1c28347fc" [[package]] name = "event-listener" @@ -1437,11 +1526,22 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29e56284f00d94c1bc7fd3c77027b4623c88c1f53d8d2394c6199f2921dea325" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + [[package]] name = "fancy-regex" -version = "0.7.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6b8560a05112eb52f04b00e5d3790c0dd75d9d980eb8a122fb23b92a623ccf" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" dependencies = [ "bit-set", "regex", @@ -1456,6 +1556,21 @@ dependencies = [ "instant", ] +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + [[package]] name = "file_dialog" version = "0.1.0" @@ -1467,9 +1582,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.25" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", "miniz_oxide", @@ -1487,15 +1602,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared 0.1.1", -] - [[package]] name = "foreign-types" version = "0.5.0" @@ -1503,7 +1609,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared 0.3.1", + "foreign-types-shared", ] [[package]] @@ -1514,15 +1620,9 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.48", ] -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -1531,9 +1631,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -1556,7 +1656,7 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ - "fastrand", + "fastrand 1.9.0", "futures-core", "futures-io", "memchr", @@ -1595,9 +1695,9 @@ dependencies = [ [[package]] name = "gdk-pixbuf-sys" -version = "0.16.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3092cf797a5f1210479ea38070d9ae8a5b8e9f8f1be9f32f4643c529c7d70016" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" dependencies = [ "gio-sys", "glib-sys", @@ -1608,9 +1708,9 @@ dependencies = [ [[package]] name = "gdk-sys" -version = "0.16.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76354f97a913e55b984759a997b693aa7dc71068c9e98bcce51aa167a0a5c5a" +checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -1635,36 +1735,36 @@ dependencies = [ [[package]] name = "gethostname" -version = "0.2.3" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ "libc", - "winapi", + "windows-targets 0.48.5", ] [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "gimli" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "gio-sys" -version = "0.16.3" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9b693b8e39d042a95547fc258a7b07349b1f0b48f4b2fa3108ba3c51c0b5229" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" dependencies = [ "glib-sys", "gobject-sys", @@ -1686,25 +1786,19 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.16.3" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" dependencies = [ "libc", "system-deps", ] -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - [[package]] name = "glow" -version = "0.12.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca0fe580e4b60a8ab24a868bc08e2f03cbcb20d3d676601fa909386713333728" +checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" dependencies = [ "js-sys", "slotmap", @@ -1714,11 +1808,11 @@ dependencies = [ [[package]] name = "glutin" -version = "0.30.8" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f9b771a65f0a1e3ddb6aa16f867d87dc73c922411c255e6c4ab7f6d45c7327" +checksum = "005459a22af86adc706522d78d360101118e2638ec21df3852fcc626e0dbb212" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "cfg_aliases", "cgl", "core-foundation", @@ -1726,42 +1820,43 @@ dependencies = [ "glutin_egl_sys", "glutin_glx_sys", "glutin_wgl_sys", - "libloading 0.7.4", - "objc2", + "icrate", + "libloading 0.8.0", + "objc2 0.4.1", "once_cell", - "raw-window-handle", - "wayland-sys 0.30.1", - "windows-sys 0.45.0", + "raw-window-handle 0.5.2", + "wayland-sys", + "windows-sys 0.48.0", "x11-dl", ] [[package]] name = "glutin-winit" -version = "0.3.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629a873fc04062830bfe8f97c03773bcd7b371e23bcc465d0a61448cd1588fa4" +checksum = "1ebcdfba24f73b8412c5181e56f092b5eff16671c514ce896b258a0a64bd7735" dependencies = [ "cfg_aliases", "glutin", - "raw-window-handle", + "raw-window-handle 0.5.2", "winit", ] [[package]] name = "glutin_egl_sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b3bcbddc51573b977fc6dca5d93867e4f29682cdbaf5d13e48f4fa4346d4d87" +checksum = "77cc5623f5309ef433c3dd4ca1223195347fe62c413da8e2fdd0eb76db2d9bcd" dependencies = [ "gl_generator", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] name = "glutin_glx_sys" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b53cb5fe568964aa066a3ba91eac5ecbac869fb0842cd0dc9e412434f1a1494" +checksum = "a165fd686c10dcc2d45380b35796e577eacfd43d4660ee741ec8ebe2201b3b4f" dependencies = [ "gl_generator", "x11-dl", @@ -1769,18 +1864,18 @@ dependencies = [ [[package]] name = "glutin_wgl_sys" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef89398e90033fc6bc65e9bd42fd29bbbfd483bda5b56dc5562f455550618165" +checksum = "6c8098adac955faa2d31079b65dc48841251f69efd3ac25477903fc424362ead" dependencies = [ "gl_generator", ] [[package]] name = "gobject-sys" -version = "0.16.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" dependencies = [ "glib-sys", "libc", @@ -1793,7 +1888,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.3.1", + "bitflags 2.4.0", "gpu-alloc-types", ] @@ -1803,47 +1898,47 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.3.1", + "bitflags 2.4.0", ] [[package]] name = "gpu-allocator" -version = "0.22.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce95f9e2e11c2c6fadfce42b5af60005db06576f231f5c92550fdded43c423e8" +checksum = "6f56f6318968d03c18e1bcf4857ff88c61157e9da8e47c5f29055d60e1228884" dependencies = [ - "backtrace", "log", + "presser", "thiserror", "winapi", - "windows 0.44.0", + "windows 0.51.1", ] [[package]] name = "gpu-descriptor" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0c02e1ba0bdb14e965058ca34e09c020f8e507a760df1121728e0aef68d57a" +checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "gpu-descriptor-types", "hashbrown", ] [[package]] name = "gpu-descriptor-types" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "363e3677e55ad168fef68cf9de3a4a310b53124c5e784c53a1d70e92d23f2126" +checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", ] [[package]] name = "gtk-sys" -version = "0.16.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b5f8946685d5fe44497007786600c2f368ff6b1e61a16251c89f72a97520a3" +checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" dependencies = [ "atk-sys", "cairo-sys-rs", @@ -1865,23 +1960,24 @@ checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ - "ahash 0.7.6", + "ahash", + "allocator-api2", ] [[package]] name = "hassle-rs" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1397650ee315e8891a0df210707f0fc61771b0cc518c3023896064c5407cb3b0" +checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" dependencies = [ - "bitflags 1.3.2", - "com-rs", + "bitflags 2.4.0", + "com", "libc", - "libloading 0.7.4", + "libloading 0.8.0", "thiserror", "widestring", "winapi", @@ -1898,6 +1994,7 @@ name = "hello_world" version = "0.1.0" dependencies = [ "eframe", + "egui_extras", "env_logger", ] @@ -1919,18 +2016,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -1961,16 +2049,16 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "iana-time-zone" -version = "0.1.56" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows 0.48.0", + "windows-core", ] [[package]] @@ -1983,16 +2071,21 @@ dependencies = [ ] [[package]] -name = "ident_case" -version = "1.0.1" +name = "icrate" +version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +checksum = "99d3aaff8a54577104bafdf686ff18565c3b6903ca5782a2026ef06e2c7aa319" +dependencies = [ + "block2 0.3.0", + "dispatch", + "objc2 0.4.1", +] [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -2000,9 +2093,9 @@ dependencies = [ [[package]] name = "image" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" dependencies = [ "bytemuck", "byteorder", @@ -2013,19 +2106,29 @@ dependencies = [ "png", ] +[[package]] +name = "images" +version = "0.1.0" +dependencies = [ + "eframe", + "egui_extras", + "env_logger", + "image", +] + [[package]] name = "imagesize" -version = "0.10.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df19da1e92fbfec043ca97d622955381b1f3ee72a180ec999912df31b1ccd951" +checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" [[package]] name = "indexmap" -version = "1.9.3" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ - "autocfg", + "equivalent", "hashbrown", ] @@ -2036,31 +2139,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", ] [[package]] name = "io-lifetimes" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.1", + "hermit-abi", "libc", "windows-sys 0.48.0", ] [[package]] name = "is-terminal" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", - "rustix", + "hermit-abi", + "rustix 0.38.21", "windows-sys 0.48.0", ] @@ -2075,9 +2174,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jni" @@ -2118,9 +2217,9 @@ checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ "wasm-bindgen", ] @@ -2135,12 +2234,12 @@ dependencies = [ [[package]] name = "khronos-egl" -version = "4.1.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2352bd1d0bceb871cb9d40f24360c8133c11d7486b68b5381c1dd1a32015e3" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ "libc", - "libloading 0.7.4", + "libloading 0.8.0", "pkg-config", ] @@ -2152,30 +2251,18 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kurbo" -version = "0.8.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a53776d271cfb873b17c618af0298445c88afc52837f3e948fa3fafd131f449" +checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" dependencies = [ "arrayvec", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" -version = "0.2.144" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libloading" @@ -2218,17 +2305,23 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +[[package]] +name = "linux-raw-sys" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" + [[package]] name = "litrs" -version = "0.2.3" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9275e0933cf8bb20f008924c0cb07a0692fe54d8064996520bf998de9eb79aa" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", @@ -2236,18 +2329,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "lz4_flex" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b8c72594ac26bfd34f2d99dfced2edfaddfe8a476e3ff2ca0eb293d925c4f83" +checksum = "3ea9b256699eda7b0387ffbc776dd625e28bde3918446381781245b7a50349d8" [[package]] name = "malloc_buf" @@ -2260,43 +2350,68 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "memmap2" -version = "0.5.10" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +checksum = "deaba38d7abf1d4cca21cc89e932e542ba2b9258664d2a9ef0e61512039c9375" dependencies = [ "libc", ] [[package]] name = "memoffset" -version = "0.6.5" +version = "0.7.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 = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] [[package]] name = "metal" -version = "0.26.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "623b5e6cefd76e58f774bd3cc0c6f5c7615c58c03a97815245a25c3c9bdee318" +checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" dependencies = [ - "bitflags 2.3.1", + "bitflags 2.4.0", "block", "core-graphics-types", - "foreign-types 0.5.0", + "foreign-types", "log", "objc", "paste", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess2" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a3333bb1609500601edc766a39b4c1772874a4ce26022f4d866854dc020c41" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2305,11 +2420,12 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", + "simd-adler32", ] [[package]] @@ -2319,25 +2435,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" [[package]] -name = "mio" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" -dependencies = [ - "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.45.0", +name = "multiple_viewports" +version = "0.1.0" +dependencies = [ + "eframe", + "env_logger", ] [[package]] name = "naga" -version = "0.13.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ceaaa4eedaece7e4ec08c55c640ba03dbb73fb812a6570a59bcf1930d0f70e" +checksum = "8878eb410fc90853da3908aebfe61d73d26d4437ef850b70050461f939509899" dependencies = [ "bit-set", - "bitflags 2.3.1", + "bitflags 2.4.0", "codespan-reporting", "hexf-parse", "indexmap", @@ -2352,15 +2464,17 @@ dependencies = [ [[package]] name = "ndk" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "jni-sys", + "log", "ndk-sys", "num_enum", - "raw-window-handle", + "raw-window-handle 0.5.2", + "raw-window-handle 0.6.0", "thiserror", ] @@ -2370,68 +2484,25 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" -[[package]] -name = "ndk-glue" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0434fabdd2c15e0aab768ca31d5b7b333717f03cf02037d5a0a3ff3c278ed67f" -dependencies = [ - "libc", - "log", - "ndk", - "ndk-context", - "ndk-macro", - "ndk-sys", - "once_cell", - "parking_lot", -] - -[[package]] -name = "ndk-macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" -dependencies = [ - "darling", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "ndk-sys" -version = "0.4.1+23.1.7779620" +version = "0.5.0+25.2.9519653" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" dependencies = [ "jni-sys", ] [[package]] name = "nix" -version = "0.24.3" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", - "memoffset", -] - -[[package]] -name = "nix" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset", - "pin-utils", + "memoffset 0.7.1", ] [[package]] @@ -2473,32 +2544,32 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", ] [[package]] name = "num_enum" -version = "0.5.11" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +checksum = "683751d591e6d81200c39fb0d1032608b77724f34114db54f571ff1317b337c0" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.11" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +checksum = "6c11e44798ad209ccdd91fc192f0526a369a01234f7373e1b141c96d7cee4f0e" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.48", ] [[package]] @@ -2528,15 +2599,31 @@ version = "0.2.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b9834c1e95694a05a828b59f55fa2afec6288359cda67146126b3f90a55d7" +[[package]] +name = "objc-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e1d07c6eab1ce8b6382b8e3c7246fe117ff3f8b34be065f5ebace6749fe845" + [[package]] name = "objc2" version = "0.3.0-beta.3.patch-leaks.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e01640f9f2cb1220bbe80325e179e532cb3379ebcd1bf2279d703c19fe3a468" dependencies = [ - "block2", - "objc-sys", - "objc2-encode", + "block2 0.2.0-alpha.6", + "objc-sys 0.2.0-beta.2", + "objc2-encode 2.0.0-pre.2", +] + +[[package]] +name = "objc2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d" +dependencies = [ + "objc-sys 0.3.1", + "objc2-encode 3.0.0", ] [[package]] @@ -2545,9 +2632,15 @@ version = "2.0.0-pre.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abfcac41015b00a120608fdaa6938c44cb983fee294351cc4bac7638b4e50512" dependencies = [ - "objc-sys", + "objc-sys 0.2.0-beta.2", ] +[[package]] +name = "objc2-encode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666" + [[package]] name = "objc_exception" version = "0.1.2" @@ -2568,18 +2661,18 @@ dependencies = [ [[package]] name = "object" -version = "0.30.3" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.17.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "oorandom" @@ -2589,9 +2682,9 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "orbclient" -version = "0.3.45" +version = "0.3.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "221d488cd70617f1bd599ed8ceb659df2147d9393717954d82a0f5e8032a6ab1" +checksum = "8378ac0dfbd4e7895f2d2c1f1345cab3836910baf3a300b000d04250f0c8428f" dependencies = [ "redox_syscall 0.3.5", ] @@ -2606,12 +2699,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "os_str_bytes" -version = "6.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" - [[package]] name = "owned_ttf_parser" version = "0.19.0" @@ -2621,17 +2708,11 @@ dependencies = [ "ttf-parser", ] -[[package]] -name = "oxilangtag" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d91edf4fbb970279443471345a4e8c491bf05bb283b3e6c88e4e606fd8c181b" - [[package]] name = "pango-sys" -version = "0.16.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e134909a9a293e04d2cc31928aa95679c5e4df954d0b85483159bd20d8f047f" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" dependencies = [ "glib-sys", "gobject-sys", @@ -2641,9 +2722,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" +checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" [[package]] name = "parking_lot" @@ -2657,34 +2738,28 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", + "redox_syscall 0.3.5", "smallvec", - "windows-sys 0.45.0", + "windows-targets 0.48.5", ] [[package]] name = "paste" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" - -[[package]] -name = "peeking_take_while" -version = "0.1.2" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pico-args" @@ -2694,9 +2769,9 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -2704,6 +2779,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand 2.0.1", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.27" @@ -2712,36 +2798,38 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "plist" -version = "1.4.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5329b8f106a176ab0dce4aae5da86bfcb139bb74fb00882859e03745011f3635" +checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" dependencies = [ "base64", "indexmap", "line-wrap", - "quick-xml", + "quick-xml 0.31.0", "serde", - "time 0.3.21", + "time", ] [[package]] name = "png" -version = "0.17.7" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" +checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" dependencies = [ "bitflags 1.3.2", "crc32fast", + "fdeflate", "flate2", "miniz_oxide", ] [[package]] name = "poll-promise" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf2a02372dfae23c9c01267fb296b8a3413bb4e45fbd589c3ac73c6dcfbb305" +checksum = "5f6a58fecbf9da8965bcdb20ce4fd29788d1acee68ddbb64f0ba1b81bccdb7df" dependencies = [ + "document-features", "static_assertions", ] @@ -2761,12 +2849,32 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "polling" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53b6af1f60f36f8c2ac2aad5459d75a5a9b4be1e8cdd40264f315d78193e531" +dependencies = [ + "cfg-if", + "concurrent-queue", + "pin-project-lite", + "rustix 0.38.21", + "tracing", + "windows-sys 0.48.0", +] + [[package]] name = "pollster" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2774,14 +2882,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] -name = "prettyplease" -version = "0.2.5" +name = "presser" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617feabb81566b593beb4886fb8c1f38064169dae4dccad0e3220160c3b37203" -dependencies = [ - "proc-macro2", - "syn 2.0.16", -] +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" [[package]] name = "proc-macro-crate" @@ -2795,30 +2899,29 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.58" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] [[package]] name = "profiling" -version = "1.0.8" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332cd62e95873ea4f41f3dfd6bbbfc5b52aec892d7e8d534197c4720a0bbbab2" +checksum = "0f0f7f43585c34e4fdd7497d746bc32e14458cf11c69341cc0587b1d825dde42" [[package]] name = "puffin" -version = "0.16.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76425abd4e1a0ad4bd6995dd974b52f414fca9974171df8e3708b3e660d05a21" +checksum = "b9f76ad4bb049fded4e572df72cbb6381ff5d1f41f85c3a04b56e4eca287a02f" dependencies = [ "anyhow", "bincode", "byteorder", "cfg-if", - "instant", "lz4_flex", "once_cell", "parking_lot", @@ -2827,13 +2930,14 @@ dependencies = [ [[package]] name = "puffin_http" -version = "0.13.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13bffc600c35913d282ae1e96a6ffcdf36dc7a7cdb9310e0ba15914d258c8193" +checksum = "4936c085e48efc86f6d96609dc5086d1d236afe3ec4676f09b157a4f4be83ff6" dependencies = [ "anyhow", "crossbeam-channel", "log", + "parking_lot", "puffin", ] @@ -2849,18 +2953,27 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.26.0" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.27" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -2896,16 +3009,36 @@ dependencies = [ ] [[package]] -name = "range-alloc" -version = "0.1.3" +name = "raw-window-handle" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" [[package]] name = "raw-window-handle" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" +checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544" + +[[package]] +name = "rayon" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] [[package]] name = "rctree" @@ -2944,26 +3077,32 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.2" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1a59b5d8e97dee33696bf13c5ba8ab85341c002922fba050069326b9c498974" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.2", + "regex-automata", + "regex-syntax", ] [[package]] -name = "regex-syntax" -version = "0.6.29" +name = "regex-automata" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "renderdoc-sys" @@ -2973,9 +3112,9 @@ checksum = "216080ab382b992234dda86873c18d4c48358f5cfcb70fd693d7f6f2131b628b" [[package]] name = "resvg" -version = "0.28.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c115863f2d3621999cf187e318bc92b16402dfeff6a48c74df700d77381394c1" +checksum = "cadccb3d99a9efb8e5e00c16fbb732cbe400db2ec7fc004697ee7d97d86cf1f4" dependencies = [ "log", "pico-args", @@ -2985,26 +3124,14 @@ dependencies = [ "usvg", ] -[[package]] -name = "retained_image" -version = "0.1.0" -dependencies = [ - "eframe", - "egui_extras", - "env_logger", - "image", -] - [[package]] name = "rfd" -version = "0.11.4" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe664af397d2b6a13a8ba1d172a2b5c87c6c5149039edbf8fa122b98c9ed96f" +checksum = "c0d8ab342bcc5436e04d3a4c1e09e17d74958bfaddf8d5fad6f85607df0f994f" dependencies = [ - "async-io", "block", "dispatch", - "futures-util", "glib-sys", "gobject-sys", "gtk-sys", @@ -3013,11 +3140,11 @@ dependencies = [ "objc", "objc-foundation", "objc_id", - "raw-window-handle", + "raw-window-handle 0.5.2", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows 0.44.0", + "windows-sys 0.48.0", ] [[package]] @@ -3046,23 +3173,21 @@ dependencies = [ [[package]] name = "ron" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300a51053b1cb55c80b7a9fde4120726ddf25ca241a1cbb926626f62fb136bff" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64", - "bitflags 1.3.2", + "bitflags 2.4.0", "serde", + "serde_derive", ] [[package]] name = "roxmltree" -version = "0.15.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9de9831a129b122e7e61f242db509fa9d0838008bf0b29bb0624669edfe48a" -dependencies = [ - "xmlparser", -] +checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" [[package]] name = "rustc-demangle" @@ -3078,35 +3203,58 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.37.19" +version = "0.37.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +checksum = "d4eb579851244c2c03e7c24f501c3432bed80b8f720af1d6e5b0e0f01555a035" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys 0.4.11", "windows-sys 0.48.0", ] [[package]] name = "rustls" -version = "0.20.8" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ "log", "ring", + "rustls-webpki", "sct", - "webpki", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +dependencies = [ + "ring", + "untrusted", ] [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "safemem" @@ -3128,6 +3276,7 @@ name = "save_plot" version = "0.1.0" dependencies = [ "eframe", + "egui_plot", "env_logger", "image", "rfd", @@ -3141,9 +3290,9 @@ checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "screenshot" @@ -3152,7 +3301,6 @@ dependencies = [ "eframe", "env_logger", "image", - "itertools", ] [[package]] @@ -3167,9 +3315,9 @@ dependencies = [ [[package]] name = "sctk-adwaita" -version = "0.5.4" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda4e97be1fd174ccc2aae81c8b694e803fa99b34e8fd0f057a9d70698e3ed09" +checksum = "82b2eaf3a5b264a521b988b2e73042e742df700c4f962cde845d1541adb46550" dependencies = [ "ab_glyph", "log", @@ -3180,29 +3328,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.163" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.48", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "itoa", "ryu", @@ -3211,20 +3359,20 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.12" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" +checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.48", ] [[package]] name = "serde_spanned" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" dependencies = [ "serde", ] @@ -3239,9 +3387,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -3249,10 +3397,19 @@ dependencies = [ ] [[package]] -name = "shlex" -version = "1.1.0" +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "simplecss" @@ -3265,15 +3422,15 @@ dependencies = [ [[package]] name = "siphasher" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] @@ -3289,67 +3446,63 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.10.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "smithay-client-toolkit" -version = "0.16.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f307c47d32d2715eb2e0ece5589057820e0e5e70d07c247d1063e844e107f454" +checksum = "60e3d9941fa3bacf7c2bf4b065304faa14164151254cd16ce1b1bc8fc381600f" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "calloop", - "dlib", - "lazy_static", + "calloop-wayland-source", + "cursor-icon", + "libc", "log", "memmap2", - "nix 0.24.3", - "pkg-config", + "rustix 0.38.21", + "thiserror", + "wayland-backend", "wayland-client", + "wayland-csd-frame", "wayland-cursor", "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", ] [[package]] name = "smithay-clipboard" -version = "0.6.6" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a345c870a1fae0b1b779085e81b51e614767c239e93503588e54c5b17f4b0e8" +checksum = "0bb62b280ce5a5cba847669933a0948d00904cf83845c944eae96a4738cea1a6" dependencies = [ + "libc", "smithay-client-toolkit", - "wayland-client", + "wayland-backend", ] [[package]] -name = "socket2" -version = "0.4.9" +name = "smol_str" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "74212e6bbe9a4352329b2f68ba3130c15a3f26fe88ff22dbdc6cdd58fa85e99c" dependencies = [ - "libc", - "winapi", + "serde", ] [[package]] -name = "speech-dispatcher" -version = "0.16.0" +name = "socket2" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5727d53c474ba5ada07784ad7d203cf896a74854cfee0eb32376b00759eb2972" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ - "lazy_static", "libc", - "speech-dispatcher-sys", -] - -[[package]] -name = "speech-dispatcher-sys" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c3e8acdf2b1f4bb13f1813b40b52f3edf4cc94d8a55fe713a584f672a10388d" -dependencies = [ - "bindgen", + "winapi", ] [[package]] @@ -3360,12 +3513,11 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spirv" -version = "0.2.0+1.5.4" +version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 1.3.2", - "num-traits", + "bitflags 2.4.0", ] [[package]] @@ -3374,42 +3526,22 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "str-buf" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" - [[package]] name = "strict-num" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df65f20698aeed245efdde3628a6b559ea1239bbb871af1b6e3b58c413b2bd1" -dependencies = [ - "float-cmp", -] - -[[package]] -name = "strsim" -version = "0.10.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "svg" -version = "0.1.0" -dependencies = [ - "eframe", - "egui_extras", - "env_logger", +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", ] [[package]] name = "svgtypes" -version = "0.8.2" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22975e8a2bac6a76bb54f898a6b18764633b00e780330f0b689f65afb3975564" +checksum = "6e44e288cd960318917cbd540340968b90becc8bc81f171345d706e7a89d9d70" dependencies = [ + "kurbo", "siphasher", ] @@ -3426,9 +3558,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.16" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -3437,21 +3569,19 @@ dependencies = [ [[package]] name = "syntect" -version = "5.0.0" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6c454c27d9d7d9a84c7803aaa3c50cd088d2906fe3c6e42da3209aa623576a8" +checksum = "e02b4b303bf8d08bfeb0445cba5068a3d306b6baece1d5582171a9bf49188f91" dependencies = [ "bincode", "bitflags 1.3.2", "fancy-regex", "flate2", "fnv", - "lazy_static", "once_cell", "plist", - "regex-syntax 0.6.29", + "regex-syntax", "serde", - "serde_derive", "serde_json", "thiserror", "walkdir", @@ -3460,9 +3590,9 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.1.0" +version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5fa6fb9ee296c0dc2df41a656ca7948546d061958115ddb0bcaae43ad0d17d2" +checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3" dependencies = [ "cfg-expr", "heck", @@ -3473,76 +3603,77 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.7" +version = "0.12.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5" +checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" [[package]] name = "tempfile" -version = "3.5.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", - "fastrand", + "fastrand 2.0.1", "redox_syscall 0.3.5", - "rustix", - "windows-sys 0.45.0", + "rustix 0.38.21", + "windows-sys 0.48.0", ] [[package]] name = "termcolor" -version = "1.2.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +name = "test_inline_glow_paint" +version = "0.1.0" +dependencies = [ + "eframe", + "env_logger", +] + +[[package]] +name = "test_viewports" +version = "0.1.0" +dependencies = [ + "eframe", + "env_logger", +] [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", -] - -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", + "syn 2.0.48", ] [[package]] name = "time" -version = "0.3.21" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ + "deranged", "itoa", + "powerfmt", "serde", "time-core", "time-macros", @@ -3550,38 +3681,39 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.9" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] [[package]] name = "tiny-skia" -version = "0.8.4" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8493a203431061e901613751931f047d1971337153f96d0e5e363d6dbf6a67" +checksum = "b6a067b809476893fce6a254cf285850ff69c847e6cfbade6a20b655b6c7e80d" dependencies = [ "arrayref", "arrayvec", "bytemuck", "cfg-if", + "log", "png", "tiny-skia-path", ] [[package]] name = "tiny-skia-path" -version = "0.8.4" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adbfb5d3f3dd57a0e11d12f4f13d4ebbbc1b5c15b7ab0a156d030b21da5f677c" +checksum = "5de35e8a90052baaaf61f171680ac2f8e925a1e43ea9d2e3a00514772250e541" dependencies = [ "arrayref", "bytemuck", @@ -3615,9 +3747,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" -version = "0.7.4" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" dependencies = [ "serde", "serde_spanned", @@ -3627,18 +3759,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.9" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d964908cec0d030b812013af25a0e57fddfadb1e066ecc6681d86253129d4f" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", "serde", @@ -3661,13 +3793,13 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.48", ] [[package]] @@ -3681,33 +3813,9 @@ dependencies = [ [[package]] name = "ttf-parser" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44dcf002ae3b32cd25400d6df128c5babec3927cd1eb7ce813cfff20eb6c3746" - -[[package]] -name = "tts" -version = "0.25.5" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e339627916d1e1425f806c68b57d7eb6f9486ef0500829b1324556bef3b4fa2d" -dependencies = [ - "cocoa-foundation", - "core-foundation", - "dyn-clonable", - "jni", - "lazy_static", - "libc", - "log", - "ndk-context", - "ndk-glue", - "objc", - "oxilangtag", - "speech-dispatcher", - "thiserror", - "wasm-bindgen", - "web-sys", - "windows 0.48.0", -] +checksum = "a464a4b34948a5f67fddd2b823c62d9d92e44be75058b99939eae6c5b6960b33" [[package]] name = "type-map" @@ -3720,9 +3828,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uds_windows" @@ -3734,6 +3842,15 @@ dependencies = [ "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" @@ -3742,9 +3859,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -3755,11 +3872,17 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "unicode-xid" @@ -3781,25 +3904,25 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "ureq" -version = "2.6.2" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d" +checksum = "f5ccd538d4a604753ebc2f17cd9946e89b77bf87f6a8e2309667c6f2e87855e3" dependencies = [ "base64", "flate2", "log", "once_cell", "rustls", + "rustls-webpki", "url", - "webpki", "webpki-roots", ] [[package]] name = "url" -version = "2.3.1" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna", @@ -3816,29 +3939,47 @@ dependencies = [ [[package]] name = "usvg" -version = "0.28.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5b7c2b30845b3348c067ca3d09e20cc6e327c288f0ca4c48698712abf432e9" +checksum = "38b0a51b72ab80ca511d126b77feeeb4fb1e972764653e61feac30adc161a756" dependencies = [ "base64", + "log", + "pico-args", + "usvg-parser", + "usvg-tree", + "xmlwriter", +] + +[[package]] +name = "usvg-parser" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd4e3c291f45d152929a31f0f6c819245e2921bfd01e7bd91201a9af39a2bdc" +dependencies = [ "data-url", "flate2", "imagesize", "kurbo", "log", - "rctree", "roxmltree", "simplecss", "siphasher", - "strict-num", "svgtypes", + "usvg-tree", ] [[package]] -name = "vec_map" -version = "0.8.2" +name = "usvg-tree" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +checksum = "8ee3d202ebdb97a6215604b8f5b4d6ef9024efd623cf2e373a6416ba976ec7d3" +dependencies = [ + "rctree", + "strict-num", + "svgtypes", + "tiny-skia-path", +] [[package]] name = "version-compare" @@ -3854,26 +3995,20 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "waker-fn" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" [[package]] name = "walkdir" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ "same-file", "winapi-util", ] -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3882,9 +4017,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3892,24 +4027,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.48", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.36" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" +checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" dependencies = [ "cfg-if", "js-sys", @@ -3919,9 +4054,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3929,123 +4064,158 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] -name = "wayland-client" -version = "0.29.5" +name = "wayland-backend" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" +checksum = "19152ddd73f45f024ed4534d9ca2594e0ef252c1847695255dae47f34df9fbe4" dependencies = [ - "bitflags 1.3.2", + "cc", "downcast-rs", - "libc", - "nix 0.24.3", + "nix", "scoped-tls", - "wayland-commons", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca7d52347346f5473bf2f56705f360e8440873052e575e55890c4fa57843ed3" +dependencies = [ + "bitflags 2.4.0", + "nix", + "wayland-backend", "wayland-scanner", - "wayland-sys 0.29.5", ] [[package]] -name = "wayland-commons" -version = "0.29.5" +name = "wayland-csd-frame" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "nix 0.24.3", - "once_cell", - "smallvec", - "wayland-sys 0.29.5", + "bitflags 2.4.0", + "cursor-icon", + "wayland-backend", ] [[package]] name = "wayland-cursor" -version = "0.29.5" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" +checksum = "a44aa20ae986659d6c77d64d808a046996a932aa763913864dc40c359ef7ad5b" dependencies = [ - "nix 0.24.3", + "nix", "wayland-client", "xcursor", ] [[package]] name = "wayland-protocols" -version = "0.29.5" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6" +checksum = "e253d7107ba913923dc253967f35e8561a3c65f914543e46843c88ddd729e21c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" +dependencies = [ + "bitflags 2.4.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +dependencies = [ + "bitflags 2.4.0", + "wayland-backend", "wayland-client", - "wayland-commons", + "wayland-protocols", "wayland-scanner", ] [[package]] name = "wayland-scanner" -version = "0.29.5" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" +checksum = "fb8e28403665c9f9513202b7e1ed71ec56fde5c107816843fb14057910b2c09c" dependencies = [ "proc-macro2", + "quick-xml 0.30.0", "quote", - "xml-rs", ] [[package]] name = "wayland-sys" -version = "0.29.5" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" +checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" dependencies = [ "dlib", - "lazy_static", + "log", + "once_cell", "pkg-config", ] [[package]] -name = "wayland-sys" -version = "0.30.1" +name = "web-sys" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b2a02ac608e07132978689a6f9bf4214949c85998c247abadd4f4129b1aa06" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" dependencies = [ - "dlib", - "lazy_static", - "log", - "pkg-config", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "web-sys" -version = "0.3.64" +name = "web-time" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "19353897b48e2c4d849a2d73cb0aeb16dc2be4e00c565abfc11eb65a806e47de" dependencies = [ "js-sys", + "once_cell", "wasm-bindgen", ] [[package]] name = "webbrowser" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd222aa310eb7532e3fd427a5d7db7e44bc0b0cf1c1e21139c345325511a85b6" +checksum = "b2c79b77f525a2d670cb40619d7d9c673d09e0666f72c591ebd7861f84a87e57" dependencies = [ "core-foundation", "home", @@ -4053,44 +4223,32 @@ dependencies = [ "log", "ndk-context", "objc", - "raw-window-handle", + "raw-window-handle 0.5.2", "url", "web-sys", ] -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] name = "wgpu" -version = "0.17.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7472f3b69449a8ae073f6ec41d05b6f846902d92a6c45313c50cb25857b736ce" +checksum = "0bfe9a310dcf2e6b85f00c46059aaeaf4184caa8e29a1ecd4b7a704c3482332d" dependencies = [ "arrayvec", "cfg-if", + "cfg_aliases", "js-sys", "log", "naga", "parking_lot", "profiling", - "raw-window-handle", + "raw-window-handle 0.6.0", "smallvec", "static_assertions", "wasm-bindgen", @@ -4103,19 +4261,22 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "0.17.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecf7454d9386f602f7399225c92dd2fbdcde52c519bc8fb0bd6fbeb388075dc2" +checksum = "6b15e451d4060ada0d99a64df44e4d590213496da7c4f245572d51071e8e30ed" dependencies = [ "arrayvec", "bit-vec", - "bitflags 2.3.1", + "bitflags 2.4.0", + "cfg_aliases", "codespan-reporting", + "indexmap", "log", "naga", + "once_cell", "parking_lot", "profiling", - "raw-window-handle", + "raw-window-handle 0.6.0", "rustc-hash", "smallvec", "thiserror", @@ -4126,19 +4287,19 @@ dependencies = [ [[package]] name = "wgpu-hal" -version = "0.17.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6654a13885a17f475e8324efb46dc6986d7aaaa98353330f8de2077b153d0101" +checksum = "11f259ceb56727fb097da108d92f8a5cbdb5b74a77f9e396bd43626f67299d61" dependencies = [ "android_system_properties", "arrayvec", "ash", - "bit-set", - "bitflags 2.3.1", + "bitflags 2.4.0", "block", + "cfg_aliases", "core-graphics-types", - "d3d12", "glow", + "glutin_wgl_sys", "gpu-alloc", "gpu-allocator", "gpu-descriptor", @@ -4151,10 +4312,10 @@ dependencies = [ "metal", "naga", "objc", + "once_cell", "parking_lot", "profiling", - "range-alloc", - "raw-window-handle", + "raw-window-handle 0.6.0", "renderdoc-sys", "rustc-hash", "smallvec", @@ -4167,26 +4328,15 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "0.17.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee64d7398d0c2f9ca48922c902ef69c42d000c759f3db41e355f4a570b052b67" +checksum = "895fcbeb772bfb049eb80b2d6e47f6c9af235284e9703c96fc0218a42ffd5af2" dependencies = [ - "bitflags 2.3.1", + "bitflags 2.4.0", "js-sys", "web-sys", ] -[[package]] -name = "which" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" -dependencies = [ - "either", - "libc", - "once_cell", -] - [[package]] name = "widestring" version = "1.0.2" @@ -4211,18 +4361,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-wsapoll" -version = "0.1.1" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] @@ -4235,29 +4376,39 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.44.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ "windows-implement", "windows-interface", - "windows-targets 0.42.2", + "windows-targets 0.48.5", ] [[package]] name = "windows" -version = "0.48.0" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.5", ] [[package]] name = "windows-implement" -version = "0.44.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce87ca8e3417b02dc2a8a22769306658670ec92d78f1bd420d6310a67c245c6" +checksum = "5e2ee588991b9e7e6c8338edf3333fbe4da35dc72092643958ebb43f0ab2c49c" dependencies = [ "proc-macro2", "quote", @@ -4266,9 +4417,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.44.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "853f69a591ecd4f810d29f17e902d40e349fb05b0b11fff63b08b826bfe39c7f" +checksum = "e6fb8df20c9bcaa8ad6ab513f7b40104840c8867d5751126e4df3b08388d0cc7" dependencies = [ "proc-macro2", "quote", @@ -4290,7 +4441,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.5", ] [[package]] @@ -4310,17 +4461,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -4331,9 +4482,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" @@ -4343,9 +4494,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" @@ -4355,9 +4506,9 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" @@ -4367,9 +4518,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" @@ -4379,9 +4530,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" @@ -4391,9 +4542,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" @@ -4403,50 +4554,64 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winit" -version = "0.28.6" +version = "0.29.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "866db3f712fffba75d31bf0cdecf357c8aeafd158c5b7ab51dba2a2b2d47f196" +checksum = "4c824f11941eeae66ec71111cc2674373c772f482b58939bb4066b642aa2ffcf" dependencies = [ + "ahash", "android-activity", - "bitflags 1.3.2", + "atomic-waker", + "bitflags 2.4.0", + "bytemuck", + "calloop", "cfg_aliases", "core-foundation", "core-graphics", - "dispatch", - "instant", + "cursor-icon", + "icrate", + "js-sys", "libc", "log", - "mio", + "memmap2", "ndk", - "objc2", + "ndk-sys", + "objc2 0.4.1", "once_cell", "orbclient", "percent-encoding", - "raw-window-handle", + "raw-window-handle 0.5.2", + "raw-window-handle 0.6.0", "redox_syscall 0.3.5", + "rustix 0.38.21", "sctk-adwaita", "smithay-client-toolkit", + "smol_str", + "unicode-segmentation", "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", "wayland-client", - "wayland-commons", "wayland-protocols", - "wayland-scanner", + "wayland-protocols-plasma", "web-sys", - "windows-sys 0.45.0", + "web-time", + "windows-sys 0.48.0", "x11-dl", + "x11rb", + "xkbcommon-dl", ] [[package]] name = "winnow" -version = "0.4.6" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ "memchr", ] @@ -4464,25 +4629,24 @@ dependencies = [ [[package]] name = "x11rb" -version = "0.10.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507" +checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" dependencies = [ + "as-raw-xcb-connection", "gethostname", - "nix 0.24.3", - "winapi", - "winapi-wsapoll", + "libc", + "libloading 0.8.0", + "once_cell", + "rustix 0.38.21", "x11rb-protocol", ] [[package]] name = "x11rb-protocol" -version = "0.10.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67" -dependencies = [ - "nix 0.24.3", -] +checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" [[package]] name = "xcursor" @@ -4493,17 +4657,46 @@ dependencies = [ "nom", ] +[[package]] +name = "xdg-home" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd" +dependencies = [ + "nix", + "winapi", +] + +[[package]] +name = "xkbcommon-dl" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6924668544c48c0133152e7eec86d644a056ca3d09275eb8d5cdb9855f9d8699" +dependencies = [ + "bitflags 2.4.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621" + [[package]] name = "xml-rs" -version = "0.8.15" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a56c84a8ccd4258aed21c92f70c0f6dea75356b6892ae27c24139da456f9336" +checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" [[package]] -name = "xmlparser" -version = "0.13.5" +name = "xmlwriter" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "yaml-rust" @@ -4516,27 +4709,29 @@ dependencies = [ [[package]] name = "zbus" -version = "3.10.0" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f770930448dd412a4a7131dd968a8e6df0064db4d7916fbbd2d6c3f26b566938" +checksum = "31de390a2d872e4cd04edd71b425e29853f786dc99317ed72d73d6fcf5ebb948" dependencies = [ "async-broadcast", "async-executor", + "async-fs", "async-io", "async-lock", + "async-process", "async-recursion", "async-task", "async-trait", + "blocking", "byteorder", "derivative", - "dirs", "enumflags2", - "event-listener", + "event-listener 2.5.3", "futures-core", "futures-sink", "futures-util", "hex", - "nix 0.25.1", + "nix", "once_cell", "ordered-stream", "rand", @@ -4547,6 +4742,7 @@ dependencies = [ "tracing", "uds_windows", "winapi", + "xdg-home", "zbus_macros", "zbus_names", "zvariant", @@ -4554,33 +4750,54 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "3.10.0" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4832059b438689017db7340580ebabba07f114eab91bf990c6e55052408b40d8" +checksum = "41d1794a946878c0e807f55a397187c11fc7a038ba5d868e7db4f3bd7760bc9d" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "regex", "syn 1.0.109", + "zvariant_utils", ] [[package]] name = "zbus_names" -version = "2.5.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82441e6033be0a741157a72951a3e4957d519698f3a824439cc131c5ba77ac2a" +checksum = "fb80bb776dbda6e23d705cf0123c3b95df99c4ebeaec6c2599d4a5419902b4a9" dependencies = [ "serde", "static_assertions", "zvariant", ] +[[package]] +name = "zerocopy" +version = "0.7.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "zvariant" -version = "3.14.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622cc473f10cef1b0d73b7b34a266be30ebdcfaea40ec297dd8cbda088f9f93c" +checksum = "44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c" dependencies = [ "byteorder", "enumflags2", @@ -4592,9 +4809,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "3.14.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d9c1b57352c25b778257c661f3c4744b7cefb7fc09dd46909a153cce7773da2" +checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd" dependencies = [ "proc-macro-crate", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index c98179f8f7b..c3839499667 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/egui_demo_lib", "crates/egui_extras", "crates/egui_glow", + "crates/egui_plot", "crates/egui-wgpu", "crates/egui-winit", "crates/egui", @@ -15,6 +16,13 @@ members = [ "examples/*", ] +[workspace.package] +edition = "2021" +license = "MIT OR Apache-2.0" +rust-version = "1.72" +version = "0.26.2" + + [profile.release] # lto = true # VERY slightly smaller wasm # opt-level = 's' # 10-20% smaller wasm compared to `opt-level = 3` @@ -24,16 +32,53 @@ opt-level = 2 # fast and small wasm, basically same as `opt-level = 's'` # debug = true # include debug symbols, useful when profiling wasm +panic = "abort" # This leads to better optimizations and smaller binaries (and is the default in Wasm anyways). + [profile.dev] # Can't leave this on by default, because it breaks the Windows build. Related: https://github.com/rust-lang/cargo/issues/4897 # split-debuginfo = "unpacked" # faster debug builds on mac # opt-level = 1 # Make debug builds run faster -# Optimize all dependencies even in debug builds (does not affect workspace packages): +panic = "abort" # This leads to better optimizations and smaller binaries (and is the default in Wasm anyways). + [profile.dev.package."*"] +# Optimize all dependencies even in debug builds (does not affect workspace packages): opt-level = 2 + [workspace.dependencies] +emath = { version = "0.26.2", path = "crates/emath", default-features = false } +ecolor = { version = "0.26.2", path = "crates/ecolor", default-features = false } +epaint = { version = "0.26.2", path = "crates/epaint", default-features = false } +egui = { version = "0.26.2", path = "crates/egui", default-features = false } +egui_plot = { version = "0.26.2", path = "crates/egui_plot", default-features = false } +egui-winit = { version = "0.26.2", path = "crates/egui-winit", default-features = false } +egui_extras = { version = "0.26.2", path = "crates/egui_extras", default-features = false } +egui-wgpu = { version = "0.26.2", path = "crates/egui-wgpu", default-features = false } +egui_demo_lib = { version = "0.26.2", path = "crates/egui_demo_lib", default-features = false } +egui_glow = { version = "0.26.2", path = "crates/egui_glow", default-features = false } +eframe = { version = "0.26.2", path = "crates/eframe", default-features = false } + +#TODO(emilk): make more things workspace dependencies +ahash = { version = "0.8.6", default-features = false, features = [ + "no-rng", # we don't need DOS-protection, so we let users opt-in to it instead + "std", +] } +backtrace = "0.3" +criterion = { version = "0.5.1", default-features = false } +document-features = " 0.2.8" +glow = "0.13" +log = { version = "0.4", features = ["std"] } +nohash-hasher = "0.2" +parking_lot = "0.12" +puffin = "0.19" +puffin_http = "0.16" +raw-window-handle = "0.6.0" thiserror = "1.0.37" -wgpu = "0.17.0" +web-time = "0.2" # Timekeeping for native and web +wgpu = { version = "0.19.1", default-features = false, features = [ + # Make the renderer `Sync` even on wasm32, because it makes the code simpler: + "fragile-send-sync-non-atomic-wasm", +] } +winit = { version = "0.29.4", default-features = false } diff --git a/Cranky.toml b/Cranky.toml index aaa2d8d6d74..db236bff421 100644 --- a/Cranky.toml +++ b/Cranky.toml @@ -1,23 +1,32 @@ # https://github.com/ericseppanen/cargo-cranky # cargo install cargo-cranky && cargo cranky +# See also clippy.toml deny = ["unsafe_code"] warn = [ "clippy::all", + "clippy::as_ptr_cast_mut", "clippy::await_holding_lock", "clippy::bool_to_int_with_if", + "clippy::branches_sharing_code", "clippy::char_lit_as_u8", "clippy::checked_conversions", + "clippy::clear_with_drain", "clippy::cloned_instead_of_copied", "clippy::dbg_macro", "clippy::debug_assert_with_mut_call", + "clippy::default_union_representation", "clippy::derive_partial_eq_without_eq", - "clippy::disallowed_methods", - "clippy::disallowed_script_idents", + "clippy::disallowed_macros", # See clippy.toml + "clippy::disallowed_methods", # See clippy.toml + "clippy::disallowed_names", # See clippy.toml + "clippy::disallowed_script_idents", # See clippy.toml + "clippy::disallowed_types", # See clippy.toml "clippy::doc_link_with_quotes", "clippy::doc_markdown", "clippy::empty_enum", + "clippy::empty_line_after_outer_attr", "clippy::enum_glob_use", "clippy::equatable_if_let", "clippy::exit", @@ -32,6 +41,7 @@ warn = [ "clippy::fn_params_excessive_bools", "clippy::fn_to_numeric_cast_any", "clippy::from_iter_instead_of_collect", + "clippy::get_unwrap", "clippy::if_let_mutex", "clippy::implicit_clone", "clippy::imprecise_flops", @@ -42,14 +52,18 @@ warn = [ "clippy::iter_on_empty_collections", "clippy::iter_on_single_items", "clippy::large_digit_groups", + "clippy::large_include_file", "clippy::large_stack_arrays", + "clippy::large_stack_frames", "clippy::large_types_passed_by_value", "clippy::let_unit_value", "clippy::linkedlist", "clippy::lossy_float_literal", "clippy::macro_use_imports", "clippy::manual_assert", + "clippy::manual_clamp", "clippy::manual_instant_elapsed", + "clippy::manual_let_else", "clippy::manual_ok_or", "clippy::manual_string_new", "clippy::map_err_ignore", @@ -75,30 +89,49 @@ warn = [ "clippy::nonstandard_macro_braces", "clippy::option_option", "clippy::path_buf_push_overwrite", + "clippy::print_stdout", "clippy::ptr_as_ptr", + "clippy::ptr_cast_constness", + "clippy::pub_without_shorthand", "clippy::rc_mutex", + "clippy::redundant_type_annotations", "clippy::ref_option_ref", "clippy::rest_pat_in_fully_bound_structs", "clippy::same_functions_in_if_condition", "clippy::semicolon_if_nothing_returned", + "clippy::significant_drop_tightening", "clippy::single_match_else", "clippy::str_to_string", "clippy::string_add_assign", "clippy::string_add", "clippy::string_lit_as_bytes", "clippy::string_to_string", + "clippy::suspicious_command_arg_space", + "clippy::suspicious_xor_used_as_pow", "clippy::todo", "clippy::trailing_empty_array", "clippy::trait_duplication_in_bounds", + "clippy::transmute_ptr_to_ptr", + "clippy::tuple_array_conversions", + "clippy::unchecked_duration_subtraction", + "clippy::undocumented_unsafe_blocks", "clippy::unimplemented", "clippy::uninlined_format_args", + "clippy::unnecessary_box_returns", + "clippy::unnecessary_safety_comment", + "clippy::unnecessary_safety_doc", + "clippy::unnecessary_self_imports", + "clippy::unnecessary_struct_initialization", "clippy::unnecessary_wraps", "clippy::unnested_or_patterns", "clippy::unused_peekable", "clippy::unused_rounding", "clippy::unused_self", + "clippy::use_self", "clippy::useless_transmute", "clippy::verbose_file_reads", + "clippy::wildcard_dependencies", + "clippy::wildcard_imports", "clippy::zero_sized_map_values", "elided_lifetimes_in_paths", "future_incompatible", @@ -108,18 +141,36 @@ warn = [ "rustdoc::missing_crate_level_docs", "semicolon_in_expressions_from_macros", "trivial_numeric_casts", + "unsafe_op_in_unsafe_fn", # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668 "unused_extern_crates", "unused_import_braces", "unused_lifetimes", + + + # Enable when we update MSRV: + # "clippy::implied_bounds_in_impls", + # "clippy::needless_pass_by_ref_mut", + # "clippy::readonly_write_lock", + # "clippy::should_panic_without_expect", + # "clippy::string_lit_chars_any", ] allow = [ - "clippy::manual_range_contains", # This one is just annoying + "clippy::manual_range_contains", # this one is just worse imho + "clippy::significant_drop_tightening", # A lot of false positives - # Some of these we should try to put in "warn": - "clippy::type_complexity", + # TODO(emilk): enable more of these lints: + "clippy::cloned_instead_of_copied", + "clippy::let_underscore_untyped", + "clippy::missing_assert_message", + "clippy::missing_errors_doc", + "clippy::print_stderr", # TODO(emilk): use `log` crate instead + "clippy::self_named_module_files", # False positives + "clippy::too_many_lines", "clippy::undocumented_unsafe_blocks", + "clippy::unwrap_used", + "clippy::useless_let_if_seq", # False positives + "clippy::wildcard_imports", # We do this a lot "trivial_casts", - "unsafe_op_in_unsafe_fn", # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668 "unused_qualifications", ] diff --git a/README.md b/README.md index ae9cf01fe0a..2f4d5d19a83 100644 --- a/README.md +++ b/README.md @@ -9,29 +9,26 @@ [![Apache](https://img.shields.io/badge/license-Apache-blue.svg)](https://github.com/emilk/egui/blob/master/LICENSE-APACHE) [![Discord](https://img.shields.io/discord/900275882684477440?label=egui%20discord)](https://discord.gg/JFcEma9bJq) + +
+ + +egui development is sponsored by [Rerun](https://www.rerun.io/), a startup building
+an SDK for visualizing streams of multimodal data. +
+ +--- + 👉 [Click to run the web demo](https://www.egui.rs/#demo) 👈 -egui (pronounced "e-gooey") is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and [in your favorite game engine](#integrations) (or will soon). +egui (pronounced "e-gooey") is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and [in your favorite game engine](#integrations). egui aims to be the easiest-to-use Rust GUI library, and the simplest way to make a web app in Rust. egui can be used anywhere you can draw textured triangles, which means you can easily integrate it into your game engine of choice. -Sections: +[`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) is the official egui framework, which supports writing apps for Web, Linux, Mac, Windows, and Android. -* [Example](#example) -* [Quick start](#quick-start) -* [Demo](#demo) -* [Goals](#goals) -* [Who is egui for?](#who-is-egui-for) -* [State / features](#state) -* [Integrations](#integrations) -* [Why immediate mode](#why-immediate-mode) -* [FAQ](#faq) -* [Other](#other) -* [Credits](#credits) - -([egui 的中文翻译文档 / chinese translation](https://github.com/Re-Ch-Love/egui-doc-cn/blob/main/README_zh-hans.md)) ## Example @@ -42,13 +39,32 @@ ui.horizontal(|ui| { ui.text_edit_singleline(&mut name); }); ui.add(egui::Slider::new(&mut age, 0..=120).text("age")); -if ui.button("Click each year").clicked() { +if ui.button("Increment").clicked() { age += 1; } ui.label(format!("Hello '{name}', age {age}")); +ui.image(egui::include_image!("ferris.png")); ``` - +Dark mode     Light mode + +## Sections: + +* [Example](#example) +* [Quick start](#quick-start) +* [Demo](#demo) +* [Goals](#goals) +* [State / features](#state) +* [Dependencies](#dependencies) +* [Who is egui for?](#who-is-egui-for) +* [Integrations](#integrations) +* [Why immediate mode](#why-immediate-mode) +* [FAQ](#faq) +* [Other](#other) +* [Credits](#credits) + +([egui 的中文翻译文档 / chinese translation](https://github.com/Re-Ch-Love/egui-doc-cn/blob/main/README_zh-hans.md)) + ## Quick start @@ -60,7 +76,7 @@ If you have questions, use [GitHub Discussions](https://github.com/emilk/egui/di ## Demo -[Click to run egui web demo](https://www.egui.rs/#demo) (works in any browser with WASM and WebGL support). Uses [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe). +[Click to run egui web demo](https://www.egui.rs/#demo) (works in any browser with Wasm and WebGL support). Uses [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe). To test the demo app locally, run `cargo run --release -p egui_demo_app`. @@ -87,7 +103,7 @@ On Fedora Rawhide you need to run: * Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs) * Modular: You should be able to use small parts of egui and combine them in new ways * Safe: there is no `unsafe` code in egui -* Minimal dependencies: [`ab_glyph`](https://crates.io/crates/ab_glyph) [`ahash`](https://crates.io/crates/ahash) [`nohash-hasher`](https://crates.io/crates/nohash-hasher) [`parking_lot`](https://crates.io/crates/parking_lot) +* Minimal dependencies egui is *not* a framework. egui is a library you call into, not an environment you program for. @@ -99,54 +115,56 @@ egui is *not* a framework. egui is a library you call into, not an environment y * Native looking interface * Advanced and flexible layouts (that's fundamentally incompatible with immediate mode) -## Who is egui for? +## State -egui aims to be the best choice when you want a simple way to create a GUI, or you want to add a GUI to a game engine. +egui is in active development. It works well for what it does, but it lacks many features and the interfaces are still in flux. New releases will have breaking changes. -If you are not using Rust, egui is not for you. If you want a GUI that looks native, egui is not for you. If you want something that doesn't break when you upgrade it, egui isn't for you (yet). +Still, egui can be used to create professional looking applications, like [the Rerun Viewer](https://app.rerun.io/). -But if you are writing something interactive in Rust that needs a simple GUI, egui may be for you. +### Features -### egui vs Dear ImGui +* Widgets: label, text button, hyperlink, checkbox, radio button, slider, draggable value, text editing, color picker, spinner +* Images +* Layouts: horizontal, vertical, columns, automatic wrapping +* Text editing: multiline, copy/paste, undo, emoji supports +* Windows: move, resize, name, minimize and close. Automatically sized and positioned. +* Regions: resizing, vertical scrolling, collapsing headers (sections), panels +* Rendering: Anti-aliased rendering of lines, circles, text and convex polygons. +* Tooltips on hover +* Accessibility via [AccessKit](https://accesskit.dev/) +* Label text selection +* And more! -The obvious alternative to egui is [`imgui-rs`](https://github.com/Gekkio/imgui-rs), the Rust wrapper around the C++ library [Dear ImGui](https://github.com/ocornut/imgui). Dear ImGui is a great library (and the main inspiration for egui), with a lot more features and polish. However, egui provides some benefits for Rust users: + -* egui is pure Rust -* egui is easily compiled to WASM -* egui lets you use native Rust string types (`imgui-rs` forces you to use annoying macros and wrappers for zero-terminated strings) -* [Writing your own widgets in egui is simple](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs) +Light Theme: -egui also tries to improve your experience in other small ways: + -* Windows are automatically sized based on their contents -* Windows are automatically positioned to not overlap with each other -* Some subtle animations make egui come alive -So in summary: +## Dependencies +`egui` has a minimal set of default dependencies: -* egui: pure Rust, new, exciting, work in progress -* Dear ImGui: feature rich, well tested, cumbersome Rust integration +* [`ab_glyph`](https://crates.io/crates/ab_glyph) +* [`ahash`](https://crates.io/crates/ahash) +* [`nohash-hasher`](https://crates.io/crates/nohash-hasher) +* [`parking_lot`](https://crates.io/crates/parking_lot) -## State +Heavier dependencies are kept out of `egui`, even as opt-in. +No code that isn't fully Wasm-friendly is part of `egui`. -egui is in active development. It works well for what it does, but it lacks many features and the interfaces are still in flux. New releases will have breaking changes. +To load images into `egui` you can use the official [`egui_extras`](https://github.com/emilk/egui/tree/master/crates/egui_extras) crate. -### Features +[`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) on the other hand has a lot of dependencies, including [`winit`](https://crates.io/crates/winit), [`image`](https://crates.io/crates/image), graphics crates, clipboard crates, etc, -* Widgets: label, text button, hyperlink, checkbox, radio button, slider, draggable value, text editing, combo box, color picker -* Layouts: horizontal, vertical, columns, automatic wrapping -* Text editing: multiline, copy/paste, undo, emoji supports -* Windows: move, resize, name, minimize and close. Automatically sized and positioned. -* Regions: resizing, vertical scrolling, collapsing headers (sections) -* Rendering: Anti-aliased rendering of lines, circles, text and convex polygons. -* Tooltips on hover -* More +## Who is egui for? - +egui aims to be the best choice when you want a simple way to create a GUI, or you want to add a GUI to a game engine. -Light Theme: +If you are not using Rust, egui is not for you. If you want a GUI that looks native, egui is not for you. If you want something that doesn't break when you upgrade it, egui isn't for you (yet). + +But if you are writing something interactive in Rust that needs a simple GUI, egui may be for you. - ## Integrations @@ -156,7 +174,7 @@ egui itself doesn't know or care on what OS it is running or how to render thing An integration needs to do the following each frame: * **Input**: Gather input (mouse, touches, keyboard, screen size, etc) and give it to egui -* Run the application code +* Call into the application GUI code * **Output**: Handle egui output (cursor changes, paste, texture allocations, …) * **Painting**: Render the triangle mesh egui produces (see [OpenGL example](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs)) @@ -164,89 +182,41 @@ An integration needs to do the following each frame: These are the official egui integrations: -* [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) for compiling the same app to web/wasm and desktop/native. Uses `egui-winit` and `egui_glow` or `egui-wgpu`. -* [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) for rendering egui with [glow](https://github.com/grovesNL/glow) on native and web, and for making native apps. -* [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu) for [wgpu](https://crates.io/crates/wgpu) (WebGPU API). -* [`egui-winit`](https://github.com/emilk/egui/tree/master/crates/egui-winit) for integrating with [winit](https://github.com/rust-windowing/winit). -* [`egui_glium`](https://github.com/emilk/egui/tree/master/crates/egui_glium) for compiling native apps with [Glium](https://github.com/glium/glium) (DEPRECATED - looking for new maintainer). +* [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) for compiling the same app to web/wasm and desktop/native. Uses `egui-winit` and `egui_glow` or `egui-wgpu` +* [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) for rendering egui with [glow](https://github.com/grovesNL/glow) on native and web, and for making native apps +* [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu) for [wgpu](https://crates.io/crates/wgpu) (WebGPU API) +* [`egui-winit`](https://github.com/emilk/egui/tree/master/crates/egui-winit) for integrating with [winit](https://github.com/rust-windowing/winit) ### 3rd party integrations -* [`amethyst_egui`](https://github.com/jgraef/amethyst_egui) for [the Amethyst game engine](https://amethyst.rs/). -* [`bevy_egui`](https://github.com/mvlabat/bevy_egui) for [the Bevy game engine](https://bevyengine.org/). -* [`egui_glfw_gl`](https://github.com/cohaereo/egui_glfw_gl) for [GLFW](https://crates.io/crates/glfw). -* [`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-tetra`](https://crates.io/crates/egui-tetra) 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). -* [`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). - -Missing an integration for the thing you're working on? Create one, it's easy! +* [`amethyst_egui`](https://github.com/jgraef/amethyst_egui) for [the Amethyst game engine](https://amethyst.rs/) +* [`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-tetra`](https://crates.io/crates/egui-tetra) 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) +* [`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) ### Writing your own egui integration - -You need to collect [`egui::RawInput`](https://docs.rs/egui/latest/egui/struct.RawInput.html) and handle [`egui::FullOutput`](https://docs.rs/egui/latest/egui/struct.FullOutput.html). The basic structure is this: - -``` rust -let mut egui_ctx = egui::CtxRef::default(); - -// Game loop: -loop { - // Gather input (mouse, touches, keyboard, screen size, etc): - let raw_input: egui::RawInput = my_integration.gather_input(); - let full_output = egui_ctx.run(raw_input, |egui_ctx| { - my_app.ui(egui_ctx); // add panels, windows and widgets to `egui_ctx` here - }); - let clipped_primitives = egui_ctx.tessellate(full_output.shapes); // creates triangles to paint - - my_integration.paint(&full_output.textures_delta, clipped_primitives); - - let platform_output = full_output.platform_output; - my_integration.set_cursor_icon(platform_output.cursor_icon); - if !platform_output.copied_text.is_empty() { - my_integration.set_clipboard_text(platform_output.copied_text); - } - // See `egui::FullOutput` and `egui::PlatformOutput` for more -} -``` - -For a reference OpenGL backend, see [the `egui_glium` painter](https://github.com/emilk/egui/blob/master/crates/egui_glium/src/painter.rs) or [the `egui_glow` painter](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs). - -### Debugging your integration - -#### Things look jagged - -* Turn off backface culling. - -#### My text is blurry - -* Make sure you set the proper `pixels_per_point` in the input to egui. -* Make sure the texture sampler is not off by half a pixel. Try nearest-neighbor sampler to check. - -#### My windows are too transparent or too dark - -* egui uses premultiplied alpha, so make sure your blending function is `(ONE, ONE_MINUS_SRC_ALPHA)`. -* Make sure your texture sampler is clamped (`GL_CLAMP_TO_EDGE`). -* egui prefers linear color spaces for all blending so: - * Use an sRGBA-aware texture if available (e.g. `GL_SRGB8_ALPHA8`). - * Otherwise: remember to decode gamma in the fragment shader. - * Decode the gamma of the incoming vertex colors in your vertex shader. - * Turn on sRGBA/linear framebuffer if available (`GL_FRAMEBUFFER_SRGB`). - * Otherwise: gamma-encode the colors before you write them again. +Missing an integration for the thing you're working on? Create one, it's easy! +See . ## Why immediate mode @@ -288,7 +258,7 @@ For "atomic" widgets (e.g. a button) `egui` knows the size before showing it, so #### CPU usage Since an immediate mode GUI does a full layout each frame, the layout code needs to be quick. If you have a very complex GUI this can tax the CPU. In particular, having a very large UI in a scroll area (with very long scrollback) can be slow, as the content needs to be laid out each frame. -If you design the GUI with this in mind and refrain from huge scroll areas (or only lay out the part that is in view) then the performance hit is generally pretty small. For most cases you can expect `egui` to take up 1-2 ms per frame, but `egui` still has a lot of room for optimization (it's not something I've focused on yet). You can also set up `egui` to only repaint when there is interaction (e.g. mouse movement). +If you design the GUI with this in mind and refrain from huge scroll areas (or only lay out the part that is in view) then the performance hit is generally pretty small. For most cases you can expect `egui` to take up 1-2 ms per frame, but `egui` still has a lot of room for optimization (it's not something I've focused on yet). `egui` only repaints when there is interaction (e.g. mouse movement) or an animation, so if your app is idle, no CPU is wasted. If your GUI is highly interactive, then immediate mode may actually be more performant compared to retained mode. Go to any web page and resize the browser window, and you'll notice that the browser is very slow to do the layout and eats a lot of CPU doing it. Resize a window in `egui` by contrast, and you'll get smooth 60 FPS at no extra CPU cost. @@ -296,7 +266,7 @@ If your GUI is highly interactive, then immediate mode may actually be more perf #### IDs There are some GUI state that you want the GUI library to retain, even in an immediate mode library such as `egui`. This includes position and sizes of windows and how far the user has scrolled in some UI. In these cases you need to provide `egui` with a seed of a unique identifier (unique within the parent UI). For instance: by default `egui` uses the window titles as unique IDs to store window positions. If you want two windows with the same name (or one window with a dynamic name) you must provide some other ID source to `egui` (some unique integer or string). -`egui` also needs to track which widget is being interacted with (e.g. which slider is being dragged). `egui` uses unique id:s for this awell, but in this case the IDs are automatically generated, so there is no need for the user to worry about it. In particular, having two buttons with the same name is no problem (this is in contrast with [`Dear ImGui`](https://github.com/ocornut/imgui)). +`egui` also needs to track which widget is being interacted with (e.g. which slider is being dragged). `egui` uses unique IDs for this as well, but in this case the IDs are automatically generated, so there is no need for the user to worry about it. In particular, having two buttons with the same name is no problem (this is in contrast with [`Dear ImGui`](https://github.com/ocornut/imgui)). Overall, ID handling is a rare inconvenience, and not a big disadvantage. @@ -306,12 +276,14 @@ Overall, ID handling is a rare inconvenience, and not a big disadvantage. Also see [GitHub Discussions](https://github.com/emilk/egui/discussions/categories/q-a). ### Can I use `egui` with non-latin characters? -Yes! But you need to install your own font (`.ttf` or `.otf`) using `Context::set_fonts`. +Yes! But you need to install your own font (`.ttf` or `.otf`) using [`Context::set_fonts`](https://docs.rs/egui/latest/egui/struct.Context.html#method.set_fonts). ### Can I customize the look of egui? Yes! You can customize the colors, spacing, fonts and sizes of everything using `Context::set_style`. -Here is an example (from https://github.com/AlexxxRu/TinyPomodoro): +This is not yet as powerful as say CSS, [but this is going to improve](https://github.com/emilk/egui/issues/3284). + +Here is an example (from https://github.com/a-liashenko/TinyPomodoro): @@ -319,10 +291,14 @@ Here is an example (from https://github.com/AlexxxRu/TinyPomodoro): If you call `.await` in your GUI code, the UI will freeze, which is very bad UX. Instead, keep the GUI thread non-blocking and communicate with any concurrent tasks (`async` tasks or other threads) with something like: * Channels (e.g. [`std::sync::mpsc::channel`](https://doc.rust-lang.org/std/sync/mpsc/fn.channel.html)). Make sure to use [`try_recv`](https://doc.rust-lang.org/std/sync/mpsc/struct.Receiver.html#method.try_recv) so you don't block the gui thread! * `Arc>` (background thread sets a value; GUI thread reads it) -* [`poll_promise::Promise`](https://docs.rs/poll-promise) (example: [`examples/download_image/`](https://github.com/emilk/egui/blob/master/examples/download_image/)) +* [`poll_promise::Promise`](https://docs.rs/poll-promise) * [`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? + +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/). + ### What about accessibility, such as screen readers? egui includes optional support for [AccessKit](https://accesskit.dev/), which currently implements the native accessibility APIs on Windows and macOS. This feature is enabled by default in eframe. For platforms that AccessKit doesn't yet support, including web, there is an experimental built-in screen reader; in [the web demo](https://www.egui.rs/#demo) you can enable it in the "Backend" tab. @@ -336,16 +312,16 @@ That is the job of *the integration* or *backend*. It is common to use `egui` from a game engine (using e.g. [`bevy_egui`](https://docs.rs/bevy_egui)), but you can also use `egui` stand-alone using `eframe`. `eframe` has integration for web and native, and handles input and rendering. -The _frame_ in `eframe` stands both for the frame in which your egui app resides and also for "framework" (`frame` is a framework, `egui` is a library). +The _frame_ in `eframe` stands both for the frame in which your egui app resides and also for "framework" (`eframe` is a framework, `egui` is a library). ### How do I render 3D stuff in an egui area? There are multiple ways to combine egui with 3D. The simplest way is to use a 3D library and have egui sit on top of the 3D view. See for instance [`bevy_egui`](https://github.com/mvlabat/bevy_egui) or [`three-d`](https://github.com/asny/three-d). -If you want to embed 3D into an egui view there are two options. +If you want to embed 3D into an egui view there are two options: #### `Shape::Callback` Example: -* +* `Shape::Callback` will call your code when egui gets painted, to show anything using whatever the background rendering context is. When using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) this will be [`glow`](https://github.com/grovesNL/glow). Other integrations will give you other rendering contexts, if they support `Shape::Callback` at all. @@ -354,7 +330,6 @@ You can also render your 3D scene to a texture and display it using [`ui.image( Examples: * Using [`egui-miniquad`]( https://github.com/not-fl3/egui-miniquad): https://github.com/not-fl3/egui-miniquad/blob/master/examples/render_to_egui_image.rs -* Using [`egui_glium`](https://github.com/emilk/egui/tree/master/crates/egui_glium): . ## Other @@ -363,9 +338,9 @@ Examples: All coordinates are in screen space coordinates, with (0, 0) in the top left corner -All coordinates are in "points" which may consist of many physical pixels. +All coordinates are in logical "points" which may consist of many physical pixels. -All colors have premultiplied alpha. +All colors have premultiplied alpha, unless otherwise stated. egui uses the builder pattern for construction widgets. For instance: `ui.add(Label::new("Hello").text_color(RED));` I am not a big fan of the builder pattern (it is quite verbose both in implementation and in use) but until Rust has named, default arguments it is the best we can do. To alleviate some of the verbosity there are common-case helper functions, like `ui.label("Hello");`. @@ -385,20 +360,23 @@ The library was originally called "Emigui", but was renamed to "egui" in 2020. ## Credits -egui author and maintainer: Emil Ernerfeldt [(@emilk](https://github.com/emilk)). +egui author and maintainer: Emil Ernerfeldt ([@emilk](https://github.com/emilk)). Notable contributions by: -* [@n2](https://github.com/n2): [Mobile web input and IME support](https://github.com/emilk/egui/pull/253). -* [@optozorax](https://github.com/optozorax): [Arbitrary widget data storage](https://github.com/emilk/egui/pull/257). -* [@quadruple-output](https://github.com/quadruple-output): [Multitouch](https://github.com/emilk/egui/pull/306). -* [@EmbersArc](https://github.com/EmbersArc): [Plots](https://github.com/emilk/egui/pulls?q=+is%3Apr+author%3AEmbersArc). -* [@AsmPrgmC3](https://github.com/AsmPrgmC3): [Proper sRGBA blending for web](https://github.com/emilk/egui/pull/650). -* [@AlexApps99](https://github.com/AlexApps99): [`egui_glow`](https://github.com/emilk/egui/pull/685). -* [@mankinskin](https://github.com/mankinskin): [Context menus](https://github.com/emilk/egui/pull/543). -* [@t18b219k](https://github.com/t18b219k): [Port glow painter to web](https://github.com/emilk/egui/pull/868). -* [@danielkeller](https://github.com/danielkeller): [`Context` refactor](https://github.com/emilk/egui/pull/1050). -* [@MaximOsipenko](https://github.com/MaximOsipenko): [`Context` lock refactor](https://github.com/emilk/egui/pull/2625). +* [@n2](https://github.com/n2): [Mobile web input and IME support](https://github.com/emilk/egui/pull/253) +* [@optozorax](https://github.com/optozorax): [Arbitrary widget data storage](https://github.com/emilk/egui/pull/257) +* [@quadruple-output](https://github.com/quadruple-output): [Multitouch](https://github.com/emilk/egui/pull/306) +* [@EmbersArc](https://github.com/EmbersArc): [Plots](https://github.com/emilk/egui/pulls?q=+is%3Apr+author%3AEmbersArc) +* [@AsmPrgmC3](https://github.com/AsmPrgmC3): [Proper sRGBA blending for web](https://github.com/emilk/egui/pull/650) +* [@AlexApps99](https://github.com/AlexApps99): [`egui_glow`](https://github.com/emilk/egui/pull/685) +* [@mankinskin](https://github.com/mankinskin): [Context menus](https://github.com/emilk/egui/pull/543) +* [@t18b219k](https://github.com/t18b219k): [Port glow painter to web](https://github.com/emilk/egui/pull/868) +* [@danielkeller](https://github.com/danielkeller): [`Context` refactor](https://github.com/emilk/egui/pull/1050) +* [@MaximOsipenko](https://github.com/MaximOsipenko): [`Context` lock refactor](https://github.com/emilk/egui/pull/2625) +* [@mwcampbell](https://github.com/mwcampbell): [AccessKit](https://github.com/AccessKit/accesskit) [integration](https://github.com/emilk/egui/pull/2294) +* [@hasenbanck](https://github.com/hasenbanck), [@s-nie](https://github.com/s-nie), [@Wumpf](https://github.com/Wumpf): [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu) +* [@jprochazk](https://github.com/jprochazk): [egui image API](https://github.com/emilk/egui/issues/3291) * And [many more](https://github.com/emilk/egui/graphs/contributors?type=a). egui is licensed under [MIT](LICENSE-MIT) OR [Apache-2.0](LICENSE-APACHE). @@ -415,8 +393,8 @@ Default fonts: ---
- + -egui development is sponsored by [Rerun](https://www.rerun.io/), a startup doing
-visualizations for computer vision and robotics. +egui development is sponsored by [Rerun](https://www.rerun.io/), a startup building
+an SDK for visualizing streams of multimodal data.
diff --git a/bacon.toml b/bacon.toml index 46b1bfff523..63d72eeb055 100644 --- a/bacon.toml +++ b/bacon.toml @@ -68,7 +68,7 @@ need_stdout = true [keybindings] i = "job:initial" c = "job:cranky" -w = "job:wasm" +a = "job:wasm" d = "job:doc-open" t = "job:test" r = "job:run" diff --git a/clippy.toml b/clippy.toml index 38feddcf002..93d7874068e 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,10 +1,77 @@ # There is also a scripts/clippy_wasm/clippy.toml which forbids some mthods that are not available in wasm. -msrv = "1.67" +# ----------------------------------------------------------------------------- +# Section identical to scripts/clippy_wasm/clippy.toml: + +msrv = "1.72" + +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 dosctrings 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", "..", ] diff --git a/crates/ecolor/CHANGELOG.md b/crates/ecolor/CHANGELOG.md index 228275e045a..8f60af3df13 100644 --- a/crates/ecolor/CHANGELOG.md +++ b/crates/ecolor/CHANGELOG.md @@ -3,7 +3,32 @@ All notable changes to the `ecolor` crate will be noted in this file. This file is updated upon each release. -Changes since the last release can be found by running the `scripts/generate_changelog.py` script. +Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. + + +## 0.26.2 - 2024-02-14 +* Nothing new + + +## 0.26.1 - 2024-02-11 +* Nothing new + + +## 0.26.0 - 2024-02-05 +* Nothing new + + +## 0.25.0 - 2024-01-08 +* Add `Color32::from_hex` and `Color32::to_hex` [#3570](https://github.com/emilk/egui/pull/3570) [#3777](https://github.com/emilk/egui/pull/3777) (thanks [@YgorSouza](https://github.com/YgorSouza)!) + + +## 0.24.1 - 2023-11-30 +* Optimize color conversions [#3666](https://github.com/emilk/egui/pull/3666) + + +## 0.24.0 - 2023-11-23 +* Update MSRV to Rust 1.72 [#3595](https://github.com/emilk/egui/pull/3595) +* Add `#[inline]` to all color-related function [38b4234](https://github.com/emilk/egui/commit/38b4234c3282a7c044c18b77234ee8c204efe171) ## 0.22.0 - 2023-05-23 diff --git a/crates/ecolor/Cargo.toml b/crates/ecolor/Cargo.toml index ea2f895fdd9..4cdbea9124b 100644 --- a/crates/ecolor/Cargo.toml +++ b/crates/ecolor/Cargo.toml @@ -1,15 +1,15 @@ [package] name = "ecolor" -version = "0.22.0" +version.workspace = true authors = [ "Emil Ernerfeldt ", "Andreas Reich ", ] description = "Color structs and color conversion utilities" -edition = "2021" -rust-version = "1.67" +edition.workspace = true +rust-version.workspace = true homepage = "https://github.com/emilk/egui" -license = "MIT OR Apache-2.0" +license.workspace = true readme = "README.md" repository = "https://github.com/emilk/egui" categories = ["mathematics", "encoding"] @@ -44,7 +44,7 @@ cint = { version = "0.3.1", optional = true } color-hex = { version = "0.2.0", optional = true } ## Enable this when generating docs. -document-features = { version = "0.2", optional = true } +document-features = { workspace = true, optional = true } ## Allow serialization using [`serde`](https://docs.rs/serde). serde = { version = "1", optional = true, features = ["derive"] } diff --git a/crates/ecolor/src/cint_impl.rs b/crates/ecolor/src/cint_impl.rs index a730fda485f..03758bba7ec 100644 --- a/crates/ecolor/src/cint_impl.rs +++ b/crates/ecolor/src/cint_impl.rs @@ -10,7 +10,7 @@ impl From>> for Color32 { alpha: a, } = srgba; - Color32::from_rgba_unmultiplied(r, g, b, a) + Self::from_rgba_unmultiplied(r, g, b, a) } } @@ -23,7 +23,7 @@ impl From>> for Color32 { alpha: a, } = srgba; - Color32::from_rgba_premultiplied(r, g, b, a) + Self::from_rgba_premultiplied(r, g, b, a) } } @@ -31,7 +31,7 @@ impl From for PremultipliedAlpha> { fn from(col: Color32) -> Self { let (r, g, b, a) = col.to_tuple(); - PremultipliedAlpha { + Self { color: EncodedSrgb { r, g, b }, alpha: a, } @@ -51,7 +51,7 @@ impl From>> for Color32 { let b = linear_u8_from_linear_f32(b); let a = linear_u8_from_linear_f32(a); - Color32::from_rgba_premultiplied(r, g, b, a) + Self::from_rgba_premultiplied(r, g, b, a) } } @@ -65,7 +65,7 @@ impl From for PremultipliedAlpha> { let b = linear_f32_from_linear_u8(b); let a = linear_f32_from_linear_u8(a); - PremultipliedAlpha { + Self { color: EncodedSrgb { r, g, b }, alpha: a, } @@ -85,7 +85,7 @@ impl From>> for Rgba { alpha: a, } = srgba; - Rgba([r, g, b, a]) + Self([r, g, b, a]) } } @@ -93,7 +93,7 @@ impl From for PremultipliedAlpha> { fn from(col: Rgba) -> Self { let (r, g, b, a) = col.to_tuple(); - PremultipliedAlpha { + Self { color: LinearSrgb { r, g, b }, alpha: a, } @@ -113,7 +113,7 @@ impl From>> for Hsva { alpha: a, } = srgba; - Hsva::new(h, s, v, a) + Self::new(h, s, v, a) } } @@ -121,7 +121,7 @@ impl From for Alpha> { fn from(col: Hsva) -> Self { let Hsva { h, s, v, a } = col; - Alpha { + Self { color: Hsv { h, s, v }, alpha: a, } @@ -153,7 +153,7 @@ impl From for Alpha> { fn from(col: HsvaGamma) -> Self { let Hsva { h, s, v, a } = col.into(); - Alpha { + Self { color: Hsv { h, s, v }, alpha: a, } diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index 09e68116f09..0b60176f5cd 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -7,6 +7,8 @@ use crate::{gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_f32_from_ /// /// Internally this uses 0-255 gamma space `sRGBA` color with premultiplied alpha. /// Alpha channel is in linear space. +/// +/// The special value of alpha=0 means the color is to be treated as an additive color. #[repr(C)] #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -16,14 +18,14 @@ pub struct Color32(pub(crate) [u8; 4]); impl std::ops::Index for Color32 { type Output = u8; - #[inline(always)] + #[inline] fn index(&self, index: usize) -> &u8 { &self.0[index] } } impl std::ops::IndexMut for Color32 { - #[inline(always)] + #[inline] fn index_mut(&mut self, index: usize) -> &mut u8 { &mut self.0[index] } @@ -32,54 +34,64 @@ impl std::ops::IndexMut for Color32 { impl Color32 { // Mostly follows CSS names: - pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0); - pub const BLACK: Color32 = Color32::from_rgb(0, 0, 0); - pub const DARK_GRAY: Color32 = Color32::from_rgb(96, 96, 96); - pub const GRAY: Color32 = Color32::from_rgb(160, 160, 160); - pub const LIGHT_GRAY: Color32 = Color32::from_rgb(220, 220, 220); - pub const WHITE: Color32 = Color32::from_rgb(255, 255, 255); + pub const TRANSPARENT: Self = Self::from_rgba_premultiplied(0, 0, 0, 0); + pub const BLACK: Self = Self::from_rgb(0, 0, 0); + pub const DARK_GRAY: Self = Self::from_rgb(96, 96, 96); + pub const GRAY: Self = Self::from_rgb(160, 160, 160); + pub const LIGHT_GRAY: Self = Self::from_rgb(220, 220, 220); + pub const WHITE: Self = Self::from_rgb(255, 255, 255); - pub const BROWN: Color32 = Color32::from_rgb(165, 42, 42); - pub const DARK_RED: Color32 = Color32::from_rgb(0x8B, 0, 0); - pub const RED: Color32 = Color32::from_rgb(255, 0, 0); - pub const LIGHT_RED: Color32 = Color32::from_rgb(255, 128, 128); + pub const BROWN: Self = Self::from_rgb(165, 42, 42); + pub const DARK_RED: Self = Self::from_rgb(0x8B, 0, 0); + pub const RED: Self = Self::from_rgb(255, 0, 0); + pub const LIGHT_RED: Self = Self::from_rgb(255, 128, 128); - pub const YELLOW: Color32 = Color32::from_rgb(255, 255, 0); - pub const LIGHT_YELLOW: Color32 = Color32::from_rgb(255, 255, 0xE0); - pub const KHAKI: Color32 = Color32::from_rgb(240, 230, 140); + pub const YELLOW: Self = Self::from_rgb(255, 255, 0); + pub const LIGHT_YELLOW: Self = Self::from_rgb(255, 255, 0xE0); + pub const KHAKI: Self = Self::from_rgb(240, 230, 140); - pub const DARK_GREEN: Color32 = Color32::from_rgb(0, 0x64, 0); - pub const GREEN: Color32 = Color32::from_rgb(0, 255, 0); - pub const LIGHT_GREEN: Color32 = Color32::from_rgb(0x90, 0xEE, 0x90); + pub const DARK_GREEN: Self = Self::from_rgb(0, 0x64, 0); + pub const GREEN: Self = Self::from_rgb(0, 255, 0); + pub const LIGHT_GREEN: Self = Self::from_rgb(0x90, 0xEE, 0x90); - pub const DARK_BLUE: Color32 = Color32::from_rgb(0, 0, 0x8B); - pub const BLUE: Color32 = Color32::from_rgb(0, 0, 255); - pub const LIGHT_BLUE: Color32 = Color32::from_rgb(0xAD, 0xD8, 0xE6); + pub const DARK_BLUE: Self = Self::from_rgb(0, 0, 0x8B); + pub const BLUE: Self = Self::from_rgb(0, 0, 255); + pub const LIGHT_BLUE: Self = Self::from_rgb(0xAD, 0xD8, 0xE6); - pub const GOLD: Color32 = Color32::from_rgb(255, 215, 0); + pub const GOLD: Self = Self::from_rgb(255, 215, 0); - pub const DEBUG_COLOR: Color32 = Color32::from_rgba_premultiplied(0, 200, 0, 128); + pub const DEBUG_COLOR: Self = Self::from_rgba_premultiplied(0, 200, 0, 128); /// An ugly color that is planned to be replaced before making it to the screen. - pub const TEMPORARY_COLOR: Color32 = Color32::from_rgb(64, 254, 0); + /// + /// This is an invalid color, in that it does not correspond to a valid multiplied color, + /// nor to an additive color. + /// + /// This is used as a special color key, + /// i.e. often taken to mean "no color". + pub const PLACEHOLDER: Self = Self::from_rgba_premultiplied(64, 254, 0, 128); + + #[deprecated = "Renamed to PLACEHOLDER"] + pub const TEMPORARY_COLOR: Self = Self::PLACEHOLDER; - #[inline(always)] + #[inline] pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self { Self([r, g, b, 255]) } - #[inline(always)] + #[inline] pub const fn from_rgb_additive(r: u8, g: u8, b: u8) -> Self { Self([r, g, b, 0]) } /// From `sRGBA` with premultiplied alpha. - #[inline(always)] + #[inline] pub const fn from_rgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { Self([r, g, b, a]) } /// From `sRGBA` WITHOUT premultiplied alpha. + #[inline] pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { if a == 255 { Self::from_rgb(r, g, b) // common-case optimization @@ -99,74 +111,83 @@ impl Color32 { } } - #[inline(always)] + #[inline] pub const fn from_gray(l: u8) -> Self { Self([l, l, l, 255]) } - #[inline(always)] + #[inline] pub const fn from_black_alpha(a: u8) -> Self { Self([0, 0, 0, a]) } + #[inline] pub fn from_white_alpha(a: u8) -> Self { Rgba::from_white_alpha(linear_f32_from_linear_u8(a)).into() } - #[inline(always)] + #[inline] pub const fn from_additive_luminance(l: u8) -> Self { Self([l, l, l, 0]) } - #[inline(always)] + #[inline] pub const fn is_opaque(&self) -> bool { self.a() == 255 } - #[inline(always)] + #[inline] pub const fn r(&self) -> u8 { self.0[0] } - #[inline(always)] + #[inline] pub const fn g(&self) -> u8 { self.0[1] } - #[inline(always)] + #[inline] pub const fn b(&self) -> u8 { self.0[2] } - #[inline(always)] + #[inline] pub const fn a(&self) -> u8 { self.0[3] } /// Returns an opaque version of self + #[inline] pub fn to_opaque(self) -> Self { Rgba::from(self).to_opaque().into() } /// Returns an additive version of self - #[inline(always)] + #[inline] pub const fn additive(self) -> Self { let [r, g, b, _] = self.to_array(); Self([r, g, b, 0]) } + /// Is the alpha=0 ? + #[inline] + pub fn is_additive(self) -> bool { + self.a() == 0 + } + /// Premultiplied RGBA - #[inline(always)] + #[inline] pub const fn to_array(&self) -> [u8; 4] { [self.r(), self.g(), self.b(), self.a()] } /// Premultiplied RGBA - #[inline(always)] + #[inline] pub const fn to_tuple(&self) -> (u8, u8, u8, u8) { (self.r(), self.g(), self.b(), self.a()) } + #[inline] pub fn to_srgba_unmultiplied(&self) -> [u8; 4] { Rgba::from(*self).to_srgba_unmultiplied() } @@ -177,7 +198,7 @@ impl Color32 { /// /// This is perceptually even, and faster that [`Self::linear_multiply`]. #[inline] - pub fn gamma_multiply(self, factor: f32) -> Color32 { + pub fn gamma_multiply(self, factor: f32) -> Self { crate::ecolor_assert!(0.0 <= factor && factor <= 1.0); let Self([r, g, b, a]) = self; Self([ @@ -192,7 +213,8 @@ impl Color32 { /// /// This is using linear space, which is not perceptually even. /// You may want to use [`Self::gamma_multiply`] instead. - pub fn linear_multiply(self, factor: f32) -> Color32 { + #[inline] + pub fn linear_multiply(self, factor: f32) -> Self { crate::ecolor_assert!(0.0 <= factor && factor <= 1.0); // As an unfortunate side-effect of using premultiplied alpha // we need a somewhat expensive conversion to linear space and back. diff --git a/crates/ecolor/src/hex_color_macro.rs b/crates/ecolor/src/hex_color_macro.rs index 450e6a8692a..16f8cc2b8d8 100644 --- a/crates/ecolor/src/hex_color_macro.rs +++ b/crates/ecolor/src/hex_color_macro.rs @@ -1,7 +1,12 @@ /// Construct a [`crate::Color32`] from a hex RGB or RGBA string. /// +/// Requires the "color-hex" feature. +/// +/// See also [`crate::Color32::from_hex`] and [`crate::Color32::to_hex`]. +/// /// ``` /// # use ecolor::{hex_color, Color32}; +/// assert_eq!(hex_color!("#202122"), Color32::from_hex("#202122").unwrap()); /// assert_eq!(hex_color!("#202122"), Color32::from_rgb(0x20, 0x21, 0x22)); /// assert_eq!(hex_color!("#abcdef12"), Color32::from_rgba_unmultiplied(0xab, 0xcd, 0xef, 0x12)); /// ``` diff --git a/crates/ecolor/src/hex_color_runtime.rs b/crates/ecolor/src/hex_color_runtime.rs new file mode 100644 index 00000000000..3163fc5af7b --- /dev/null +++ b/crates/ecolor/src/hex_color_runtime.rs @@ -0,0 +1,223 @@ +//! Convert colors to and from the hex-color string format at runtime +//! +//! Supports the 3, 4, 6, and 8-digit formats, according to the specification in +//! + +use std::{fmt::Display, str::FromStr}; + +use crate::Color32; + +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +/// A wrapper around Color32 that converts to and from a hex-color string +/// +/// Implements [`Display`] and [`FromStr`] to convert to and from the hex string. +pub enum HexColor { + /// 3 hexadecimal digits, one for each of the r, g, b channels + Hex3(Color32), + + /// 4 hexadecimal digits, one for each of the r, g, b, a channels + Hex4(Color32), + + /// 6 hexadecimal digits, two for each of the r, g, b channels + Hex6(Color32), + + /// 8 hexadecimal digits, one for each of the r, g, b, a channels + Hex8(Color32), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ParseHexColorError { + MissingHash, + InvalidLength, + InvalidInt(std::num::ParseIntError), +} + +impl FromStr for HexColor { + type Err = ParseHexColorError; + + fn from_str(s: &str) -> Result { + s.strip_prefix('#') + .ok_or(ParseHexColorError::MissingHash) + .and_then(Self::from_str_without_hash) + } +} + +impl Display for HexColor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Hex3(color) => { + let [r, g, b, _] = color.to_srgba_unmultiplied().map(|u| u >> 4); + f.write_fmt(format_args!("#{r:x}{g:x}{b:x}")) + } + Self::Hex4(color) => { + let [r, g, b, a] = color.to_srgba_unmultiplied().map(|u| u >> 4); + f.write_fmt(format_args!("#{r:x}{g:x}{b:x}{a:x}")) + } + Self::Hex6(color) => { + let [r, g, b, _] = color.to_srgba_unmultiplied(); + let u = u32::from_be_bytes([0, r, g, b]); + f.write_fmt(format_args!("#{u:06x}")) + } + Self::Hex8(color) => { + let [r, g, b, a] = color.to_srgba_unmultiplied(); + let u = u32::from_be_bytes([r, g, b, a]); + f.write_fmt(format_args!("#{u:08x}")) + } + } + } +} + +impl HexColor { + /// Retrieves the inner [`Color32`] + #[inline] + pub fn color(&self) -> Color32 { + match self { + Self::Hex3(color) | Self::Hex4(color) | Self::Hex6(color) | Self::Hex8(color) => *color, + } + } + + /// Parses a string as a hex color without the leading `#` character + /// + /// # Errors + /// Returns an error if the length of the string does not correspond to one of the standard + /// formats (3, 4, 6, or 8), or if it contains non-hex characters. + #[inline] + pub fn from_str_without_hash(s: &str) -> Result { + match s.len() { + 3 => { + let [r, gb] = u16::from_str_radix(s, 16) + .map_err(ParseHexColorError::InvalidInt)? + .to_be_bytes(); + let [r, g, b] = [r, gb >> 4, gb & 0x0f].map(|u| u << 4 | u); + Ok(Self::Hex3(Color32::from_rgb(r, g, b))) + } + 4 => { + let [r_g, b_a] = u16::from_str_radix(s, 16) + .map_err(ParseHexColorError::InvalidInt)? + .to_be_bytes(); + let [r, g, b, a] = [r_g >> 4, r_g & 0x0f, b_a >> 4, b_a & 0x0f].map(|u| u << 4 | u); + Ok(Self::Hex4(Color32::from_rgba_unmultiplied(r, g, b, a))) + } + 6 => { + let [_, r, g, b] = u32::from_str_radix(s, 16) + .map_err(ParseHexColorError::InvalidInt)? + .to_be_bytes(); + Ok(Self::Hex6(Color32::from_rgb(r, g, b))) + } + 8 => { + let [r, g, b, a] = u32::from_str_radix(s, 16) + .map_err(ParseHexColorError::InvalidInt)? + .to_be_bytes(); + Ok(Self::Hex8(Color32::from_rgba_unmultiplied(r, g, b, a))) + } + _ => Err(ParseHexColorError::InvalidLength)?, + } + } +} + +impl Color32 { + /// Parses a color from a hex string. + /// + /// Supports the 3, 4, 6, and 8-digit formats, according to the specification in + /// + /// + /// To parse hex colors at compile-time (e.g. for use in `const` contexts) + /// use the macro [`crate::hex_color!`] instead. + /// + /// # Example + /// ```rust + /// use ecolor::Color32; + /// assert_eq!(Ok(Color32::RED), Color32::from_hex("#ff0000")); + /// assert_eq!(Ok(Color32::GREEN), Color32::from_hex("#00ff00ff")); + /// assert_eq!(Ok(Color32::BLUE), Color32::from_hex("#00f")); + /// assert_eq!(Ok(Color32::TRANSPARENT), Color32::from_hex("#0000")); + /// ``` + /// + /// # Errors + /// Returns an error if the string doesn't start with the hash `#` character, if the remaining + /// length does not correspond to one of the standard formats (3, 4, 6, or 8), if it contains + /// non-hex characters. + pub fn from_hex(hex: &str) -> Result { + HexColor::from_str(hex).map(|h| h.color()) + } + + /// Formats the color as a hex string. + /// + /// # Example + /// ```rust + /// use ecolor::Color32; + /// assert_eq!(Color32::RED.to_hex(), "#ff0000ff"); + /// assert_eq!(Color32::GREEN.to_hex(), "#00ff00ff"); + /// assert_eq!(Color32::BLUE.to_hex(), "#0000ffff"); + /// assert_eq!(Color32::TRANSPARENT.to_hex(), "#00000000"); + /// ``` + /// + /// Uses the 8-digit format described in , + /// as that is the only format that is lossless. + /// For other formats, see [`HexColor`]. + #[inline] + pub fn to_hex(&self) -> String { + HexColor::Hex8(*self).to_string() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hex_string_formats() { + use Color32 as C; + use HexColor as H; + let cases = [ + (H::Hex3(C::RED), "#f00"), + (H::Hex4(C::RED), "#f00f"), + (H::Hex6(C::RED), "#ff0000"), + (H::Hex8(C::RED), "#ff0000ff"), + (H::Hex3(C::GREEN), "#0f0"), + (H::Hex4(C::GREEN), "#0f0f"), + (H::Hex6(C::GREEN), "#00ff00"), + (H::Hex8(C::GREEN), "#00ff00ff"), + (H::Hex3(C::BLUE), "#00f"), + (H::Hex4(C::BLUE), "#00ff"), + (H::Hex6(C::BLUE), "#0000ff"), + (H::Hex8(C::BLUE), "#0000ffff"), + (H::Hex3(C::WHITE), "#fff"), + (H::Hex4(C::WHITE), "#ffff"), + (H::Hex6(C::WHITE), "#ffffff"), + (H::Hex8(C::WHITE), "#ffffffff"), + (H::Hex3(C::BLACK), "#000"), + (H::Hex4(C::BLACK), "#000f"), + (H::Hex6(C::BLACK), "#000000"), + (H::Hex8(C::BLACK), "#000000ff"), + (H::Hex4(C::TRANSPARENT), "#0000"), + (H::Hex8(C::TRANSPARENT), "#00000000"), + ]; + for (color, string) in cases { + assert_eq!(color.to_string(), string, "{color:?} <=> {string}"); + assert_eq!( + H::from_str(string).unwrap(), + color, + "{color:?} <=> {string}" + ); + } + } + + #[test] + fn hex_string_round_trip() { + use Color32 as C; + let cases = [ + C::from_rgba_unmultiplied(10, 20, 30, 0), + C::from_rgba_unmultiplied(10, 20, 30, 40), + C::from_rgba_unmultiplied(10, 20, 30, 255), + C::from_rgba_unmultiplied(0, 20, 30, 0), + C::from_rgba_unmultiplied(10, 0, 30, 40), + C::from_rgba_unmultiplied(10, 20, 0, 255), + ]; + for color in cases { + assert_eq!(C::from_hex(color.to_hex().as_str()), Ok(color)); + } + } +} diff --git a/crates/ecolor/src/hsva.rs b/crates/ecolor/src/hsva.rs index 8a68cb93502..5f5430cf02d 100644 --- a/crates/ecolor/src/hsva.rs +++ b/crates/ecolor/src/hsva.rs @@ -21,55 +21,61 @@ pub struct Hsva { } impl Hsva { + #[inline] pub fn new(h: f32, s: f32, v: f32, a: f32) -> Self { Self { h, s, v, a } } /// From `sRGBA` with premultiplied alpha - pub fn from_srgba_premultiplied(srgba: [u8; 4]) -> Self { + #[inline] + pub fn from_srgba_premultiplied([r, g, b, a]: [u8; 4]) -> Self { Self::from_rgba_premultiplied( - linear_f32_from_gamma_u8(srgba[0]), - linear_f32_from_gamma_u8(srgba[1]), - linear_f32_from_gamma_u8(srgba[2]), - linear_f32_from_linear_u8(srgba[3]), + linear_f32_from_gamma_u8(r), + linear_f32_from_gamma_u8(g), + linear_f32_from_gamma_u8(b), + linear_f32_from_linear_u8(a), ) } /// From `sRGBA` without premultiplied alpha - pub fn from_srgba_unmultiplied(srgba: [u8; 4]) -> Self { + #[inline] + pub fn from_srgba_unmultiplied([r, g, b, a]: [u8; 4]) -> Self { Self::from_rgba_unmultiplied( - linear_f32_from_gamma_u8(srgba[0]), - linear_f32_from_gamma_u8(srgba[1]), - linear_f32_from_gamma_u8(srgba[2]), - linear_f32_from_linear_u8(srgba[3]), + linear_f32_from_gamma_u8(r), + linear_f32_from_gamma_u8(g), + linear_f32_from_gamma_u8(b), + linear_f32_from_linear_u8(a), ) } /// From linear RGBA with premultiplied alpha + #[inline] pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { #![allow(clippy::many_single_char_names)] if a == 0.0 { if r == 0.0 && b == 0.0 && a == 0.0 { - Hsva::default() + Self::default() } else { - Hsva::from_additive_rgb([r, g, b]) + Self::from_additive_rgb([r, g, b]) } } else { let (h, s, v) = hsv_from_rgb([r / a, g / a, b / a]); - Hsva { h, s, v, a } + Self { h, s, v, a } } } /// From linear RGBA without premultiplied alpha + #[inline] pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { #![allow(clippy::many_single_char_names)] let (h, s, v) = hsv_from_rgb([r, g, b]); - Hsva { h, s, v, a } + Self { h, s, v, a } } + #[inline] pub fn from_additive_rgb(rgb: [f32; 3]) -> Self { let (h, s, v) = hsv_from_rgb(rgb); - Hsva { + Self { h, s, v, @@ -77,11 +83,22 @@ impl Hsva { } } + #[inline] + pub fn from_additive_srgb([r, g, b]: [u8; 3]) -> Self { + Self::from_additive_rgb([ + linear_f32_from_gamma_u8(r), + linear_f32_from_gamma_u8(g), + linear_f32_from_gamma_u8(b), + ]) + } + + #[inline] pub fn from_rgb(rgb: [f32; 3]) -> Self { let (h, s, v) = hsv_from_rgb(rgb); - Hsva { h, s, v, a: 1.0 } + Self { h, s, v, a: 1.0 } } + #[inline] pub fn from_srgb([r, g, b]: [u8; 3]) -> Self { Self::from_rgb([ linear_f32_from_gamma_u8(r), @@ -92,14 +109,17 @@ impl Hsva { // ------------------------------------------------------------------------ + #[inline] pub fn to_opaque(self) -> Self { Self { a: 1.0, ..self } } + #[inline] pub fn to_rgb(&self) -> [f32; 3] { rgb_from_hsv((self.h, self.s, self.v)) } + #[inline] pub fn to_srgb(&self) -> [u8; 3] { let [r, g, b] = self.to_rgb(); [ @@ -109,6 +129,7 @@ impl Hsva { ] } + #[inline] pub fn to_rgba_premultiplied(&self) -> [f32; 4] { let [r, g, b, a] = self.to_rgba_unmultiplied(); let additive = a < 0.0; @@ -119,13 +140,17 @@ impl Hsva { } } + /// To linear space rgba in 0-1 range. + /// /// Represents additive colors using a negative alpha. + #[inline] pub fn to_rgba_unmultiplied(&self) -> [f32; 4] { - let Hsva { h, s, v, a } = *self; + let Self { h, s, v, a } = *self; let [r, g, b] = rgb_from_hsv((h, s, v)); [r, g, b, a] } + #[inline] pub fn to_srgba_premultiplied(&self) -> [u8; 4] { let [r, g, b, a] = self.to_rgba_premultiplied(); [ @@ -136,6 +161,8 @@ impl Hsva { ] } + /// To gamma-space 0-255. + #[inline] pub fn to_srgba_unmultiplied(&self) -> [u8; 4] { let [r, g, b, a] = self.to_rgba_unmultiplied(); [ @@ -148,30 +175,35 @@ impl Hsva { } impl From for Rgba { - fn from(hsva: Hsva) -> Rgba { - Rgba(hsva.to_rgba_premultiplied()) + #[inline] + fn from(hsva: Hsva) -> Self { + Self(hsva.to_rgba_premultiplied()) } } impl From for Hsva { - fn from(rgba: Rgba) -> Hsva { + #[inline] + fn from(rgba: Rgba) -> Self { Self::from_rgba_premultiplied(rgba.0[0], rgba.0[1], rgba.0[2], rgba.0[3]) } } impl From for Color32 { - fn from(hsva: Hsva) -> Color32 { - Color32::from(Rgba::from(hsva)) + #[inline] + fn from(hsva: Hsva) -> Self { + Self::from(Rgba::from(hsva)) } } impl From for Hsva { - fn from(srgba: Color32) -> Hsva { - Hsva::from(Rgba::from(srgba)) + #[inline] + fn from(srgba: Color32) -> Self { + Self::from(Rgba::from(srgba)) } } /// All ranges in 0-1, rgb is linear. +#[inline] pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) { #![allow(clippy::many_single_char_names)] let min = r.min(g.min(b)); @@ -195,6 +227,7 @@ pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) { } /// All ranges in 0-1, rgb is linear. +#[inline] pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] { #![allow(clippy::many_single_char_names)] let h = (h.fract() + 1.0).fract(); // wrap diff --git a/crates/ecolor/src/hsva_gamma.rs b/crates/ecolor/src/hsva_gamma.rs index 3135ef100b4..67e167677db 100644 --- a/crates/ecolor/src/hsva_gamma.rs +++ b/crates/ecolor/src/hsva_gamma.rs @@ -18,21 +18,21 @@ pub struct HsvaGamma { } impl From for Rgba { - fn from(hsvag: HsvaGamma) -> Rgba { + fn from(hsvag: HsvaGamma) -> Self { Hsva::from(hsvag).into() } } impl From for Color32 { - fn from(hsvag: HsvaGamma) -> Color32 { + fn from(hsvag: HsvaGamma) -> Self { Rgba::from(hsvag).into() } } impl From for Hsva { - fn from(hsvag: HsvaGamma) -> Hsva { + fn from(hsvag: HsvaGamma) -> Self { let HsvaGamma { h, s, v, a } = hsvag; - Hsva { + Self { h, s, v: linear_from_gamma(v), @@ -42,21 +42,21 @@ impl From for Hsva { } impl From for HsvaGamma { - fn from(rgba: Rgba) -> HsvaGamma { + fn from(rgba: Rgba) -> Self { Hsva::from(rgba).into() } } impl From for HsvaGamma { - fn from(srgba: Color32) -> HsvaGamma { + fn from(srgba: Color32) -> Self { Hsva::from(srgba).into() } } impl From for HsvaGamma { - fn from(hsva: Hsva) -> HsvaGamma { + fn from(hsva: Hsva) -> Self { let Hsva { h, s, v, a } = hsva; - HsvaGamma { + Self { h, s, v: gamma_from_linear(v), diff --git a/crates/ecolor/src/lib.rs b/crates/ecolor/src/lib.rs index 9bc42c4cf79..e3c077edd50 100644 --- a/crates/ecolor/src/lib.rs +++ b/crates/ecolor/src/lib.rs @@ -12,8 +12,6 @@ #[cfg(feature = "cint")] mod cint_impl; -#[cfg(feature = "cint")] -pub use cint_impl::*; mod color32; pub use color32::*; @@ -30,12 +28,15 @@ mod hex_color_macro; mod rgba; pub use rgba::*; +mod hex_color_runtime; +pub use hex_color_runtime::*; + // ---------------------------------------------------------------------------- // Color conversion: impl From for Rgba { - fn from(srgba: Color32) -> Rgba { - Rgba([ + fn from(srgba: Color32) -> Self { + Self([ linear_f32_from_gamma_u8(srgba.0[0]), linear_f32_from_gamma_u8(srgba.0[1]), linear_f32_from_gamma_u8(srgba.0[2]), @@ -45,8 +46,8 @@ impl From for Rgba { } impl From for Color32 { - fn from(rgba: Rgba) -> Color32 { - Color32([ + fn from(rgba: Rgba) -> Self { + Self([ gamma_u8_from_linear_f32(rgba.0[0]), gamma_u8_from_linear_f32(rgba.0[1]), gamma_u8_from_linear_f32(rgba.0[2]), @@ -93,7 +94,7 @@ pub fn linear_u8_from_linear_f32(a: f32) -> u8 { } fn fast_round(r: f32) -> u8 { - (r + 0.5).floor() as _ // rust does a saturating cast since 1.45 + (r + 0.5) as _ // rust does a saturating cast since 1.45 } #[test] diff --git a/crates/ecolor/src/rgba.rs b/crates/ecolor/src/rgba.rs index f9d671fd5c1..2a61d311ee4 100644 --- a/crates/ecolor/src/rgba.rs +++ b/crates/ecolor/src/rgba.rs @@ -13,20 +13,20 @@ pub struct Rgba(pub(crate) [f32; 4]); impl std::ops::Index for Rgba { type Output = f32; - #[inline(always)] + #[inline] fn index(&self, index: usize) -> &f32 { &self.0[index] } } impl std::ops::IndexMut for Rgba { - #[inline(always)] + #[inline] fn index_mut(&mut self, index: usize) -> &mut f32 { &mut self.0[index] } } -#[inline(always)] +#[inline] pub(crate) fn f32_hash(state: &mut H, f: f32) { if f == 0.0 { state.write_u8(0); @@ -38,7 +38,7 @@ pub(crate) fn f32_hash(state: &mut H, f: f32) { } } -#[allow(clippy::derive_hash_xor_eq)] +#[allow(clippy::derived_hash_with_manual_eq)] impl std::hash::Hash for Rgba { #[inline] fn hash(&self, state: &mut H) { @@ -50,24 +50,24 @@ impl std::hash::Hash for Rgba { } impl Rgba { - pub const TRANSPARENT: Rgba = Rgba::from_rgba_premultiplied(0.0, 0.0, 0.0, 0.0); - pub const BLACK: Rgba = Rgba::from_rgb(0.0, 0.0, 0.0); - pub const WHITE: Rgba = Rgba::from_rgb(1.0, 1.0, 1.0); - pub const RED: Rgba = Rgba::from_rgb(1.0, 0.0, 0.0); - pub const GREEN: Rgba = Rgba::from_rgb(0.0, 1.0, 0.0); - pub const BLUE: Rgba = Rgba::from_rgb(0.0, 0.0, 1.0); - - #[inline(always)] + pub const TRANSPARENT: Self = Self::from_rgba_premultiplied(0.0, 0.0, 0.0, 0.0); + pub const BLACK: Self = Self::from_rgb(0.0, 0.0, 0.0); + pub const WHITE: Self = Self::from_rgb(1.0, 1.0, 1.0); + pub const RED: Self = Self::from_rgb(1.0, 0.0, 0.0); + pub const GREEN: Self = Self::from_rgb(0.0, 1.0, 0.0); + pub const BLUE: Self = Self::from_rgb(0.0, 0.0, 1.0); + + #[inline] pub const fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { Self([r, g, b, a]) } - #[inline(always)] + #[inline] pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { Self([r * a, g * a, b * a, a]) } - #[inline(always)] + #[inline] pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { let r = linear_f32_from_gamma_u8(r); let g = linear_f32_from_gamma_u8(g); @@ -76,7 +76,7 @@ impl Rgba { Self::from_rgba_premultiplied(r, g, b, a) } - #[inline(always)] + #[inline] pub fn from_srgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { let r = linear_f32_from_gamma_u8(r); let g = linear_f32_from_gamma_u8(g); @@ -85,16 +85,17 @@ impl Rgba { Self::from_rgba_premultiplied(r * a, g * a, b * a, a) } - #[inline(always)] + #[inline] pub const fn from_rgb(r: f32, g: f32, b: f32) -> Self { Self([r, g, b, 1.0]) } - #[inline(always)] + #[inline] pub const fn from_gray(l: f32) -> Self { Self([l, l, l, 1.0]) } + #[inline] pub fn from_luminance_alpha(l: f32, a: f32) -> Self { crate::ecolor_assert!(0.0 <= l && l <= 1.0); crate::ecolor_assert!(0.0 <= a && a <= 1.0); @@ -102,28 +103,34 @@ impl Rgba { } /// Transparent black - #[inline(always)] + #[inline] pub fn from_black_alpha(a: f32) -> Self { crate::ecolor_assert!(0.0 <= a && a <= 1.0); Self([0.0, 0.0, 0.0, a]) } /// Transparent white - #[inline(always)] + #[inline] pub fn from_white_alpha(a: f32) -> Self { crate::ecolor_assert!(0.0 <= a && a <= 1.0, "a: {}", a); Self([a, a, a, a]) } /// Return an additive version of this color (alpha = 0) - #[inline(always)] + #[inline] pub fn additive(self) -> Self { let [r, g, b, _] = self.0; Self([r, g, b, 0.0]) } + /// Is the alpha=0 ? + #[inline] + pub fn is_additive(self) -> bool { + self.a() == 0.0 + } + /// Multiply with e.g. 0.5 to make us half transparent - #[inline(always)] + #[inline] pub fn multiply(self, alpha: f32) -> Self { Self([ alpha * self[0], @@ -133,22 +140,22 @@ impl Rgba { ]) } - #[inline(always)] + #[inline] pub fn r(&self) -> f32 { self.0[0] } - #[inline(always)] + #[inline] pub fn g(&self) -> f32 { self.0[1] } - #[inline(always)] + #[inline] pub fn b(&self) -> f32 { self.0[2] } - #[inline(always)] + #[inline] pub fn a(&self) -> f32 { self.0[3] } @@ -160,6 +167,7 @@ impl Rgba { } /// Returns an opaque version of self + #[inline] pub fn to_opaque(&self) -> Self { if self.a() == 0.0 { // Additive or fully transparent black. @@ -175,18 +183,19 @@ impl Rgba { } /// Premultiplied RGBA - #[inline(always)] + #[inline] pub fn to_array(&self) -> [f32; 4] { [self.r(), self.g(), self.b(), self.a()] } /// Premultiplied RGBA - #[inline(always)] + #[inline] pub fn to_tuple(&self) -> (f32, f32, f32, f32) { (self.r(), self.g(), self.b(), self.a()) } /// unmultiply the alpha + #[inline] pub fn to_rgba_unmultiplied(&self) -> [f32; 4] { let a = self.a(); if a == 0.0 { @@ -198,6 +207,7 @@ impl Rgba { } /// unmultiply the alpha + #[inline] pub fn to_srgba_unmultiplied(&self) -> [u8; 4] { let [r, g, b, a] = self.to_rgba_unmultiplied(); [ @@ -210,11 +220,11 @@ impl Rgba { } impl std::ops::Add for Rgba { - type Output = Rgba; + type Output = Self; - #[inline(always)] - fn add(self, rhs: Rgba) -> Rgba { - Rgba([ + #[inline] + fn add(self, rhs: Self) -> Self { + Self([ self[0] + rhs[0], self[1] + rhs[1], self[2] + rhs[2], @@ -223,12 +233,12 @@ impl std::ops::Add for Rgba { } } -impl std::ops::Mul for Rgba { - type Output = Rgba; +impl std::ops::Mul for Rgba { + type Output = Self; - #[inline(always)] - fn mul(self, other: Rgba) -> Rgba { - Rgba([ + #[inline] + fn mul(self, other: Self) -> Self { + Self([ self[0] * other[0], self[1] * other[1], self[2] * other[2], @@ -238,11 +248,11 @@ impl std::ops::Mul for Rgba { } impl std::ops::Mul for Rgba { - type Output = Rgba; + type Output = Self; - #[inline(always)] - fn mul(self, factor: f32) -> Rgba { - Rgba([ + #[inline] + fn mul(self, factor: f32) -> Self { + Self([ self[0] * factor, self[1] * factor, self[2] * factor, @@ -254,7 +264,7 @@ impl std::ops::Mul for Rgba { impl std::ops::Mul for f32 { type Output = Rgba; - #[inline(always)] + #[inline] fn mul(self, rgba: Rgba) -> Rgba { Rgba([ self * rgba[0], diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index 072a54e8cd9..f0566955899 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -4,7 +4,142 @@ All notable changes to the `eframe` crate. NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/CHANGELOG.md), [`egui_glow`](../egui_glow/CHANGELOG.md),and [`egui-wgpu`](../egui-wgpu/CHANGELOG.md) have their own changelogs! This file is updated upon each release. -Changes since the last release can be found by running the `scripts/generate_changelog.py` script. +Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. + + +## 0.26.2 - 2024-02-14 +* Add `winuser` feature to `winapi` to fix unresolved import [#4037](https://github.com/emilk/egui/pull/4037) (thanks [@varphone](https://github.com/varphone)!) + + +## 0.26.1 - 2024-02-11 +* Fix high CPU usage on Windows when app is minimized [#3985](https://github.com/emilk/egui/pull/3985) (thanks [@rustbasic](https://github.com/rustbasic)!) +* Update to document-features 0.2.8 [#4003](https://github.com/emilk/egui/pull/4003) + + +## 0.26.0 - 2024-02-05 +* Update `wgpu` to 0.19 [#3824](https://github.com/emilk/egui/pull/3824) +* Disable the default features of `wgpu` [#3875](https://github.com/emilk/egui/pull/3875) +* Much more accurate `cpu_usage` timing [#3913](https://github.com/emilk/egui/pull/3913) +* Update to puffin 0.19 [#3940](https://github.com/emilk/egui/pull/3940) + +#### Desktop/Native: +* Keep `ViewportInfo::maximized` and `minimized` up-to-date on Windows [#3831](https://github.com/emilk/egui/pull/3831) (thanks [@rustbasic](https://github.com/rustbasic)!) +* Handle `IconData::default()` without crashing [#3842](https://github.com/emilk/egui/pull/3842) +* Fix Android crash on resume [#3847](https://github.com/emilk/egui/pull/3847) [#3867](https://github.com/emilk/egui/pull/3867) (thanks [@Garoven](https://github.com/Garoven)!) +* Add `WgpuConfiguration::desired_maximum_frame_latency` [#3874](https://github.com/emilk/egui/pull/3874) +* Don't call `App::update` on minimized windows [#3877](https://github.com/emilk/egui/pull/3877) (thanks [@rustbasic](https://github.com/rustbasic)!) + +#### Web: +* When using `wgpu` on web, `eframe` will try to use WebGPU if available, then fall back to WebGL [#3824](https://github.com/emilk/egui/pull/3824) [#3895](https://github.com/emilk/egui/pull/3895) (thanks [@Wumpf](https://github.com/Wumpf)!) + + +## 0.25.0 - 2024-01-08 +* If both `glow` and `wgpu` features are enabled, default to `wgpu` [#3717](https://github.com/emilk/egui/pull/3717) + +#### Desktop/Native: +* Update to winit 0.29 [#3649](https://github.com/emilk/egui/pull/3649) (thanks [@fornwall](https://github.com/fornwall)!) +* Make glow `Send + Sync` again [#3646](https://github.com/emilk/egui/pull/3646) (thanks [@surban](https://github.com/surban)!) +* Bug fix: framebuffer clear when using glow with multi-viewports [#3713](https://github.com/emilk/egui/pull/3713) +* Fix: Let `accesskit` process window events [#3733](https://github.com/emilk/egui/pull/3733) (thanks [@DataTriny](https://github.com/DataTriny)!) + +#### Web: +* Fix building the `wasm32` docs for `docs.rs` [#3757](https://github.com/emilk/egui/pull/3757) + + +## 0.24.1 - 2023-11-30 +#### Desktop/Native: +* Fix window flashing white on launch [#3631](https://github.com/emilk/egui/pull/3631) (thanks [@zeozeozeo](https://github.com/zeozeozeo)!) +* Fix windowing problems when using the `x11` feature on Linux [#3643](https://github.com/emilk/egui/pull/3643) +* Fix bugs when there are multiple monitors with different scales [#3663](https://github.com/emilk/egui/pull/3663) +* `glow` backend: clear framebuffer color before calling `App::update` [#3665](https://github.com/emilk/egui/pull/3665) + +#### Web: +* Fix click-to-copy on Safari [#3621](https://github.com/emilk/egui/pull/3621) +* Don't throw away frames on click/copy/cut [#3623](https://github.com/emilk/egui/pull/3623) +* Remove dependency on `tts` [#3651](https://github.com/emilk/egui/pull/3651) + + +## 0.24.0 - 2023-11-23 +* Multiple viewports/windows [#3172](https://github.com/emilk/egui/pull/3172) (thanks [@konkitoman](https://github.com/konkitoman)!) +* Replace `eframe::Frame` commands and `WindowInfo` with egui [#3564](https://github.com/emilk/egui/pull/3564) +* Use `egui::ViewportBuilder` in `eframe::NativeOptions` [#3572](https://github.com/emilk/egui/pull/3572) +* Remove warm-starting [#3574](https://github.com/emilk/egui/pull/3574) +* Fix copy and cut on Safari [#3513](https://github.com/emilk/egui/pull/3513) (thanks [@lunixbochs](https://github.com/lunixbochs)!) +* Update puffin to 0.18 [#3600](https://github.com/emilk/egui/pull/3600) +* Update MSRV to Rust 1.72 [#3595](https://github.com/emilk/egui/pull/3595) + +### Breaking changes: +Most settings in `NativeOptions` have been moved to `NativeOptions::viewport`, which uses the new `egui::ViewportBuilder`: + +```diff + let native_options = eframe::nativeOptions { +- initial_window_size: Some(egui::vec2(320.0, 240.0)), +- drag_and_drop_support: true, ++ viewport: egui::ViewportBuilder::default() ++ .with_inner_size([320.0, 240.0]) ++ .with_drag_and_drop(true), + ..Default::default() + }; +``` + +`NativeOptions::fullsize_content` has been replaced with four settings: `ViewportBuilder::with_fullsize_content_view`, `with_title_shown`, `with_titlebar_shown`, `with_titlebar_buttons_shown` + +`frame.info().window_info` is gone, replaced with `ctx.input(|i| i.viewport())`. + +`frame.info().native_pixels_per_point` is replaced with `ctx.input(|i| i.raw.native_pixels_per_point)`. + +Most commands in `eframe::Frame` has been replaced with `egui::ViewportCommand`, so So `frame.close()` becomes `ctx.send_viewport_cmd(ViewportCommand::Close)`, etc. + +`App::on_close_event` has been replaced with `ctx.input(|i| i.viewport().close_requested())` and `ctx.send_viewport_cmd(ViewportCommand::CancelClose)`. + +`eframe::IconData` is now `egui::IconData`. + +`eframe::IconData::try_from_png_bytes` is now `eframe::icon_data::from_png_bytes`. + +`App::post_rendering` is gone. Screenshots are taken with `ctx.send_viewport_cmd(ViewportCommand::Screenshots)` and are returned in `egui::Event` which you can check with: +``` rust +ui.input(|i| { + for event in &i.raw.events { + if let egui::Event::Screenshot { viewport_id, image } = event { + // handle it here + } + } +}); +``` + + +## 0.23.0 - 2023-09-27 +* Update MSRV to Rust 1.70.0 [#3310](https://github.com/emilk/egui/pull/3310) +* Update to puffin 0.16 [#3144](https://github.com/emilk/egui/pull/3144) +* Update to `wgpu` 0.17.0 [#3170](https://github.com/emilk/egui/pull/3170) (thanks [@Aaron1011](https://github.com/Aaron1011)!) +* Improved wgpu callbacks [#3253](https://github.com/emilk/egui/pull/3253) (thanks [@Wumpf](https://github.com/Wumpf)!) +* Improve documentation of `eframe`, especially for wasm32 [#3295](https://github.com/emilk/egui/pull/3295) +* `eframe::Frame::info` returns a reference [#3301](https://github.com/emilk/egui/pull/3301) (thanks [@Barugon](https://github.com/Barugon)!) +* Move `App::persist_window` to `NativeOptions` and `App::max_size_points` to `WebOptions` [#3397](https://github.com/emilk/egui/pull/3397) + +#### Desktop/Native: +* Only show on-screen-keyboard and IME when editing text [#3362](https://github.com/emilk/egui/pull/3362) (thanks [@Barugon](https://github.com/Barugon)!) +* Add `eframe::storage_dir` [#3286](https://github.com/emilk/egui/pull/3286) +* Add `NativeOptions::window_builder` for more customization [#3390](https://github.com/emilk/egui/pull/3390) (thanks [@twop](https://github.com/twop)!) +* Better restore Window position on Mac when on secondary monitor [#3239](https://github.com/emilk/egui/pull/3239) +* Fix iOS support in `eframe` [#3241](https://github.com/emilk/egui/pull/3241) (thanks [@lucasmerlin](https://github.com/lucasmerlin)!) +* Speed up `eframe` state storage [#3353](https://github.com/emilk/egui/pull/3353) (thanks [@sebbert](https://github.com/sebbert)!) +* Allow users to opt-out of default `winit` features [#3228](https://github.com/emilk/egui/pull/3228) +* Expose Raw Window and Display Handles [#3073](https://github.com/emilk/egui/pull/3073) (thanks [@bash](https://github.com/bash)!) +* Use window title as fallback when app_id is not set [#3107](https://github.com/emilk/egui/pull/3107) (thanks [@jacekpoz](https://github.com/jacekpoz)!) +* Sleep a bit only when minimized [#3139](https://github.com/emilk/egui/pull/3139) (thanks [@icedrocket](https://github.com/icedrocket)!) +* Prevent text from being cleared when selected due to winit IME [#3376](https://github.com/emilk/egui/pull/3376) (thanks [@YgorSouza](https://github.com/YgorSouza)!) +* Fix android app quit on resume with glow backend [#3080](https://github.com/emilk/egui/pull/3080) (thanks [@tkkcc](https://github.com/tkkcc)!) +* Fix panic with persistence without window [#3167](https://github.com/emilk/egui/pull/3167) (thanks [@sagebind](https://github.com/sagebind)!) +* Only call `run_return` twice on Windows [#3053](https://github.com/emilk/egui/pull/3053) (thanks [@pan93412](https://github.com/pan93412)!) +* Gracefully catch error saving state to disk [#3230](https://github.com/emilk/egui/pull/3230) +* Recognize numpad enter/plus/minus [#3285](https://github.com/emilk/egui/pull/3285) +* Add more puffin profile scopes to `eframe` [#3330](https://github.com/emilk/egui/pull/3330) [#3332](https://github.com/emilk/egui/pull/3332) + +#### Web: +* Update to wasm-bindgen 0.2.87 [#3237](https://github.com/emilk/egui/pull/3237) +* Remove `Function()` invocation from eframe text_agent to bypass "unsafe-eval" restrictions in Chrome browser extensions. [#3349](https://github.com/emilk/egui/pull/3349) (thanks [@aspect](https://github.com/aspect)!) +* Fix docs about web [#3026](https://github.com/emilk/egui/pull/3026) (thanks [@kerryeon](https://github.com/kerryeon)!) ## 0.22.0 - 2023-05-23 diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index fd4076ed088..da1aea6badf 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "eframe" -version = "0.22.0" +version.workspace = true authors = ["Emil Ernerfeldt "] description = "egui framework - write GUI apps that compiles to web and/or natively" -edition = "2021" -rust-version = "1.67" +edition.workspace = true +rust-version.workspace = true homepage = "https://github.com/emilk/egui/tree/master/crates/eframe" -license = "MIT OR Apache-2.0" +license.workspace = true readme = "README.md" repository = "https://github.com/emilk/egui/tree/master/crates/eframe" categories = ["gui", "game-development"] @@ -21,6 +21,7 @@ include = [ [package.metadata.docs.rs] all-features = true +rustc-args = ["--cfg=web_sys_unstable_apis"] targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] [lib] @@ -32,6 +33,7 @@ default = [ "default_fonts", "glow", "wayland", + "web_screen_reader", "winit/default", "x11", ] @@ -39,18 +41,29 @@ default = [ ## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/). accesskit = ["egui/accesskit", "egui-winit/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 +# rely on Winit to pull in a suitable version (unlike most Rust crates, any version conflicts won't link) +# - It's also important that we don't impose an android-activity backend by taking this choice away from applications. + +## Enable the `game-activity` backend via `egui-winit` on Android +android-game-activity = ["egui-winit/android-game-activity"] +## Enable the `native-activity` backend via `egui-winit` on Android +android-native-activity = ["egui-winit/android-native-activity"] + ## If set, egui will use `include_bytes!` to bundle some fonts. ## If you plan on specifying your own fonts you may disable this feature. default_fonts = ["egui/default_fonts"] ## Use [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow). -glow = ["dep:glow", "dep:egui_glow", "dep:glutin", "dep:glutin-winit"] - -## Enables wayland support and fixes clipboard issue. -wayland = ["egui-winit/wayland"] - -## Enables compiling for x11. -x11 = ["egui-winit/x11"] +glow = [ + "dep:egui_glow", + "dep:glow", + "dep:glutin-winit", + "dep:glutin", + "dep:rwh_05", + "winit/rwh_05", +] ## Enable saving app state to disk. persistence = [ @@ -63,86 +76,114 @@ persistence = [ ## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. ## -## Only enabled on native, because of the low resolution (1ms) of time keeping in browsers. ## `eframe` will call `puffin::GlobalProfiler::lock().new_frame()` for you -puffin = ["dep:puffin", "egui_glow?/puffin", "egui-wgpu?/puffin"] +## +## 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", +] + +## 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. -web_screen_reader = ["tts"] - -## 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 the examples. -__screenshot = [] +## For other platforms, use the `accesskit` feature instead. +web_screen_reader = [ + "web-sys/SpeechSynthesis", + "web-sys/SpeechSynthesisUtterance", +] ## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)). +## ## This overrides the `glow` feature. -wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster", "dep:raw-window-handle"] +## +## By default, only WebGPU is enabled on web. +## If you want to enable WebGL, you need to turn on the `webgl` feature of crate `wgpu`: +## +## ```toml +## wgpu = { version = "*", features = ["webgpu", "webgl"] } +## ``` +## +## By default, eframe will prefer WebGPU over WebGL, but +## you can configure this at run-time with [`NativeOptions::wgpu_options`]. +wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"] -# 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 -# rely on Winit to pull in a suitable version (unlike most Rust crates, any version conflicts won't link) -# - It's also important that we don't impose an android-activity backend by taking this choice away from applications. +## Enables compiling for x11. +x11 = ["egui-winit/x11", "egui-wgpu?/x11", "egui_glow?/x11"] -## Enable the `native-activity` backend via `egui-winit` on Android -android-native-activity = ["egui-winit/android-native-activity"] -## Enable the `game-activity` backend via `egui-winit` on Android -android-game-activity = ["egui-winit/android-game-activity"] +## 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. +__screenshot = [] [dependencies] -egui = { version = "0.22.0", path = "../egui", default-features = false, features = [ +egui = { workspace = true, default-features = false, features = [ "bytemuck", "log", ] } -log = { version = "0.4", features = ["std"] } + +document-features.workspace = true +log.workspace = true +parking_lot.workspace = true +raw-window-handle.workspace = true +static_assertions = "1.1.0" thiserror.workspace = true +web-time.workspace = true -#! ### Optional dependencies -## Enable this when generating docs. -document-features = { version = "0.2", optional = true } +# Optional dependencies -egui_glow = { version = "0.22.0", path = "../egui_glow", optional = true, default-features = false } -glow = { version = "0.12", optional = true } +egui_glow = { workspace = true, optional = true, default-features = false } +glow = { workspace = true, optional = true } +# glutin stuck on old version of raw-window-handle: +rwh_05 = { package = "raw-window-handle", version = "0.5.2", optional = true, features = [ + "std", +] } ron = { version = "0.8", optional = true, features = ["integer128"] } serde = { version = "1", optional = true, features = ["derive"] } -static_assertions = "1.1.0" # ------------------------------------------- # native: [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -egui-winit = { version = "0.22.0", path = "../egui-winit", default-features = false, features = [ +egui-winit = { workspace = true, default-features = false, features = [ "clipboard", "links", ] } image = { version = "0.24", default-features = false, features = [ "png", ] } # Needed for app icon -raw-window-handle = { version = "0.5.0" } -winit = { version = "0.28.1", default-features = false } +winit = { workspace = true, default-features = false, features = ["rwh_06"] } # optional native: directories-next = { version = "2", optional = true } -egui-wgpu = { version = "0.22.0", path = "../egui-wgpu", optional = true, features = [ +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 # we can expose these to user so that they can select which backends they want to enable to avoid compiling useless deps. # this can be done at the same time we expose x11/wayland features of winit crate. -glutin = { version = "0.30", optional = true } -glutin-winit = { version = "0.3.0", optional = true } -puffin = { version = "0.16", optional = true } -wgpu = { workspace = true, optional = true } +glutin = { version = "0.31", optional = true } +glutin-winit = { version = "0.4", 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 + "metal", + "webgpu", +] } # mac: [target.'cfg(any(target_os = "macos"))'.dependencies] -cocoa = "0.24.1" +cocoa = "0.25.0" objc = "0.2.7" # windows: [target.'cfg(any(target_os = "windows"))'.dependencies] -winapi = "0.3.9" +winapi = { version = "0.3.9", features = ["winuser"] } # ------------------------------------------- # web: @@ -150,7 +191,7 @@ winapi = "0.3.9" bytemuck = "1.7" js-sys = "0.3" percent-encoding = "2.1" -wasm-bindgen = "0.2.87" +wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" web-sys = { version = "0.3.58", features = [ "BinaryType", @@ -197,7 +238,9 @@ web-sys = { version = "0.3.58", features = [ ] } # optional web: -egui-wgpu = { version = "0.22.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit -raw-window-handle = { version = "0.5.2", optional = true } -tts = { version = "0.25", optional = true, default-features = false } -wgpu = { workspace = true, optional = true } +egui-wgpu = { workspace = true, optional = true } # if wgpu is used, use it without (!) winit +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 + "webgpu", +] } diff --git a/crates/eframe/README.md b/crates/eframe/README.md index 55993943d07..ac57d999844 100644 --- a/crates/eframe/README.md +++ b/crates/eframe/README.md @@ -5,7 +5,7 @@ ![MIT](https://img.shields.io/badge/license-MIT-blue.svg) ![Apache](https://img.shields.io/badge/license-Apache-blue.svg) -`eframe` is the official framework library for writing apps using [`egui`](https://github.com/emilk/egui). The app can be compiled both to run natively (cross platform) or be compiled to a web app (using WASM). +`eframe` is the official framework library for writing apps using [`egui`](https://github.com/emilk/egui). The app can be compiled both to run natively (for Linux, Mac, Windows, and Android) or as a web app (using [Wasm](https://en.wikipedia.org/wiki/WebAssembly)). To get started, see the [examples](https://github.com/emilk/egui/tree/master/examples). To learn how to set up `eframe` for web and native, go to and follow the instructions there! @@ -28,6 +28,7 @@ You need to either use `edition = "2021"`, or set `resolver = "2"` in the `[work You can opt-in to the using [`egui_wgpu`](https://github.com/emilk/egui/tree/master/crates/egui_wgpu) for rendering by enabling the `wgpu` feature and setting `NativeOptions::renderer` to `Renderer::Wgpu`. +To get copy-paste working on web, you need to compile with `export RUSTFLAGS=--cfg=web_sys_unstable_apis`. ## Alternatives `eframe` is not the only way to write an app using `egui`! You can also try [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad), [`bevy_egui`](https://github.com/mvlabat/bevy_egui), [`egui_sdl2_gl`](https://github.com/ArjunNair/egui_sdl2_gl), and others. @@ -35,15 +36,15 @@ You can opt-in to the using [`egui_wgpu`](https://github.com/emilk/egui/tree/mas You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/winit) to build your own app as demonstrated in . -## Problems with running egui on the web -`eframe` uses WebGL (via [`glow`](https://crates.io/crates/glow)) and WASM, and almost nothing else from the web tech stack. This has some benefits, but also produces some challenges and serious downsides. +## Limitations when running egui on the web +`eframe` uses WebGL (via [`glow`](https://crates.io/crates/glow)) and Wasm, and almost nothing else from the web tech stack. This has some benefits, but also produces some challenges and serious downsides. * Rendering: Getting pixel-perfect rendering right on the web is very difficult. * Search: you cannot search an egui web page like you would a normal web page. * Bringing up an on-screen keyboard on mobile: there is no JS function to do this, so `eframe` fakes it by adding some invisible DOM elements. It doesn't always work. * Mobile text editing is not as good as for a normal web app. -* Accessibility: There is an experimental screen reader for `eframe`, but it has to be enabled explicitly. There is no JS function to ask "Does the user want a screen reader?" (and there should probably not be such a function, due to user tracking/integrity concerns). * No integration with browser settings for colors and fonts. +* Accessibility: There is an experimental screen reader for `eframe`, but it has to be enabled explicitly. There is no JS function to ask "Does the user want a screen reader?" (and there should probably not be such a function, due to user tracking/integrity concerns). `egui` supports [AccessKit](https://github.com/AccessKit/accesskit), but as of early 2024, AccessKit lacks a Web backend. In many ways, `eframe` is trying to make the browser do something it wasn't designed to do (though there are many things browser vendors could do to improve how well libraries like egui work). @@ -51,13 +52,14 @@ The suggested use for `eframe` are for web apps where performance and responsive ## Companion crates -Not all rust crates work when compiled to WASM, but here are some useful crates have been designed to work well both natively and as WASM: +Not all rust crates work when compiled to Wasm, but here are some useful crates have been designed to work well both natively and as Wasm: -* Audio: [`cpal`](https://github.com/RustAudio/cpal). -* HTTP client: [`ehttp`](https://github.com/emilk/ehttp) and [`reqwest`](https://github.com/seanmonstar/reqwest). -* Time: [`chrono`](https://github.com/chronotope/chrono). -* WebSockets: [`ewebsock`](https://github.com/rerun-io/ewebsock). +* Audio: [`cpal`](https://github.com/RustAudio/cpal) +* File dialogs: [rfd](https://docs.rs/rfd/latest/rfd/) +* HTTP client: [`ehttp`](https://github.com/emilk/ehttp) and [`reqwest`](https://github.com/seanmonstar/reqwest) +* Time: [`chrono`](https://github.com/chronotope/chrono) +* WebSockets: [`ewebsock`](https://github.com/rerun-io/ewebsock) ## Name -The _frame_ in `eframe` stands both for the frame in which your `egui` app resides and also for "framework" (`frame` is a framework, `egui` is a library). +The _frame_ in `eframe` stands both for the frame in which your `egui` app resides and also for "framework" (`eframe` is a framework, `egui` is a library). diff --git a/crates/eframe/src/epi/mod.rs b/crates/eframe/src/epi.rs similarity index 52% rename from crates/eframe/src/epi/mod.rs rename to crates/eframe/src/epi.rs index fd4f0280f16..5ee29e457fe 100644 --- a/crates/eframe/src/epi/mod.rs +++ b/crates/eframe/src/epi.rs @@ -6,29 +6,24 @@ #![warn(missing_docs)] // Let's keep `epi` well-documented. -#[cfg(not(target_arch = "wasm32"))] -mod icon_data; - -#[cfg(not(target_arch = "wasm32"))] -pub use icon_data::IconData; - #[cfg(target_arch = "wasm32")] use std::any::Any; #[cfg(not(target_arch = "wasm32"))] #[cfg(any(feature = "glow", feature = "wgpu"))] -pub use crate::native::run::UserEvent; +pub use crate::native::winit_integration::UserEvent; #[cfg(not(target_arch = "wasm32"))] use raw_window_handle::{ - HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, + DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, + RawWindowHandle, WindowHandle, }; #[cfg(not(target_arch = "wasm32"))] use static_assertions::assert_not_impl_any; #[cfg(not(target_arch = "wasm32"))] #[cfg(any(feature = "glow", feature = "wgpu"))] -pub use winit::event_loop::EventLoopBuilder; +pub use winit::{event_loop::EventLoopBuilder, window::WindowBuilder}; /// Hook into the building of an event loop before it is run /// @@ -38,6 +33,14 @@ pub use winit::event_loop::EventLoopBuilder; #[cfg(any(feature = "glow", feature = "wgpu"))] pub type EventLoopBuilderHook = Box)>; +/// Hook into the building of a the native window. +/// +/// You can configure any platform specific details required on top of the default configuration +/// done by `eframe`. +#[cfg(not(target_arch = "wasm32"))] +#[cfg(any(feature = "glow", feature = "wgpu"))] +pub type WindowBuilderHook = Box egui::ViewportBuilder>; + /// This is how your app is created. /// /// You can use the [`CreationContext`] to setup egui, restore state, setup OpenGL things, etc. @@ -64,6 +67,10 @@ pub struct CreationContext<'s> { #[cfg(feature = "glow")] pub gl: Option>, + /// The `get_proc_address` wrapper of underlying GL context + #[cfg(feature = "glow")] + pub get_proc_address: Option<&'s dyn Fn(&std::ffi::CStr) -> *const std::ffi::c_void>, + /// The underlying WGPU render state. /// /// Only available when compiling with the `wgpu` feature and using [`Renderer::Wgpu`]. @@ -74,30 +81,28 @@ pub struct CreationContext<'s> { /// Raw platform window handle #[cfg(not(target_arch = "wasm32"))] - pub(crate) raw_window_handle: RawWindowHandle, + pub(crate) raw_window_handle: Result, /// Raw platform display handle for window #[cfg(not(target_arch = "wasm32"))] - pub(crate) raw_display_handle: RawDisplayHandle, + pub(crate) raw_display_handle: Result, } -// Implementing `Clone` would violate the guarantees of `HasRawWindowHandle` and `HasRawDisplayHandle`. -#[cfg(not(target_arch = "wasm32"))] -assert_not_impl_any!(CreationContext<'_>: Clone); - #[allow(unsafe_code)] #[cfg(not(target_arch = "wasm32"))] -unsafe impl HasRawWindowHandle for CreationContext<'_> { - fn raw_window_handle(&self) -> RawWindowHandle { - self.raw_window_handle +impl HasWindowHandle for CreationContext<'_> { + fn window_handle(&self) -> Result, HandleError> { + // Safety: the lifetime is correct. + unsafe { Ok(WindowHandle::borrow_raw(self.raw_window_handle.clone()?)) } } } #[allow(unsafe_code)] #[cfg(not(target_arch = "wasm32"))] -unsafe impl HasRawDisplayHandle for CreationContext<'_> { - fn raw_display_handle(&self) -> RawDisplayHandle { - self.raw_display_handle +impl HasDisplayHandle for CreationContext<'_> { + fn display_handle(&self) -> Result, HandleError> { + // Safety: the lifetime is correct. + unsafe { Ok(DisplayHandle::borrow_raw(self.raw_display_handle.clone()?)) } } } @@ -112,13 +117,17 @@ pub trait App { /// The [`egui::Context`] can be cloned and saved if you like. /// /// To force a repaint, call [`egui::Context::request_repaint`] at any time (e.g. from another thread). + /// + /// This is called for the root viewport ([`egui::ViewportId::ROOT`]). + /// Use [`egui::Context::show_viewport_deferred`] to spawn additional viewports (windows). + /// (A "viewport" in egui means an native OS window). fn update(&mut self, ctx: &egui::Context, frame: &mut Frame); /// Get a handle to the app. /// /// Can be used from web to interact or other external context. /// - /// You need to implement this if you want to be able to access the application from JS using [`crate::web::backend::AppRunner`]. + /// You need to implement this if you want to be able to access the application from JS using [`crate::WebRunner::app_mut`]. /// /// This is needed because downcasting `Box` -> `Box` to get &`ConcreteApp` is not simple in current rust. /// @@ -139,34 +148,14 @@ pub trait App { /// Only called when the "persistence" feature is enabled. /// /// On web the state is stored to "Local Storage". - /// On native the path is picked using [`directories_next::ProjectDirs::data_dir`](https://docs.rs/directories-next/2.0.0/directories_next/struct.ProjectDirs.html#method.data_dir) which is: - /// * Linux: `/home/UserName/.local/share/APP_ID` - /// * macOS: `/Users/UserName/Library/Application Support/APP_ID` - /// * Windows: `C:\Users\UserName\AppData\Roaming\APP_ID` /// - /// where `APP_ID` is determined by either [`NativeOptions::app_id`] or - /// the title argument to [`crate::run_native`]. + /// On native the path is picked using [`crate::storage_dir`]. fn save(&mut self, _storage: &mut dyn Storage) {} - /// Called when the user attempts to close the desktop window and/or quit the application. - /// - /// By returning `false` the closing will be aborted. To continue the closing return `true`. - /// - /// A scenario where this method will be run is after pressing the close button on a native - /// window, which allows you to ask the user whether they want to do something before exiting. - /// See the example at for practical usage. - /// - /// It will _not_ be called on the web or when the window is forcefully closed. - #[cfg(not(target_arch = "wasm32"))] - #[doc(alias = "exit")] - #[doc(alias = "quit")] - fn on_close_event(&mut self) -> bool { - true - } - /// Called once on shutdown, after [`Self::save`]. /// - /// If you need to abort an exit use [`Self::on_close_event`]. + /// If you need to abort an exit check `ctx.input(|i| i.viewport().close_requested())` + /// and respond with [`egui::ViewportCommand::CancelClose`]. /// /// To get a [`glow`] context you need to compile with the `glow` feature flag, /// and run eframe with the glow backend. @@ -187,13 +176,6 @@ pub trait App { std::time::Duration::from_secs(30) } - /// The size limit of the web app canvas. - /// - /// By default the max size is [`egui::Vec2::INFINITY`], i.e. unlimited. - fn max_size_points(&self) -> egui::Vec2 { - egui::Vec2::INFINITY - } - /// Background color values for the app, e.g. what is sent to `gl.clearColor`. /// /// This is the background of your windows if you don't set a central panel. @@ -213,34 +195,29 @@ pub trait App { // _visuals.window_fill() would also be a natural choice } - /// Controls whether or not the native window position and size will be - /// persisted (only if the "persistence" feature is enabled). - fn persist_native_window(&self) -> bool { - true - } - /// Controls whether or not the egui memory (window positions etc) will be /// persisted (only if the "persistence" feature is enabled). fn persist_egui_memory(&self) -> bool { true } - /// If `true` a warm-up call to [`Self::update`] will be issued where - /// `ctx.memory(|mem| mem.everything_is_visible())` will be set to `true`. + /// A hook for manipulating or filtering raw input before it is processed by [`Self::update`]. /// - /// This can help pre-caching resources loaded by different parts of the UI, preventing stutter later on. + /// This function provides a way to modify or filter input events before they are processed by egui. /// - /// In this warm-up call, all painted shapes will be ignored. + /// It can be used to prevent specific keyboard shortcuts or mouse events from being processed by egui. /// - /// The default is `false`, and it is unlikely you will want to change this. - fn warm_up_enabled(&self) -> bool { - false - } - - /// Called each time after the rendering the UI. + /// Additionally, it can be used to inject custom keyboard or mouse events into the input stream, which can be useful for implementing features like a virtual keyboard. + /// + /// # Arguments + /// + /// * `_ctx` - The context of the egui, which provides access to the current state of the egui. + /// * `_raw_input` - The raw input events that are about to be processed. This can be modified to change the input that egui processes. /// - /// Can be used to access pixel data with [`Frame::screenshot`] - fn post_rendering(&mut self, _window_size_px: [u32; 2], _frame: &Frame) {} + /// # Note + /// + /// This function does not return a value. Any changes to the input should be made directly to `_raw_input`. + fn raw_input_hook(&mut self, _ctx: &egui::Context, _raw_input: &mut egui::RawInput) {} } /// Selects the level of hardware graphics acceleration. @@ -261,73 +238,26 @@ pub enum HardwareAcceleration { /// Options controlling the behavior of a native window. /// -/// Only a single native window is currently supported. +/// Additional windows can be opened using (egui viewports)[`egui::viewport`]. +/// +/// Set the window title and size using [`Self::viewport`]. +/// +/// ### Application id +/// [`egui::ViewportBuilder::with_app_id`] is used for determining the folder to persist the app to. +/// +/// On native the path is picked using [`crate::storage_dir`]. +/// +/// If you don't set an app id, the title argument to [`crate::run_native`] +/// will be used as app id instead. #[cfg(not(target_arch = "wasm32"))] pub struct NativeOptions { - /// Sets whether or not the window will always be on top of other windows at initialization. - pub always_on_top: bool, - - /// Show window in maximized mode - pub maximized: bool, - - /// On desktop: add window decorations (i.e. a frame around your app)? - /// If false it will be difficult to move and resize the app. - pub decorated: bool, - - /// Start in (borderless) fullscreen? - /// - /// Default: `false`. - pub fullscreen: bool, - - /// On Mac: the window doesn't have a titlebar, but floating window buttons. - /// - /// See [winit's documentation][with_fullsize_content_view] for information on Mac-specific options. - /// - /// [with_fullsize_content_view]: https://docs.rs/winit/latest/x86_64-apple-darwin/winit/platform/macos/trait.WindowBuilderExtMacOS.html#tymethod.with_fullsize_content_view - #[cfg(target_os = "macos")] - pub fullsize_content: bool, - - /// On Windows: enable drag and drop support. Drag and drop can - /// not be disabled on other platforms. - /// - /// See [winit's documentation][drag_and_drop] for information on why you - /// might want to disable this on windows. + /// Controls the native window of the root viewport. /// - /// [drag_and_drop]: https://docs.rs/winit/latest/x86_64-pc-windows-msvc/winit/platform/windows/trait.WindowBuilderExtWindows.html#tymethod.with_drag_and_drop - pub drag_and_drop_support: bool, - - /// The application icon, e.g. in the Windows task bar or the alt-tab menu. + /// This is where you set things like window title and size. /// - /// The default icon is a white `e` on a black background (for "egui" or "eframe"). - /// If you prefer the OS default, set this to `None`. - pub icon_data: Option, - - /// The initial (inner) position of the native window in points (logical pixels). - pub initial_window_pos: Option, - - /// The initial inner size of the native window in points (logical pixels). - pub initial_window_size: Option, - - /// The minimum inner window size in points (logical pixels). - pub min_window_size: Option, - - /// The maximum inner window size in points (logical pixels). - pub max_window_size: Option, - - /// Should the app window be resizable? - pub resizable: bool, - - /// On desktop: make the window transparent. - /// You control the transparency with [`App::clear_color()`]. - /// You should avoid having a [`egui::CentralPanel`], or make sure its frame is also transparent. - pub transparent: bool, - - /// On desktop: mouse clicks pass through the window, used for non-interactable overlays - /// Generally you would use this in conjunction with always_on_top - pub mouse_passthrough: bool, - - /// Whether grant focus when window initially opened. True by default. - pub active: bool, + /// If you don't set an icon, a default egui icon will be used. + /// To avoid this, set the icon to [`egui::IconData::default`]. + pub viewport: egui::ViewportBuilder, /// Turn on vertical syncing, limiting the FPS to the display refresh rate. /// @@ -389,7 +319,7 @@ pub struct NativeOptions { /// /// This feature was introduced in . /// - /// When `true`, [`winit::platform::run_return::EventLoopExtRunReturn::run_return`] is used. + /// When `true`, [`winit::platform::run_on_demand::EventLoopExtRunOnDemand`] is used. /// When `false`, [`winit::event_loop::EventLoop::run`] is used. pub run_and_return: bool, @@ -402,6 +332,15 @@ pub struct NativeOptions { #[cfg(any(feature = "glow", feature = "wgpu"))] pub event_loop_builder: Option, + /// Hook into the building of a window. + /// + /// Specify a callback here in case you need to make platform specific changes to the + /// window appearance. + /// + /// Note: A [`NativeOptions`] clone will not include any `window_builder` hook. + #[cfg(any(feature = "glow", feature = "wgpu"))] + pub window_builder: Option, + #[cfg(feature = "glow")] /// Needed for cross compiling for VirtualBox VMSVGA driver with OpenGL ES 2.0 and OpenGL 2.1 which doesn't support SRGB texture. /// See . @@ -420,65 +359,26 @@ pub struct NativeOptions { #[cfg(feature = "wgpu")] pub wgpu_options: egui_wgpu::WgpuConfiguration, - /// The application id, used for determining the folder to persist the app to. - /// - /// On native the path is picked using [`directories_next::ProjectDirs::data_dir`](https://docs.rs/directories-next/2.0.0/directories_next/struct.ProjectDirs.html#method.data_dir) which is: - /// * Linux: `/home/UserName/.local/share/APP_ID` - /// * macOS: `/Users/UserName/Library/Application Support/APP_ID` - /// * Windows: `C:\Users\UserName\AppData\Roaming\APP_ID` - /// - /// If you don't set [`Self::app_id`], the title argument to [`crate::run_native`] - /// will be used instead. - /// - /// ### On Wayland - /// On Wayland this sets the Application ID for the window. - /// - /// The application ID is used in several places of the compositor, e.g. for - /// grouping windows of the same application. It is also important for - /// connecting the configuration of a `.desktop` file with the window, by - /// using the application ID as file name. This allows e.g. a proper icon - /// handling under Wayland. - /// - /// See [Waylands XDG shell documentation][xdg-shell] for more information - /// on this Wayland-specific option. - /// - /// [xdg-shell]: https://wayland.app/protocols/xdg-shell#xdg_toplevel:request:set_app_id - /// - /// # Example - /// ``` no_run - /// fn main() -> eframe::Result<()> { - /// - /// let mut options = eframe::NativeOptions::default(); - /// // Set the application ID for Wayland only on Linux - /// #[cfg(target_os = "linux")] - /// { - /// options.app_id = Some("egui-example".to_string()); - /// } - /// - /// eframe::run_simple_native("My egui App", options, move |ctx, _frame| { - /// egui::CentralPanel::default().show(ctx, |ui| { - /// ui.heading("My egui Application"); - /// }); - /// }) - /// } - /// ``` - pub app_id: Option, + /// Controls whether or not the native window position and size will be + /// persisted (only if the "persistence" feature is enabled). + pub persist_window: bool, } #[cfg(not(target_arch = "wasm32"))] impl Clone for NativeOptions { fn clone(&self) -> Self { Self { - icon_data: self.icon_data.clone(), + viewport: self.viewport.clone(), #[cfg(any(feature = "glow", feature = "wgpu"))] event_loop_builder: None, // Skip any builder callbacks if cloning + #[cfg(any(feature = "glow", feature = "wgpu"))] + window_builder: None, // Skip any builder callbacks if cloning + #[cfg(feature = "wgpu")] wgpu_options: self.wgpu_options.clone(), - app_id: self.app_id.clone(), - ..*self } } @@ -488,29 +388,7 @@ impl Clone for NativeOptions { impl Default for NativeOptions { fn default() -> Self { Self { - always_on_top: false, - maximized: false, - decorated: true, - fullscreen: false, - - #[cfg(target_os = "macos")] - fullsize_content: false, - - // We set a default "egui" or "eframe" icon, which is usually more distinctive than the default OS icon. - icon_data: Some( - IconData::try_from_png_bytes(&include_bytes!("../../data/icon.png")[..]).unwrap(), - ), - - drag_and_drop_support: true, - initial_window_pos: None, - initial_window_size: None, - min_window_size: None, - max_window_size: None, - resizable: true, - transparent: false, - mouse_passthrough: false, - - active: true, + viewport: Default::default(), vsync: true, multisampling: 0, @@ -528,6 +406,9 @@ impl Default for NativeOptions { #[cfg(any(feature = "glow", feature = "wgpu"))] event_loop_builder: None, + #[cfg(any(feature = "glow", feature = "wgpu"))] + window_builder: None, + #[cfg(feature = "glow")] shader_version: None, @@ -536,7 +417,7 @@ impl Default for NativeOptions { #[cfg(feature = "wgpu")] wgpu_options: egui_wgpu::WgpuConfiguration::default(), - app_id: None, + persist_window: true, } } } @@ -574,6 +455,11 @@ pub struct WebOptions { /// Configures wgpu instance/device/adapter/surface creation and renderloop. #[cfg(feature = "wgpu")] pub wgpu_options: egui_wgpu::WgpuConfiguration, + + /// The size limit of the web app canvas. + /// + /// By default the max size is [`egui::Vec2::INFINITY`], i.e. unlimited. + pub max_size_points: egui::Vec2, } #[cfg(target_arch = "wasm32")] @@ -589,6 +475,8 @@ impl Default for WebOptions { #[cfg(feature = "wgpu")] wgpu_options: egui_wgpu::WgpuConfiguration::default(), + + max_size_points: egui::Vec2::INFINITY, } } } @@ -659,16 +547,23 @@ pub enum Renderer { #[cfg(any(feature = "glow", feature = "wgpu"))] impl Default for Renderer { fn default() -> Self { + #[cfg(not(feature = "glow"))] + #[cfg(not(feature = "wgpu"))] + compile_error!("eframe: you must enable at least one of the rendering backend features: 'glow' or 'wgpu'"); + #[cfg(feature = "glow")] + #[cfg(not(feature = "wgpu"))] return Self::Glow; #[cfg(not(feature = "glow"))] #[cfg(feature = "wgpu")] return Self::Wgpu; - #[cfg(not(feature = "glow"))] - #[cfg(not(feature = "wgpu"))] - compile_error!("eframe: you must enable at least one of the rendering backend features: 'glow' or 'wgpu'"); + // By default, only the `glow` feature is enabled, so if the user added `wgpu` to the feature list + // they probably wanted to use wgpu: + #[cfg(feature = "glow")] + #[cfg(feature = "wgpu")] + return Self::Wgpu; } } @@ -707,14 +602,11 @@ impl std::str::FromStr for Renderer { /// Represents the surroundings of your app. /// /// It provides methods to inspect the surroundings (are we on the web?), -/// allocate textures, and change settings (e.g. window size). +/// access to persistent storage, and access to the rendering backend. pub struct Frame { /// Information about the integration. pub(crate) info: IntegrationInfo, - /// Where the app can issue commands back to the integration. - pub(crate) output: backend::AppOutput, - /// A place where you can store custom data in a way that persists when you restart the app. pub(crate) storage: Option>, @@ -726,37 +618,34 @@ pub struct Frame { #[cfg(feature = "wgpu")] pub(crate) wgpu_render_state: Option, - /// If [`Frame::request_screenshot`] was called during a frame, this field will store the screenshot - /// such that it can be retrieved during [`App::post_rendering`] with [`Frame::screenshot`] - #[cfg(not(target_arch = "wasm32"))] - pub(crate) screenshot: std::cell::Cell>, - /// Raw platform window handle #[cfg(not(target_arch = "wasm32"))] - pub(crate) raw_window_handle: RawWindowHandle, + pub(crate) raw_window_handle: Result, /// Raw platform display handle for window #[cfg(not(target_arch = "wasm32"))] - pub(crate) raw_display_handle: RawDisplayHandle, + pub(crate) raw_display_handle: Result, } -// Implementing `Clone` would violate the guarantees of `HasRawWindowHandle` and `HasRawDisplayHandle`. +// Implementing `Clone` would violate the guarantees of `HasWindowHandle` and `HasDisplayHandle`. #[cfg(not(target_arch = "wasm32"))] assert_not_impl_any!(Frame: Clone); #[allow(unsafe_code)] #[cfg(not(target_arch = "wasm32"))] -unsafe impl HasRawWindowHandle for Frame { - fn raw_window_handle(&self) -> RawWindowHandle { - self.raw_window_handle +impl HasWindowHandle for Frame { + fn window_handle(&self) -> Result, HandleError> { + // Safety: the lifetime is correct. + unsafe { Ok(WindowHandle::borrow_raw(self.raw_window_handle.clone()?)) } } } #[allow(unsafe_code)] #[cfg(not(target_arch = "wasm32"))] -unsafe impl HasRawDisplayHandle for Frame { - fn raw_display_handle(&self) -> RawDisplayHandle { - self.raw_display_handle +impl HasDisplayHandle for Frame { + fn display_handle(&self) -> Result, HandleError> { + // Safety: the lifetime is correct. + unsafe { Ok(DisplayHandle::borrow_raw(self.raw_display_handle.clone()?)) } } } @@ -770,8 +659,8 @@ impl Frame { } /// Information about the integration. - pub fn info(&self) -> IntegrationInfo { - self.info.clone() + pub fn info(&self) -> &IntegrationInfo { + &self.info } /// A place where you can store custom data in a way that persists when you restart the app. @@ -779,67 +668,6 @@ impl Frame { self.storage.as_deref() } - /// Request the current frame's pixel data. Needs to be retrieved by calling [`Frame::screenshot`] - /// during [`App::post_rendering`]. - #[cfg(not(target_arch = "wasm32"))] - pub fn request_screenshot(&mut self) { - self.output.screenshot_requested = true; - } - - /// Cancel a request made with [`Frame::request_screenshot`]. - #[cfg(not(target_arch = "wasm32"))] - pub fn cancel_screenshot_request(&mut self) { - self.output.screenshot_requested = false; - } - - /// During [`App::post_rendering`], use this to retrieve the pixel data that was requested during - /// [`App::update`] via [`Frame::request_screenshot`]. - /// - /// Returns None if: - /// * Called in [`App::update`] - /// * [`Frame::request_screenshot`] wasn't called on this frame during [`App::update`] - /// * The rendering backend doesn't support this feature (yet). Currently implemented for wgpu and glow, but not with wasm as target. - /// * Wgpu's GL target is active (not yet supported) - /// * Retrieving the data was unsuccessful in some way. - /// - /// See also [`egui::ColorImage::region`] - /// - /// ## Example generating a capture of everything within a square of 100 pixels located at the top left of the app and saving it with the [`image`](crates.io/crates/image) crate: - /// ``` - /// struct MyApp; - /// - /// impl eframe::App for MyApp { - /// fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { - /// // In real code the app would render something here - /// frame.request_screenshot(); - /// // Things that are added to the frame after the call to - /// // request_screenshot() will still be included. - /// } - /// - /// fn post_rendering(&mut self, _window_size: [u32; 2], frame: &eframe::Frame) { - /// if let Some(screenshot) = frame.screenshot() { - /// let pixels_per_point = frame.info().native_pixels_per_point; - /// let region = egui::Rect::from_two_pos( - /// egui::Pos2::ZERO, - /// egui::Pos2{ x: 100., y: 100. }, - /// ); - /// let top_left_corner = screenshot.region(®ion, pixels_per_point); - /// image::save_buffer( - /// "top_left.png", - /// top_left_corner.as_raw(), - /// top_left_corner.width() as u32, - /// top_left_corner.height() as u32, - /// image::ColorType::Rgba8, - /// ).unwrap(); - /// } - /// } - /// } - /// ``` - #[cfg(not(target_arch = "wasm32"))] - pub fn screenshot(&self) -> Option { - self.screenshot.take() - } - /// A place where you can store custom data in a way that persists when you restart the app. pub fn storage_mut(&mut self) -> Option<&mut (dyn Storage + 'static)> { self.storage.as_deref_mut() @@ -871,141 +699,6 @@ impl Frame { pub fn wgpu_render_state(&self) -> Option<&egui_wgpu::RenderState> { self.wgpu_render_state.as_ref() } - - /// Tell `eframe` to close the desktop window. - /// - /// The window will not close immediately, but at the end of the this frame. - /// - /// Calling this will likely result in the app quitting, unless - /// you have more code after the call to [`crate::run_native`]. - #[cfg(not(target_arch = "wasm32"))] - #[doc(alias = "exit")] - #[doc(alias = "quit")] - pub fn close(&mut self) { - log::debug!("eframe::Frame::close called"); - self.output.close = true; - } - - /// Minimize or unminimize window. (native only) - #[cfg(not(target_arch = "wasm32"))] - pub fn set_minimized(&mut self, minimized: bool) { - self.output.minimized = Some(minimized); - } - - /// Bring the window into focus (native only). Has no effect on Wayland, or if the window is minimized or invisible. - /// - /// This method puts the window on top of other applications and takes input focus away from them, - /// which, if unexpected, will disturb the user. - #[cfg(not(target_arch = "wasm32"))] - pub fn focus(&mut self) { - self.output.focus = Some(true); - } - - /// If the window is unfocused, attract the user's attention (native only). - /// - /// Typically, this means that the window will flash on the taskbar, or bounce, until it is interacted with. - /// - /// When the window comes into focus, or if `None` is passed, the attention request will be automatically reset. - /// - /// See [winit's documentation][user_attention_details] for platform-specific effect details. - /// - /// [user_attention_details]: https://docs.rs/winit/latest/winit/window/enum.UserAttentionType.html - #[cfg(not(target_arch = "wasm32"))] - pub fn request_user_attention(&mut self, kind: egui::UserAttentionType) { - self.output.attention = Some(kind); - } - - /// Maximize or unmaximize window. (native only) - #[cfg(not(target_arch = "wasm32"))] - pub fn set_maximized(&mut self, maximized: bool) { - self.output.maximized = Some(maximized); - } - - /// Tell `eframe` to close the desktop window. - #[cfg(not(target_arch = "wasm32"))] - #[deprecated = "Renamed `close`"] - pub fn quit(&mut self) { - self.close(); - } - - /// Set the desired inner size of the window (in egui points). - #[cfg(not(target_arch = "wasm32"))] - pub fn set_window_size(&mut self, size: egui::Vec2) { - self.output.window_size = Some(size); - self.info.window_info.size = size; // so that subsequent calls see the updated value - } - - /// Set the desired title of the window. - #[cfg(not(target_arch = "wasm32"))] - pub fn set_window_title(&mut self, title: &str) { - self.output.window_title = Some(title.to_owned()); - } - - /// Set whether to show window decorations (i.e. a frame around you app). - /// - /// If false it will be difficult to move and resize the app. - #[cfg(not(target_arch = "wasm32"))] - pub fn set_decorations(&mut self, decorated: bool) { - self.output.decorated = Some(decorated); - } - - /// Turn borderless fullscreen on/off (native only). - #[cfg(not(target_arch = "wasm32"))] - pub fn set_fullscreen(&mut self, fullscreen: bool) { - self.output.fullscreen = Some(fullscreen); - self.info.window_info.fullscreen = fullscreen; // so that subsequent calls see the updated value - } - - /// set the position of the outer window. - #[cfg(not(target_arch = "wasm32"))] - pub fn set_window_pos(&mut self, pos: egui::Pos2) { - self.output.window_pos = Some(pos); - self.info.window_info.position = Some(pos); // so that subsequent calls see the updated value - } - - /// When called, the native window will follow the - /// movement of the cursor while the primary mouse button is down. - /// - /// Does not work on the web. - #[cfg(not(target_arch = "wasm32"))] - pub fn drag_window(&mut self) { - self.output.drag_window = true; - } - - /// Set the visibility of the window. - #[cfg(not(target_arch = "wasm32"))] - pub fn set_visible(&mut self, visible: bool) { - self.output.visible = Some(visible); - } - - /// On desktop: Set the window always on top. - /// - /// (Wayland desktop currently not supported) - #[cfg(not(target_arch = "wasm32"))] - pub fn set_always_on_top(&mut self, always_on_top: bool) { - self.output.always_on_top = Some(always_on_top); - } - - /// On desktop: Set the window to be centered. - /// - /// (Wayland desktop currently not supported) - #[cfg(not(target_arch = "wasm32"))] - pub fn set_centered(&mut self) { - if let Some(monitor_size) = self.info.window_info.monitor_size { - let inner_size = self.info.window_info.size; - if monitor_size.x > 1.0 && monitor_size.y > 1.0 { - let x = (monitor_size.x - inner_size.x) / 2.0; - let y = (monitor_size.y - inner_size.y) / 2.0; - self.set_window_pos(egui::Pos2 { x, y }); - } - } - } - - /// for integrations only: call once per frame - #[cfg(any(feature = "glow", feature = "wgpu"))] - pub(crate) fn take_app_output(&mut self) -> backend::AppOutput { - std::mem::take(&mut self.output) - } } /// Information about the web environment (if applicable). @@ -1019,45 +712,13 @@ pub struct WebInfo { pub location: Location, } -/// Information about the application's main window, if available. -#[cfg(not(target_arch = "wasm32"))] -#[derive(Clone, Debug)] -pub struct WindowInfo { - /// Coordinates of the window's outer top left corner, relative to the top left corner of the first display. - /// - /// Unit: egui points (logical pixels). - /// - /// `None` = unknown. - pub position: Option, - - /// Are we in fullscreen mode? - pub fullscreen: bool, - - /// Are we minimized? - pub minimized: bool, - - /// Are we maximized? - pub maximized: bool, - - /// Is the window focused and able to receive input? - /// - /// This should be the same as [`egui::InputState::focused`]. - pub focused: bool, - - /// Window inner size in egui points (logical pixels). - pub size: egui::Vec2, - - /// Current monitor size in egui points (logical pixels) - pub monitor_size: Option, -} - /// Information about the URL. /// /// Everything has been percent decoded (`%20` -> ` ` etc). #[cfg(target_arch = "wasm32")] #[derive(Clone, Debug)] pub struct Location { - /// The full URL (`location.href`) without the hash. + /// The full URL (`location.href`) without the hash, percent-decoded. /// /// Example: `"http://www.example.com:80/index.html?foo=bar"`. pub url: String, @@ -1097,8 +758,8 @@ pub struct Location { /// The parsed "query" part of "www.example.com/index.html?query#fragment". /// - /// "foo=42&bar%20" is parsed as `{"foo": "42", "bar ": ""}` - pub query_map: std::collections::BTreeMap, + /// "foo=hello&bar%20&foo=world" is parsed as `{"bar ": [""], "foo": ["hello", "world"]}` + pub query_map: std::collections::BTreeMap>, /// `location.origin` /// @@ -1118,16 +779,15 @@ pub struct IntegrationInfo { /// `None` means "don't know". pub system_theme: Option, - /// Seconds of cpu usage (in seconds) of UI code on the previous frame. + /// Seconds of cpu usage (in seconds) on the previous frame. + /// + /// 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`. + /// /// `None` if this is the first frame. pub cpu_usage: Option, - - /// The OS native pixels-per-point - pub native_pixels_per_point: Option, - - /// The position and size of the native window. - #[cfg(not(target_arch = "wasm32"))] - pub window_info: WindowInfo, } // ---------------------------------------------------------------------------- @@ -1166,12 +826,14 @@ 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); storage .get_string(key) .and_then(|value| match ron::from_str(&value) { Ok(value) => Some(value), Err(err) => { - log::warn!("Failed to decode RON: {err}"); + // This happens on when we break the format, e.g. when updating egui. + log::debug!("Failed to decode RON: {err}"); None } }) @@ -1180,6 +842,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); 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), @@ -1188,68 +851,3 @@ pub fn set_value(storage: &mut dyn Storage, key: &str, valu /// [`Storage`] key used for app pub const APP_KEY: &str = "app"; - -// ---------------------------------------------------------------------------- - -/// You only need to look here if you are writing a backend for `epi`. -pub(crate) mod backend { - /// Action that can be taken by the user app. - #[derive(Clone, Debug, Default)] - #[must_use] - pub struct AppOutput { - /// Set to `true` to close the native window (which often quits the app). - #[cfg(not(target_arch = "wasm32"))] - pub close: bool, - - /// Set to some size to resize the outer window (e.g. glium window) to this size. - #[cfg(not(target_arch = "wasm32"))] - pub window_size: Option, - - /// Set to some string to rename the outer window (e.g. glium window) to this title. - #[cfg(not(target_arch = "wasm32"))] - pub window_title: Option, - - /// Set to some bool to change window decorations. - #[cfg(not(target_arch = "wasm32"))] - pub decorated: Option, - - /// Set to some bool to change window fullscreen. - #[cfg(not(target_arch = "wasm32"))] // TODO: implement fullscreen on web - pub fullscreen: Option, - - /// Set to true to drag window while primary mouse button is down. - #[cfg(not(target_arch = "wasm32"))] - pub drag_window: bool, - - /// Set to some position to move the outer window (e.g. glium window) to this position - #[cfg(not(target_arch = "wasm32"))] - pub window_pos: Option, - - /// Set to some bool to change window visibility. - #[cfg(not(target_arch = "wasm32"))] - pub visible: Option, - - /// Set to some bool to tell the window always on top. - #[cfg(not(target_arch = "wasm32"))] - pub always_on_top: Option, - - /// Set to some bool to minimize or unminimize window. - #[cfg(not(target_arch = "wasm32"))] - pub minimized: Option, - - /// Set to some bool to maximize or unmaximize window. - #[cfg(not(target_arch = "wasm32"))] - pub maximized: Option, - - /// Set to some bool to focus window. - #[cfg(not(target_arch = "wasm32"))] - pub focus: Option, - - /// Set to request a user's attention to the native window. - #[cfg(not(target_arch = "wasm32"))] - pub attention: Option, - - #[cfg(not(target_arch = "wasm32"))] - pub screenshot_requested: bool, - } -} diff --git a/crates/eframe/src/epi/icon_data.rs b/crates/eframe/src/epi/icon_data.rs deleted file mode 100644 index c62b6a689a1..00000000000 --- a/crates/eframe/src/epi/icon_data.rs +++ /dev/null @@ -1,67 +0,0 @@ -/// Image data for an application icon. -/// -/// Use a square image, e.g. 256x256 pixels. -/// You can use a transparent background. -#[derive(Clone)] -pub struct IconData { - /// RGBA pixels, with separate/unmultiplied alpha. - pub rgba: Vec, - - /// Image width. This should be a multiple of 4. - pub width: u32, - - /// Image height. This should be a multiple of 4. - pub height: u32, -} - -impl IconData { - /// Convert into [`image::RgbaImage`] - /// - /// # Errors - /// If this is not a valid png. - pub fn try_from_png_bytes(png_bytes: &[u8]) -> Result { - crate::profile_function!(); - let image = image::load_from_memory(png_bytes)?; - Ok(Self::from_image(image)) - } - - fn from_image(image: image::DynamicImage) -> Self { - let image = image.into_rgba8(); - Self { - width: image.width(), - height: image.height(), - rgba: image.into_raw(), - } - } - - /// Convert into [`image::RgbaImage`] - /// - /// # Errors - /// If `width*height != 4 * rgba.len()`, or if the image is too big. - pub fn to_image(&self) -> Result { - crate::profile_function!(); - let Self { - rgba, - width, - height, - } = self.clone(); - image::RgbaImage::from_raw(width, height, rgba).ok_or_else(|| "Invalid IconData".to_owned()) - } - - /// Encode as PNG. - /// - /// # Errors - /// The image is invalid, or the PNG encoder failed. - pub fn to_png_bytes(&self) -> Result, String> { - crate::profile_function!(); - let image = self.to_image()?; - let mut png_bytes: Vec = Vec::new(); - image - .write_to( - &mut std::io::Cursor::new(&mut png_bytes), - image::ImageOutputFormat::Png, - ) - .map_err(|err| err.to_string())?; - Ok(png_bytes) - } -} diff --git a/crates/eframe/src/icon_data.rs b/crates/eframe/src/icon_data.rs new file mode 100644 index 00000000000..847228f9ec0 --- /dev/null +++ b/crates/eframe/src/icon_data.rs @@ -0,0 +1,62 @@ +//! Helpers for loading [`egui::IconData`]. + +use egui::IconData; + +/// Helpers for working with [`IconData`]. +pub trait IconDataExt { + /// Convert into [`image::RgbaImage`] + /// + /// # Errors + /// If `width*height != 4 * rgba.len()`, or if the image is too big. + fn to_image(&self) -> Result; + + /// Encode as PNG. + /// + /// # Errors + /// The image is invalid, or the PNG encoder failed. + fn to_png_bytes(&self) -> Result, String>; +} + +/// Load the contents of .png file. +/// +/// # Errors +/// If this is not a valid png. +pub fn from_png_bytes(png_bytes: &[u8]) -> Result { + crate::profile_function!(); + let image = image::load_from_memory(png_bytes)?; + Ok(from_image(image)) +} + +fn from_image(image: image::DynamicImage) -> IconData { + let image = image.into_rgba8(); + IconData { + width: image.width(), + height: image.height(), + rgba: image.into_raw(), + } +} + +impl IconDataExt for IconData { + fn to_image(&self) -> Result { + crate::profile_function!(); + let Self { + rgba, + width, + height, + } = self.clone(); + image::RgbaImage::from_raw(width, height, rgba).ok_or_else(|| "Invalid IconData".to_owned()) + } + + fn to_png_bytes(&self) -> Result, String> { + crate::profile_function!(); + let image = self.to_image()?; + let mut png_bytes: Vec = Vec::new(); + image + .write_to( + &mut std::io::Cursor::new(&mut png_bytes), + image::ImageOutputFormat::Png, + ) + .map_err(|err| err.to_string())?; + Ok(png_bytes) + } +} diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 8093339e8cb..98576358a70 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -9,6 +9,21 @@ //! In short, you implement [`App`] (especially [`App::update`]) and then //! call [`crate::run_native`] from your `main.rs`, and/or use `eframe::WebRunner` from your `lib.rs`. //! +//! ## Compiling for web +//! To get copy-paste working on web, you need to compile with +//! `export RUSTFLAGS=--cfg=web_sys_unstable_apis`. +//! +//! You need to install the `wasm32` target with `rustup target add wasm32-unknown-unknown`. +//! +//! Build the `.wasm` using `cargo build --target wasm32-unknown-unknown` +//! and then use [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) to generate the JavaScript glue code. +//! +//! See the [`eframe_template` repository](https://github.com/emilk/eframe_template/) for more. +//! +//! ## Simplified usage +//! If your app is only for native, and you don't need advanced features like state persistence, +//! then you can use the simpler function [`run_simple_native`]. +//! //! ## Usage, native: //! ``` no_run //! use eframe::egui; @@ -50,7 +65,7 @@ //! #[derive(Clone)] //! #[wasm_bindgen] //! pub struct WebHandle { -//! runner: WebRunner, +//! runner: eframe::WebRunner, //! } //! //! # #[cfg(target_arch = "wasm32")] @@ -64,7 +79,7 @@ //! eframe::WebLogger::init(log::LevelFilter::Debug).ok(); //! //! Self { -//! runner: WebRunner::new(), +//! runner: eframe::WebRunner::new(), //! } //! } //! @@ -82,6 +97,7 @@ //! //! // The following are optional: //! +//! /// Shut down eframe and clean up resources. //! #[wasm_bindgen] //! pub fn destroy(&self) { //! self.runner.destroy(); @@ -113,14 +129,11 @@ //! } //! ``` //! -//! ## Simplified usage -//! If your app is only for native, and you don't need advanced features like state persistence, -//! then you can use the simpler function [`run_simple_native`]. -//! //! ## Feature flags -#![cfg_attr(feature = "document-features", doc = document_features::document_features!())] +#![doc = document_features::document_features!()] //! +#![warn(missing_docs)] // let's keep eframe well-documented #![allow(clippy::needless_doctest_main)] // Re-export all useful libraries: @@ -137,6 +150,8 @@ mod epi; // Re-export everything in `epi` so `eframe` users don't have to care about what `epi` is: pub use epi::*; +pub(crate) mod stopwatch; + // ---------------------------------------------------------------------------- // When compiling for web @@ -159,10 +174,24 @@ pub use web::{WebLogger, WebRunner}; #[cfg(any(feature = "glow", feature = "wgpu"))] mod native; +#[cfg(not(target_arch = "wasm32"))] +#[cfg(any(feature = "glow", feature = "wgpu"))] +#[cfg(feature = "persistence")] +pub use native::file_storage::storage_dir; + +#[cfg(not(target_arch = "wasm32"))] +pub mod icon_data; + /// This is how you start a native (desktop) app. /// -/// The first argument is name of your app, used for the title bar of the native window -/// and the save location of persistence (see [`App::save`]). +/// The first argument is name of your app, which is a an identifier +/// used for the save location of persistence (see [`App::save`]). +/// It is also used as the application id on wayland. +/// If you set no title on the viewport, the app id will be used +/// as the title. +/// +/// For details about application ID conventions, see the +/// [Desktop Entry Spec](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#desktop-file-id) /// /// Call from `fn main` like this: /// ``` no_run @@ -202,17 +231,30 @@ mod native; #[allow(clippy::needless_pass_by_value)] pub fn run_native( app_name: &str, - native_options: NativeOptions, + mut native_options: NativeOptions, app_creator: AppCreator, ) -> Result<()> { - let renderer = native_options.renderer; - #[cfg(not(feature = "__screenshot"))] assert!( std::env::var("EFRAME_SCREENSHOT_TO").is_err(), "EFRAME_SCREENSHOT_TO found without compiling with the '__screenshot' feature" ); + if native_options.viewport.title.is_none() { + native_options.viewport.title = Some(app_name.to_owned()); + } + + let renderer = native_options.renderer; + + #[cfg(all(feature = "glow", feature = "wgpu"))] + { + match renderer { + Renderer::Glow => "glow", + Renderer::Wgpu => "wgpu", + }; + log::info!("Both the glow and wgpu renderers are available. Using {renderer}."); + } + match renderer { #[cfg(feature = "glow")] Renderer::Glow => { @@ -251,7 +293,7 @@ pub fn run_native( /// .labelled_by(name_label.id); /// }); /// ui.add(egui::Slider::new(&mut age, 0..=120).text("age")); -/// if ui.button("Click each year").clicked() { +/// if ui.button("Increment").clicked() { /// age += 1; /// } /// ui.label(format!("Hello '{name}', age {age}")); @@ -273,7 +315,7 @@ pub fn run_simple_native( update_fun: U, } - impl App for SimpleApp { + impl App for SimpleApp { fn update(&mut self, ctx: &egui::Context, frame: &mut Frame) { (self.update_fun)(ctx, frame); } @@ -291,28 +333,42 @@ pub fn run_simple_native( /// The different problems that can occur when trying to run `eframe`. #[derive(thiserror::Error, Debug)] pub enum Error { + /// An error from [`winit`]. #[cfg(not(target_arch = "wasm32"))] #[error("winit error: {0}")] Winit(#[from] winit::error::OsError), + /// An error from [`winit::event_loop::EventLoop`]. + #[cfg(not(target_arch = "wasm32"))] + #[error("winit EventLoopError: {0}")] + WinitEventLoop(#[from] winit::error::EventLoopError), + + /// An error from [`glutin`] when using [`glow`]. #[cfg(all(feature = "glow", not(target_arch = "wasm32")))] #[error("glutin error: {0}")] Glutin(#[from] glutin::error::Error), + /// An error from [`glutin`] when using [`glow`]. #[cfg(all(feature = "glow", not(target_arch = "wasm32")))] - #[error("Found no glutin configs matching the template: {0:?}. error: {1:?}")] + #[error("Found no glutin configs matching the template: {0:?}. Error: {1:?}")] NoGlutinConfigs(glutin::config::ConfigTemplate, Box), + /// An error from [`glutin`] when using [`glow`]. + #[cfg(feature = "glow")] + #[error("egui_glow: {0}")] + OpenGL(#[from] egui_glow::PainterError), + + /// An error from [`wgpu`]. #[cfg(feature = "wgpu")] #[error("WGPU error: {0}")] Wgpu(#[from] egui_wgpu::WgpuError), } -pub type Result = std::result::Result; +/// Short for `Result`. +pub type Result = std::result::Result; // --------------------------------------------------------------------------- -#[cfg(not(target_arch = "wasm32"))] mod profiling_scopes { #![allow(unused_macros)] #![allow(unused_imports)] @@ -321,6 +377,7 @@ mod profiling_scopes { 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)*); }; } @@ -330,11 +387,12 @@ mod profiling_scopes { 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; } -#[cfg(not(target_arch = "wasm32"))] +#[allow(unused_imports)] pub(crate) use profiling_scopes::*; diff --git a/crates/eframe/src/native/app_icon.rs b/crates/eframe/src/native/app_icon.rs index 20a49a6e27c..f169420863c 100644 --- a/crates/eframe/src/native/app_icon.rs +++ b/crates/eframe/src/native/app_icon.rs @@ -2,16 +2,24 @@ //! //! TODO(emilk): port this to [`winit`]. -use crate::IconData; +use std::sync::Arc; + +use egui::IconData; pub struct AppTitleIconSetter { title: String, - icon_data: Option, + icon_data: Option>, status: AppIconStatus, } impl AppTitleIconSetter { - pub fn new(title: String, icon_data: Option) -> Self { + pub fn new(title: String, mut icon_data: Option>) -> Self { + if let Some(icon) = &icon_data { + if **icon == IconData::default() { + icon_data = None; + } + } + Self { title, icon_data, @@ -22,7 +30,7 @@ impl AppTitleIconSetter { /// Call once per frame; we will set the icon when we can. pub fn update(&mut self) { if self.status == AppIconStatus::NotSetTryAgain { - self.status = set_title_and_icon(&self.title, self.icon_data.as_ref()); + self.status = set_title_and_icon(&self.title, self.icon_data.as_deref()); } } } @@ -71,6 +79,7 @@ fn set_title_and_icon(_title: &str, _icon_data: Option<&IconData>) -> AppIconSta #[cfg(target_os = "windows")] #[allow(unsafe_code)] fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus { + use crate::icon_data::IconDataExt as _; use winapi::um::winuser; // We would get fairly far already with winit's `set_window_icon` (which is exposed to eframe) actually! @@ -191,6 +200,9 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus { #[cfg(target_os = "macos")] #[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!(); + use cocoa::{ appkit::{NSApp, NSApplication, NSImage, NSMenu, NSWindow}, base::{id, nil}, @@ -213,6 +225,10 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS // SAFETY: Accessing raw data from icon in a read-only manner. Icon data is static! unsafe { let app = NSApp(); + if app.is_null() { + log::debug!("NSApp is null"); + return AppIconStatus::NotSetIgnored; + } if let Some(png_bytes) = png_bytes { let data = NSData::dataWithBytes_length_( @@ -220,14 +236,27 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS png_bytes.as_ptr().cast::(), png_bytes.len() as u64, ); + + log::trace!("NSImage::initWithData…"); let app_icon = NSImage::initWithData_(NSImage::alloc(nil), data); + + crate::profile_scope!("setApplicationIconImage_"); + log::trace!("setApplicationIconImage…"); app.setApplicationIconImage_(app_icon); } // Change the title in the top bar - for python processes this would be again "python" otherwise. let main_menu = app.mainMenu(); - let app_menu: id = msg_send![main_menu.itemAtIndex_(0), submenu]; - app_menu.setTitle_(NSString::alloc(nil).init_str(title)); + if !main_menu.is_null() { + let item = main_menu.itemAtIndex_(0); + if !item.is_null() { + let app_menu: id = msg_send![item, submenu]; + if !app_menu.is_null() { + crate::profile_scope!("setTitle_"); + app_menu.setTitle_(NSString::alloc(nil).init_str(title)); + } + } + } // The title in the Dock apparently can't be changed. // At least these people didn't figure it out either: diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 725522eb070..f27d011202e 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -1,205 +1,101 @@ -use winit::event_loop::EventLoopWindowTarget; - -#[cfg(target_os = "macos")] -use winit::platform::macos::WindowBuilderExtMacOS as _; - -use raw_window_handle::{HasRawDisplayHandle as _, HasRawWindowHandle as _}; - -#[cfg(feature = "accesskit")] -use egui::accesskit; -use egui::NumExt as _; -#[cfg(feature = "accesskit")] -use egui_winit::accesskit_winit; -use egui_winit::{native_pixels_per_point, EventResponse, WindowSettings}; +//! Common tools used by [`super::glow_integration`] and [`super::wgpu_integration`]. -use crate::{epi, Theme, WindowInfo}; +use web_time::Instant; +use winit::event_loop::EventLoopWindowTarget; -#[derive(Default)] -pub struct WindowState { - // We cannot simply call `winit::Window::is_minimized/is_maximized` - // because that deadlocks on mac. - pub minimized: bool, - pub maximized: bool, -} +use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _}; -pub fn points_to_size(points: egui::Vec2) -> winit::dpi::LogicalSize { - winit::dpi::LogicalSize { - width: points.x as f64, - height: points.y as f64, - } -} +use egui::{DeferredViewportUiCallback, NumExt as _, ViewportBuilder, ViewportId}; +use egui_winit::{EventResponse, WindowSettings}; -pub fn read_window_info( - window: &winit::window::Window, - pixels_per_point: f32, - window_state: &WindowState, -) -> WindowInfo { - let position = window - .outer_position() - .ok() - .map(|pos| pos.to_logical::(pixels_per_point.into())) - .map(|pos| egui::Pos2 { x: pos.x, y: pos.y }); - - let monitor = window.current_monitor().is_some(); - let monitor_size = if monitor { - let size = window - .current_monitor() - .unwrap() - .size() - .to_logical::(pixels_per_point.into()); - Some(egui::vec2(size.width, size.height)) - } else { - None - }; +use crate::{epi, Theme}; - let size = window - .inner_size() - .to_logical::(pixels_per_point.into()); - - // NOTE: calling window.is_minimized() or window.is_maximized() deadlocks on Mac. - - WindowInfo { - position, - fullscreen: window.fullscreen().is_some(), - minimized: window_state.minimized, - maximized: window_state.maximized, - focused: window.has_focus(), - size: egui::Vec2 { - x: size.width, - y: size.height, - }, - monitor_size, - } -} - -pub fn window_builder( +pub fn viewport_builder( + egui_zoom_factor: f32, event_loop: &EventLoopWindowTarget, - title: &str, - native_options: &epi::NativeOptions, + native_options: &mut epi::NativeOptions, window_settings: Option, -) -> winit::window::WindowBuilder { - let epi::NativeOptions { - maximized, - decorated, - fullscreen, - #[cfg(target_os = "macos")] - fullsize_content, - drag_and_drop_support, - icon_data, - initial_window_pos, - initial_window_size, - min_window_size, - max_window_size, - resizable, - transparent, - centered, - active, - .. - } = native_options; - - let window_icon = icon_data.clone().and_then(load_icon); - - let mut window_builder = winit::window::WindowBuilder::new() - .with_title(title) - .with_decorations(*decorated) - .with_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None))) - .with_maximized(*maximized) - .with_resizable(*resizable) - .with_transparent(*transparent) - .with_window_icon(window_icon) - .with_active(*active) - // Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279 - // We must also keep the window hidden until AccessKit is initialized. - .with_visible(false); - - #[cfg(target_os = "macos")] - if *fullsize_content { - window_builder = window_builder - .with_title_hidden(true) - .with_titlebar_transparent(true) - .with_fullsize_content_view(true); - } +) -> ViewportBuilder { + crate::profile_function!(); - #[cfg(all(feature = "wayland", target_os = "linux"))] - { - use winit::platform::wayland::WindowBuilderExtWayland as _; - match &native_options.app_id { - Some(app_id) => window_builder = window_builder.with_name(app_id, ""), - None => window_builder = window_builder.with_name(title, ""), - } - } - - if let Some(min_size) = *min_window_size { - window_builder = window_builder.with_min_inner_size(points_to_size(min_size)); - } - if let Some(max_size) = *max_window_size { - window_builder = window_builder.with_max_inner_size(points_to_size(max_size)); - } - - window_builder = window_builder_drag_and_drop(window_builder, *drag_and_drop_support); + let mut viewport_builder = native_options.viewport.clone(); + // Always use the default window size / position on iOS. Trying to restore the previous position + // causes the window to be shown too small. + #[cfg(not(target_os = "ios"))] let inner_size_points = if let Some(mut window_settings) = window_settings { // Restore pos/size from previous session - window_settings.clamp_size_to_sane_values(largest_monitor_point_size(event_loop)); - window_settings.clamp_position_to_monitors(event_loop); + window_settings + .clamp_size_to_sane_values(largest_monitor_point_size(egui_zoom_factor, event_loop)); + window_settings.clamp_position_to_monitors(egui_zoom_factor, event_loop); - window_builder = window_settings.initialize_window_builder(window_builder); + viewport_builder = window_settings.initialize_viewport_builder(viewport_builder); window_settings.inner_size_points() } else { - if let Some(pos) = *initial_window_pos { - window_builder = window_builder.with_position(winit::dpi::LogicalPosition { - x: pos.x as f64, - y: pos.y as f64, - }); + if let Some(pos) = viewport_builder.position { + viewport_builder = viewport_builder.with_position(pos); } - if let Some(initial_window_size) = *initial_window_size { - let initial_window_size = - initial_window_size.at_most(largest_monitor_point_size(event_loop)); - window_builder = window_builder.with_inner_size(points_to_size(initial_window_size)); + if let Some(initial_window_size) = viewport_builder.inner_size { + let initial_window_size = initial_window_size + .at_most(largest_monitor_point_size(egui_zoom_factor, event_loop)); + viewport_builder = viewport_builder.with_inner_size(initial_window_size); } - *initial_window_size + viewport_builder.inner_size }; - if *centered { + #[cfg(not(target_os = "ios"))] + if native_options.centered { + crate::profile_scope!("center"); if let Some(monitor) = event_loop.available_monitors().next() { - let monitor_size = monitor.size().to_logical::(monitor.scale_factor()); + let monitor_size = monitor + .size() + .to_logical::(egui_zoom_factor as f64 * monitor.scale_factor()); let inner_size = inner_size_points.unwrap_or(egui::Vec2 { x: 800.0, y: 600.0 }); - if monitor_size.width > 0.0 && monitor_size.height > 0.0 { - let x = (monitor_size.width - inner_size.x as f64) / 2.0; - let y = (monitor_size.height - inner_size.y as f64) / 2.0; - window_builder = window_builder.with_position(winit::dpi::LogicalPosition { x, y }); + if 0.0 < monitor_size.width && 0.0 < monitor_size.height { + let x = (monitor_size.width - inner_size.x) / 2.0; + let y = (monitor_size.height - inner_size.y) / 2.0; + viewport_builder = viewport_builder.with_position([x, y]); } } } - window_builder + match std::mem::take(&mut native_options.window_builder) { + Some(hook) => hook(viewport_builder), + None => viewport_builder, + } } -pub fn apply_native_options_to_window( +pub fn apply_window_settings( window: &winit::window::Window, - native_options: &crate::NativeOptions, window_settings: Option, ) { - use winit::window::WindowLevel; - window.set_window_level(if native_options.always_on_top { - WindowLevel::AlwaysOnTop - } else { - WindowLevel::Normal - }); + crate::profile_function!(); if let Some(window_settings) = window_settings { window_settings.initialize_window(window); } } -fn largest_monitor_point_size(event_loop: &EventLoopWindowTarget) -> egui::Vec2 { +fn largest_monitor_point_size( + egui_zoom_factor: f32, + event_loop: &EventLoopWindowTarget, +) -> egui::Vec2 { + crate::profile_function!(); + let mut max_size = egui::Vec2::ZERO; - for monitor in event_loop.available_monitors() { - let size = monitor.size().to_logical::(monitor.scale_factor()); + let available_monitors = { + crate::profile_scope!("available_monitors"); + event_loop.available_monitors() + }; + + for monitor in available_monitors { + let size = monitor + .size() + .to_logical::(egui_zoom_factor as f64 * monitor.scale_factor()); let size = egui::vec2(size.width, size.height); max_size = max_size.max(size); } @@ -211,123 +107,12 @@ fn largest_monitor_point_size(event_loop: &EventLoopWindowTarget) -> egui: } } -fn load_icon(icon_data: epi::IconData) -> Option { - winit::window::Icon::from_rgba(icon_data.rgba, icon_data.width, icon_data.height).ok() -} - -#[cfg(target_os = "windows")] -fn window_builder_drag_and_drop( - window_builder: winit::window::WindowBuilder, - enable: bool, -) -> winit::window::WindowBuilder { - use winit::platform::windows::WindowBuilderExtWindows as _; - window_builder.with_drag_and_drop(enable) -} - -#[cfg(not(target_os = "windows"))] -fn window_builder_drag_and_drop( - window_builder: winit::window::WindowBuilder, - _enable: bool, -) -> winit::window::WindowBuilder { - // drag and drop can only be disabled on windows - window_builder -} - -pub fn handle_app_output( - window: &winit::window::Window, - current_pixels_per_point: f32, - app_output: epi::backend::AppOutput, - window_state: &mut WindowState, -) { - let epi::backend::AppOutput { - close: _, - window_size, - window_title, - decorated, - fullscreen, - drag_window, - window_pos, - visible: _, // handled in post_present - always_on_top, - screenshot_requested: _, // handled by the rendering backend, - minimized, - maximized, - focus, - attention, - } = app_output; - - if let Some(decorated) = decorated { - window.set_decorations(decorated); - } - - if let Some(window_size) = window_size { - window.set_inner_size( - winit::dpi::PhysicalSize { - width: (current_pixels_per_point * window_size.x).round(), - height: (current_pixels_per_point * window_size.y).round(), - } - .to_logical::(native_pixels_per_point(window) as f64), - ); - } - - if let Some(fullscreen) = fullscreen { - window.set_fullscreen(fullscreen.then_some(winit::window::Fullscreen::Borderless(None))); - } - - if let Some(window_title) = window_title { - window.set_title(&window_title); - } - - if let Some(window_pos) = window_pos { - window.set_outer_position(winit::dpi::LogicalPosition { - x: window_pos.x as f64, - y: window_pos.y as f64, - }); - } - - if drag_window { - let _ = window.drag_window(); - } - - if let Some(always_on_top) = always_on_top { - use winit::window::WindowLevel; - window.set_window_level(if always_on_top { - WindowLevel::AlwaysOnTop - } else { - WindowLevel::Normal - }); - } - - if let Some(minimized) = minimized { - window.set_minimized(minimized); - window_state.minimized = minimized; - } - - if let Some(maximized) = maximized { - window.set_maximized(maximized); - window_state.maximized = maximized; - } - - if !window.has_focus() { - if focus == Some(true) { - window.focus_window(); - } else if let Some(attention) = attention { - use winit::window::UserAttentionType; - window.request_user_attention(match attention { - egui::UserAttentionType::Reset => None, - egui::UserAttentionType::Critical => Some(UserAttentionType::Critical), - egui::UserAttentionType::Informational => Some(UserAttentionType::Informational), - }); - } - } -} - // ---------------------------------------------------------------------------- /// For loading/saving app state and/or egui memory to disk. pub fn create_storage(_app_name: &str) -> Option> { #[cfg(feature = "persistence")] - if let Some(storage) = super::file_storage::FileStorage::from_app_name(_app_name) { + if let Some(storage) = super::file_storage::FileStorage::from_app_id(_app_name) { return Some(Box::new(storage)); } None @@ -336,27 +121,31 @@ pub fn create_storage(_app_name: &str) -> Option> { // ---------------------------------------------------------------------------- /// Everything needed to make a winit-based integration for [`epi`]. +/// +/// Only one instance per app (not one per viewport). pub struct EpiIntegration { pub frame: epi::Frame, - last_auto_save: std::time::Instant, + last_auto_save: Instant, + pub beginning: Instant, + is_first_frame: bool, + pub frame_start: Instant, pub egui_ctx: egui::Context, pending_full_output: egui::FullOutput, - egui_winit: egui_winit::State, /// When set, it is time to close the native window. close: bool, can_drag_window: bool, - window_state: WindowState, follow_system_theme: bool, + #[cfg(feature = "persistence")] + persist_window: bool, app_icon_setter: super::app_icon::AppTitleIconSetter, } impl EpiIntegration { #[allow(clippy::too_many_arguments)] - pub fn new( - event_loop: &EventLoopWindowTarget, - max_texture_side: usize, + pub fn new( + egui_ctx: egui::Context, window: &winit::window::Window, system_theme: Option, app_name: &str, @@ -365,90 +154,71 @@ impl EpiIntegration { #[cfg(feature = "glow")] gl: Option>, #[cfg(feature = "wgpu")] wgpu_render_state: Option, ) -> Self { - let egui_ctx = egui::Context::default(); - - let memory = load_egui_memory(storage.as_deref()).unwrap_or_default(); - egui_ctx.memory_mut(|mem| *mem = memory); - - let native_pixels_per_point = window.scale_factor() as f32; - - let window_state = WindowState { - minimized: window.is_minimized().unwrap_or(false), - maximized: window.is_maximized(), - }; - let frame = epi::Frame { info: epi::IntegrationInfo { system_theme, cpu_usage: None, - native_pixels_per_point: Some(native_pixels_per_point), - window_info: read_window_info(window, egui_ctx.pixels_per_point(), &window_state), - }, - output: epi::backend::AppOutput { - visible: Some(true), - ..Default::default() }, storage, #[cfg(feature = "glow")] gl, #[cfg(feature = "wgpu")] wgpu_render_state, - screenshot: std::cell::Cell::new(None), - raw_display_handle: window.raw_display_handle(), - raw_window_handle: window.raw_window_handle(), + raw_display_handle: window.display_handle().map(|h| h.as_raw()), + raw_window_handle: window.window_handle().map(|h| h.as_raw()), }; - let mut egui_winit = egui_winit::State::new(event_loop); - egui_winit.set_max_texture_side(max_texture_side); - egui_winit.set_pixels_per_point(native_pixels_per_point); + let icon = native_options + .viewport + .icon + .clone() + .unwrap_or_else(|| std::sync::Arc::new(load_default_egui_icon())); let app_icon_setter = super::app_icon::AppTitleIconSetter::new( - app_name.to_owned(), - native_options.icon_data.clone(), + native_options + .viewport + .title + .clone() + .unwrap_or_else(|| app_name.to_owned()), + Some(icon), ); Self { frame, - last_auto_save: std::time::Instant::now(), + last_auto_save: Instant::now(), egui_ctx, - egui_winit, pending_full_output: Default::default(), close: false, can_drag_window: false, - window_state, follow_system_theme: native_options.follow_system_theme, + #[cfg(feature = "persistence")] + persist_window: native_options.persist_window, app_icon_setter, + beginning: Instant::now(), + is_first_frame: true, + frame_start: Instant::now(), } } #[cfg(feature = "accesskit")] - pub fn init_accesskit + Send>( - &mut self, + pub fn init_accesskit + Send>( + &self, + egui_winit: &mut egui_winit::State, window: &winit::window::Window, event_loop_proxy: winit::event_loop::EventLoopProxy, ) { - let egui_ctx = self.egui_ctx.clone(); - self.egui_winit - .init_accesskit(window, event_loop_proxy, move || { - // This function is called when an accessibility client - // (e.g. screen reader) makes its first request. If we got here, - // we know that an accessibility tree is actually wanted. - egui_ctx.enable_accesskit(); - // Enqueue a repaint so we'll receive a full tree update soon. - egui_ctx.request_repaint(); - egui_ctx.accesskit_placeholder_tree_update() - }); - } - - pub fn warm_up(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) { crate::profile_function!(); - let saved_memory: egui::Memory = self.egui_ctx.memory(|mem| mem.clone()); - self.egui_ctx - .memory_mut(|mem| mem.set_everything_is_visible(true)); - let full_output = self.update(app, window); - self.pending_full_output.append(full_output); // Handle it next frame - self.egui_ctx.memory_mut(|mem| *mem = saved_memory); // We don't want to remember that windows were huge. - self.egui_ctx.clear_animations(); + + let egui_ctx = self.egui_ctx.clone(); + egui_winit.init_accesskit(window, event_loop_proxy, move || { + // This function is called when an accessibility client + // (e.g. screen reader) makes its first request. If we got here, + // we know that an accessibility tree is actually wanted. + egui_ctx.enable_accesskit(); + // Enqueue a repaint so we'll receive a full tree update soon. + egui_ctx.request_repaint(); + egui_ctx.accesskit_placeholder_tree_update() + }); } /// If `true`, it is time to close the native window. @@ -456,19 +226,17 @@ impl EpiIntegration { self.close } - pub fn on_event( + pub fn on_window_event( &mut self, - app: &mut dyn epi::App, - event: &winit::event::WindowEvent<'_>, + window: &winit::window::Window, + egui_winit: &mut egui_winit::State, + event: &winit::event::WindowEvent, ) -> EventResponse { + crate::profile_function!(egui_winit::short_window_event_description(event)); + use winit::event::{ElementState, MouseButton, WindowEvent}; match event { - WindowEvent::CloseRequested => { - log::debug!("Received WindowEvent::CloseRequested"); - self.close = app.on_close_event(); - log::debug!("App::on_close_event returned {}", self.close); - } WindowEvent::Destroyed => { log::debug!("Received WindowEvent::Destroyed"); self.close = true; @@ -478,9 +246,6 @@ impl EpiIntegration { state: ElementState::Pressed, .. } => self.can_drag_window = true, - WindowEvent::ScaleFactorChanged { scale_factor, .. } => { - self.frame.info.native_pixels_per_point = Some(*scale_factor as _); - } WindowEvent::ThemeChanged(winit_theme) if self.follow_system_theme => { let theme = theme_from_winit_theme(*winit_theme); self.frame.info.system_theme = Some(theme); @@ -489,92 +254,79 @@ impl EpiIntegration { _ => {} } - self.egui_winit.on_event(&self.egui_ctx, event) + egui_winit.on_window_event(window, event) } - #[cfg(feature = "accesskit")] - pub fn on_accesskit_action_request(&mut self, request: accesskit::ActionRequest) { - self.egui_winit.on_accesskit_action_request(request); + pub fn pre_update(&mut self) { + self.app_icon_setter.update(); } + /// Run user code - this can create immediate viewports, so hold no locks over this! + /// + /// If `viewport_ui_cb` is None, we are in the root viewport and will call [`crate::App::update`]. pub fn update( &mut self, app: &mut dyn epi::App, - window: &winit::window::Window, + viewport_ui_cb: Option<&DeferredViewportUiCallback>, + mut raw_input: egui::RawInput, ) -> egui::FullOutput { - let frame_start = std::time::Instant::now(); + raw_input.time = Some(self.beginning.elapsed().as_secs_f64()); - self.app_icon_setter.update(); + let close_requested = raw_input.viewport().close_requested(); - self.frame.info.window_info = - read_window_info(window, self.egui_ctx.pixels_per_point(), &self.window_state); - let raw_input = self.egui_winit.take_egui_input(window); + app.raw_input_hook(&self.egui_ctx, &mut raw_input); - // Run user code: let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { - crate::profile_scope!("App::update"); - app.update(egui_ctx, &mut self.frame); + if let Some(viewport_ui_cb) = viewport_ui_cb { + // Child viewport + crate::profile_scope!("viewport_callback"); + viewport_ui_cb(egui_ctx); + } else { + crate::profile_scope!("App::update"); + app.update(egui_ctx, &mut self.frame); + } }); - self.pending_full_output.append(full_output); - let full_output = std::mem::take(&mut self.pending_full_output); - - { - let mut app_output = self.frame.take_app_output(); - app_output.drag_window &= self.can_drag_window; // Necessary on Windows; see https://github.com/emilk/egui/pull/1108 - self.can_drag_window = false; - if app_output.close { - self.close = app.on_close_event(); - log::debug!("App::on_close_event returned {}", self.close); - } - self.frame.output.visible = app_output.visible; // this is handled by post_present - self.frame.output.screenshot_requested = app_output.screenshot_requested; - if self.frame.output.attention.is_some() { - self.frame.output.attention = None; + let is_root_viewport = viewport_ui_cb.is_none(); + if is_root_viewport && close_requested { + let canceled = full_output.viewport_output[&ViewportId::ROOT] + .commands + .contains(&egui::ViewportCommand::CancelClose); + if canceled { + log::debug!("Closing of root viewport canceled with ViewportCommand::CancelClose"); + } else { + log::debug!("Closing root viewport (ViewportCommand::CancelClose was not sent)"); + self.close = true; } - handle_app_output( - window, - self.egui_ctx.pixels_per_point(), - app_output, - &mut self.window_state, - ); } - let frame_time = frame_start.elapsed().as_secs_f64() as f32; - self.frame.info.cpu_usage = Some(frame_time); - - full_output + self.pending_full_output.append(full_output); + std::mem::take(&mut self.pending_full_output) } - pub fn post_rendering(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) { - let inner_size = window.inner_size(); - let window_size_px = [inner_size.width, inner_size.height]; - - app.post_rendering(window_size_px, &self.frame); + pub fn report_frame_time(&mut self, seconds: f32) { + self.frame.info.cpu_usage = Some(seconds); } - pub fn post_present(&mut self, window: &winit::window::Window) { - if let Some(visible) = self.frame.output.visible.take() { - window.set_visible(visible); + pub fn post_rendering(&mut self, window: &winit::window::Window) { + crate::profile_function!(); + 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); } } - pub fn handle_platform_output( - &mut self, - window: &winit::window::Window, - platform_output: egui::PlatformOutput, - ) { - self.egui_winit - .handle_platform_output(window, &self.egui_ctx, platform_output); - } - // ------------------------------------------------------------------------ // Persistence stuff: - pub fn maybe_autosave(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) { - let now = std::time::Instant::now(); + pub fn maybe_autosave( + &mut self, + app: &mut dyn epi::App, + window: Option<&winit::window::Window>, + ) { + let now = Instant::now(); if now - self.last_auto_save > app.auto_save_interval() { - self.save(app, Some(window)); + self.save(app, window); self.last_auto_save = now; } } @@ -586,12 +338,12 @@ impl EpiIntegration { crate::profile_function!(); if let Some(window) = _window { - if _app.persist_native_window() { + if self.persist_window { crate::profile_scope!("native_window"); epi::set_value( storage, STORAGE_WINDOW_KEY, - &WindowSettings::from_display(window), + &WindowSettings::from_window(self.egui_ctx.zoom_factor(), window), ); } } @@ -611,6 +363,11 @@ impl EpiIntegration { } } +fn load_default_egui_icon() -> egui::IconData { + crate::profile_function!(); + crate::icon_data::from_png_bytes(&include_bytes!("../../data/icon.png")[..]).unwrap() +} + #[cfg(feature = "persistence")] const STORAGE_EGUI_MEMORY_KEY: &str = "egui"; @@ -618,6 +375,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!(); #[cfg(feature = "persistence")] { epi::get_value(_storage?, STORAGE_WINDOW_KEY) @@ -627,6 +385,7 @@ pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option) -> Option { + crate::profile_function!(); #[cfg(feature = "persistence")] { epi::get_value(_storage?, STORAGE_EGUI_MEMORY_KEY) diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index 2f316a8c9e2..51c668abdeb 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -1,8 +1,24 @@ use std::{ collections::HashMap, + io::Write, path::{Path, PathBuf}, }; +/// The folder where `eframe` will store its state. +/// +/// The given `app_id` is either the +/// [`egui::ViewportBuilder::app_id`] of [`crate::NativeOptions::viewport`] +/// or the title argument to [`crate::run_native`]. +/// +/// On native the path is picked using [`directories_next::ProjectDirs::data_dir`](https://docs.rs/directories-next/2.0.0/directories_next/struct.ProjectDirs.html#method.data_dir) which is: +/// * Linux: `/home/UserName/.local/share/APP_ID` +/// * macOS: `/Users/UserName/Library/Application Support/APP_ID` +/// * Windows: `C:\Users\UserName\AppData\Roaming\APP_ID` +pub fn storage_dir(app_id: &str) -> Option { + directories_next::ProjectDirs::from("", "", app_id) + .map(|proj_dirs| proj_dirs.data_dir().to_path_buf()) +} + // ---------------------------------------------------------------------------- /// A key-value store backed by a [RON](https://github.com/ron-rs/ron) file on disk. @@ -17,6 +33,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"); join_handle.join().ok(); } } @@ -24,7 +41,8 @@ impl Drop for FileStorage { impl FileStorage { /// Store the state in this .ron file. - pub fn from_ron_filepath(ron_filepath: impl Into) -> Self { + fn from_ron_filepath(ron_filepath: impl Into) -> Self { + crate::profile_function!(); let ron_filepath: PathBuf = ron_filepath.into(); log::debug!("Loading app state from {:?}…", ron_filepath); Self { @@ -36,9 +54,9 @@ impl FileStorage { } /// Find a good place to put the files that the OS likes. - pub fn from_app_name(app_name: &str) -> Option { - if let Some(proj_dirs) = directories_next::ProjectDirs::from("", "", app_name) { - let data_dir = proj_dirs.data_dir().to_path_buf(); + pub fn from_app_id(app_id: &str) -> Option { + crate::profile_function!(app_id); + if let Some(data_dir) = storage_dir(app_id) { if let Err(err) = std::fs::create_dir_all(&data_dir) { log::warn!( "Saving disabled: Failed to create app path at {:?}: {}", @@ -70,6 +88,7 @@ impl crate::Storage for FileStorage { fn flush(&mut self) { if self.dirty { + crate::profile_function!(); self.dirty = false; let file_path = self.ron_filepath.clone(); @@ -80,11 +99,12 @@ impl crate::Storage for FileStorage { join_handle.join().ok(); } - match std::thread::Builder::new() + let result = std::thread::Builder::new() .name("eframe_persist".to_owned()) .spawn(move || { save_to_disk(&file_path, &kv); - }) { + }); + match result { Ok(join_handle) => { self.last_save_join_handle = Some(join_handle); } @@ -109,10 +129,14 @@ fn save_to_disk(file_path: &PathBuf, kv: &HashMap) { match std::fs::File::create(file_path) { Ok(file) => { + let mut writer = std::io::BufWriter::new(file); let config = Default::default(); - if let Err(err) = ron::ser::to_writer_pretty(file, &kv, config) { - log::warn!("Failed to serialize app state: {err}"); + crate::profile_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())) + { + log::warn!("Failed to serialize app state: {}", err); } else { log::trace!("Persisted to {:?}", file_path); } @@ -129,6 +153,7 @@ fn read_ron(ron_path: impl AsRef) -> Option where T: serde::de::DeserializeOwned, { + crate::profile_function!(); 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 new file mode 100644 index 00000000000..8b1f16ec807 --- /dev/null +++ b/crates/eframe/src/native/glow_integration.rs @@ -0,0 +1,1508 @@ +//! Note that this file contains code very similar to [`wgpu_integration`]. +//! When making changes to one you often also want to apply it to the other. +//! +//! This is also very complex code, and not very pretty. +//! There is a bunch of improvements we could do, +//! like removing a bunch of `unwraps`. + +#![allow(clippy::arc_with_non_send_sync)] // glow::Context was accidentally non-Sync in glow 0.13, but that will be fixed in future releases of glow: https://github.com/grovesNL/glow/commit/c4a5f7151b9b4bbb380faa06ec27415235d1bf7e + +use std::{cell::RefCell, rc::Rc, sync::Arc, time::Instant}; + +use glutin::{ + config::GlConfig, + context::NotCurrentGlContext, + display::GetGlDisplay, + prelude::{GlDisplay, PossiblyCurrentGlContext}, + surface::GlSurface, +}; +use winit::{ + event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}, + window::{Window, WindowId}, +}; + +use egui::{ + epaint::ahash::HashMap, DeferredViewportUiCallback, ImmediateViewport, NumExt as _, + ViewportBuilder, ViewportClass, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, + ViewportInfo, ViewportOutput, +}; +#[cfg(feature = "accesskit")] +use egui_winit::accesskit_winit; + +use crate::{ + native::{epi_integration::EpiIntegration, winit_integration::create_egui_context}, + App, AppCreator, CreationContext, NativeOptions, Result, Storage, +}; + +use super::{ + winit_integration::{EventResult, UserEvent, WinitApp}, + *, +}; + +// ---------------------------------------------------------------------------- +// Types: + +pub struct GlowWinitApp { + repaint_proxy: Arc>>, + app_name: String, + native_options: NativeOptions, + running: Option, + + // Note that since this `AppCreator` is FnOnce we are currently unable to support + // re-initializing the `GlowWinitRunning` state on Android if the application + // suspends and resumes. + app_creator: Option, +} + +/// State that is initialized when the application is first starts running via +/// a Resumed event. On Android this ensures that any graphics state is only +/// initialized once the application has an associated `SurfaceView`. +struct GlowWinitRunning { + integration: EpiIntegration, + app: Box, + + // These needs to be shared with the immediate viewport renderer, hence the Rc/Arc/RefCells: + glutin: Rc>, + + // NOTE: one painter shared by all viewports. + painter: Rc>, +} + +/// This struct will contain both persistent and temporary glutin state. +/// +/// Platform Quirks: +/// * Microsoft Windows: requires that we create a window before opengl context. +/// * Android: window and surface should be destroyed when we receive a suspend event. recreate on resume event. +/// +/// winit guarantees that we will get a Resumed event on startup on all platforms. +/// * Before Resumed event: `gl_config`, `gl_context` can be created at any time. on windows, a window must be created to get `gl_context`. +/// * Resumed: `gl_surface` will be created here. `window` will be re-created here for android. +/// * Suspended: on android, we drop window + surface. on other platforms, we don't get Suspended event. +/// +/// The setup is divided between the `new` fn and `on_resume` fn. we can just assume that `on_resume` is a continuation of +/// `new` fn on all platforms. only on android, do we get multiple resumed events because app can be suspended. +struct GlutinWindowContext { + egui_ctx: egui::Context, + + swap_interval: glutin::surface::SwapInterval, + gl_config: glutin::config::Config, + + max_texture_side: Option, + + current_gl_context: Option, + not_current_gl_context: Option, + + viewports: ViewportIdMap, + viewport_from_window: HashMap, + window_from_viewport: ViewportIdMap, + + focused_viewport: Option, +} + +struct Viewport { + ids: ViewportIdPair, + class: ViewportClass, + builder: ViewportBuilder, + info: ViewportInfo, + screenshot_requested: bool, + + /// The user-callback that shows the ui. + /// None for immediate viewports. + viewport_ui_cb: Option>, + + // These three live and die together. + // TODO(emilk): clump them together into one struct! + gl_surface: Option>, + window: Option>, + egui_winit: Option, +} + +// ---------------------------------------------------------------------------- + +impl GlowWinitApp { + pub fn new( + event_loop: &EventLoop, + app_name: &str, + native_options: NativeOptions, + app_creator: AppCreator, + ) -> Self { + crate::profile_function!(); + Self { + repaint_proxy: Arc::new(egui::mutex::Mutex::new(event_loop.create_proxy())), + app_name: app_name.to_owned(), + native_options, + running: None, + app_creator: Some(app_creator), + } + } + + #[allow(unsafe_code)] + fn create_glutin_windowed_context( + egui_ctx: &egui::Context, + event_loop: &EventLoopWindowTarget, + storage: Option<&dyn Storage>, + native_options: &mut NativeOptions, + ) -> Result<(GlutinWindowContext, egui_glow::Painter)> { + crate::profile_function!(); + + let window_settings = epi_integration::load_window_settings(storage); + + let winit_window_builder = epi_integration::viewport_builder( + egui_ctx.zoom_factor(), + event_loop, + native_options, + window_settings, + ) + .with_visible(false); // Start hidden until we render the first frame to fix white flash on startup (https://github.com/emilk/egui/pull/3631) + + let mut glutin_window_context = unsafe { + GlutinWindowContext::new(egui_ctx, winit_window_builder, native_options, event_loop)? + }; + + // Creates the window - must come before we create our glow context + glutin_window_context.initialize_window(ViewportId::ROOT, event_loop)?; + + { + let viewport = &glutin_window_context.viewports[&ViewportId::ROOT]; + let window = viewport.window.as_ref().unwrap(); // Can't fail - we just called `initialize_all_viewports` + epi_integration::apply_window_settings(window, window_settings); + } + + let gl = unsafe { + crate::profile_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"); + + glutin_window_context.get_proc_address(&s) + })) + }; + + let painter = egui_glow::Painter::new(gl, "", native_options.shader_version)?; + + Ok((glutin_window_context, painter)) + } + + fn init_run_state( + &mut self, + event_loop: &EventLoopWindowTarget, + ) -> Result<&mut GlowWinitRunning> { + crate::profile_function!(); + + let storage = epi_integration::create_storage( + self.native_options + .viewport + .app_id + .as_ref() + .unwrap_or(&self.app_name), + ); + + let egui_ctx = create_egui_context(storage.as_deref()); + + let (mut glutin, painter) = Self::create_glutin_windowed_context( + &egui_ctx, + event_loop, + storage.as_deref(), + &mut self.native_options, + )?; + let gl = painter.gl().clone(); + + let max_texture_side = painter.max_texture_side(); + glutin.max_texture_side = Some(max_texture_side); + for viewport in glutin.viewports.values_mut() { + if let Some(egui_winit) = viewport.egui_winit.as_mut() { + egui_winit.set_max_texture_side(max_texture_side); + } + } + + let system_theme = + winit_integration::system_theme(&glutin.window(ViewportId::ROOT), &self.native_options); + + let integration = EpiIntegration::new( + egui_ctx, + &glutin.window(ViewportId::ROOT), + system_theme, + &self.app_name, + &self.native_options, + storage, + Some(gl.clone()), + #[cfg(feature = "wgpu")] + None, + ); + + { + let event_loop_proxy = self.repaint_proxy.clone(); + integration + .egui_ctx + .set_request_repaint_callback(move |info| { + log::trace!("request_repaint_callback: {info:?}"); + let when = Instant::now() + info.delay; + let frame_nr = info.current_frame_nr; + event_loop_proxy + .lock() + .send_event(UserEvent::RequestRepaint { + viewport_id: info.viewport_id, + when, + frame_nr, + }) + .ok(); + }); + } + + #[cfg(feature = "accesskit")] + { + let event_loop_proxy = self.repaint_proxy.lock().clone(); + let viewport = glutin.viewports.get_mut(&ViewportId::ROOT).unwrap(); + if let Viewport { + window: Some(window), + egui_winit: Some(egui_winit), + .. + } = viewport + { + integration.init_accesskit(egui_winit, window, event_loop_proxy); + } + } + + let theme = system_theme.unwrap_or(self.native_options.default_theme); + integration.egui_ctx.set_visuals(theme.egui_visuals()); + + if self + .native_options + .viewport + .mouse_passthrough + .unwrap_or(false) + { + if let Err(err) = glutin.window(ViewportId::ROOT).set_cursor_hittest(false) { + log::warn!("set_cursor_hittest(false) failed: {err}"); + } + } + + let app_creator = std::mem::take(&mut self.app_creator) + .expect("Single-use AppCreator has unexpectedly already been taken"); + + let app = { + // Use latest raw_window_handle for eframe compatibility + use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _}; + + let get_proc_address = |addr: &_| glutin.get_proc_address(addr); + let window = glutin.window(ViewportId::ROOT); + let cc = CreationContext { + egui_ctx: integration.egui_ctx.clone(), + integration_info: integration.frame.info().clone(), + storage: integration.frame.storage(), + gl: Some(gl), + get_proc_address: Some(&get_proc_address), + #[cfg(feature = "wgpu")] + wgpu_render_state: None, + 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"); + app_creator(&cc) + }; + + let glutin = Rc::new(RefCell::new(glutin)); + let painter = Rc::new(RefCell::new(painter)); + + { + // Create weak pointers so that we don't keep + // state alive for too long. + let glutin = Rc::downgrade(&glutin); + let painter = Rc::downgrade(&painter); + let beginning = integration.beginning; + + let event_loop: *const EventLoopWindowTarget = event_loop; + + egui::Context::set_immediate_viewport_renderer(move |egui_ctx, immediate_viewport| { + if let (Some(glutin), Some(painter)) = (glutin.upgrade(), painter.upgrade()) { + // SAFETY: the event loop lives longer than + // the Rc:s we just upgraded above. + #[allow(unsafe_code)] + let event_loop = unsafe { event_loop.as_ref().unwrap() }; + + render_immediate_viewport( + event_loop, + egui_ctx, + &glutin, + &painter, + beginning, + immediate_viewport, + ); + } else { + log::warn!("render_sync_callback called after window closed"); + } + }); + } + + Ok(self.running.insert(GlowWinitRunning { + glutin, + painter, + integration, + app, + })) + } +} + +impl WinitApp for GlowWinitApp { + fn frame_nr(&self, viewport_id: ViewportId) -> u64 { + self.running + .as_ref() + .map_or(0, |r| r.integration.egui_ctx.frame_nr_for(viewport_id)) + } + + fn is_focused(&self, window_id: WindowId) -> bool { + if let Some(running) = &self.running { + let glutin = running.glutin.borrow(); + if let Some(window_id) = glutin.viewport_from_window.get(&window_id) { + return glutin.focused_viewport == Some(*window_id); + } + } + + false + } + + fn integration(&self) -> Option<&EpiIntegration> { + self.running.as_ref().map(|r| &r.integration) + } + + fn window(&self, window_id: WindowId) -> Option> { + let running = self.running.as_ref()?; + let glutin = running.glutin.borrow(); + let viewport_id = *glutin.viewport_from_window.get(&window_id)?; + if let Some(viewport) = glutin.viewports.get(&viewport_id) { + viewport.window.clone() + } else { + None + } + } + + fn window_id_from_viewport_id(&self, id: ViewportId) -> Option { + self.running + .as_ref() + .and_then(|r| r.glutin.borrow().window_from_viewport.get(&id).copied()) + } + + fn save_and_destroy(&mut self) { + if let Some(mut running) = self.running.take() { + crate::profile_function!(); + + running.integration.save( + running.app.as_mut(), + Some(&running.glutin.borrow().window(ViewportId::ROOT)), + ); + running.app.on_exit(Some(running.painter.borrow().gl())); + running.painter.borrow_mut().destroy(); + } + } + + fn run_ui_and_paint( + &mut self, + event_loop: &EventLoopWindowTarget, + window_id: WindowId, + ) -> EventResult { + if let Some(running) = &mut self.running { + running.run_ui_and_paint(event_loop, window_id) + } else { + EventResult::Wait + } + } + + fn on_event( + &mut self, + event_loop: &EventLoopWindowTarget, + event: &winit::event::Event, + ) -> Result { + crate::profile_function!(winit_integration::short_event_description(event)); + + Ok(match event { + winit::event::Event::Resumed => { + log::debug!("Event::Resumed"); + + let running = if let Some(running) = &mut self.running { + // Not the first resume event. Create all outstanding windows. + running + .glutin + .borrow_mut() + .initialize_all_windows(event_loop); + running + } else { + // First resume event. Created our root window etc. + self.init_run_state(event_loop)? + }; + let window_id = running.glutin.borrow().window_from_viewport[&ViewportId::ROOT]; + EventResult::RepaintNow(window_id) + } + + winit::event::Event::Suspended => { + if let Some(running) = &mut self.running { + running.glutin.borrow_mut().on_suspend()?; + } + EventResult::Wait + } + + winit::event::Event::WindowEvent { event, window_id } => { + if let Some(running) = &mut self.running { + running.on_window_event(*window_id, event) + } else { + EventResult::Wait + } + } + + winit::event::Event::DeviceEvent { + device_id: _, + event: winit::event::DeviceEvent::MouseMotion { delta }, + } => { + if let Some(running) = &mut self.running { + let mut glutin = running.glutin.borrow_mut(); + if let Some(viewport) = glutin + .focused_viewport + .and_then(|viewport| glutin.viewports.get_mut(&viewport)) + { + if let Some(egui_winit) = viewport.egui_winit.as_mut() { + egui_winit.on_mouse_motion(*delta); + } + + if let Some(window) = viewport.window.as_ref() { + EventResult::RepaintNext(window.id()) + } else { + EventResult::Wait + } + } else { + EventResult::Wait + } + } else { + EventResult::Wait + } + } + + #[cfg(feature = "accesskit")] + winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest( + accesskit_winit::ActionRequestEvent { request, window_id }, + )) => { + if let Some(running) = &self.running { + let mut glutin = running.glutin.borrow_mut(); + if let Some(viewport_id) = glutin.viewport_from_window.get(window_id).copied() { + if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) { + if let Some(egui_winit) = &mut viewport.egui_winit { + crate::profile_scope!("on_accesskit_action_request"); + egui_winit.on_accesskit_action_request(request.clone()); + } + } + } + // As a form of user input, accessibility actions should + // lead to a repaint. + EventResult::RepaintNext(*window_id) + } else { + EventResult::Wait + } + } + _ => EventResult::Wait, + }) + } +} + +impl GlowWinitRunning { + fn run_ui_and_paint( + &mut self, + event_loop: &EventLoopWindowTarget, + window_id: WindowId, + ) -> EventResult { + crate::profile_function!(); + + let Some(viewport_id) = self + .glutin + .borrow() + .viewport_from_window + .get(&window_id) + .copied() + else { + return EventResult::Wait; + }; + + #[cfg(feature = "puffin")] + puffin::GlobalProfiler::lock().new_frame(); + + let mut frame_timer = crate::stopwatch::Stopwatch::new(); + frame_timer.start(); + + { + let glutin = self.glutin.borrow(); + let viewport = &glutin.viewports[&viewport_id]; + let is_immediate = viewport.viewport_ui_cb.is_none(); + if is_immediate && viewport_id != ViewportId::ROOT { + // This will only happen if this is an immediate viewport. + // That means that the viewport cannot be rendered by itself and needs his parent to be rendered. + if let Some(parent_viewport) = glutin.viewports.get(&viewport.ids.parent) { + if let Some(window) = parent_viewport.window.as_ref() { + return EventResult::RepaintNext(window.id()); + } + } + return EventResult::Wait; + } + } + + let (raw_input, viewport_ui_cb) = { + let mut glutin = self.glutin.borrow_mut(); + let egui_ctx = glutin.egui_ctx.clone(); + let viewport = glutin.viewports.get_mut(&viewport_id).unwrap(); + let Some(window) = viewport.window.as_ref() else { + return EventResult::Wait; + }; + egui_winit::update_viewport_info(&mut viewport.info, &egui_ctx, window); + + let egui_winit = viewport.egui_winit.as_mut().unwrap(); + let mut raw_input = egui_winit.take_egui_input(window); + let viewport_ui_cb = viewport.viewport_ui_cb.clone(); + + self.integration.pre_update(); + + raw_input.time = Some(self.integration.beginning.elapsed().as_secs_f64()); + raw_input.viewports = glutin + .viewports + .iter() + .map(|(id, viewport)| (*id, viewport.info.clone())) + .collect(); + + (raw_input, viewport_ui_cb) + }; + + let clear_color = self + .app + .clear_color(&self.integration.egui_ctx.style().visuals); + + let has_many_viewports = self.glutin.borrow().viewports.len() > 1; + let clear_before_update = !has_many_viewports; // HACK: for some reason, an early clear doesn't "take" on Mac with multiple viewports. + + if clear_before_update { + // clear before we call update, so users can paint between clear-color and egui windows: + + let mut glutin = self.glutin.borrow_mut(); + let GlutinWindowContext { + viewports, + current_gl_context, + .. + } = &mut *glutin; + let viewport = &viewports[&viewport_id]; + let window = viewport.window.as_ref().unwrap(); + let gl_surface = viewport.gl_surface.as_ref().unwrap(); + + let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); + + { + frame_timer.pause(); + change_gl_context(current_gl_context, gl_surface); + frame_timer.resume(); + } + + self.painter + .borrow() + .clear(screen_size_in_pixels, clear_color); + } + + // ------------------------------------------------------------ + // The update function, which could call immediate viewports, + // so make sure we don't hold any locks here required by the immediate viewports rendeer. + + let full_output = + self.integration + .update(self.app.as_mut(), viewport_ui_cb.as_deref(), raw_input); + + // ------------------------------------------------------------ + + let Self { + integration, + app, + glutin, + painter, + .. + } = self; + + let mut glutin = glutin.borrow_mut(); + let mut painter = painter.borrow_mut(); + + let egui::FullOutput { + platform_output, + textures_delta, + shapes, + pixels_per_point, + viewport_output, + } = full_output; + + let GlutinWindowContext { + viewports, + current_gl_context, + .. + } = &mut *glutin; + + let viewport = viewports.get_mut(&viewport_id).unwrap(); + viewport.info.events.clear(); // they should have been processed + let window = viewport.window.clone().unwrap(); + let gl_surface = viewport.gl_surface.as_ref().unwrap(); + let egui_winit = viewport.egui_winit.as_mut().unwrap(); + + egui_winit.handle_platform_output(&window, platform_output); + + let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point); + + { + // We may need to switch contexts again, because of immediate viewports: + frame_timer.pause(); + change_gl_context(current_gl_context, gl_surface); + frame_timer.resume(); + } + + let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); + + if !clear_before_update { + painter.clear(screen_size_in_pixels, clear_color); + } + + painter.paint_and_update_textures( + screen_size_in_pixels, + pixels_per_point, + &clipped_primitives, + &textures_delta, + ); + + { + let screenshot_requested = std::mem::take(&mut viewport.screenshot_requested); + if screenshot_requested { + let screenshot = painter.read_screen_rgba(screen_size_in_pixels); + egui_winit + .egui_input_mut() + .events + .push(egui::Event::Screenshot { + viewport_id, + image: screenshot.into(), + }); + } + integration.post_rendering(&window); + } + + { + // vsync - don't count as frame-time: + frame_timer.pause(); + crate::profile_scope!("swap_buffers"); + if let Err(err) = gl_surface.swap_buffers( + current_gl_context + .as_ref() + .expect("failed to get current context to swap buffers"), + ) { + log::error!("swap_buffers failed: {err}"); + } + frame_timer.resume(); + } + + // give it time to settle: + #[cfg(feature = "__screenshot")] + if integration.egui_ctx.frame_nr() == 2 { + if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") { + save_screeshot_and_exit(&path, &painter, screen_size_in_pixels); + } + } + + glutin.handle_viewport_output(event_loop, &integration.egui_ctx, viewport_output); + + integration.report_frame_time(frame_timer.total_time_sec()); // don't count auto-save time as part of regular frame time + + integration.maybe_autosave(app.as_mut(), Some(&window)); + + 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"); + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + if integration.should_close() { + EventResult::Exit + } else { + EventResult::Wait + } + } + + fn on_window_event( + &mut self, + window_id: WindowId, + event: &winit::event::WindowEvent, + ) -> EventResult { + crate::profile_function!(egui_winit::short_window_event_description(event)); + + let mut glutin = self.glutin.borrow_mut(); + let viewport_id = glutin.viewport_from_window.get(&window_id).copied(); + + // On Windows, if a window is resized by the user, it should repaint synchronously, inside the + // event handler. + // + // If this is not done, the compositor will assume that the window does not want to redraw, + // and continue ahead. + // + // In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver + // new frames to the compositor in time. + // + // The flickering is technically glutin or glow's fault, but we should be responding properly + // to resizes anyway, as doing so avoids dropping frames. + // + // See: https://github.com/emilk/egui/issues/903 + let mut repaint_asap = false; + + match event { + winit::event::WindowEvent::Focused(new_focused) => { + glutin.focused_viewport = new_focused.then(|| viewport_id).flatten(); + } + + winit::event::WindowEvent::Resized(physical_size) => { + // Resize with 0 width and height is used by winit to signal a minimize event on Windows. + // See: https://github.com/rust-windowing/winit/issues/208 + // This solves an issue where the app would panic when minimizing on Windows. + if 0 < physical_size.width && 0 < physical_size.height { + if let Some(viewport_id) = viewport_id { + repaint_asap = true; + glutin.resize(viewport_id, *physical_size); + } + } + } + + winit::event::WindowEvent::CloseRequested => { + if viewport_id == Some(ViewportId::ROOT) && self.integration.should_close() { + log::debug!( + "Received WindowEvent::CloseRequested for main viewport - shutting down." + ); + return EventResult::Exit; + } + + log::debug!("Received WindowEvent::CloseRequested for viewport {viewport_id:?}"); + + if let Some(viewport_id) = viewport_id { + if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) { + // Tell viewport it should close: + viewport.info.events.push(egui::ViewportEvent::Close); + + // We may need to repaint both us and our parent to close the window, + // and perhaps twice (once to notice the close-event, once again to enforce it). + // `request_repaint_of` does a double-repaint though: + self.integration.egui_ctx.request_repaint_of(viewport_id); + self.integration + .egui_ctx + .request_repaint_of(viewport.ids.parent); + } + } + } + + _ => {} + } + + if self.integration.should_close() { + return EventResult::Exit; + } + + let mut event_response = egui_winit::EventResponse { + consumed: false, + repaint: false, + }; + if let Some(viewport_id) = viewport_id { + if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) { + if let (Some(window), Some(egui_winit)) = + (&viewport.window, &mut viewport.egui_winit) + { + event_response = self.integration.on_window_event(window, egui_winit, event); + } + } else { + log::trace!("Ignoring event: no viewport for {viewport_id:?}"); + } + } else { + log::trace!("Ignoring event: no viewport_id"); + } + + if event_response.repaint { + if repaint_asap { + EventResult::RepaintNow(window_id) + } else { + EventResult::RepaintNext(window_id) + } + } else { + EventResult::Wait + } + } +} + +fn change_gl_context( + current_gl_context: &mut Option, + gl_surface: &glutin::surface::Surface, +) { + crate::profile_function!(); + + let not_current = { + crate::profile_scope!("make_not_current"); + current_gl_context + .take() + .unwrap() + .make_not_current() + .unwrap() + }; + crate::profile_scope!("make_current"); + *current_gl_context = Some(not_current.make_current(gl_surface).unwrap()); +} + +impl GlutinWindowContext { + #[allow(unsafe_code)] + unsafe fn new( + egui_ctx: &egui::Context, + viewport_builder: ViewportBuilder, + native_options: &NativeOptions, + event_loop: &EventLoopWindowTarget, + ) -> Result { + crate::profile_function!(); + + // There is a lot of complexity with opengl creation, + // so prefer extensive logging to get all the help we can to debug issues. + + use glutin::prelude::*; + // convert native options to glutin options + let hardware_acceleration = match native_options.hardware_acceleration { + crate::HardwareAcceleration::Required => Some(true), + crate::HardwareAcceleration::Preferred => None, + crate::HardwareAcceleration::Off => Some(false), + }; + let swap_interval = if native_options.vsync { + glutin::surface::SwapInterval::Wait(std::num::NonZeroU32::new(1).unwrap()) + } else { + glutin::surface::SwapInterval::DontWait + }; + /* opengl setup flow goes like this: + 1. we create a configuration for opengl "Display" / "Config" creation + 2. choose between special extensions like glx or egl or wgl and use them to create config/display + 3. opengl context configuration + 4. opengl context creation + */ + // start building config for gl display + let config_template_builder = glutin::config::ConfigTemplateBuilder::new() + .prefer_hardware_accelerated(hardware_acceleration) + .with_depth_size(native_options.depth_buffer) + .with_stencil_size(native_options.stencil_buffer) + .with_transparency(native_options.viewport.transparent.unwrap_or(false)); + // we don't know if multi sampling option is set. so, check if its more than 0. + let config_template_builder = if native_options.multisampling > 0 { + config_template_builder.with_multisampling( + native_options + .multisampling + .try_into() + .expect("failed to fit multisamples option of native_options into u8"), + ) + } else { + config_template_builder + }; + + log::debug!("trying to create glutin Display with config: {config_template_builder:?}"); + + // 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 + .with_window_builder(Some(egui_winit::create_winit_window_builder( + egui_ctx, + event_loop, + viewport_builder.clone(), + ))); + + let (window, gl_config) = { + crate::profile_scope!("DisplayBuilder::build"); + + display_builder + .build( + event_loop, + config_template_builder.clone(), + |mut config_iterator| { + let config = config_iterator.next().expect( + "failed to find a matching configuration for creating glutin config", + ); + log::debug!( + "using the first config from config picker closure. config: {config:?}" + ); + config + }, + ) + .map_err(|e| crate::Error::NoGlutinConfigs(config_template_builder.build(), e))? + }; + if let Some(window) = &window { + egui_winit::apply_viewport_builder_to_window(egui_ctx, window, &viewport_builder); + } + + let gl_display = gl_config.display(); + log::debug!( + "successfully created GL Display with version: {} and supported features: {:?}", + gl_display.version_string(), + gl_display.supported_features() + ); + let glutin_raw_window_handle = window.as_ref().map(|w| { + use rwh_05::HasRawWindowHandle as _; // glutin stuck on old version of raw-window-handle + w.raw_window_handle() + }); + log::debug!("creating gl context using raw window handle: {glutin_raw_window_handle:?}"); + + // create gl context. if core context cannot be created, try gl es context as fallback. + let context_attributes = + glutin::context::ContextAttributesBuilder::new().build(glutin_raw_window_handle); + let fallback_context_attributes = glutin::context::ContextAttributesBuilder::new() + .with_context_api(glutin::context::ContextApi::Gles(None)) + .build(glutin_raw_window_handle); + + let gl_context_result = unsafe { + crate::profile_scope!("create_context"); + gl_config + .display() + .create_context(&gl_config, &context_attributes) + }; + + let gl_context = match gl_context_result { + Ok(it) => it, + Err(err) => { + log::warn!("Failed to create context using default context attributes {context_attributes:?} due to error: {err}"); + log::debug!( + "Retrying with fallback context attributes: {fallback_context_attributes:?}" + ); + unsafe { + gl_config + .display() + .create_context(&gl_config, &fallback_context_attributes)? + } + } + }; + let not_current_gl_context = Some(gl_context); + + let mut viewport_from_window = HashMap::default(); + let mut window_from_viewport = ViewportIdMap::default(); + let mut info = ViewportInfo::default(); + if let Some(window) = &window { + viewport_from_window.insert(window.id(), ViewportId::ROOT); + window_from_viewport.insert(ViewportId::ROOT, window.id()); + info.minimized = window.is_minimized(); + info.maximized = Some(window.is_maximized()); + } + + let mut viewports = ViewportIdMap::default(); + viewports.insert( + ViewportId::ROOT, + Viewport { + ids: ViewportIdPair::ROOT, + class: ViewportClass::Root, + builder: viewport_builder, + info, + screenshot_requested: false, + viewport_ui_cb: None, + gl_surface: None, + window: window.map(Arc::new), + egui_winit: None, + }, + ); + + // the fun part with opengl gl is that we never know whether there is an error. the context creation might have failed, but + // it could keep working until we try to make surface current or swap buffers or something else. future glutin improvements might + // help us start from scratch again if we fail context creation and go back to preferEgl or try with different config etc.. + // https://github.com/emilk/egui/pull/2541#issuecomment-1370767582 + + let mut slf = Self { + egui_ctx: egui_ctx.clone(), + swap_interval, + gl_config, + current_gl_context: None, + not_current_gl_context, + viewports, + viewport_from_window, + max_texture_side: None, + window_from_viewport, + focused_viewport: Some(ViewportId::ROOT), + }; + + slf.initialize_window(ViewportId::ROOT, event_loop)?; + + Ok(slf) + } + + /// Create a surface, window, and winit integration for all viewports lacking any of that. + /// + /// Errors will be logged. + fn initialize_all_windows(&mut self, event_loop: &EventLoopWindowTarget) { + crate::profile_function!(); + + let viewports: Vec = self.viewports.keys().copied().collect(); + + for viewport_id in viewports { + if let Err(err) = self.initialize_window(viewport_id, event_loop) { + log::error!("Failed to initialize a window for viewport {viewport_id:?}: {err}"); + } + } + } + + /// Create a surface, window, and winit integration for the viewport, if missing. + #[allow(unsafe_code)] + pub(crate) fn initialize_window( + &mut self, + viewport_id: ViewportId, + event_loop: &EventLoopWindowTarget, + ) -> Result<()> { + crate::profile_function!(); + + let viewport = self + .viewports + .get_mut(&viewport_id) + .expect("viewport doesn't exist"); + + let window = if let Some(window) = &mut viewport.window { + window + } else { + log::debug!("Creating a window for viewport {viewport_id:?}"); + let window_builder = egui_winit::create_winit_window_builder( + &self.egui_ctx, + event_loop, + viewport.builder.clone(), + ); + if window_builder.transparent() && self.gl_config.supports_transparency() == Some(false) + { + log::error!("Cannot create transparent window: the GL config does not support it"); + } + let window = + glutin_winit::finalize_window(event_loop, window_builder, &self.gl_config)?; + egui_winit::apply_viewport_builder_to_window( + &self.egui_ctx, + &window, + &viewport.builder, + ); + viewport.info.minimized = window.is_minimized(); + viewport.info.maximized = Some(window.is_maximized()); + viewport.window.insert(Arc::new(window)) + }; + + viewport.egui_winit.get_or_insert_with(|| { + log::debug!("Initializing egui_winit for viewport {viewport_id:?}"); + egui_winit::State::new( + self.egui_ctx.clone(), + viewport_id, + event_loop, + Some(window.scale_factor() as f32), + self.max_texture_side, + ) + }); + + if viewport.gl_surface.is_none() { + log::debug!("Creating a gl_surface for viewport {viewport_id:?}"); + + // surface attributes + let (width_px, height_px): (u32, u32) = window.inner_size().into(); + let width_px = std::num::NonZeroU32::new(width_px.at_least(1)).unwrap(); + let height_px = std::num::NonZeroU32::new(height_px.at_least(1)).unwrap(); + let surface_attributes = { + use rwh_05::HasRawWindowHandle as _; // glutin stuck on old version of raw-window-handle + glutin::surface::SurfaceAttributesBuilder::::new() + .build(window.raw_window_handle(), width_px, height_px) + }; + + log::trace!("creating surface with attributes: {surface_attributes:?}"); + let gl_surface = unsafe { + self.gl_config + .display() + .create_window_surface(&self.gl_config, &surface_attributes)? + }; + + log::trace!("surface created successfully: {gl_surface:?}. making context current"); + + let not_current_gl_context = + if let Some(not_current_context) = self.not_current_gl_context.take() { + not_current_context + } else { + self.current_gl_context + .take() + .unwrap() + .make_not_current() + .unwrap() + }; + let current_gl_context = not_current_gl_context.make_current(&gl_surface)?; + + // try setting swap interval. but its not absolutely necessary, so don't panic on failure. + log::trace!("made context current. setting swap interval for surface"); + if let Err(err) = gl_surface.set_swap_interval(¤t_gl_context, self.swap_interval) + { + log::warn!("Failed to set swap interval due to error: {err}"); + } + + // we will reach this point only once in most platforms except android. + // create window/surface/make context current once and just use them forever. + + viewport.gl_surface = Some(gl_surface); + + self.current_gl_context = Some(current_gl_context); + } + + self.viewport_from_window.insert(window.id(), viewport_id); + self.window_from_viewport.insert(viewport_id, window.id()); + + Ok(()) + } + + /// only applies for android. but we basically drop surface + window and make context not current + fn on_suspend(&mut self) -> Result<()> { + log::debug!("received suspend event. dropping window and surface"); + for viewport in self.viewports.values_mut() { + viewport.gl_surface = None; + viewport.window = None; + } + if let Some(current) = self.current_gl_context.take() { + log::debug!("context is current, so making it non-current"); + self.not_current_gl_context = Some(current.make_not_current()?); + } else { + log::debug!("context is already not current??? could be duplicate suspend event"); + } + Ok(()) + } + + fn viewport(&self, viewport_id: ViewportId) -> &Viewport { + self.viewports + .get(&viewport_id) + .expect("viewport doesn't exist") + } + + fn window(&self, viewport_id: ViewportId) -> Arc { + self.viewport(viewport_id) + .window + .clone() + .expect("winit window doesn't exist") + } + + fn resize(&mut self, viewport_id: ViewportId, physical_size: winit::dpi::PhysicalSize) { + let width_px = std::num::NonZeroU32::new(physical_size.width.at_least(1)).unwrap(); + let height_px = std::num::NonZeroU32::new(physical_size.height.at_least(1)).unwrap(); + + if let Some(viewport) = self.viewports.get(&viewport_id) { + if let Some(gl_surface) = &viewport.gl_surface { + self.current_gl_context = Some( + self.current_gl_context + .take() + .unwrap() + .make_not_current() + .unwrap() + .make_current(gl_surface) + .unwrap(), + ); + gl_surface.resize( + self.current_gl_context + .as_ref() + .expect("failed to get current context to resize surface"), + width_px, + height_px, + ); + } + } + } + + fn get_proc_address(&self, addr: &std::ffi::CStr) -> *const std::ffi::c_void { + self.gl_config.display().get_proc_address(addr) + } + + fn handle_viewport_output( + &mut self, + event_loop: &EventLoopWindowTarget, + egui_ctx: &egui::Context, + viewport_output: ViewportIdMap, + ) { + crate::profile_function!(); + + let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); + + for ( + viewport_id, + ViewportOutput { + parent, + class, + builder, + viewport_ui_cb, + commands, + repaint_delay: _, // ignored - we listened to the repaint callback instead + }, + ) in viewport_output + { + let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent); + + let viewport = initialize_or_update_viewport( + egui_ctx, + &mut self.viewports, + ids, + class, + builder, + viewport_ui_cb, + self.focused_viewport, + ); + + if let Some(window) = &viewport.window { + let is_viewport_focused = self.focused_viewport == Some(viewport_id); + egui_winit::process_viewport_commands( + egui_ctx, + &mut viewport.info, + commands, + window, + is_viewport_focused, + &mut viewport.screenshot_requested, + ); + } + } + + // Create windows for any new viewports: + self.initialize_all_windows(event_loop); + + // GC old viewports + self.viewports + .retain(|id, _| active_viewports_ids.contains(id)); + self.viewport_from_window + .retain(|_, id| active_viewports_ids.contains(id)); + self.window_from_viewport + .retain(|id, _| active_viewports_ids.contains(id)); + } +} + +fn initialize_or_update_viewport<'vp>( + egu_ctx: &egui::Context, + viewports: &'vp mut ViewportIdMap, + ids: ViewportIdPair, + class: ViewportClass, + mut builder: ViewportBuilder, + viewport_ui_cb: Option>, + focused_viewport: Option, +) -> &'vp mut Viewport { + crate::profile_function!(); + + if builder.icon.is_none() { + // Inherit icon from parent + builder.icon = viewports + .get_mut(&ids.parent) + .and_then(|vp| vp.builder.icon.clone()); + } + + match viewports.entry(ids.this) { + std::collections::hash_map::Entry::Vacant(entry) => { + // New viewport: + log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title); + entry.insert(Viewport { + ids, + class, + builder, + info: Default::default(), + screenshot_requested: false, + viewport_ui_cb, + window: None, + egui_winit: None, + gl_surface: None, + }) + } + + std::collections::hash_map::Entry::Occupied(mut entry) => { + // Patch an existing viewport: + let viewport = entry.get_mut(); + + viewport.ids.parent = ids.parent; + viewport.class = class; + viewport.viewport_ui_cb = viewport_ui_cb; + + let (delta_commands, recreate) = viewport.builder.patch(builder); + + if recreate { + log::debug!( + "Recreating window for viewport {:?} ({:?})", + ids.this, + viewport.builder.title + ); + viewport.window = None; + viewport.egui_winit = None; + } else if let Some(window) = &viewport.window { + let is_viewport_focused = focused_viewport == Some(ids.this); + egui_winit::process_viewport_commands( + egu_ctx, + &mut viewport.info, + delta_commands, + window, + is_viewport_focused, + &mut viewport.screenshot_requested, + ); + } + + entry.into_mut() + } + } +} + +/// This is called (via a callback) by user code to render immediate viewports, +/// i.e. viewport that are directly nested inside a parent viewport. +fn render_immediate_viewport( + event_loop: &EventLoopWindowTarget, + egui_ctx: &egui::Context, + glutin: &RefCell, + painter: &RefCell, + beginning: Instant, + immediate_viewport: ImmediateViewport<'_>, +) { + crate::profile_function!(); + + let ImmediateViewport { + ids, + builder, + viewport_ui_cb, + } = immediate_viewport; + + let viewport_id = ids.this; + + { + let mut glutin = glutin.borrow_mut(); + + initialize_or_update_viewport( + egui_ctx, + &mut glutin.viewports, + ids, + ViewportClass::Immediate, + builder, + None, + None, + ); + + if let Err(err) = glutin.initialize_window(viewport_id, event_loop) { + log::error!( + "Failed to initialize a window for immediate viewport {viewport_id:?}: {err}" + ); + return; + } + } + + let input = { + let mut glutin = glutin.borrow_mut(); + + let Some(viewport) = glutin.viewports.get_mut(&viewport_id) else { + return; + }; + let (Some(egui_winit), Some(window)) = (&mut viewport.egui_winit, &viewport.window) else { + return; + }; + egui_winit::update_viewport_info(&mut viewport.info, egui_ctx, window); + + let mut raw_input = egui_winit.take_egui_input(window); + raw_input.viewports = glutin + .viewports + .iter() + .map(|(id, viewport)| (*id, viewport.info.clone())) + .collect(); + raw_input.time = Some(beginning.elapsed().as_secs_f64()); + raw_input + }; + + // --------------------------------------------------- + // Call the user ui-code, which could re-entrantly call this function again! + // No locks may be hold while calling this function. + + let egui::FullOutput { + platform_output, + textures_delta, + shapes, + pixels_per_point, + viewport_output, + } = egui_ctx.run(input, |ctx| { + viewport_ui_cb(ctx); + }); + + // --------------------------------------------------- + + let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); + + let mut glutin = glutin.borrow_mut(); + + let GlutinWindowContext { + current_gl_context, + viewports, + .. + } = &mut *glutin; + + let Some(viewport) = viewports.get_mut(&viewport_id) else { + return; + }; + + viewport.info.events.clear(); // they should have been processed + + let (Some(egui_winit), Some(window), Some(gl_surface)) = ( + &mut viewport.egui_winit, + &viewport.window, + &viewport.gl_surface, + ) else { + return; + }; + + let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); + + { + crate::profile_function!("context-switch"); + *current_gl_context = Some( + current_gl_context + .take() + .unwrap() + .make_not_current() + .unwrap() + .make_current(gl_surface) + .unwrap(), + ); + } + + let current_gl_context = current_gl_context.as_ref().unwrap(); + + if !gl_surface.is_current(current_gl_context) { + log::error!( + "egui::show_viewport_immediate: viewport {:?} ({:?}) was not created on main thread.", + viewport.ids.this, + viewport.builder.title + ); + } + + egui_glow::painter::clear( + painter.borrow().gl(), + screen_size_in_pixels, + [0.0, 0.0, 0.0, 0.0], + ); + + painter.borrow_mut().paint_and_update_textures( + screen_size_in_pixels, + pixels_per_point, + &clipped_primitives, + &textures_delta, + ); + + { + crate::profile_scope!("swap_buffers"); + if let Err(err) = gl_surface.swap_buffers(current_gl_context) { + log::error!("swap_buffers failed: {err}"); + } + } + + egui_winit.handle_platform_output(window, platform_output); + + glutin.handle_viewport_output(event_loop, egui_ctx, viewport_output); +} + +#[cfg(feature = "__screenshot")] +fn save_screeshot_and_exit( + path: &str, + painter: &egui_glow::Painter, + screen_size_in_pixels: [u32; 2], +) { + assert!( + path.ends_with(".png"), + "Expected EFRAME_SCREENSHOT_TO to end with '.png', got {path:?}" + ); + let screenshot = painter.read_screen_rgba(screen_size_in_pixels); + image::save_buffer( + path, + screenshot.as_raw(), + screenshot.width() as u32, + screenshot.height() as u32, + image::ColorType::Rgba8, + ) + .unwrap_or_else(|err| { + panic!("Failed to save screenshot to {path:?}: {err}"); + }); + eprintln!("Screenshot saved to {path:?}."); + + #[allow(clippy::exit)] + std::process::exit(0); +} diff --git a/crates/eframe/src/native/mod.rs b/crates/eframe/src/native/mod.rs index 8b606155e20..5fadd561014 100644 --- a/crates/eframe/src/native/mod.rs +++ b/crates/eframe/src/native/mod.rs @@ -5,3 +5,11 @@ pub mod run; /// File storage which can be used by native backends. #[cfg(feature = "persistence")] pub mod file_storage; + +pub(crate) mod winit_integration; + +#[cfg(feature = "glow")] +mod glow_integration; + +#[cfg(feature = "wgpu")] +mod wgpu_integration; diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index bd084a50129..896d640de36 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -1,93 +1,23 @@ -//! Note that this file contains two similar paths - one for [`glow`], one for [`wgpu`]. -//! When making changes to one you often also want to apply it to the other. +use std::{cell::RefCell, time::Instant}; -use std::time::Instant; +use winit::event_loop::{EventLoop, EventLoopBuilder}; -use raw_window_handle::{HasRawDisplayHandle as _, HasRawWindowHandle as _}; -use winit::event_loop::{ - ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget, -}; - -#[cfg(feature = "accesskit")] -use egui_winit::accesskit_winit; -use egui_winit::winit; +use egui::epaint::ahash::HashMap; -use crate::{epi, Result}; +use crate::{ + epi, + native::winit_integration::{short_event_description, EventResult}, + Result, +}; -use super::epi_integration::{self, EpiIntegration}; +use super::winit_integration::{UserEvent, WinitApp}; // ---------------------------------------------------------------------------- -#[derive(Debug)] -pub enum UserEvent { - RequestRepaint { - when: Instant, - - /// What the frame number was when the repaint was _requested_. - frame_nr: u64, - }, - - #[cfg(feature = "accesskit")] - AccessKitActionRequest(accesskit_winit::ActionRequestEvent), -} - -#[cfg(feature = "accesskit")] -impl From for UserEvent { - fn from(inner: accesskit_winit::ActionRequestEvent) -> Self { - Self::AccessKitActionRequest(inner) - } -} - -// ---------------------------------------------------------------------------- - -pub use epi::NativeOptions; - -#[derive(Debug)] -enum EventResult { - Wait, - - /// Causes a synchronous repaint inside the event handler. This should only - /// be used in special situations if the window must be repainted while - /// handling a specific event. This occurs on Windows when handling resizes. - /// - /// `RepaintNow` creates a new frame synchronously, and should therefore - /// only be used for extremely urgent repaints. - RepaintNow, - - /// Queues a repaint for once the event loop handles its next redraw. Exists - /// so that multiple input events can be handled in one frame. Does not - /// cause any delay like `RepaintNow`. - RepaintNext, - - RepaintAt(Instant), - - Exit, -} - -trait WinitApp { - /// The current frame number, as reported by egui. - fn frame_nr(&self) -> u64; - - fn is_focused(&self) -> bool; - - fn integration(&self) -> Option<&EpiIntegration>; - - fn window(&self) -> Option<&winit::window::Window>; - - fn save_and_destroy(&mut self); - - fn run_ui_and_paint(&mut self) -> EventResult; - - fn on_event( - &mut self, - event_loop: &EventLoopWindowTarget, - event: &winit::event::Event<'_, UserEvent>, - ) -> Result; -} - fn create_event_loop_builder( native_options: &mut epi::NativeOptions, ) -> EventLoopBuilder { + crate::profile_function!(); let mut event_loop_builder = winit::event_loop::EventLoopBuilder::with_user_event(); if let Some(hook) = std::mem::take(&mut native_options.event_loop_builder) { @@ -97,69 +27,91 @@ fn create_event_loop_builder( event_loop_builder } +fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result> { + crate::profile_function!(); + let mut builder = create_event_loop_builder(native_options); + + crate::profile_scope!("EventLoopBuilder::build"); + Ok(builder.build()?) +} + /// Access a thread-local event loop. /// /// We reuse the event-loop so we can support closing and opening an eframe window /// multiple times. This is just a limitation of winit. fn with_event_loop( mut native_options: epi::NativeOptions, - f: impl FnOnce(&mut EventLoop, NativeOptions) -> R, -) -> R { - use std::cell::RefCell; + f: impl FnOnce(&mut EventLoop, epi::NativeOptions) -> R, +) -> Result { thread_local!(static EVENT_LOOP: RefCell>> = RefCell::new(None)); EVENT_LOOP.with(|event_loop| { // Since we want to reference NativeOptions when creating the EventLoop we can't // do that as part of the lazy thread local storage initialization and so we instead // create the event loop lazily here - let mut event_loop = event_loop.borrow_mut(); - let event_loop = event_loop - .get_or_insert_with(|| create_event_loop_builder(&mut native_options).build()); - f(event_loop, native_options) + let mut event_loop_lock = event_loop.borrow_mut(); + let event_loop = if let Some(event_loop) = &mut *event_loop_lock { + event_loop + } else { + event_loop_lock.insert(create_event_loop(&mut native_options)?) + }; + Ok(f(event_loop, native_options)) }) } +#[cfg(not(target_os = "ios"))] fn run_and_return( event_loop: &mut EventLoop, mut winit_app: impl WinitApp, ) -> Result<()> { - use winit::platform::run_return::EventLoopExtRunReturn as _; + use winit::{event_loop::ControlFlow, platform::run_on_demand::EventLoopExtRunOnDemand}; - log::debug!("Entering the winit event loop (run_return)…"); + log::trace!("Entering the winit event loop (run_on_demand)…"); - let mut next_repaint_time = Instant::now(); + // When to repaint what window + let mut windows_next_repaint_times = HashMap::default(); let mut returned_result = Ok(()); - event_loop.run_return(|event, event_loop, control_flow| { + event_loop.run_on_demand(|event, event_loop_window_target| { + crate::profile_scope!("winit_event", short_event_description(&event)); + + log::trace!("winit event: {event:?}"); + + if matches!(event, winit::event::Event::AboutToWait) { + return; // early-out: don't trigger another wait + } + let event_result = match &event { - winit::event::Event::LoopDestroyed => { - // On Mac, Cmd-Q we get here and then `run_return` doesn't return (despite its name), + winit::event::Event::LoopExiting => { + // On Mac, Cmd-Q we get here and then `run_on_demand` doesn't return (despite its name), // so we need to save state now: - log::debug!("Received Event::LoopDestroyed - saving app state…"); + log::debug!("Received Event::LoopExiting - saving app state…"); winit_app.save_and_destroy(); - *control_flow = ControlFlow::Exit; return; } - // Platform-dependent event handlers to workaround a winit bug - // See: https://github.com/rust-windowing/winit/issues/987 - // See: https://github.com/rust-windowing/winit/issues/1619 - #[cfg(target_os = "windows")] - winit::event::Event::RedrawEventsCleared => { - next_repaint_time = extremely_far_future(); - winit_app.run_ui_and_paint() - } - #[cfg(not(target_os = "windows"))] - winit::event::Event::RedrawRequested(_) => { - next_repaint_time = extremely_far_future(); - winit_app.run_ui_and_paint() + winit::event::Event::WindowEvent { + event: winit::event::WindowEvent::RedrawRequested, + window_id, + } => { + windows_next_repaint_times.remove(window_id); + winit_app.run_ui_and_paint(event_loop_window_target, *window_id) } - winit::event::Event::UserEvent(UserEvent::RequestRepaint { when, frame_nr }) => { - if winit_app.frame_nr() == *frame_nr { + winit::event::Event::UserEvent(UserEvent::RequestRepaint { + when, + frame_nr, + viewport_id, + }) => { + let current_frame_nr = winit_app.frame_nr(*viewport_id); + if current_frame_nr == *frame_nr || current_frame_nr == *frame_nr + 1 { log::trace!("UserEvent::RequestRepaint scheduling repaint at {when:?}"); - EventResult::RepaintAt(*when) + if let Some(window_id) = winit_app.window_id_from_viewport_id(*viewport_id) { + EventResult::RepaintAt(window_id, *when) + } else { + EventResult::Wait + } } else { log::trace!("Got outdated UserEvent::RequestRepaint"); EventResult::Wait // old request - we've already repainted @@ -173,19 +125,13 @@ fn run_and_return( EventResult::Wait } - winit::event::Event::WindowEvent { window_id, .. } - if winit_app.window().is_none() - || *window_id != winit_app.window().unwrap().id() => - { - // This can happen if we close a window, and then reopen a new one, - // or if we have multiple windows open. - EventResult::Wait - } - - event => match winit_app.on_event(event_loop, event) { - Ok(event_result) => event_result, + event => match winit_app.on_event(event_loop_window_target, event) { + Ok(event_result) => { + log::trace!("event_result: {event_result:?}"); + event_result + } Err(err) => { - log::error!("Exiting because of error: {err:?} on event {event:?}"); + log::error!("Exiting because of error: {err} during event {event:?}"); returned_result = Err(err); EventResult::Exit } @@ -193,48 +139,76 @@ fn run_and_return( }; match event_result { - EventResult::Wait => {} - EventResult::RepaintNow => { - log::trace!("Repaint caused by winit::Event: {:?}", event); + EventResult::Wait => { + event_loop_window_target.set_control_flow(ControlFlow::Wait); + } + EventResult::RepaintNow(window_id) => { + log::trace!( + "RepaintNow of {window_id:?} caused by {}", + short_event_description(&event) + ); if cfg!(target_os = "windows") { // Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280 - next_repaint_time = extremely_far_future(); - winit_app.run_ui_and_paint(); + windows_next_repaint_times.remove(&window_id); + + winit_app.run_ui_and_paint(event_loop_window_target, window_id); } else { // Fix for https://github.com/emilk/egui/issues/2425 - next_repaint_time = Instant::now(); + windows_next_repaint_times.insert(window_id, Instant::now()); } } - EventResult::RepaintNext => { - log::trace!("Repaint caused by winit::Event: {:?}", event); - next_repaint_time = Instant::now(); - } - EventResult::RepaintAt(repaint_time) => { - next_repaint_time = next_repaint_time.min(repaint_time); + EventResult::RepaintNext(window_id) => { + log::trace!( + "RepaintNext of {window_id:?} caused by {}", + short_event_description(&event) + ); + windows_next_repaint_times.insert(window_id, Instant::now()); + } + EventResult::RepaintAt(window_id, repaint_time) => { + windows_next_repaint_times.insert( + window_id, + windows_next_repaint_times + .get(&window_id) + .map_or(repaint_time, |last| (*last).min(repaint_time)), + ); } EventResult::Exit => { log::debug!("Asking to exit event loop…"); winit_app.save_and_destroy(); - *control_flow = ControlFlow::Exit; + event_loop_window_target.exit(); return; } } - *control_flow = if next_repaint_time <= Instant::now() { - if let Some(window) = winit_app.window() { - log::trace!("request_redraw"); - window.request_redraw(); - } - next_repaint_time = extremely_far_future(); - ControlFlow::Poll - } else { - let time_until_next = next_repaint_time.saturating_duration_since(Instant::now()); - if time_until_next < std::time::Duration::from_secs(10_000) { - log::trace!("WaitUntil {time_until_next:?}"); + let mut next_repaint_time = windows_next_repaint_times.values().min().copied(); + + windows_next_repaint_times.retain(|window_id, repaint_time| { + if Instant::now() < *repaint_time { + return true; // not yet ready + }; + + next_repaint_time = None; + event_loop_window_target.set_control_flow(ControlFlow::Poll); + + if let Some(window) = winit_app.window(*window_id) { + log::trace!("request_redraw for {window_id:?}"); + let is_minimized = window.is_minimized().unwrap_or(false); + if is_minimized { + false + } else { + window.request_redraw(); + true + } + } else { + log::trace!("No window found for {window_id:?}"); + false } - ControlFlow::WaitUntil(next_repaint_time) + }); + + if let Some(next_repaint_time) = next_repaint_time { + event_loop_window_target.set_control_flow(ControlFlow::WaitUntil(next_repaint_time)); }; - }); + })?; log::debug!("eframe window closed"); @@ -247,1280 +221,210 @@ fn run_and_return( // we only apply this approach on Windows to minimize the affect. #[cfg(target_os = "windows")] { - event_loop.run_return(|_, _, control_flow| { - control_flow.set_exit(); - }); + event_loop + .run_on_demand(|_, event_loop_window_target| { + event_loop_window_target.exit(); + }) + .ok(); } returned_result } -fn run_and_exit(event_loop: EventLoop, mut winit_app: impl WinitApp + 'static) -> ! { - log::debug!("Entering the winit event loop (run)…"); +fn run_and_exit( + event_loop: EventLoop, + mut winit_app: impl WinitApp + 'static, +) -> Result<()> { + use winit::event_loop::ControlFlow; + log::trace!("Entering the winit event loop (run)…"); + + // When to repaint what window + let mut windows_next_repaint_times = HashMap::default(); - let mut next_repaint_time = Instant::now(); + event_loop.run(move |event, event_loop_window_target| { + crate::profile_scope!("winit_event", short_event_description(&event)); - event_loop.run(move |event, event_loop, control_flow| { - let event_result = match event { - winit::event::Event::LoopDestroyed => { - log::debug!("Received Event::LoopDestroyed"); + log::trace!("winit event: {event:?}"); + + if matches!(event, winit::event::Event::AboutToWait) { + return; // early-out: don't trigger another wait + } + + let event_result = match &event { + winit::event::Event::LoopExiting => { + log::debug!("Received Event::LoopExiting"); EventResult::Exit } - // Platform-dependent event handlers to workaround a winit bug - // See: https://github.com/rust-windowing/winit/issues/987 - // See: https://github.com/rust-windowing/winit/issues/1619 - winit::event::Event::RedrawEventsCleared if cfg!(target_os = "windows") => { - next_repaint_time = extremely_far_future(); - winit_app.run_ui_and_paint() - } - winit::event::Event::RedrawRequested(_) if !cfg!(target_os = "windows") => { - next_repaint_time = extremely_far_future(); - winit_app.run_ui_and_paint() + winit::event::Event::WindowEvent { + event: winit::event::WindowEvent::RedrawRequested, + window_id, + } => { + windows_next_repaint_times.remove(window_id); + winit_app.run_ui_and_paint(event_loop_window_target, *window_id) } - winit::event::Event::UserEvent(UserEvent::RequestRepaint { when, frame_nr }) => { - if winit_app.frame_nr() == frame_nr { - EventResult::RepaintAt(when) + winit::event::Event::UserEvent(UserEvent::RequestRepaint { + when, + frame_nr, + viewport_id, + }) => { + let current_frame_nr = winit_app.frame_nr(*viewport_id); + if current_frame_nr == *frame_nr || current_frame_nr == *frame_nr + 1 { + if let Some(window_id) = winit_app.window_id_from_viewport_id(*viewport_id) { + EventResult::RepaintAt(window_id, *when) + } else { + EventResult::Wait + } } else { + log::trace!("Got outdated UserEvent::RequestRepaint"); EventResult::Wait // old request - we've already repainted } } winit::event::Event::NewEvents(winit::event::StartCause::ResumeTimeReached { .. - }) => EventResult::Wait, // We just woke up to check next_repaint_time + }) => { + log::trace!("Woke up to check next_repaint_time"); + EventResult::Wait + } - event => match winit_app.on_event(event_loop, &event) { - Ok(event_result) => event_result, + event => match winit_app.on_event(event_loop_window_target, event) { + Ok(event_result) => { + log::trace!("event_result: {event_result:?}"); + event_result + } Err(err) => { - panic!("eframe encountered a fatal error: {err}"); + panic!("eframe encountered a fatal error: {err} during event {event:?}"); } }, }; match event_result { - EventResult::Wait => {} - EventResult::RepaintNow => { + EventResult::Wait => { + event_loop_window_target.set_control_flow(ControlFlow::Wait); + } + EventResult::RepaintNow(window_id) => { + log::trace!("RepaintNow caused by {}", short_event_description(&event)); if cfg!(target_os = "windows") { // Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280 - next_repaint_time = extremely_far_future(); - winit_app.run_ui_and_paint(); + windows_next_repaint_times.remove(&window_id); + + winit_app.run_ui_and_paint(event_loop_window_target, window_id); } else { // Fix for https://github.com/emilk/egui/issues/2425 - next_repaint_time = Instant::now(); + windows_next_repaint_times.insert(window_id, Instant::now()); } } - EventResult::RepaintNext => { - next_repaint_time = Instant::now(); + EventResult::RepaintNext(window_id) => { + log::trace!("RepaintNext caused by {}", short_event_description(&event)); + windows_next_repaint_times.insert(window_id, Instant::now()); } - EventResult::RepaintAt(repaint_time) => { - next_repaint_time = next_repaint_time.min(repaint_time); + EventResult::RepaintAt(window_id, repaint_time) => { + windows_next_repaint_times.insert( + window_id, + windows_next_repaint_times + .get(&window_id) + .map_or(repaint_time, |last| (*last).min(repaint_time)), + ); } EventResult::Exit => { log::debug!("Quitting - saving app state…"); winit_app.save_and_destroy(); + + log::debug!("Exiting with return code 0"); #[allow(clippy::exit)] std::process::exit(0); } } - *control_flow = if next_repaint_time <= Instant::now() { - if let Some(window) = winit_app.window() { - window.request_redraw(); - } - next_repaint_time = extremely_far_future(); - ControlFlow::Poll - } else { - ControlFlow::WaitUntil(next_repaint_time) - }; - }) -} - -// ---------------------------------------------------------------------------- -/// Run an egui app -#[cfg(feature = "glow")] -mod glow_integration { - use std::sync::Arc; - - use egui::NumExt as _; - use glutin::{ - display::GetGlDisplay, - prelude::{GlDisplay, NotCurrentGlContextSurfaceAccessor, PossiblyCurrentGlContext}, - surface::GlSurface, - }; - - use super::*; - - // Note: that the current Glutin API design tightly couples the GL context with - // the Window which means it's not practically possible to just destroy the - // window and re-create a new window while continuing to use the same GL context. - // - // For now this means it's not possible to support Android as well as we can with - // wgpu because we're basically forced to destroy and recreate _everything_ when - // the application suspends and resumes. - // - // There is work in progress to improve the Glutin API so it has a separate Surface - // API that would allow us to just destroy a Window/Surface when suspending, see: - // https://github.com/rust-windowing/glutin/pull/1435 - // - - /// State that is initialized when the application is first starts running via - /// a Resumed event. On Android this ensures that any graphics state is only - /// initialized once the application has an associated `SurfaceView`. - struct GlowWinitRunning { - gl: Arc, - painter: egui_glow::Painter, - integration: epi_integration::EpiIntegration, - app: Box, - // Conceptually this will be split out eventually so that the rest of the state - // can be persistent. - gl_window: GlutinWindowContext, - } - - /// This struct will contain both persistent and temporary glutin state. - /// - /// Platform Quirks: - /// * Microsoft Windows: requires that we create a window before opengl context. - /// * Android: window and surface should be destroyed when we receive a suspend event. recreate on resume event. - /// - /// winit guarantees that we will get a Resumed event on startup on all platforms. - /// * Before Resumed event: `gl_config`, `gl_context` can be created at any time. on windows, a window must be created to get `gl_context`. - /// * Resumed: `gl_surface` will be created here. `window` will be re-created here for android. - /// * Suspended: on android, we drop window + surface. on other platforms, we don't get Suspended event. - /// - /// The setup is divided between the `new` fn and `on_resume` fn. we can just assume that `on_resume` is a continuation of - /// `new` fn on all platforms. only on android, do we get multiple resumed events because app can be suspended. - struct GlutinWindowContext { - builder: winit::window::WindowBuilder, - swap_interval: glutin::surface::SwapInterval, - gl_config: glutin::config::Config, - current_gl_context: Option, - gl_surface: Option>, - not_current_gl_context: Option, - window: Option, - } - - impl GlutinWindowContext { - /// There is a lot of complexity with opengl creation, so prefer extensive logging to get all the help we can to debug issues. - /// - #[allow(unsafe_code)] - unsafe fn new( - winit_window_builder: winit::window::WindowBuilder, - native_options: &epi::NativeOptions, - event_loop: &EventLoopWindowTarget, - ) -> Result { - use glutin::prelude::*; - // convert native options to glutin options - let hardware_acceleration = match native_options.hardware_acceleration { - crate::HardwareAcceleration::Required => Some(true), - crate::HardwareAcceleration::Preferred => None, - crate::HardwareAcceleration::Off => Some(false), - }; - let swap_interval = if native_options.vsync { - glutin::surface::SwapInterval::Wait(std::num::NonZeroU32::new(1).unwrap()) - } else { - glutin::surface::SwapInterval::DontWait - }; - /* opengl setup flow goes like this: - 1. we create a configuration for opengl "Display" / "Config" creation - 2. choose between special extensions like glx or egl or wgl and use them to create config/display - 3. opengl context configuration - 4. opengl context creation - */ - // start building config for gl display - let config_template_builder = glutin::config::ConfigTemplateBuilder::new() - .prefer_hardware_accelerated(hardware_acceleration) - .with_depth_size(native_options.depth_buffer) - .with_stencil_size(native_options.stencil_buffer) - .with_transparency(native_options.transparent); - // we don't know if multi sampling option is set. so, check if its more than 0. - let config_template_builder = if native_options.multisampling > 0 { - config_template_builder.with_multisampling( - native_options - .multisampling - .try_into() - .expect("failed to fit multisamples option of native_options into u8"), - ) - } else { - config_template_builder - }; - - log::debug!( - "trying to create glutin Display with config: {:?}", - &config_template_builder - ); - // create gl display. this may probably create a window too on most platforms. definitely on `MS windows`. never on android. - let (window, gl_config) = 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::ApiPrefence::FallbackEgl) // https://github.com/emilk/egui/issues/2520#issuecomment-1367841150 - .with_window_builder(Some(winit_window_builder.clone())) - .build( - event_loop, - config_template_builder.clone(), - |mut config_iterator| { - let config = config_iterator.next().expect( - "failed to find a matching configuration for creating glutin config", - ); - log::debug!( - "using the first config from config picker closure. config: {:?}", - &config - ); - config - }, - ) - .map_err(|e| crate::Error::NoGlutinConfigs(config_template_builder.build(), e))?; - - let gl_display = gl_config.display(); - log::debug!( - "successfully created GL Display with version: {} and supported features: {:?}", - gl_display.version_string(), - gl_display.supported_features() - ); - let raw_window_handle = window.as_ref().map(|w| w.raw_window_handle()); - log::debug!( - "creating gl context using raw window handle: {:?}", - raw_window_handle - ); - - // create gl context. if core context cannot be created, try gl es context as fallback. - let context_attributes = - glutin::context::ContextAttributesBuilder::new().build(raw_window_handle); - let fallback_context_attributes = glutin::context::ContextAttributesBuilder::new() - .with_context_api(glutin::context::ContextApi::Gles(None)) - .build(raw_window_handle); - let gl_context = match gl_config - .display() - .create_context(&gl_config, &context_attributes) - { - Ok(it) => it, - Err(err) => { - log::warn!("failed to create context using default context attributes {context_attributes:?} due to error: {err}"); - log::debug!("retrying with fallback context attributes: {fallback_context_attributes:?}"); - gl_config - .display() - .create_context(&gl_config, &fallback_context_attributes)? - } - }; - let not_current_gl_context = Some(gl_context); - - // the fun part with opengl gl is that we never know whether there is an error. the context creation might have failed, but - // it could keep working until we try to make surface current or swap buffers or something else. future glutin improvements might - // help us start from scratch again if we fail context creation and go back to preferEgl or try with different config etc.. - // https://github.com/emilk/egui/pull/2541#issuecomment-1370767582 - Ok(GlutinWindowContext { - builder: winit_window_builder, - swap_interval, - gl_config, - current_gl_context: None, - window, - gl_surface: None, - not_current_gl_context, - }) - } - - /// This will be run after `new`. on android, it might be called multiple times over the course of the app's lifetime. - /// roughly, - /// 1. check if window already exists. otherwise, create one now. - /// 2. create attributes for surface creation. - /// 3. create surface. - /// 4. make surface and context current. - /// - /// we presently assume that we will - #[allow(unsafe_code)] - fn on_resume(&mut self, event_loop: &EventLoopWindowTarget) -> Result<()> { - if self.gl_surface.is_some() { - log::warn!("on_resume called even thought we already have a surface. early return"); - return Ok(()); - } - log::debug!("running on_resume fn."); - // make sure we have a window or create one. - let window = self.window.take().unwrap_or_else(|| { - log::debug!("window doesn't exist yet. creating one now with finalize_window"); - glutin_winit::finalize_window(event_loop, self.builder.clone(), &self.gl_config) - .expect("failed to finalize glutin window") - }); - // surface attributes - let (width, height): (u32, u32) = window.inner_size().into(); - let width = std::num::NonZeroU32::new(width.at_least(1)).unwrap(); - let height = std::num::NonZeroU32::new(height.at_least(1)).unwrap(); - let surface_attributes = - glutin::surface::SurfaceAttributesBuilder::::new() - .build(window.raw_window_handle(), width, height); - log::debug!( - "creating surface with attributes: {:?}", - &surface_attributes - ); - // create surface - let gl_surface = unsafe { - self.gl_config - .display() - .create_window_surface(&self.gl_config, &surface_attributes)? - }; - log::debug!("surface created successfully: {gl_surface:?}.making context current"); - // make surface and context current. - let not_current_gl_context = self - .not_current_gl_context - .take() - .expect("failed to get not current context after resume event. impossible!"); - let current_gl_context = not_current_gl_context.make_current(&gl_surface)?; - // try setting swap interval. but its not absolutely necessary, so don't panic on failure. - log::debug!("made context current. setting swap interval for surface"); - if let Err(e) = gl_surface.set_swap_interval(¤t_gl_context, self.swap_interval) { - log::error!("failed to set swap interval due to error: {e:?}"); - } - // we will reach this point only once in most platforms except android. - // create window/surface/make context current once and just use them forever. - self.gl_surface = Some(gl_surface); - self.current_gl_context = Some(current_gl_context); - self.window = Some(window); - Ok(()) - } - - /// only applies for android. but we basically drop surface + window and make context not current - fn on_suspend(&mut self) -> Result<()> { - log::debug!("received suspend event. dropping window and surface"); - self.gl_surface.take(); - self.window.take(); - if let Some(current) = self.current_gl_context.take() { - log::debug!("context is current, so making it non-current"); - self.not_current_gl_context = Some(current.make_not_current()?); - } else { - log::debug!("context is already not current??? could be duplicate suspend event"); - } - Ok(()) - } - - fn window(&self) -> &winit::window::Window { - self.window.as_ref().expect("winit window doesn't exist") - } - - fn resize(&self, physical_size: winit::dpi::PhysicalSize) { - let width = std::num::NonZeroU32::new(physical_size.width.at_least(1)).unwrap(); - let height = std::num::NonZeroU32::new(physical_size.height.at_least(1)).unwrap(); - self.gl_surface - .as_ref() - .expect("failed to get surface to resize") - .resize( - self.current_gl_context - .as_ref() - .expect("failed to get current context to resize surface"), - width, - height, - ); - } - - fn swap_buffers(&self) -> glutin::error::Result<()> { - self.gl_surface - .as_ref() - .expect("failed to get surface to swap buffers") - .swap_buffers( - self.current_gl_context - .as_ref() - .expect("failed to get current context to swap buffers"), - ) - } - - fn get_proc_address(&self, addr: &std::ffi::CStr) -> *const std::ffi::c_void { - self.gl_config.display().get_proc_address(addr) - } - } - - struct GlowWinitApp { - repaint_proxy: Arc>>, - app_name: String, - native_options: epi::NativeOptions, - running: Option, - - // Note that since this `AppCreator` is FnOnce we are currently unable to support - // re-initializing the `GlowWinitRunning` state on Android if the application - // suspends and resumes. - app_creator: Option, - is_focused: bool, - } - - impl GlowWinitApp { - fn new( - event_loop: &EventLoop, - app_name: &str, - native_options: epi::NativeOptions, - app_creator: epi::AppCreator, - ) -> Self { - Self { - repaint_proxy: Arc::new(egui::mutex::Mutex::new(event_loop.create_proxy())), - app_name: app_name.to_owned(), - native_options, - running: None, - app_creator: Some(app_creator), - is_focused: true, - } - } - - #[allow(unsafe_code)] - fn create_glutin_windowed_context( - event_loop: &EventLoopWindowTarget, - storage: Option<&dyn epi::Storage>, - title: &str, - native_options: &NativeOptions, - ) -> Result<(GlutinWindowContext, glow::Context)> { - crate::profile_function!(); - - let window_settings = epi_integration::load_window_settings(storage); - - let winit_window_builder = - epi_integration::window_builder(event_loop, title, native_options, window_settings); - let mut glutin_window_context = unsafe { - GlutinWindowContext::new(winit_window_builder, native_options, event_loop)? - }; - glutin_window_context.on_resume(event_loop)?; - - if let Some(window) = &glutin_window_context.window { - epi_integration::apply_native_options_to_window( - window, - native_options, - window_settings, - ); - } - - let gl = unsafe { - 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"); - - glutin_window_context.get_proc_address(&s) - }) - }; - - Ok((glutin_window_context, gl)) - } - - fn init_run_state(&mut self, event_loop: &EventLoopWindowTarget) -> Result<()> { - let storage = epi_integration::create_storage( - self.native_options - .app_id - .as_ref() - .unwrap_or(&self.app_name), - ); - - let (gl_window, gl) = Self::create_glutin_windowed_context( - event_loop, - storage.as_deref(), - &self.app_name, - &self.native_options, - )?; - let gl = Arc::new(gl); - - let painter = - egui_glow::Painter::new(gl.clone(), "", self.native_options.shader_version) - .unwrap_or_else(|err| panic!("An OpenGL error occurred: {err}\n")); - - let system_theme = system_theme(gl_window.window(), &self.native_options); - let mut integration = epi_integration::EpiIntegration::new( - event_loop, - painter.max_texture_side(), - gl_window.window(), - system_theme, - &self.app_name, - &self.native_options, - storage, - Some(gl.clone()), - #[cfg(feature = "wgpu")] - None, - ); - #[cfg(feature = "accesskit")] - { - integration.init_accesskit(gl_window.window(), self.repaint_proxy.lock().clone()); - } - let theme = system_theme.unwrap_or(self.native_options.default_theme); - integration.egui_ctx.set_visuals(theme.egui_visuals()); - - gl_window.window().set_ime_allowed(true); - if self.native_options.mouse_passthrough { - gl_window.window().set_cursor_hittest(false).unwrap(); - } - - { - let event_loop_proxy = self.repaint_proxy.clone(); - integration - .egui_ctx - .set_request_repaint_callback(move |info| { - log::trace!("request_repaint_callback: {info:?}"); - let when = Instant::now() + info.after; - let frame_nr = info.current_frame_nr; - event_loop_proxy - .lock() - .send_event(UserEvent::RequestRepaint { when, frame_nr }) - .ok(); - }); - } - - let app_creator = std::mem::take(&mut self.app_creator) - .expect("Single-use AppCreator has unexpectedly already been taken"); - let mut app = app_creator(&epi::CreationContext { - egui_ctx: integration.egui_ctx.clone(), - integration_info: integration.frame.info(), - storage: integration.frame.storage(), - gl: Some(gl.clone()), - #[cfg(feature = "wgpu")] - wgpu_render_state: None, - raw_display_handle: gl_window.window().raw_display_handle(), - raw_window_handle: gl_window.window().raw_window_handle(), - }); - - if app.warm_up_enabled() { - integration.warm_up(app.as_mut(), gl_window.window()); - } - - self.running = Some(GlowWinitRunning { - gl_window, - gl, - painter, - integration, - app, - }); - - Ok(()) - } - } - - impl WinitApp for GlowWinitApp { - fn frame_nr(&self) -> u64 { - self.running - .as_ref() - .map_or(0, |r| r.integration.egui_ctx.frame_nr()) - } - - fn is_focused(&self) -> bool { - self.is_focused - } - - fn integration(&self) -> Option<&EpiIntegration> { - self.running.as_ref().map(|r| &r.integration) - } - - fn window(&self) -> Option<&winit::window::Window> { - self.running.as_ref().map(|r| r.gl_window.window()) - } + let mut next_repaint_time = windows_next_repaint_times.values().min().copied(); - fn save_and_destroy(&mut self) { - if let Some(mut running) = self.running.take() { - running - .integration - .save(running.app.as_mut(), running.gl_window.window.as_ref()); - running.app.on_exit(Some(&running.gl)); - running.painter.destroy(); + windows_next_repaint_times.retain(|window_id, repaint_time| { + if Instant::now() < *repaint_time { + return true; // not yet ready } - } - - fn run_ui_and_paint(&mut self) -> EventResult { - if let Some(running) = &mut self.running { - if running.gl_window.window.is_none() { - return EventResult::Wait; - } - - #[cfg(feature = "puffin")] - puffin::GlobalProfiler::lock().new_frame(); - crate::profile_scope!("frame"); - - let GlowWinitRunning { - gl_window, - gl, - app, - integration, - painter, - } = running; - let window = gl_window.window(); - - let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); - - egui_glow::painter::clear( - gl, - screen_size_in_pixels, - app.clear_color(&integration.egui_ctx.style().visuals), - ); - - let egui::FullOutput { - platform_output, - repaint_after, - textures_delta, - shapes, - } = integration.update(app.as_mut(), window); - - integration.handle_platform_output(window, platform_output); - - let clipped_primitives = { - crate::profile_scope!("tessellate"); - integration.egui_ctx.tessellate(shapes) - }; - - painter.paint_and_update_textures( - screen_size_in_pixels, - integration.egui_ctx.pixels_per_point(), - &clipped_primitives, - &textures_delta, - ); - - let screenshot_requested = &mut integration.frame.output.screenshot_requested; - - if *screenshot_requested { - *screenshot_requested = false; - let screenshot = painter.read_screen_rgba(screen_size_in_pixels); - integration.frame.screenshot.set(Some(screenshot)); - } - - integration.post_rendering(app.as_mut(), window); - - { - crate::profile_scope!("swap_buffers"); - gl_window.swap_buffers().unwrap(); - } - - integration.post_present(window); - - #[cfg(feature = "__screenshot")] - // give it time to settle: - if integration.egui_ctx.frame_nr() == 2 { - if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") { - assert!( - path.ends_with(".png"), - "Expected EFRAME_SCREENSHOT_TO to end with '.png', got {path:?}" - ); - let screenshot = painter.read_screen_rgba(screen_size_in_pixels); - image::save_buffer( - &path, - screenshot.as_raw(), - screenshot.width() as u32, - screenshot.height() as u32, - image::ColorType::Rgba8, - ) - .unwrap_or_else(|err| { - panic!("Failed to save screenshot to {path:?}: {err}"); - }); - eprintln!("Screenshot saved to {path:?}."); - std::process::exit(0); - } - } + next_repaint_time = None; + event_loop_window_target.set_control_flow(ControlFlow::Poll); - let control_flow = if integration.should_close() { - EventResult::Exit - } else if repaint_after.is_zero() { - EventResult::RepaintNext - } else if let Some(repaint_after_instant) = - std::time::Instant::now().checked_add(repaint_after) - { - // if repaint_after is something huge and can't be added to Instant, - // we will use `ControlFlow::Wait` instead. - // technically, this might lead to some weird corner cases where the user *WANTS* - // winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own - // egui backend impl i guess. - EventResult::RepaintAt(repaint_after_instant) + if let Some(window) = winit_app.window(*window_id) { + log::trace!("request_redraw for {window_id:?}"); + let is_minimized = window.is_minimized().unwrap_or(false); + if is_minimized { + false } else { - EventResult::Wait - }; - - integration.maybe_autosave(app.as_mut(), window); - - 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!("bg_sleep"); - std::thread::sleep(std::time::Duration::from_millis(10)); + window.request_redraw(); + true } - - control_flow } else { - EventResult::Wait + log::trace!("No window found for {window_id:?}"); + false } - } + }); - fn on_event( - &mut self, - event_loop: &EventLoopWindowTarget, - event: &winit::event::Event<'_, UserEvent>, - ) -> Result { - Ok(match event { - winit::event::Event::Resumed => { - // first resume event. - // we can actually move this outside of event loop. - // and just run the on_resume fn of gl_window - if self.running.is_none() { - self.init_run_state(event_loop)?; - } else { - // not the first resume event. create whatever you need. - self.running - .as_mut() - .unwrap() - .gl_window - .on_resume(event_loop)?; - } - EventResult::RepaintNow - } - winit::event::Event::Suspended => { - self.running.as_mut().unwrap().gl_window.on_suspend()?; + if let Some(next_repaint_time) = next_repaint_time { + // WaitUntil seems to not work on iOS + #[cfg(target_os = "ios")] + winit_app + .get_window_winit_id(ViewportId::ROOT) + .map(|window_id| { + winit_app + .window(window_id) + .map(|window| window.request_redraw()) + }); + + event_loop_window_target.set_control_flow(ControlFlow::WaitUntil(next_repaint_time)); + }; + })?; - EventResult::Wait - } + log::debug!("winit event loop unexpectedly returned"); - winit::event::Event::WindowEvent { event, .. } => { - if let Some(running) = &mut self.running { - // On Windows, if a window is resized by the user, it should repaint synchronously, inside the - // event handler. - // - // If this is not done, the compositor will assume that the window does not want to redraw, - // and continue ahead. - // - // In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver - // new frames to the compositor in time. - // - // The flickering is technically glutin or glow's fault, but we should be responding properly - // to resizes anyway, as doing so avoids dropping frames. - // - // See: https://github.com/emilk/egui/issues/903 - let mut repaint_asap = false; - - match &event { - winit::event::WindowEvent::Focused(new_focused) => { - self.is_focused = *new_focused; - } - winit::event::WindowEvent::Resized(physical_size) => { - repaint_asap = true; - - // Resize with 0 width and height is used by winit to signal a minimize event on Windows. - // See: https://github.com/rust-windowing/winit/issues/208 - // This solves an issue where the app would panic when minimizing on Windows. - if physical_size.width > 0 && physical_size.height > 0 { - running.gl_window.resize(*physical_size); - } - } - winit::event::WindowEvent::ScaleFactorChanged { - new_inner_size, - .. - } => { - repaint_asap = true; - running.gl_window.resize(**new_inner_size); - } - winit::event::WindowEvent::CloseRequested - if running.integration.should_close() => - { - log::debug!("Received WindowEvent::CloseRequested"); - return Ok(EventResult::Exit); - } - _ => {} - } - - let event_response = - running.integration.on_event(running.app.as_mut(), event); - - if running.integration.should_close() { - EventResult::Exit - } else if event_response.repaint { - if repaint_asap { - EventResult::RepaintNow - } else { - EventResult::RepaintNext - } - } else { - EventResult::Wait - } - } else { - EventResult::Wait - } - } - #[cfg(feature = "accesskit")] - winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest( - accesskit_winit::ActionRequestEvent { request, .. }, - )) => { - if let Some(running) = &mut self.running { - running - .integration - .on_accesskit_action_request(request.clone()); - // As a form of user input, accessibility actions should - // lead to a repaint. - EventResult::RepaintNext - } else { - EventResult::Wait - } - } - _ => EventResult::Wait, - }) - } - } - - pub fn run_glow( - app_name: &str, - mut native_options: epi::NativeOptions, - app_creator: epi::AppCreator, - ) -> Result<()> { - if native_options.run_and_return { - with_event_loop(native_options, |event_loop, native_options| { - let glow_eframe = - GlowWinitApp::new(event_loop, app_name, native_options, app_creator); - run_and_return(event_loop, glow_eframe) - }) - } else { - let event_loop = create_event_loop_builder(&mut native_options).build(); - let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator); - run_and_exit(event_loop, glow_eframe); - } - } + Ok(()) } -#[cfg(feature = "glow")] -pub use glow_integration::run_glow; // ---------------------------------------------------------------------------- -#[cfg(feature = "wgpu")] -mod wgpu_integration { - use std::sync::Arc; - - use super::*; - - /// State that is initialized when the application is first starts running via - /// a Resumed event. On Android this ensures that any graphics state is only - /// initialized once the application has an associated `SurfaceView`. - struct WgpuWinitRunning { - painter: egui_wgpu::winit::Painter, - integration: epi_integration::EpiIntegration, - app: Box, - } - - struct WgpuWinitApp { - repaint_proxy: Arc>>, - app_name: String, - native_options: epi::NativeOptions, - app_creator: Option, - running: Option, - - /// Window surface state that's initialized when the app starts running via a Resumed event - /// and on Android will also be destroyed if the application is paused. - window: Option, - is_focused: bool, - } - - impl WgpuWinitApp { - fn new( - event_loop: &EventLoop, - app_name: &str, - native_options: epi::NativeOptions, - app_creator: epi::AppCreator, - ) -> Self { - #[cfg(feature = "__screenshot")] - assert!( - std::env::var("EFRAME_SCREENSHOT_TO").is_err(), - "EFRAME_SCREENSHOT_TO not yet implemented for wgpu backend" - ); - - Self { - repaint_proxy: Arc::new(std::sync::Mutex::new(event_loop.create_proxy())), - app_name: app_name.to_owned(), - native_options, - running: None, - window: None, - app_creator: Some(app_creator), - is_focused: true, - } - } - - fn create_window( - event_loop: &EventLoopWindowTarget, - storage: Option<&dyn epi::Storage>, - title: &str, - native_options: &NativeOptions, - ) -> std::result::Result { - let window_settings = epi_integration::load_window_settings(storage); - let window_builder = - epi_integration::window_builder(event_loop, title, native_options, window_settings); - let window = window_builder.build(event_loop)?; - epi_integration::apply_native_options_to_window( - &window, - native_options, - window_settings, - ); - Ok(window) - } - - #[allow(unsafe_code)] - fn set_window( - &mut self, - window: winit::window::Window, - ) -> std::result::Result<(), egui_wgpu::WgpuError> { - self.window = Some(window); - if let Some(running) = &mut self.running { - pollster::block_on(running.painter.set_window(self.window.as_ref()))?; - } - Ok(()) - } - - #[allow(unsafe_code)] - #[cfg(target_os = "android")] - fn drop_window(&mut self) -> std::result::Result<(), egui_wgpu::WgpuError> { - self.window = None; - if let Some(running) = &mut self.running { - pollster::block_on(running.painter.set_window(None))?; - } - Ok(()) - } - - fn init_run_state( - &mut self, - event_loop: &EventLoopWindowTarget, - storage: Option>, - window: winit::window::Window, - ) -> std::result::Result<(), egui_wgpu::WgpuError> { - #[allow(unsafe_code, unused_mut, unused_unsafe)] - let mut painter = egui_wgpu::winit::Painter::new( - self.native_options.wgpu_options.clone(), - self.native_options.multisampling.max(1) as _, - egui_wgpu::depth_format_from_bits( - self.native_options.depth_buffer, - self.native_options.stencil_buffer, - ), - self.native_options.transparent, - ); - pollster::block_on(painter.set_window(Some(&window)))?; - - let wgpu_render_state = painter.render_state(); - - let system_theme = system_theme(&window, &self.native_options); - let mut integration = epi_integration::EpiIntegration::new( - event_loop, - painter.max_texture_side().unwrap_or(2048), - &window, - system_theme, - &self.app_name, - &self.native_options, - storage, - #[cfg(feature = "glow")] - None, - wgpu_render_state.clone(), - ); - #[cfg(feature = "accesskit")] - { - integration.init_accesskit(&window, self.repaint_proxy.lock().unwrap().clone()); - } - let theme = system_theme.unwrap_or(self.native_options.default_theme); - integration.egui_ctx.set_visuals(theme.egui_visuals()); - - window.set_ime_allowed(true); - - { - let event_loop_proxy = self.repaint_proxy.clone(); - integration - .egui_ctx - .set_request_repaint_callback(move |info| { - log::trace!("request_repaint_callback: {info:?}"); - let when = Instant::now() + info.after; - let frame_nr = info.current_frame_nr; - event_loop_proxy - .lock() - .unwrap() - .send_event(UserEvent::RequestRepaint { when, frame_nr }) - .ok(); - }); - } - - let app_creator = std::mem::take(&mut self.app_creator) - .expect("Single-use AppCreator has unexpectedly already been taken"); - let mut app = app_creator(&epi::CreationContext { - egui_ctx: integration.egui_ctx.clone(), - integration_info: integration.frame.info(), - storage: integration.frame.storage(), - #[cfg(feature = "glow")] - gl: None, - wgpu_render_state, - raw_display_handle: window.raw_display_handle(), - raw_window_handle: window.raw_window_handle(), - }); - - if app.warm_up_enabled() { - integration.warm_up(app.as_mut(), &window); - } - - self.running = Some(WgpuWinitRunning { - painter, - integration, - app, - }); - self.window = Some(window); - - Ok(()) - } - } - - impl WinitApp for WgpuWinitApp { - fn frame_nr(&self) -> u64 { - self.running - .as_ref() - .map_or(0, |r| r.integration.egui_ctx.frame_nr()) - } - - fn is_focused(&self) -> bool { - self.is_focused - } - - fn integration(&self) -> Option<&EpiIntegration> { - self.running.as_ref().map(|r| &r.integration) - } - - fn window(&self) -> Option<&winit::window::Window> { - self.window.as_ref() - } - - fn save_and_destroy(&mut self) { - if let Some(mut running) = self.running.take() { - running - .integration - .save(running.app.as_mut(), self.window.as_ref()); - - #[cfg(feature = "glow")] - running.app.on_exit(None); - - #[cfg(not(feature = "glow"))] - running.app.on_exit(); - - running.painter.destroy(); - } - } - - fn run_ui_and_paint(&mut self) -> EventResult { - if let (Some(running), Some(window)) = (&mut self.running, &self.window) { - #[cfg(feature = "puffin")] - puffin::GlobalProfiler::lock().new_frame(); - crate::profile_scope!("frame"); - - let WgpuWinitRunning { - app, - integration, - painter, - } = running; - - let egui::FullOutput { - platform_output, - repaint_after, - textures_delta, - shapes, - } = integration.update(app.as_mut(), window); - - integration.handle_platform_output(window, platform_output); - - let clipped_primitives = { - crate::profile_scope!("tessellate"); - integration.egui_ctx.tessellate(shapes) - }; - - let screenshot_requested = &mut integration.frame.output.screenshot_requested; - - let screenshot = painter.paint_and_update_textures( - integration.egui_ctx.pixels_per_point(), - app.clear_color(&integration.egui_ctx.style().visuals), - &clipped_primitives, - &textures_delta, - *screenshot_requested, - ); - *screenshot_requested = false; - integration.frame.screenshot.set(screenshot); - - integration.post_rendering(app.as_mut(), window); - integration.post_present(window); - - let control_flow = if integration.should_close() { - EventResult::Exit - } else if repaint_after.is_zero() { - EventResult::RepaintNext - } else if let Some(repaint_after_instant) = - std::time::Instant::now().checked_add(repaint_after) - { - // if repaint_after is something huge and can't be added to Instant, - // we will use `ControlFlow::Wait` instead. - // technically, this might lead to some weird corner cases where the user *WANTS* - // winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own - // egui backend impl i guess. - EventResult::RepaintAt(repaint_after_instant) - } else { - EventResult::Wait - }; - - integration.maybe_autosave(app.as_mut(), window); - - 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!("bg_sleep"); - std::thread::sleep(std::time::Duration::from_millis(10)); - } - - control_flow - } else { - EventResult::Wait - } - } - - fn on_event( - &mut self, - event_loop: &EventLoopWindowTarget, - event: &winit::event::Event<'_, UserEvent>, - ) -> Result { - Ok(match event { - winit::event::Event::Resumed => { - if let Some(running) = &self.running { - if self.window.is_none() { - let window = Self::create_window( - event_loop, - running.integration.frame.storage(), - &self.app_name, - &self.native_options, - )?; - self.set_window(window)?; - } - } else { - let storage = epi_integration::create_storage( - self.native_options - .app_id - .as_ref() - .unwrap_or(&self.app_name), - ); - let window = Self::create_window( - event_loop, - storage.as_deref(), - &self.app_name, - &self.native_options, - )?; - self.init_run_state(event_loop, storage, window)?; - } - EventResult::RepaintNow - } - winit::event::Event::Suspended => { - #[cfg(target_os = "android")] - self.drop_window()?; - EventResult::Wait - } - - winit::event::Event::WindowEvent { event, .. } => { - if let Some(running) = &mut self.running { - // On Windows, if a window is resized by the user, it should repaint synchronously, inside the - // event handler. - // - // If this is not done, the compositor will assume that the window does not want to redraw, - // and continue ahead. - // - // In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver - // new frames to the compositor in time. - // - // The flickering is technically glutin or glow's fault, but we should be responding properly - // to resizes anyway, as doing so avoids dropping frames. - // - // See: https://github.com/emilk/egui/issues/903 - let mut repaint_asap = false; - - match &event { - winit::event::WindowEvent::Focused(new_focused) => { - self.is_focused = *new_focused; - } - winit::event::WindowEvent::Resized(physical_size) => { - repaint_asap = true; - - // Resize with 0 width and height is used by winit to signal a minimize event on Windows. - // See: https://github.com/rust-windowing/winit/issues/208 - // This solves an issue where the app would panic when minimizing on Windows. - if physical_size.width > 0 && physical_size.height > 0 { - running.painter.on_window_resized( - physical_size.width, - physical_size.height, - ); - } - } - winit::event::WindowEvent::ScaleFactorChanged { - new_inner_size, - .. - } => { - repaint_asap = true; - running - .painter - .on_window_resized(new_inner_size.width, new_inner_size.height); - } - winit::event::WindowEvent::CloseRequested - if running.integration.should_close() => - { - log::debug!("Received WindowEvent::CloseRequested"); - return Ok(EventResult::Exit); - } - _ => {} - }; - - let event_response = - running.integration.on_event(running.app.as_mut(), event); - if running.integration.should_close() { - EventResult::Exit - } else if event_response.repaint { - if repaint_asap { - EventResult::RepaintNow - } else { - EventResult::RepaintNext - } - } else { - EventResult::Wait - } - } else { - EventResult::Wait - } - } - #[cfg(feature = "accesskit")] - winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest( - accesskit_winit::ActionRequestEvent { request, .. }, - )) => { - if let Some(running) = &mut self.running { - running - .integration - .on_accesskit_action_request(request.clone()); - // As a form of user input, accessibility actions should - // lead to a repaint. - EventResult::RepaintNext - } else { - EventResult::Wait - } - } - _ => EventResult::Wait, - }) - } +#[cfg(feature = "glow")] +pub fn run_glow( + app_name: &str, + mut native_options: epi::NativeOptions, + app_creator: epi::AppCreator, +) -> Result<()> { + use super::glow_integration::GlowWinitApp; + + #[cfg(not(target_os = "ios"))] + if native_options.run_and_return { + return with_event_loop(native_options, |event_loop, native_options| { + let glow_eframe = GlowWinitApp::new(event_loop, app_name, native_options, app_creator); + run_and_return(event_loop, glow_eframe) + })?; } - pub fn run_wgpu( - app_name: &str, - mut native_options: epi::NativeOptions, - app_creator: epi::AppCreator, - ) -> Result<()> { - if native_options.run_and_return { - with_event_loop(native_options, |event_loop, native_options| { - let wgpu_eframe = - WgpuWinitApp::new(event_loop, app_name, native_options, app_creator); - run_and_return(event_loop, wgpu_eframe) - }) - } else { - let event_loop = create_event_loop_builder(&mut native_options).build(); - let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator); - run_and_exit(event_loop, wgpu_eframe); - } - } + let event_loop = create_event_loop(&mut native_options)?; + let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator); + run_and_exit(event_loop, glow_eframe) } -#[cfg(feature = "wgpu")] -pub use wgpu_integration::run_wgpu; - // ---------------------------------------------------------------------------- -fn system_theme(window: &winit::window::Window, options: &NativeOptions) -> Option { - if options.follow_system_theme { - window - .theme() - .map(super::epi_integration::theme_from_winit_theme) - } else { - None +#[cfg(feature = "wgpu")] +pub fn run_wgpu( + app_name: &str, + mut native_options: epi::NativeOptions, + app_creator: epi::AppCreator, +) -> Result<()> { + use super::wgpu_integration::WgpuWinitApp; + + #[cfg(not(target_os = "ios"))] + if native_options.run_and_return { + return with_event_loop(native_options, |event_loop, native_options| { + let wgpu_eframe = WgpuWinitApp::new(event_loop, app_name, native_options, app_creator); + run_and_return(event_loop, wgpu_eframe) + })?; } -} - -// ---------------------------------------------------------------------------- -fn extremely_far_future() -> std::time::Instant { - std::time::Instant::now() + std::time::Duration::from_secs(10_000_000_000) + let event_loop = create_event_loop(&mut native_options)?; + let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator); + run_and_exit(event_loop, wgpu_eframe) } diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs new file mode 100644 index 00000000000..61bf157c007 --- /dev/null +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -0,0 +1,1132 @@ +//! Note that this file contains code very similar to [`glow_integration`]. +//! When making changes to one you often also want to apply it to the other. +//! +//! This is also very complex code, and not very pretty. +//! There is a bunch of improvements we could do, +//! like removing a bunch of `unwraps`. + +use std::{cell::RefCell, rc::Rc, sync::Arc, time::Instant}; + +use parking_lot::Mutex; +use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _}; +use winit::{ + event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}, + window::{Window, WindowId}, +}; + +use egui::{ + ahash::HashMap, DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportBuilder, + ViewportClass, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, + ViewportOutput, +}; +#[cfg(feature = "accesskit")] +use egui_winit::accesskit_winit; + +use crate::{ + native::{epi_integration::EpiIntegration, winit_integration::EventResult}, + App, AppCreator, CreationContext, NativeOptions, Result, Storage, UserEvent, +}; + +use super::{winit_integration::WinitApp, *}; + +// ---------------------------------------------------------------------------- +// Types: + +pub struct WgpuWinitApp { + repaint_proxy: Arc>>, + app_name: String, + native_options: NativeOptions, + + /// Set at initialization, then taken and set to `None` in `init_run_state`. + app_creator: Option, + + /// Set when we are actually up and running. + running: Option, +} + +/// State that is initialized when the application is first starts running via +/// a Resumed event. On Android this ensures that any graphics state is only +/// initialized once the application has an associated `SurfaceView`. +struct WgpuWinitRunning { + integration: EpiIntegration, + + /// The users application. + app: Box, + + /// Wrapped in an `Rc>` so it can be re-entrantly shared via a weak-pointer. + shared: Rc>, +} + +/// Everything needed by the immediate viewport renderer.\ +/// +/// This is shared by all viewports. +/// +/// Wrapped in an `Rc>` so it can be re-entrantly shared via a weak-pointer. +pub struct SharedState { + egui_ctx: egui::Context, + viewports: Viewports, + painter: egui_wgpu::winit::Painter, + viewport_from_window: HashMap, + focused_viewport: Option, +} + +pub type Viewports = ViewportIdMap; + +pub struct Viewport { + ids: ViewportIdPair, + class: ViewportClass, + builder: ViewportBuilder, + info: ViewportInfo, + screenshot_requested: bool, + + /// `None` for sync viewports. + viewport_ui_cb: Option>, + + /// Window surface state that's initialized when the app starts running via a Resumed event + /// and on Android will also be destroyed if the application is paused. + window: Option>, + + /// `window` and `egui_winit` are initialized together. + egui_winit: Option, +} + +// ---------------------------------------------------------------------------- + +impl WgpuWinitApp { + pub fn new( + event_loop: &EventLoop, + app_name: &str, + native_options: NativeOptions, + app_creator: AppCreator, + ) -> Self { + crate::profile_function!(); + + #[cfg(feature = "__screenshot")] + assert!( + std::env::var("EFRAME_SCREENSHOT_TO").is_err(), + "EFRAME_SCREENSHOT_TO not yet implemented for wgpu backend" + ); + + Self { + repaint_proxy: Arc::new(Mutex::new(event_loop.create_proxy())), + app_name: app_name.to_owned(), + native_options, + running: None, + app_creator: Some(app_creator), + } + } + + /// Create a window for all viewports lacking one. + fn initialized_all_windows(&mut self, event_loop: &EventLoopWindowTarget) { + let Some(running) = &mut self.running else { + return; + }; + let mut shared = running.shared.borrow_mut(); + let SharedState { + viewports, + painter, + viewport_from_window, + .. + } = &mut *shared; + + for viewport in viewports.values_mut() { + viewport.initialize_window( + event_loop, + &running.integration.egui_ctx, + viewport_from_window, + painter, + ); + } + } + + #[cfg(target_os = "android")] + fn recreate_window( + &self, + event_loop: &EventLoopWindowTarget, + running: &WgpuWinitRunning, + ) { + let SharedState { + egui_ctx, + viewports, + viewport_from_window, + painter, + .. + } = &mut *running.shared.borrow_mut(); + + initialize_or_update_viewport( + egui_ctx, + viewports, + ViewportIdPair::ROOT, + ViewportClass::Root, + self.native_options.viewport.clone(), + None, + None, + ) + .initialize_window(event_loop, egui_ctx, viewport_from_window, painter); + } + + #[cfg(target_os = "android")] + fn drop_window(&mut self) -> Result<(), egui_wgpu::WgpuError> { + if let Some(running) = &mut self.running { + let mut shared = running.shared.borrow_mut(); + shared.viewports.remove(&ViewportId::ROOT); + pollster::block_on(shared.painter.set_window(ViewportId::ROOT, None))?; + } + Ok(()) + } + + fn init_run_state( + &mut self, + egui_ctx: egui::Context, + event_loop: &EventLoopWindowTarget, + storage: Option>, + window: Window, + builder: ViewportBuilder, + ) -> Result<&mut WgpuWinitRunning, egui_wgpu::WgpuError> { + crate::profile_function!(); + + #[allow(unsafe_code, unused_mut, unused_unsafe)] + let mut painter = egui_wgpu::winit::Painter::new( + self.native_options.wgpu_options.clone(), + self.native_options.multisampling.max(1) as _, + egui_wgpu::depth_format_from_bits( + self.native_options.depth_buffer, + self.native_options.stencil_buffer, + ), + self.native_options.viewport.transparent.unwrap_or(false), + ); + + let window = Arc::new(window); + + { + crate::profile_scope!("set_window"); + pollster::block_on(painter.set_window(ViewportId::ROOT, Some(window.clone())))?; + } + + let wgpu_render_state = painter.render_state(); + + let system_theme = winit_integration::system_theme(&window, &self.native_options); + let integration = EpiIntegration::new( + egui_ctx.clone(), + &window, + system_theme, + &self.app_name, + &self.native_options, + storage, + #[cfg(feature = "glow")] + None, + wgpu_render_state.clone(), + ); + + { + let event_loop_proxy = self.repaint_proxy.clone(); + + egui_ctx.set_request_repaint_callback(move |info| { + log::trace!("request_repaint_callback: {info:?}"); + let when = Instant::now() + info.delay; + let frame_nr = info.current_frame_nr; + + event_loop_proxy + .lock() + .send_event(UserEvent::RequestRepaint { + when, + frame_nr, + viewport_id: info.viewport_id, + }) + .ok(); + }); + } + + #[allow(unused_mut)] // used for accesskit + let mut egui_winit = egui_winit::State::new( + egui_ctx.clone(), + ViewportId::ROOT, + event_loop, + Some(window.scale_factor() as f32), + painter.max_texture_side(), + ); + + #[cfg(feature = "accesskit")] + { + let event_loop_proxy = self.repaint_proxy.lock().clone(); + integration.init_accesskit(&mut egui_winit, &window, event_loop_proxy); + } + let theme = system_theme.unwrap_or(self.native_options.default_theme); + egui_ctx.set_visuals(theme.egui_visuals()); + + let app_creator = std::mem::take(&mut self.app_creator) + .expect("Single-use AppCreator has unexpectedly already been taken"); + let cc = CreationContext { + egui_ctx: egui_ctx.clone(), + integration_info: integration.frame.info().clone(), + storage: integration.frame.storage(), + #[cfg(feature = "glow")] + gl: None, + #[cfg(feature = "glow")] + get_proc_address: None, + wgpu_render_state, + raw_display_handle: window.display_handle().map(|h| h.as_raw()), + raw_window_handle: window.window_handle().map(|h| h.as_raw()), + }; + let app = { + crate::profile_scope!("user_app_creator"); + app_creator(&cc) + }; + + let mut viewport_from_window = HashMap::default(); + viewport_from_window.insert(window.id(), ViewportId::ROOT); + + let mut viewports = Viewports::default(); + viewports.insert( + ViewportId::ROOT, + Viewport { + ids: ViewportIdPair::ROOT, + class: ViewportClass::Root, + builder, + info: ViewportInfo { + minimized: window.is_minimized(), + maximized: Some(window.is_maximized()), + ..Default::default() + }, + screenshot_requested: false, + viewport_ui_cb: None, + window: Some(window), + egui_winit: Some(egui_winit), + }, + ); + + let shared = Rc::new(RefCell::new(SharedState { + egui_ctx, + viewport_from_window, + viewports, + painter, + focused_viewport: Some(ViewportId::ROOT), + })); + + { + // Create a weak pointer so that we don't keep state alive for too long. + let shared = Rc::downgrade(&shared); + let beginning = integration.beginning; + + let event_loop: *const EventLoopWindowTarget = event_loop; + + egui::Context::set_immediate_viewport_renderer(move |_egui_ctx, immediate_viewport| { + if let Some(shared) = shared.upgrade() { + // SAFETY: the event loop lives longer than + // the Rc:s we just upgraded above. + #[allow(unsafe_code)] + let event_loop = unsafe { event_loop.as_ref().unwrap() }; + + render_immediate_viewport(event_loop, beginning, &shared, immediate_viewport); + } else { + log::warn!("render_sync_callback called after window closed"); + } + }); + } + + Ok(self.running.insert(WgpuWinitRunning { + integration, + app, + shared, + })) + } +} + +impl WinitApp for WgpuWinitApp { + fn frame_nr(&self, viewport_id: ViewportId) -> u64 { + self.running + .as_ref() + .map_or(0, |r| r.integration.egui_ctx.frame_nr_for(viewport_id)) + } + + fn is_focused(&self, window_id: WindowId) -> bool { + if let Some(running) = &self.running { + let shared = running.shared.borrow(); + let viewport_id = shared.viewport_from_window.get(&window_id).copied(); + shared.focused_viewport.is_some() && shared.focused_viewport == viewport_id + } else { + false + } + } + + fn integration(&self) -> Option<&EpiIntegration> { + self.running.as_ref().map(|r| &r.integration) + } + + fn window(&self, window_id: WindowId) -> Option> { + self.running + .as_ref() + .and_then(|r| { + let shared = r.shared.borrow(); + shared + .viewport_from_window + .get(&window_id) + .and_then(|id| shared.viewports.get(id).map(|v| v.window.clone())) + }) + .flatten() + } + + fn window_id_from_viewport_id(&self, id: ViewportId) -> Option { + Some( + self.running + .as_ref()? + .shared + .borrow() + .viewports + .get(&id)? + .window + .as_ref()? + .id(), + ) + } + + fn save_and_destroy(&mut self) { + if let Some(mut running) = self.running.take() { + running.save_and_destroy(); + } + } + + fn run_ui_and_paint( + &mut self, + event_loop: &EventLoopWindowTarget, + window_id: WindowId, + ) -> EventResult { + self.initialized_all_windows(event_loop); + + if let Some(running) = &mut self.running { + running.run_ui_and_paint(window_id) + } else { + EventResult::Wait + } + } + + fn on_event( + &mut self, + event_loop: &EventLoopWindowTarget, + event: &winit::event::Event, + ) -> Result { + crate::profile_function!(winit_integration::short_event_description(event)); + + self.initialized_all_windows(event_loop); + + Ok(match event { + winit::event::Event::Resumed => { + log::debug!("Event::Resumed"); + + let running = if let Some(running) = &self.running { + #[cfg(target_os = "android")] + self.recreate_window(event_loop, running); + running + } else { + let storage = epi_integration::create_storage( + self.native_options + .viewport + .app_id + .as_ref() + .unwrap_or(&self.app_name), + ); + let egui_ctx = winit_integration::create_egui_context(storage.as_deref()); + let (window, builder) = create_window( + &egui_ctx, + event_loop, + storage.as_deref(), + &mut self.native_options, + )?; + self.init_run_state(egui_ctx, event_loop, storage, window, builder)? + }; + + EventResult::RepaintNow( + running.shared.borrow().viewports[&ViewportId::ROOT] + .window + .as_ref() + .unwrap() + .id(), + ) + } + + winit::event::Event::Suspended => { + #[cfg(target_os = "android")] + self.drop_window()?; + EventResult::Wait + } + + winit::event::Event::WindowEvent { event, window_id } => { + if let Some(running) = &mut self.running { + running.on_window_event(*window_id, event) + } else { + EventResult::Wait + } + } + + winit::event::Event::DeviceEvent { + device_id: _, + event: winit::event::DeviceEvent::MouseMotion { delta }, + } => { + if let Some(running) = &mut self.running { + let mut shared = running.shared.borrow_mut(); + if let Some(viewport) = shared + .focused_viewport + .and_then(|viewport| shared.viewports.get_mut(&viewport)) + { + if let Some(egui_winit) = viewport.egui_winit.as_mut() { + egui_winit.on_mouse_motion(*delta); + } + + if let Some(window) = viewport.window.as_ref() { + EventResult::RepaintNext(window.id()) + } else { + EventResult::Wait + } + } else { + EventResult::Wait + } + } else { + EventResult::Wait + } + } + + #[cfg(feature = "accesskit")] + winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest( + accesskit_winit::ActionRequestEvent { request, window_id }, + )) => { + if let Some(running) = &mut self.running { + let mut shared_lock = running.shared.borrow_mut(); + let SharedState { + viewport_from_window, + viewports, + .. + } = &mut *shared_lock; + if let Some(viewport) = viewport_from_window + .get(window_id) + .and_then(|id| viewports.get_mut(id)) + { + if let Some(egui_winit) = &mut viewport.egui_winit { + egui_winit.on_accesskit_action_request(request.clone()); + } + } + // As a form of user input, accessibility actions should + // lead to a repaint. + EventResult::RepaintNext(*window_id) + } else { + EventResult::Wait + } + } + _ => EventResult::Wait, + }) + } +} + +impl WgpuWinitRunning { + fn save_and_destroy(&mut self) { + crate::profile_function!(); + + let mut shared = self.shared.borrow_mut(); + if let Some(Viewport { window, .. }) = shared.viewports.get(&ViewportId::ROOT) { + self.integration.save(self.app.as_mut(), window.as_deref()); + } + + #[cfg(feature = "glow")] + self.app.on_exit(None); + + #[cfg(not(feature = "glow"))] + self.app.on_exit(); + + shared.painter.destroy(); + } + + /// This is called both for the root viewport, and all deferred viewports + fn run_ui_and_paint(&mut self, window_id: WindowId) -> EventResult { + crate::profile_function!(); + + let Some(viewport_id) = self + .shared + .borrow() + .viewport_from_window + .get(&window_id) + .copied() + else { + return EventResult::Wait; + }; + + #[cfg(feature = "puffin")] + puffin::GlobalProfiler::lock().new_frame(); + + let Self { + app, + integration, + shared, + } = self; + + let mut frame_timer = crate::stopwatch::Stopwatch::new(); + frame_timer.start(); + + let (viewport_ui_cb, raw_input) = { + crate::profile_scope!("Prepare"); + let mut shared_lock = shared.borrow_mut(); + + let SharedState { + viewports, painter, .. + } = &mut *shared_lock; + + if viewport_id != ViewportId::ROOT { + let Some(viewport) = viewports.get(&viewport_id) else { + return EventResult::Wait; + }; + + if viewport.viewport_ui_cb.is_none() { + // This will only happen if this is an immediate viewport. + // That means that the viewport cannot be rendered by itself and needs his parent to be rendered. + if let Some(viewport) = viewports.get(&viewport.ids.parent) { + if let Some(window) = viewport.window.as_ref() { + return EventResult::RepaintNext(window.id()); + } + } + return EventResult::Wait; + } + } + + let Some(viewport) = viewports.get_mut(&viewport_id) else { + return EventResult::Wait; + }; + + let Viewport { + viewport_ui_cb, + window, + egui_winit, + info, + .. + } = viewport; + + let viewport_ui_cb = viewport_ui_cb.clone(); + + let Some(window) = window else { + return EventResult::Wait; + }; + egui_winit::update_viewport_info(info, &integration.egui_ctx, window); + + { + crate::profile_scope!("set_window"); + if let Err(err) = + pollster::block_on(painter.set_window(viewport_id, Some(window.clone()))) + { + log::warn!("Failed to set window: {err}"); + } + } + + let egui_winit = egui_winit.as_mut().unwrap(); + let mut raw_input = egui_winit.take_egui_input(window); + + integration.pre_update(); + + raw_input.time = Some(integration.beginning.elapsed().as_secs_f64()); + raw_input.viewports = viewports + .iter() + .map(|(id, viewport)| (*id, viewport.info.clone())) + .collect(); + + (viewport_ui_cb, raw_input) + }; + + // ------------------------------------------------------------ + + // Runs the update, which could call immediate viewports, + // so make sure we hold no locks here! + let full_output = integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input); + + // ------------------------------------------------------------ + + let mut shared = shared.borrow_mut(); + + let SharedState { + egui_ctx, + viewports, + painter, + viewport_from_window, + focused_viewport, + } = &mut *shared; + + let Some(viewport) = viewports.get_mut(&viewport_id) else { + return EventResult::Wait; + }; + + viewport.info.events.clear(); // they should have been processed + + let Viewport { + window: Some(window), + egui_winit: Some(egui_winit), + .. + } = viewport + else { + return EventResult::Wait; + }; + + let FullOutput { + platform_output, + textures_delta, + shapes, + pixels_per_point, + viewport_output, + } = full_output; + + egui_winit.handle_platform_output(window, platform_output); + + let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); + + let screenshot_requested = std::mem::take(&mut viewport.screenshot_requested); + let (vsync_secs, screenshot) = painter.paint_and_update_textures( + viewport_id, + pixels_per_point, + app.clear_color(&egui_ctx.style().visuals), + &clipped_primitives, + &textures_delta, + screenshot_requested, + ); + if let Some(screenshot) = screenshot { + egui_winit + .egui_input_mut() + .events + .push(egui::Event::Screenshot { + viewport_id, + image: screenshot.into(), + }); + } + + integration.post_rendering(window); + + let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); + + handle_viewport_output( + &integration.egui_ctx, + viewport_output, + viewports, + *focused_viewport, + ); + + // Prune dead viewports: + viewports.retain(|id, _| active_viewports_ids.contains(id)); + viewport_from_window.retain(|_, id| active_viewports_ids.contains(id)); + painter.gc_viewports(&active_viewports_ids); + + let window = viewport_from_window + .get(&window_id) + .and_then(|id| viewports.get(id)) + .and_then(|vp| vp.window.as_ref()); + + integration.report_frame_time(frame_timer.total_time_sec() - vsync_secs); // don't count auto-save time as part of regular frame time + + integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref())); + + if let Some(window) = window { + 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"); + std::thread::sleep(std::time::Duration::from_millis(10)); + } + } + + if integration.should_close() { + EventResult::Exit + } else { + EventResult::Wait + } + } + + fn on_window_event( + &mut self, + window_id: WindowId, + event: &winit::event::WindowEvent, + ) -> EventResult { + crate::profile_function!(egui_winit::short_window_event_description(event)); + + let Self { + integration, + shared, + .. + } = self; + let mut shared = shared.borrow_mut(); + + let viewport_id = shared.viewport_from_window.get(&window_id).copied(); + + // On Windows, if a window is resized by the user, it should repaint synchronously, inside the + // event handler. + // + // If this is not done, the compositor will assume that the window does not want to redraw, + // and continue ahead. + // + // In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver + // new frames to the compositor in time. + // + // The flickering is technically glutin or glow's fault, but we should be responding properly + // to resizes anyway, as doing so avoids dropping frames. + // + // See: https://github.com/emilk/egui/issues/903 + let mut repaint_asap = false; + + match event { + winit::event::WindowEvent::Focused(new_focused) => { + shared.focused_viewport = new_focused.then(|| viewport_id).flatten(); + } + + winit::event::WindowEvent::Resized(physical_size) => { + // Resize with 0 width and height is used by winit to signal a minimize event on Windows. + // See: https://github.com/rust-windowing/winit/issues/208 + // This solves an issue where the app would panic when minimizing on Windows. + if let Some(viewport_id) = viewport_id { + use std::num::NonZeroU32; + if let (Some(width), Some(height)) = ( + NonZeroU32::new(physical_size.width), + NonZeroU32::new(physical_size.height), + ) { + repaint_asap = true; + shared.painter.on_window_resized(viewport_id, width, height); + } + } + } + + winit::event::WindowEvent::CloseRequested => { + if viewport_id == Some(ViewportId::ROOT) && integration.should_close() { + log::debug!( + "Received WindowEvent::CloseRequested for main viewport - shutting down." + ); + return EventResult::Exit; + } + + log::debug!("Received WindowEvent::CloseRequested for viewport {viewport_id:?}"); + + if let Some(viewport_id) = viewport_id { + if let Some(viewport) = shared.viewports.get_mut(&viewport_id) { + // Tell viewport it should close: + viewport.info.events.push(egui::ViewportEvent::Close); + + // We may need to repaint both us and our parent to close the window, + // and perhaps twice (once to notice the close-event, once again to enforce it). + // `request_repaint_of` does a double-repaint though: + integration.egui_ctx.request_repaint_of(viewport_id); + integration.egui_ctx.request_repaint_of(viewport.ids.parent); + } + } + } + + _ => {} + }; + + let event_response = viewport_id + .and_then(|viewport_id| { + shared.viewports.get_mut(&viewport_id).and_then(|viewport| { + Some(integration.on_window_event( + viewport.window.as_deref()?, + viewport.egui_winit.as_mut()?, + event, + )) + }) + }) + .unwrap_or_default(); + + if integration.should_close() { + EventResult::Exit + } else if event_response.repaint { + if repaint_asap { + EventResult::RepaintNow(window_id) + } else { + EventResult::RepaintNext(window_id) + } + } else { + EventResult::Wait + } + } +} + +impl Viewport { + /// Create winit window, if needed. + fn initialize_window( + &mut self, + event_loop: &EventLoopWindowTarget, + egui_ctx: &egui::Context, + windows_id: &mut HashMap, + painter: &mut egui_wgpu::winit::Painter, + ) { + if self.window.is_some() { + return; // we already have one + } + + crate::profile_function!(); + + let viewport_id = self.ids.this; + + match egui_winit::create_window(egui_ctx, event_loop, &self.builder) { + Ok(window) => { + windows_id.insert(window.id(), viewport_id); + + let window = Arc::new(window); + + if let Err(err) = + pollster::block_on(painter.set_window(viewport_id, Some(window.clone()))) + { + log::error!("on set_window: viewport_id {viewport_id:?} {err}"); + } + + self.egui_winit = Some(egui_winit::State::new( + egui_ctx.clone(), + viewport_id, + event_loop, + Some(window.scale_factor() as f32), + painter.max_texture_side(), + )); + + self.info.minimized = window.is_minimized(); + self.info.maximized = Some(window.is_maximized()); + + self.window = Some(window); + } + Err(err) => { + log::error!("Failed to create window: {err}"); + } + } + } +} + +fn create_window( + egui_ctx: &egui::Context, + event_loop: &EventLoopWindowTarget, + storage: Option<&dyn Storage>, + native_options: &mut NativeOptions, +) -> Result<(Window, ViewportBuilder), winit::error::OsError> { + crate::profile_function!(); + + let window_settings = epi_integration::load_window_settings(storage); + let viewport_builder = epi_integration::viewport_builder( + egui_ctx.zoom_factor(), + event_loop, + native_options, + window_settings, + ) + .with_visible(false); // Start hidden until we render the first frame to fix white flash on startup (https://github.com/emilk/egui/pull/3631) + + let window = egui_winit::create_window(egui_ctx, event_loop, &viewport_builder)?; + epi_integration::apply_window_settings(&window, window_settings); + Ok((window, viewport_builder)) +} + +fn render_immediate_viewport( + event_loop: &EventLoopWindowTarget, + beginning: Instant, + shared: &RefCell, + immediate_viewport: ImmediateViewport<'_>, +) { + crate::profile_function!(); + + let ImmediateViewport { + ids, + builder, + viewport_ui_cb, + } = immediate_viewport; + + let input = { + let SharedState { + egui_ctx, + viewports, + painter, + viewport_from_window, + .. + } = &mut *shared.borrow_mut(); + + let viewport = initialize_or_update_viewport( + egui_ctx, + viewports, + ids, + ViewportClass::Immediate, + builder, + None, + None, + ); + if viewport.window.is_none() { + viewport.initialize_window(event_loop, egui_ctx, viewport_from_window, painter); + } + + let (Some(window), Some(egui_winit)) = (&viewport.window, &mut viewport.egui_winit) else { + return; + }; + egui_winit::update_viewport_info(&mut viewport.info, egui_ctx, window); + + let mut input = egui_winit.take_egui_input(window); + input.viewports = viewports + .iter() + .map(|(id, viewport)| (*id, viewport.info.clone())) + .collect(); + input.time = Some(beginning.elapsed().as_secs_f64()); + input + }; + + let egui_ctx = shared.borrow().egui_ctx.clone(); + + // ------------------------------------------ + + // Run the user code, which could re-entrantly call this function again (!). + // Make sure no locks are held during this call. + let egui::FullOutput { + platform_output, + textures_delta, + shapes, + pixels_per_point, + viewport_output, + } = egui_ctx.run(input, |ctx| { + viewport_ui_cb(ctx); + }); + + // ------------------------------------------ + + let mut shared = shared.borrow_mut(); + let SharedState { + viewports, + painter, + focused_viewport, + .. + } = &mut *shared; + + let Some(viewport) = viewports.get_mut(&ids.this) else { + return; + }; + viewport.info.events.clear(); // they should have been processed + let (Some(egui_winit), Some(window)) = (&mut viewport.egui_winit, &viewport.window) else { + return; + }; + + { + crate::profile_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}", + ids.this + ); + } + } + + let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); + painter.paint_and_update_textures( + ids.this, + pixels_per_point, + [0.0, 0.0, 0.0, 0.0], + &clipped_primitives, + &textures_delta, + false, + ); + + egui_winit.handle_platform_output(window, platform_output); + + handle_viewport_output(&egui_ctx, viewport_output, viewports, *focused_viewport); +} + +/// Add new viewports, and update existing ones: +fn handle_viewport_output( + egui_ctx: &egui::Context, + viewport_output: ViewportIdMap, + viewports: &mut ViewportIdMap, + focused_viewport: Option, +) { + for ( + viewport_id, + ViewportOutput { + parent, + class, + builder, + viewport_ui_cb, + commands, + repaint_delay: _, // ignored - we listened to the repaint callback instead + }, + ) in viewport_output + { + let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent); + + let viewport = initialize_or_update_viewport( + egui_ctx, + viewports, + ids, + class, + builder, + viewport_ui_cb, + focused_viewport, + ); + + if let Some(window) = viewport.window.as_ref() { + let is_viewport_focused = focused_viewport == Some(viewport_id); + egui_winit::process_viewport_commands( + egui_ctx, + &mut viewport.info, + commands, + window, + is_viewport_focused, + &mut viewport.screenshot_requested, + ); + } + } +} + +fn initialize_or_update_viewport<'vp>( + egui_ctx: &egui::Context, + viewports: &'vp mut Viewports, + ids: ViewportIdPair, + class: ViewportClass, + mut builder: ViewportBuilder, + viewport_ui_cb: Option>, + focused_viewport: Option, +) -> &'vp mut Viewport { + crate::profile_function!(); + + if builder.icon.is_none() { + // Inherit icon from parent + builder.icon = viewports + .get_mut(&ids.parent) + .and_then(|vp| vp.builder.icon.clone()); + } + + match viewports.entry(ids.this) { + std::collections::hash_map::Entry::Vacant(entry) => { + // New viewport: + log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title); + entry.insert(Viewport { + ids, + class, + builder, + info: Default::default(), + screenshot_requested: false, + viewport_ui_cb, + window: None, + egui_winit: None, + }) + } + + std::collections::hash_map::Entry::Occupied(mut entry) => { + // Patch an existing viewport: + let viewport = entry.get_mut(); + + viewport.class = class; + viewport.ids.parent = ids.parent; + viewport.viewport_ui_cb = viewport_ui_cb; + + let (delta_commands, recreate) = viewport.builder.patch(builder); + + if recreate { + log::debug!( + "Recreating window for viewport {:?} ({:?})", + ids.this, + viewport.builder.title + ); + viewport.window = None; + viewport.egui_winit = None; + } else if let Some(window) = &viewport.window { + let is_viewport_focused = focused_viewport == Some(ids.this); + egui_winit::process_viewport_commands( + egui_ctx, + &mut viewport.info, + delta_commands, + window, + is_viewport_focused, + &mut viewport.screenshot_requested, + ); + } + + entry.into_mut() + } + } +} diff --git a/crates/eframe/src/native/winit_integration.rs b/crates/eframe/src/native/winit_integration.rs new file mode 100644 index 00000000000..541253c0818 --- /dev/null +++ b/crates/eframe/src/native/winit_integration.rs @@ -0,0 +1,133 @@ +use std::{sync::Arc, time::Instant}; + +use winit::{ + event_loop::EventLoopWindowTarget, + window::{Window, WindowId}, +}; + +use egui::ViewportId; +#[cfg(feature = "accesskit")] +use egui_winit::accesskit_winit; + +use super::epi_integration::EpiIntegration; + +/// Create an egui context, restoring it from storage if possible. +pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context { + crate::profile_function!(); + + pub const IS_DESKTOP: bool = cfg!(any( + target_os = "freebsd", + target_os = "linux", + target_os = "macos", + target_os = "openbsd", + target_os = "windows", + )); + + let egui_ctx = egui::Context::default(); + + egui_ctx.set_embed_viewports(!IS_DESKTOP); + + let memory = crate::native::epi_integration::load_egui_memory(storage).unwrap_or_default(); + egui_ctx.memory_mut(|mem| *mem = memory); + + egui_ctx +} + +/// The custom even `eframe` uses with the [`winit`] event loop. +#[derive(Debug)] +pub enum UserEvent { + /// A repaint is requested. + RequestRepaint { + /// What to repaint. + viewport_id: ViewportId, + + /// When to repaint. + when: Instant, + + /// What the frame number was when the repaint was _requested_. + frame_nr: u64, + }, + + /// A request related to [`accesskit`](https://accesskit.dev/). + #[cfg(feature = "accesskit")] + AccessKitActionRequest(accesskit_winit::ActionRequestEvent), +} + +#[cfg(feature = "accesskit")] +impl From for UserEvent { + fn from(inner: accesskit_winit::ActionRequestEvent) -> Self { + Self::AccessKitActionRequest(inner) + } +} + +pub trait WinitApp { + /// The current frame number, as reported by egui. + fn frame_nr(&self, viewport_id: ViewportId) -> u64; + + fn is_focused(&self, window_id: WindowId) -> bool; + + fn integration(&self) -> Option<&EpiIntegration>; + + fn window(&self, window_id: WindowId) -> Option>; + + fn window_id_from_viewport_id(&self, id: ViewportId) -> Option; + + fn save_and_destroy(&mut self); + + fn run_ui_and_paint( + &mut self, + event_loop: &EventLoopWindowTarget, + window_id: WindowId, + ) -> EventResult; + + fn on_event( + &mut self, + event_loop: &EventLoopWindowTarget, + event: &winit::event::Event, + ) -> crate::Result; +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum EventResult { + Wait, + + /// Causes a synchronous repaint inside the event handler. This should only + /// be used in special situations if the window must be repainted while + /// handling a specific event. This occurs on Windows when handling resizes. + /// + /// `RepaintNow` creates a new frame synchronously, and should therefore + /// only be used for extremely urgent repaints. + RepaintNow(WindowId), + + /// Queues a repaint for once the event loop handles its next redraw. Exists + /// so that multiple input events can be handled in one frame. Does not + /// cause any delay like `RepaintNow`. + RepaintNext(WindowId), + + RepaintAt(WindowId, Instant), + + Exit, +} + +pub fn system_theme(window: &Window, options: &crate::NativeOptions) -> Option { + if options.follow_system_theme { + window + .theme() + .map(super::epi_integration::theme_from_winit_theme) + } else { + None + } +} + +/// Short and fast description of an event. +/// Useful for logging and profiling. +pub fn short_event_description(event: &winit::event::Event) -> &'static str { + match event { + winit::event::Event::UserEvent(user_event) => match user_event { + UserEvent::RequestRepaint { .. } => "UserEvent::RequestRepaint", + #[cfg(feature = "accesskit")] + UserEvent::AccessKitActionRequest(_) => "UserEvent::AccessKitActionRequest", + }, + _ => egui_winit::short_generic_event_description(event), + } +} diff --git a/crates/eframe/src/stopwatch.rs b/crates/eframe/src/stopwatch.rs new file mode 100644 index 00000000000..9b0136189fc --- /dev/null +++ b/crates/eframe/src/stopwatch.rs @@ -0,0 +1,50 @@ +#![allow(dead_code)] // not everything is used on wasm + +use web_time::Instant; + +pub struct Stopwatch { + total_time_ns: u128, + + /// None = not running + start: Option, +} + +impl Stopwatch { + pub fn new() -> Self { + Self { + total_time_ns: 0, + start: None, + } + } + + pub fn start(&mut self) { + assert!(self.start.is_none()); + self.start = Some(Instant::now()); + } + + pub fn pause(&mut self) { + let start = self.start.take().unwrap(); + let duration = start.elapsed(); + self.total_time_ns += duration.as_nanos(); + } + + pub fn resume(&mut self) { + assert!(self.start.is_none()); + self.start = Some(Instant::now()); + } + + pub fn total_time_ns(&self) -> u128 { + if let Some(start) = self.start { + // Running + let duration = start.elapsed(); + self.total_time_ns + duration.as_nanos() + } else { + // Paused + self.total_time_ns + } + } + + pub fn total_time_sec(&self) -> f32 { + self.total_time_ns() as f32 * 1e-9 + } +} diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 4da93c52117..31f6e79c01c 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -1,11 +1,11 @@ use egui::TexturesDelta; -use wasm_bindgen::JsValue; use crate::{epi, App}; use super::{now_sec, web_painter::WebPainter, NeedRepaint}; pub struct AppRunner { + web_options: crate::WebOptions, pub(crate) frame: epi::Frame, egui_ctx: egui::Context, painter: super::ActiveWebPainter, @@ -13,10 +13,12 @@ pub struct AppRunner { app: Box, pub(crate) needs_repaint: std::sync::Arc, last_save_time: f64, - screen_reader: super::screen_reader::ScreenReader, - pub(crate) text_cursor_pos: Option, + pub(crate) ime: Option, pub(crate) mutable_text_under_cursor: bool, + + // Output for the last run: textures_delta: TexturesDelta, + clipped_primitives: Option>, } impl Drop for AppRunner { @@ -48,7 +50,6 @@ impl AppRunner { }, system_theme, cpu_usage: None, - native_pixels_per_point: Some(super::native_pixels_per_point()), }; let storage = LocalStorage::default(); @@ -56,7 +57,13 @@ impl AppRunner { egui_ctx.set_os(egui::os::OperatingSystem::from_user_agent( &super::user_agent().unwrap_or_default(), )); - super::load_memory(&egui_ctx); + super::storage::load_memory(&egui_ctx); + + egui_ctx.options_mut(|o| { + // On web, the browser controls the zoom factor: + o.zoom_with_keyboard = false; + o.zoom_factor = 1.0; + }); let theme = system_theme.unwrap_or(web_options.default_theme); egui_ctx.set_visuals(theme.egui_visuals()); @@ -69,6 +76,9 @@ impl AppRunner { #[cfg(feature = "glow")] gl: Some(painter.gl().clone()), + #[cfg(feature = "glow")] + get_proc_address: None, + #[cfg(all(feature = "wgpu", not(feature = "glow")))] wgpu_render_state: painter.render_state(), #[cfg(all(feature = "wgpu", feature = "glow"))] @@ -77,7 +87,6 @@ impl AppRunner { let frame = epi::Frame { info, - output: Default::default(), storage: Some(Box::new(storage)), #[cfg(feature = "glow")] @@ -93,11 +102,12 @@ impl AppRunner { { let needs_repaint = needs_repaint.clone(); egui_ctx.set_request_repaint_callback(move |info| { - needs_repaint.repaint_after(info.after.as_secs_f64()); + needs_repaint.repaint_after(info.delay.as_secs_f64()); }); } let mut runner = Self { + web_options, frame, egui_ctx, painter, @@ -105,13 +115,20 @@ impl AppRunner { app, needs_repaint, last_save_time: now_sec(), - screen_reader: Default::default(), - text_cursor_pos: None, + ime: None, mutable_text_under_cursor: false, textures_delta: Default::default(), + clipped_primitives: None, }; runner.input.raw.max_texture_side = Some(runner.painter.max_texture_side()); + runner + .input + .raw + .viewports + .entry(egui::ViewportId::ROOT) + .or_default() + .native_pixels_per_point = Some(super::native_pixels_per_point()); Ok(runner) } @@ -140,7 +157,7 @@ impl AppRunner { pub fn save(&mut self) { if self.app.persist_egui_memory() { - super::save_memory(&self.egui_ctx); + super::storage::save_memory(&self.egui_ctx); } if let Some(storage) = self.frame.storage_mut() { self.app.save(storage); @@ -148,19 +165,8 @@ impl AppRunner { self.last_save_time = now_sec(); } - pub fn canvas_id(&self) -> &str { - self.painter.canvas_id() - } - - pub fn warm_up(&mut self) { - if self.app.warm_up_enabled() { - let saved_memory: egui::Memory = self.egui_ctx.memory(|m| m.clone()); - self.egui_ctx - .memory_mut(|m| m.set_everything_is_visible(true)); - self.logic(); - self.egui_ctx.memory_mut(|m| *m = saved_memory); // We don't want to remember that windows were huge. - self.egui_ctx.clear_animations(); - } + pub fn canvas(&self) -> &web_sys::HtmlCanvasElement { + self.painter.canvas() } pub fn destroy(mut self) { @@ -168,14 +174,16 @@ impl AppRunner { self.painter.destroy(); } - /// Returns how long to wait until the next repaint. - /// - /// Call [`Self::paint`] later to paint - pub fn logic(&mut self) -> (std::time::Duration, Vec) { - let frame_start = now_sec(); + pub fn has_outstanding_paint_data(&self) -> bool { + self.clipped_primitives.is_some() + } - super::resize_canvas_to_screen_size(self.canvas_id(), self.app.max_size_points()); - let canvas_size = super::canvas_size_in_points(self.canvas_id()); + /// Runs the logic, but doesn't paint the result. + /// + /// The result can be painted later with a call to [`Self::run_and_paint`] or [`Self::paint`]. + pub fn logic(&mut self) { + super::resize_canvas_to_screen_size(self.canvas(), self.web_options.max_size_points); + let canvas_size = super::canvas_size_in_points(self.canvas()); let raw_input = self.input.new_frame(canvas_size); let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { @@ -183,43 +191,54 @@ impl AppRunner { }); let egui::FullOutput { platform_output, - repaint_after, textures_delta, shapes, + pixels_per_point, + viewport_output, } = full_output; - self.handle_platform_output(platform_output); - self.textures_delta.append(textures_delta); - let clipped_primitives = self.egui_ctx.tessellate(shapes); - - { - let app_output = self.frame.take_app_output(); - let epi::backend::AppOutput {} = app_output; + 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" + ); + } } - self.frame.info.cpu_usage = Some((now_sec() - frame_start) as f32); - - (repaint_after, clipped_primitives) + self.handle_platform_output(platform_output); + self.textures_delta.append(textures_delta); + self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point)); } /// Paint the results of the last call to [`Self::logic`]. - pub fn paint(&mut self, clipped_primitives: &[egui::ClippedPrimitive]) -> Result<(), JsValue> { + pub fn paint(&mut self) { let textures_delta = std::mem::take(&mut self.textures_delta); + let clipped_primitives = std::mem::take(&mut self.clipped_primitives); + + if let Some(clipped_primitives) = clipped_primitives { + 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, + ) { + log::error!("Failed to paint: {}", super::string_from_js_value(&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, - )?; - - Ok(()) + pub fn report_frame_time(&mut self, cpu_usage_seconds: f32) { + self.frame.info.cpu_usage = Some(cpu_usage_seconds); } fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) { + #[cfg(feature = "web_screen_reader")] if self.egui_ctx.options(|o| o.screen_reader) { - self.screen_reader - .speak(&platform_output.events_description()); + super::screen_reader::speak(&platform_output.events_description()); } let egui::PlatformOutput { @@ -228,7 +247,7 @@ impl AppRunner { copied_text, events: _, // already handled mutable_text_under_cursor, - text_cursor_pos, + ime, #[cfg(feature = "accesskit")] accesskit_update: _, // not currently implemented } = platform_output; @@ -248,9 +267,9 @@ impl AppRunner { self.mutable_text_under_cursor = mutable_text_under_cursor; - if self.text_cursor_pos != text_cursor_pos { - super::text_agent::move_text_cursor(text_cursor_pos, self.canvas_id()); - self.text_cursor_pos = text_cursor_pos; + if self.ime != ime { + super::text_agent::move_text_cursor(ime, self.canvas()); + self.ime = ime; } } } @@ -262,11 +281,11 @@ struct LocalStorage {} impl epi::Storage for LocalStorage { fn get_string(&self, key: &str) -> Option { - super::local_storage_get(key) + super::storage::local_storage_get(key) } fn set_string(&mut self, key: &str, value: String) { - super::local_storage_set(key, &value); + super::storage::local_storage_set(key, &value); } fn flush(&mut self) {} diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index dde1d897649..041fe42028a 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -10,24 +10,30 @@ use super::percent_decode; /// Data gathered between frames. #[derive(Default)] -pub struct WebInput { +pub(crate) struct WebInput { /// Required because we don't get a position on touched pub latest_touch_pos: Option, /// Required to maintain a stable touch position for multi-touch gestures. pub latest_touch_pos_id: Option, + /// The raw input to `egui`. pub raw: egui::RawInput, } impl WebInput { pub fn new_frame(&mut self, canvas_size: egui::Vec2) -> egui::RawInput { - egui::RawInput { + let mut raw_input = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Default::default(), canvas_size)), - pixels_per_point: Some(super::native_pixels_per_point()), // We ALWAYS use the native pixels-per-point time: Some(super::now_sec()), ..self.raw.take() - } + }; + raw_input + .viewports + .entry(egui::ViewportId::ROOT) + .or_default() + .native_pixels_per_point = Some(super::native_pixels_per_point()); + raw_input } pub fn on_web_page_focus_change(&mut self, focused: bool) { @@ -41,10 +47,8 @@ impl WebInput { // ---------------------------------------------------------------------------- -use std::sync::atomic::Ordering::SeqCst; - /// Stores when to do the next repaint. -pub struct NeedRepaint(Mutex); +pub(crate) struct NeedRepaint(Mutex); impl Default for NeedRepaint { fn default() -> Self { @@ -69,35 +73,23 @@ impl NeedRepaint { *repaint_time = repaint_time.min(super::now_sec() + num_seconds); } - pub fn repaint_asap(&self) { - *self.0.lock() = f64::NEG_INFINITY; - } -} - -pub struct IsDestroyed(std::sync::atomic::AtomicBool); - -impl Default for IsDestroyed { - fn default() -> Self { - Self(false.into()) - } -} - -impl IsDestroyed { - pub fn fetch(&self) -> bool { - self.0.load(SeqCst) + pub fn needs_repaint(&self) -> bool { + self.when_to_repaint() <= super::now_sec() } - pub fn set_true(&self) { - self.0.store(true, SeqCst); + pub fn repaint_asap(&self) { + *self.0.lock() = f64::NEG_INFINITY; } } // ---------------------------------------------------------------------------- +/// The User-Agent of the user's browser. pub fn user_agent() -> Option { web_sys::window()?.navigator().user_agent().ok() } +/// Get the [`epi::Location`] from the browser. pub fn web_location() -> epi::Location { let location = web_sys::window().unwrap().location(); @@ -107,44 +99,45 @@ pub fn web_location() -> epi::Location { .search() .unwrap_or_default() .strip_prefix('?') - .map(percent_decode) - .unwrap_or_default(); - - let query_map = parse_query_map(&query) - .iter() - .map(|(k, v)| ((*k).to_owned(), (*v).to_owned())) - .collect(); + .unwrap_or_default() + .to_owned(); epi::Location { + // TODO(emilk): should we really percent-decode the url? 🤷‍♂️ url: percent_decode(&location.href().unwrap_or_default()), protocol: percent_decode(&location.protocol().unwrap_or_default()), host: percent_decode(&location.host().unwrap_or_default()), hostname: percent_decode(&location.hostname().unwrap_or_default()), port: percent_decode(&location.port().unwrap_or_default()), hash, + query_map: parse_query_map(&query), query, - query_map, origin: percent_decode(&location.origin().unwrap_or_default()), } } -fn parse_query_map(query: &str) -> BTreeMap<&str, &str> { - query - .split('&') - .filter_map(|pair| { - if pair.is_empty() { - None +/// query is percent-encoded +fn parse_query_map(query: &str) -> BTreeMap> { + let mut map: BTreeMap> = Default::default(); + + for pair in query.split('&') { + if !pair.is_empty() { + if let Some((key, value)) = pair.split_once('=') { + map.entry(percent_decode(key)) + .or_default() + .push(percent_decode(value)); } else { - Some(if let Some((key, value)) = pair.split_once('=') { - (key, value) - } else { - (pair, "") - }) + map.entry(percent_decode(pair)) + .or_default() + .push(String::new()); } - }) - .collect() + } + } + + map } +// TODO(emilk): this test is never acgtually run, because this whole module is wasm32 only 🤦‍♂️ #[test] fn test_parse_query() { assert_eq!(parse_query_map(""), BTreeMap::default()); @@ -165,4 +158,11 @@ fn test_parse_query() { parse_query_map("foo&baz&&"), BTreeMap::from_iter([("foo", ""), ("baz", "")]) ); + assert_eq!( + parse_query_map("badger=data.rrd%3Fparam1%3Dfoo%26param2%3Dbar&mushroom=snake"), + BTreeMap::from_iter([ + ("badger", "data.rrd?param1=foo¶m2=bar"), + ("mushroom", "snake") + ]) + ); } diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index f44adb6929b..5e5f7b22622 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -5,41 +5,49 @@ use super::*; /// Calls `request_animation_frame` to schedule repaint. /// /// It will only paint if needed, but will always call `request_animation_frame` immediately. -fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> { +pub(crate) fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> { // Only paint and schedule if there has been no panic if let Some(mut runner_lock) = runner_ref.try_lock() { - paint_if_needed(&mut runner_lock)?; + paint_if_needed(&mut runner_lock); drop(runner_lock); - request_animation_frame(runner_ref.clone())?; + runner_ref.request_animation_frame()?; } - Ok(()) } -fn paint_if_needed(runner: &mut AppRunner) -> Result<(), JsValue> { - if runner.needs_repaint.when_to_repaint() <= now_sec() { - runner.needs_repaint.clear(); - let (repaint_after, clipped_primitives) = runner.logic(); - runner.paint(&clipped_primitives)?; - runner - .needs_repaint - .repaint_after(repaint_after.as_secs_f64()); - runner.auto_save_if_needed(); - } - Ok(()) -} +fn paint_if_needed(runner: &mut AppRunner) { + if runner.needs_repaint.needs_repaint() { + if runner.has_outstanding_paint_data() { + // We have already run the logic, e.g. in an on-click event, + // so let's only present the results: + runner.paint(); -pub fn request_animation_frame(runner_ref: WebRunner) -> Result<(), JsValue> { - let window = web_sys::window().unwrap(); - let closure = Closure::once(move || paint_and_schedule(&runner_ref)); - window.request_animation_frame(closure.as_ref().unchecked_ref())?; - closure.forget(); // We must forget it, or else the callback is canceled on drop - Ok(()) + // We schedule another repaint asap, so that we can run the actual logic + // again, which may schedule a new repaint (if there's animations): + runner.needs_repaint.repaint_asap(); + } else { + // Clear the `needs_repaint` flags _before_ + // running the logic, as the logic could cause it to be set again. + runner.needs_repaint.clear(); + + let mut stopwatch = crate::stopwatch::Stopwatch::new(); + stopwatch.start(); + + // Run user code… + runner.logic(); + + // …and paint the result. + runner.paint(); + + runner.report_frame_time(stopwatch.total_time_sec()); + } + } + runner.auto_save_if_needed(); } // ------------------------------------------------------------------------ -pub fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsValue> { +pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsValue> { let document = web_sys::window().unwrap().document().unwrap(); { @@ -71,7 +79,7 @@ pub fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsValue> { return; } - let modifiers = modifiers_from_event(&event); + let modifiers = modifiers_from_kb_event(&event); runner.input.raw.modifiers = modifiers; let key = event.key(); @@ -80,6 +88,7 @@ pub fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsValue> { if let Some(key) = egui_key { runner.input.raw.events.push(egui::Event::Key { key, + physical_key: None, // TODO pressed: true, repeat: false, // egui will fill this in for us! modifiers, @@ -141,11 +150,12 @@ pub fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsValue> { &document, "keyup", |event: web_sys::KeyboardEvent, runner| { - let modifiers = modifiers_from_event(&event); + let modifiers = modifiers_from_kb_event(&event); runner.input.raw.modifiers = modifiers; if let Some(key) = translate_key(&event.key()) { runner.input.raw.events.push(egui::Event::Key { key, + physical_key: None, // TODO pressed: false, repeat: false, modifiers, @@ -175,21 +185,47 @@ pub fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsValue> { )?; #[cfg(web_sys_unstable_apis)] - runner_ref.add_event_listener(&document, "cut", |_: web_sys::ClipboardEvent, runner| { - runner.input.raw.events.push(egui::Event::Cut); - runner.needs_repaint.repaint_asap(); - })?; + runner_ref.add_event_listener( + &document, + "cut", + |event: web_sys::ClipboardEvent, runner| { + runner.input.raw.events.push(egui::Event::Cut); + + // In Safari we are only allowed to write to the clipboard during the + // event callback, which is why we run the app logic here and now: + runner.logic(); + + // Make sure we paint the output of the above logic call asap: + runner.needs_repaint.repaint_asap(); + + event.stop_propagation(); + event.prevent_default(); + }, + )?; #[cfg(web_sys_unstable_apis)] - runner_ref.add_event_listener(&document, "copy", |_: web_sys::ClipboardEvent, runner| { - runner.input.raw.events.push(egui::Event::Copy); - runner.needs_repaint.repaint_asap(); - })?; + runner_ref.add_event_listener( + &document, + "copy", + |event: web_sys::ClipboardEvent, runner| { + runner.input.raw.events.push(egui::Event::Copy); + + // In Safari we are only allowed to write to the clipboard during the + // event callback, which is why we run the app logic here and now: + runner.logic(); + + // Make sure we paint the output of the above logic call asap: + runner.needs_repaint.repaint_asap(); + + event.stop_propagation(); + event.prevent_default(); + }, + )?; Ok(()) } -pub fn install_window_events(runner_ref: &WebRunner) -> Result<(), JsValue> { +pub(crate) fn install_window_events(runner_ref: &WebRunner) -> Result<(), JsValue> { let window = web_sys::window().unwrap(); // Save-on-close @@ -211,7 +247,7 @@ pub fn install_window_events(runner_ref: &WebRunner) -> Result<(), JsValue> { Ok(()) } -pub fn install_color_scheme_change_event(runner_ref: &WebRunner) -> Result<(), JsValue> { +pub(crate) fn install_color_scheme_change_event(runner_ref: &WebRunner) -> Result<(), JsValue> { let window = web_sys::window().unwrap(); if let Some(media_query_list) = prefers_color_scheme_dark(&window)? { @@ -230,8 +266,8 @@ pub fn install_color_scheme_change_event(runner_ref: &WebRunner) -> Result<(), J Ok(()) } -pub fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> { - let canvas = canvas_element(runner_ref.try_lock().unwrap().canvas_id()).unwrap(); +pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> { + let canvas = runner_ref.try_lock().unwrap().canvas().clone(); { let prevent_default_events = [ @@ -257,8 +293,10 @@ pub fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> { &canvas, "mousedown", |event: web_sys::MouseEvent, runner: &mut AppRunner| { + let modifiers = modifiers_from_mouse_event(&event); + runner.input.raw.modifiers = modifiers; if let Some(button) = button_from_mouse_event(&event) { - let pos = pos_from_mouse_event(runner.canvas_id(), &event); + let pos = pos_from_mouse_event(runner.canvas(), &event); let modifiers = runner.input.raw.modifiers; runner.input.raw.events.push(egui::Event::PointerButton { pos, @@ -266,6 +304,12 @@ pub fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> { pressed: true, modifiers, }); + + // In Safari we are only allowed to write to the clipboard during the + // event callback, which is why we run the app logic here and now: + runner.logic(); + + // Make sure we paint the output of the above logic call asap: runner.needs_repaint.repaint_asap(); } event.stop_propagation(); @@ -277,7 +321,9 @@ pub fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> { &canvas, "mousemove", |event: web_sys::MouseEvent, runner| { - let pos = pos_from_mouse_event(runner.canvas_id(), &event); + let modifiers = modifiers_from_mouse_event(&event); + runner.input.raw.modifiers = modifiers; + let pos = pos_from_mouse_event(runner.canvas(), &event); runner.input.raw.events.push(egui::Event::PointerMoved(pos)); runner.needs_repaint.repaint_asap(); event.stop_propagation(); @@ -286,8 +332,10 @@ pub fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> { )?; runner_ref.add_event_listener(&canvas, "mouseup", |event: web_sys::MouseEvent, runner| { + let modifiers = modifiers_from_mouse_event(&event); + runner.input.raw.modifiers = modifiers; if let Some(button) = button_from_mouse_event(&event) { - let pos = pos_from_mouse_event(runner.canvas_id(), &event); + let pos = pos_from_mouse_event(runner.canvas(), &event); let modifiers = runner.input.raw.modifiers; runner.input.raw.events.push(egui::Event::PointerButton { pos, @@ -295,6 +343,12 @@ pub fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> { pressed: false, modifiers, }); + + // In Safari we are only allowed to write to the clipboard during the + // event callback, which is why we run the app logic here and now: + runner.logic(); + + // Make sure we paint the output of the above logic call asap: runner.needs_repaint.repaint_asap(); text_agent::update_text_agent(runner); @@ -319,7 +373,7 @@ pub fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> { "touchstart", |event: web_sys::TouchEvent, runner| { let mut latest_touch_pos_id = runner.input.latest_touch_pos_id; - let pos = pos_from_touch_event(runner.canvas_id(), &event, &mut latest_touch_pos_id); + let pos = pos_from_touch_event(runner.canvas(), &event, &mut latest_touch_pos_id); runner.input.latest_touch_pos_id = latest_touch_pos_id; runner.input.latest_touch_pos = Some(pos); let modifiers = runner.input.raw.modifiers; @@ -342,7 +396,7 @@ pub fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> { "touchmove", |event: web_sys::TouchEvent, runner| { let mut latest_touch_pos_id = runner.input.latest_touch_pos_id; - let pos = pos_from_touch_event(runner.canvas_id(), &event, &mut latest_touch_pos_id); + let pos = pos_from_touch_event(runner.canvas(), &event, &mut latest_touch_pos_id); runner.input.latest_touch_pos_id = latest_touch_pos_id; runner.input.latest_touch_pos = Some(pos); runner.input.raw.events.push(egui::Event::PointerMoved(pos)); @@ -405,7 +459,7 @@ pub fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> { }); let scroll_multiplier = match unit { - egui::MouseWheelUnit::Page => canvas_size_in_points(runner.canvas_id()).y, + egui::MouseWheelUnit::Page => canvas_size_in_points(runner.canvas()).y, egui::MouseWheelUnit::Line => { #[allow(clippy::let_and_return)] let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in winit. @@ -418,7 +472,7 @@ pub fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> { // Report a zoom event in case CTRL (on Windows or Linux) or CMD (on Mac) is pressed. // This if-statement is equivalent to how `Modifiers.command` is determined in - // `modifiers_from_event()`, but we cannot directly use that fn for a [`WheelEvent`]. + // `modifiers_from_kb_event()`, but we cannot directly use that fn for a [`WheelEvent`]. if event.ctrl_key() || event.meta_key() { let factor = (delta.y / 200.0).exp(); runner.input.raw.events.push(egui::Event::Zoom(factor)); @@ -466,6 +520,7 @@ pub fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> { move |event: web_sys::DragEvent, runner| { if let Some(data_transfer) = event.data_transfer() { + // TODO(https://github.com/emilk/egui/issues/3702): support dropping folders runner.input.raw.hovered_files.clear(); runner.needs_repaint.repaint_asap(); @@ -473,6 +528,7 @@ pub fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> { for i in 0..files.length() { if let Some(file) = files.get(i) { let name = file.name(); + let mime = file.type_(); let last_modified = std::time::UNIX_EPOCH + std::time::Duration::from_millis(file.last_modified() as u64); @@ -491,6 +547,7 @@ pub fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> { runner_lock.input.raw.dropped_files.push( egui::DroppedFile { name, + mime, last_modified: Some(last_modified), bytes: Some(bytes.into()), ..Default::default() diff --git a/crates/eframe/src/web/input.rs b/crates/eframe/src/web/input.rs index eacd1704f36..4ed0227739a 100644 --- a/crates/eframe/src/web/input.rs +++ b/crates/eframe/src/web/input.rs @@ -1,7 +1,9 @@ -use super::{canvas_element, canvas_origin, AppRunner}; +use super::{canvas_origin, AppRunner}; -pub fn pos_from_mouse_event(canvas_id: &str, event: &web_sys::MouseEvent) -> egui::Pos2 { - let canvas = canvas_element(canvas_id).unwrap(); +pub fn pos_from_mouse_event( + canvas: &web_sys::HtmlCanvasElement, + event: &web_sys::MouseEvent, +) -> egui::Pos2 { let rect = canvas.get_bounding_client_rect(); egui::Pos2 { x: event.client_x() as f32 - rect.left() as f32, @@ -27,7 +29,7 @@ pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option, ) -> egui::Pos2 { @@ -35,7 +37,6 @@ pub fn pos_from_touch_event( // search for the touch we previously used for the position // (unfortunately, `event.touches()` is not a rust collection): (0..event.touches().length()) - .into_iter() .map(|i| event.touches().get(i).unwrap()) .find(|touch| egui::TouchId::from(touch.identifier()) == *touch_id_for_pos) } else { @@ -48,7 +49,7 @@ pub fn pos_from_touch_event( .or_else(|| event.touches().get(0)) .map_or(Default::default(), |touch| { *touch_id_for_pos = Some(egui::TouchId::from(touch.identifier())); - pos_from_touch(canvas_origin(canvas_id), &touch) + pos_from_touch(canvas_origin(canvas), &touch) }) } @@ -60,7 +61,7 @@ fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Po } pub fn push_touches(runner: &mut AppRunner, phase: egui::TouchPhase, event: &web_sys::TouchEvent) { - let canvas_origin = canvas_origin(runner.canvas_id()); + let canvas_origin = canvas_origin(runner.canvas()); for touch_idx in 0..event.changed_touches().length() { if let Some(touch) = event.changed_touches().item(touch_idx) { runner.input.raw.events.push(egui::Event::Touch { @@ -110,97 +111,29 @@ pub fn should_ignore_key(key: &str) -> bool { ) } -/// Web sends all all keys as strings, so it is up to us to figure out if it is +/// Web sends all keys as strings, so it is up to us to figure out if it is /// a real text input or the name of a key. pub fn translate_key(key: &str) -> Option { - use egui::Key; - - match key { - "ArrowDown" => Some(Key::ArrowDown), - "ArrowLeft" => Some(Key::ArrowLeft), - "ArrowRight" => Some(Key::ArrowRight), - "ArrowUp" => Some(Key::ArrowUp), - - "Esc" | "Escape" => Some(Key::Escape), - "Tab" => Some(Key::Tab), - "Backspace" => Some(Key::Backspace), - "Enter" => Some(Key::Enter), - "Space" | " " => Some(Key::Space), - - "Help" | "Insert" => Some(Key::Insert), - "Delete" => Some(Key::Delete), - "Home" => Some(Key::Home), - "End" => Some(Key::End), - "PageUp" => Some(Key::PageUp), - "PageDown" => Some(Key::PageDown), - - "-" => Some(Key::Minus), - "+" | "=" => Some(Key::PlusEquals), - - "0" => Some(Key::Num0), - "1" => Some(Key::Num1), - "2" => Some(Key::Num2), - "3" => Some(Key::Num3), - "4" => Some(Key::Num4), - "5" => Some(Key::Num5), - "6" => Some(Key::Num6), - "7" => Some(Key::Num7), - "8" => Some(Key::Num8), - "9" => Some(Key::Num9), + egui::Key::from_name(key) +} - "a" | "A" => Some(Key::A), - "b" | "B" => Some(Key::B), - "c" | "C" => Some(Key::C), - "d" | "D" => Some(Key::D), - "e" | "E" => Some(Key::E), - "f" | "F" => Some(Key::F), - "g" | "G" => Some(Key::G), - "h" | "H" => Some(Key::H), - "i" | "I" => Some(Key::I), - "j" | "J" => Some(Key::J), - "k" | "K" => Some(Key::K), - "l" | "L" => Some(Key::L), - "m" | "M" => Some(Key::M), - "n" | "N" => Some(Key::N), - "o" | "O" => Some(Key::O), - "p" | "P" => Some(Key::P), - "q" | "Q" => Some(Key::Q), - "r" | "R" => Some(Key::R), - "s" | "S" => Some(Key::S), - "t" | "T" => Some(Key::T), - "u" | "U" => Some(Key::U), - "v" | "V" => Some(Key::V), - "w" | "W" => Some(Key::W), - "x" | "X" => Some(Key::X), - "y" | "Y" => Some(Key::Y), - "z" | "Z" => Some(Key::Z), +pub fn modifiers_from_kb_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers { + egui::Modifiers { + alt: event.alt_key(), + ctrl: event.ctrl_key(), + shift: event.shift_key(), - "F1" => Some(Key::F1), - "F2" => Some(Key::F2), - "F3" => Some(Key::F3), - "F4" => Some(Key::F4), - "F5" => Some(Key::F5), - "F6" => Some(Key::F6), - "F7" => Some(Key::F7), - "F8" => Some(Key::F8), - "F9" => Some(Key::F9), - "F10" => Some(Key::F10), - "F11" => Some(Key::F11), - "F12" => Some(Key::F12), - "F13" => Some(Key::F13), - "F14" => Some(Key::F14), - "F15" => Some(Key::F15), - "F16" => Some(Key::F16), - "F17" => Some(Key::F17), - "F18" => Some(Key::F18), - "F19" => Some(Key::F19), - "F20" => Some(Key::F20), + // Ideally we should know if we are running or mac or not, + // but this works good enough for now. + mac_cmd: event.meta_key(), - _ => None, + // Ideally we should know if we are running or mac or not, + // but this works good enough for now. + command: event.ctrl_key() || event.meta_key(), } } -pub fn modifiers_from_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers { +pub fn modifiers_from_mouse_event(event: &web_sys::MouseEvent) -> egui::Modifiers { egui::Modifiers { alt: event.alt_key(), ctrl: event.ctrl_key(), diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index 9c3920a502e..d88e94229fd 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -3,16 +3,21 @@ #![allow(clippy::missing_errors_doc)] // So many `-> Result<_, JsValue>` mod app_runner; -pub mod backend; +mod backend; mod events; mod input; mod panic_handler; -pub mod screen_reader; -pub mod storage; mod text_agent; mod web_logger; mod web_runner; +/// Access to the browser screen reader. +#[cfg(feature = "web_screen_reader")] +pub mod screen_reader; + +/// Access to local browser storage. +pub mod storage; + pub(crate) use app_runner::AppRunner; pub use panic_handler::{PanicHandler, PanicSummary}; pub use web_logger::WebLogger; @@ -34,8 +39,6 @@ mod web_painter_wgpu; pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu; pub use backend::*; -pub use events::*; -pub use storage::*; use egui::Vec2; use wasm_bindgen::prelude::*; @@ -47,6 +50,10 @@ use crate::Theme; // ---------------------------------------------------------------------------- +pub(crate) fn string_from_js_value(value: &JsValue) -> String { + value.as_string().unwrap_or_else(|| format!("{value:#?}")) +} + /// Current time in seconds (since undefined point in time). /// /// Monotonically increasing. @@ -59,15 +66,9 @@ pub fn now_sec() -> f64 { / 1000.0 } -#[allow(dead_code)] -pub fn screen_size_in_native_points() -> Option { - let window = web_sys::window()?; - Some(egui::vec2( - window.inner_width().ok()?.as_f64()? as f32, - window.inner_height().ok()?.as_f64()? as f32, - )) -} - +/// The native GUI scale factor, taking into account the browser zoom. +/// +/// Corresponds to [`window.devicePixelRatio`](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio) in JavaScript. pub fn native_pixels_per_point() -> f32 { let pixels_per_point = web_sys::window().unwrap().device_pixel_ratio() as f32; if pixels_per_point > 0.0 && pixels_per_point.is_finite() { @@ -77,6 +78,9 @@ pub fn native_pixels_per_point() -> f32 { } } +/// Ask the browser about the preferred system theme. +/// +/// `None` means unknown. pub fn system_theme() -> Option { let dark_mode = prefers_color_scheme_dark(&web_sys::window()?) .ok()?? @@ -96,26 +100,23 @@ fn theme_from_dark_mode(dark_mode: bool) -> Theme { } } -pub fn canvas_element(canvas_id: &str) -> Option { +fn get_canvas_element_by_id(canvas_id: &str) -> Option { let document = web_sys::window()?.document()?; let canvas = document.get_element_by_id(canvas_id)?; canvas.dyn_into::().ok() } -pub fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement { - canvas_element(canvas_id) +fn get_canvas_element_by_id_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement { + get_canvas_element_by_id(canvas_id) .unwrap_or_else(|| panic!("Failed to find canvas with id {canvas_id:?}")) } -fn canvas_origin(canvas_id: &str) -> egui::Pos2 { - let rect = canvas_element(canvas_id) - .unwrap() - .get_bounding_client_rect(); +fn canvas_origin(canvas: &web_sys::HtmlCanvasElement) -> egui::Pos2 { + let rect = canvas.get_bounding_client_rect(); egui::pos2(rect.left() as f32, rect.top() as f32) } -pub fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 { - let canvas = canvas_element(canvas_id).unwrap(); +fn canvas_size_in_points(canvas: &web_sys::HtmlCanvasElement) -> egui::Vec2 { let pixels_per_point = native_pixels_per_point(); egui::vec2( canvas.width() as f32 / pixels_per_point, @@ -123,8 +124,10 @@ pub fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 { ) } -pub fn resize_canvas_to_screen_size(canvas_id: &str, max_size_points: egui::Vec2) -> Option<()> { - let canvas = canvas_element(canvas_id)?; +fn resize_canvas_to_screen_size( + canvas: &web_sys::HtmlCanvasElement, + max_size_points: egui::Vec2, +) -> Option<()> { let parent = canvas.parent_element()?; // Prefer the client width and height so that if the parent @@ -178,7 +181,8 @@ pub fn resize_canvas_to_screen_size(canvas_id: &str, max_size_points: egui::Vec2 // ---------------------------------------------------------------------------- -pub fn set_cursor_icon(cursor: egui::CursorIcon) -> Option<()> { +/// Set the cursor icon. +fn set_cursor_icon(cursor: egui::CursorIcon) -> Option<()> { let document = web_sys::window()?.document()?; document .body()? @@ -187,15 +191,16 @@ pub fn set_cursor_icon(cursor: egui::CursorIcon) -> Option<()> { .ok() } +/// Set the clipboard text. #[cfg(web_sys_unstable_apis)] -pub fn set_clipboard_text(s: &str) { +fn set_clipboard_text(s: &str) { if let Some(window) = web_sys::window() { if let Some(clipboard) = window.navigator().clipboard() { let promise = clipboard.write_text(s); let future = wasm_bindgen_futures::JsFuture::from(promise); let future = async move { if let Err(err) = future.await { - log::error!("Copy/cut action failed: {err:?}"); + log::error!("Copy/cut action failed: {}", string_from_js_value(&err)); } }; wasm_bindgen_futures::spawn_local(future); @@ -245,6 +250,7 @@ fn cursor_web_name(cursor: egui::CursorIcon) -> &'static str { } } +/// Open the given url in the browser. pub fn open_url(url: &str, new_tab: bool) -> Option<()> { let name = if new_tab { "_blank" } else { "_self" }; @@ -267,6 +273,7 @@ pub fn location_hash() -> String { ) } +/// Percent-decodes a string. pub fn percent_decode(s: &str) -> String { percent_encoding::percent_decode_str(s) .decode_utf8_lossy() diff --git a/crates/eframe/src/web/panic_handler.rs b/crates/eframe/src/web/panic_handler.rs index a22bca9de0c..b379f775d17 100644 --- a/crates/eframe/src/web/panic_handler.rs +++ b/crates/eframe/src/web/panic_handler.rs @@ -65,16 +65,19 @@ pub struct PanicSummary { } impl PanicSummary { + /// Construct a summary from a panic. pub fn new(info: &std::panic::PanicInfo<'_>) -> Self { let message = info.to_string(); let callstack = Error::new().stack(); Self { message, callstack } } + /// The panic message. pub fn message(&self) -> String { self.message.clone() } + /// The backtrace. pub fn callstack(&self) -> String { self.callstack.clone() } diff --git a/crates/eframe/src/web/screen_reader.rs b/crates/eframe/src/web/screen_reader.rs index 30895723396..addebcfb85d 100644 --- a/crates/eframe/src/web/screen_reader.rs +++ b/crates/eframe/src/web/screen_reader.rs @@ -1,48 +1,20 @@ -pub struct ScreenReader { - #[cfg(feature = "tts")] - tts: Option, -} - -#[cfg(not(feature = "tts"))] -#[allow(clippy::derivable_impls)] // False positive -impl Default for ScreenReader { - fn default() -> Self { - Self {} +/// Speak the given text out loud. +pub fn speak(text: &str) { + if text.is_empty() { + return; } -} -#[cfg(feature = "tts")] -impl Default for ScreenReader { - fn default() -> Self { - let tts = match tts::Tts::default() { - Ok(screen_reader) => { - log::debug!("Initialized screen reader."); - Some(screen_reader) - } - Err(err) => { - log::warn!("Failed to load screen reader: {}", err); - None - } - }; - Self { tts } - } -} + if let Some(window) = web_sys::window() { + log::debug!("Speaking {text:?}"); -impl ScreenReader { - #[cfg(not(feature = "tts"))] - #[allow(clippy::unused_self)] - pub fn speak(&mut self, _text: &str) {} + if let Ok(speech_synthesis) = window.speech_synthesis() { + speech_synthesis.cancel(); // interrupt previous speech, if any - #[cfg(feature = "tts")] - pub fn speak(&mut self, text: &str) { - if text.is_empty() { - return; - } - if let Some(tts) = &mut self.tts { - log::debug!("Speaking: {:?}", text); - let interrupt = true; - if let Err(err) = tts.speak(text, interrupt) { - log::warn!("Failed to read: {}", err); + if let Ok(utterance) = web_sys::SpeechSynthesisUtterance::new_with_text(text) { + utterance.set_rate(1.0); + utterance.set_pitch(1.0); + utterance.set_volume(1.0); + speech_synthesis.speak(&utterance); } } } diff --git a/crates/eframe/src/web/storage.rs b/crates/eframe/src/web/storage.rs index 9ab8b04269d..4a2a53326a1 100644 --- a/crates/eframe/src/web/storage.rs +++ b/crates/eframe/src/web/storage.rs @@ -2,16 +2,18 @@ fn local_storage() -> Option { web_sys::window()?.local_storage().ok()? } +/// Read data from local storage. pub fn local_storage_get(key: &str) -> Option { local_storage().map(|storage| storage.get_item(key).ok())?? } +/// Write data to local storage. pub fn local_storage_set(key: &str, value: &str) { local_storage().map(|storage| storage.set_item(key, value)); } #[cfg(feature = "persistence")] -pub fn load_memory(ctx: &egui::Context) { +pub(crate) fn load_memory(ctx: &egui::Context) { if let Some(memory_string) = local_storage_get("egui_memory_ron") { match ron::from_str(&memory_string) { Ok(memory) => { @@ -25,10 +27,10 @@ pub fn load_memory(ctx: &egui::Context) { } #[cfg(not(feature = "persistence"))] -pub fn load_memory(_: &egui::Context) {} +pub(crate) fn load_memory(_: &egui::Context) {} #[cfg(feature = "persistence")] -pub fn save_memory(ctx: &egui::Context) { +pub(crate) fn save_memory(ctx: &egui::Context) { match ctx.memory(|mem| ron::to_string(mem)) { Ok(ron) => { local_storage_set("egui_memory_ron", &ron); @@ -40,4 +42,4 @@ pub fn save_memory(ctx: &egui::Context) { } #[cfg(not(feature = "persistence"))] -pub fn save_memory(_: &egui::Context) {} +pub(crate) fn save_memory(_: &egui::Context) {} diff --git a/crates/eframe/src/web/text_agent.rs b/crates/eframe/src/web/text_agent.rs index 1688163b3fb..a879f99d008 100644 --- a/crates/eframe/src/web/text_agent.rs +++ b/crates/eframe/src/web/text_agent.rs @@ -5,7 +5,7 @@ use std::{cell::Cell, rc::Rc}; use wasm_bindgen::prelude::*; -use super::{canvas_element, AppRunner, WebRunner}; +use super::{AppRunner, WebRunner}; static AGENT_ID: &str = "egui_text_agent"; @@ -101,14 +101,13 @@ pub fn install_text_agent(runner_ref: &WebRunner) -> Result<(), JsValue> { // When input lost focus, focus on it again. // It is useful when user click somewhere outside canvas. + let input_refocus = input.clone(); runner_ref.add_event_listener(&input, "focusout", move |_event: web_sys::MouseEvent, _| { // Delay 10 ms, and focus again. - let func = js_sys::Function::new_no_args(&format!( - "document.getElementById('{AGENT_ID}').focus()" - )); - window - .set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10) - .unwrap(); + let input_refocus = input_refocus.clone(); + call_after_delay(std::time::Duration::from_millis(10), move || { + input_refocus.focus().ok(); + }); })?; body.append_child(&input)?; @@ -122,7 +121,7 @@ pub fn update_text_agent(runner: &mut AppRunner) -> Option<()> { let window = web_sys::window()?; let document = window.document()?; let input: HtmlInputElement = document.get_element_by_id(AGENT_ID)?.dyn_into().unwrap(); - let canvas_style = canvas_element(runner.canvas_id())?.style(); + let canvas_style = runner.canvas().style(); if runner.mutable_text_under_cursor { let is_already_editing = input.hidden(); @@ -206,12 +205,16 @@ fn is_mobile() -> Option { // candidate window moves following text element (agent), // so it appears that the IME candidate window moves with text cursor. // On mobile devices, there is no need to do that. -pub fn move_text_cursor(cursor: Option, canvas_id: &str) -> Option<()> { +pub fn move_text_cursor( + ime: Option, + canvas: &web_sys::HtmlCanvasElement, +) -> Option<()> { let style = text_agent().style(); - // Note: movint agent on mobile devices will lead to unpredictable scroll. + // Note: moving agent on mobile devices will lead to unpredictable scroll. if is_mobile() == Some(false) { - cursor.as_ref().and_then(|&egui::Pos2 { x, y }| { - let canvas = canvas_element(canvas_id)?; + ime.as_ref().and_then(|ime| { + let egui::Pos2 { x, y } = ime.cursor_rect.left_top(); + let bounding_rect = text_agent().get_bounding_client_rect(); let y = (y + (canvas.scroll_top() + canvas.offset_top()) as f32) .min(canvas.client_height() as f32 - bounding_rect.height() as f32); diff --git a/crates/eframe/src/web/web_logger.rs b/crates/eframe/src/web/web_logger.rs index 09f731065e8..18936495581 100644 --- a/crates/eframe/src/web/web_logger.rs +++ b/crates/eframe/src/web/web_logger.rs @@ -4,12 +4,14 @@ pub struct WebLogger { } impl WebLogger { - /// Pipe all [`log`] events to the web console. + /// Install a new `WebLogger`, piping all [`log`] events to the web console. pub fn init(filter: log::LevelFilter) -> Result<(), log::SetLoggerError> { log::set_max_level(filter); - log::set_boxed_logger(Box::new(WebLogger::new(filter))) + log::set_boxed_logger(Box::new(Self::new(filter))) } + /// Create a new [`WebLogger`] with the given filter, + /// but don't install it. pub fn new(filter: log::LevelFilter) -> Self { Self { filter } } @@ -17,6 +19,21 @@ impl WebLogger { impl log::Log for WebLogger { fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { + /// Never log anything less serious than a `INFO` from these crates. + const CRATES_AT_INFO_LEVEL: &[&str] = &[ + // wgpu crates spam a lot on debug level, which is really annoying + "naga", + "wgpu_core", + "wgpu_hal", + ]; + + if CRATES_AT_INFO_LEVEL + .iter() + .any(|crate_name| metadata.target().starts_with(crate_name)) + { + return metadata.level() <= log::LevelFilter::Info; + } + metadata.level() <= self.filter } diff --git a/crates/eframe/src/web/web_painter.rs b/crates/eframe/src/web/web_painter.rs index 9c7631b90eb..e4db8eac316 100644 --- a/crates/eframe/src/web/web_painter.rs +++ b/crates/eframe/src/web/web_painter.rs @@ -9,8 +9,8 @@ pub(crate) trait WebPainter { // where // Self: Sized; - /// Id of the canvas in use. - fn canvas_id(&self) -> &str; + /// Reference to the canvas in use. + fn canvas(&self) -> &web_sys::HtmlCanvasElement; /// Maximum size of a texture in one direction. fn max_texture_side(&self) -> usize; diff --git a/crates/eframe/src/web/web_painter_glow.rs b/crates/eframe/src/web/web_painter_glow.rs index b12ac1cefaa..b54f6f64423 100644 --- a/crates/eframe/src/web/web_painter_glow.rs +++ b/crates/eframe/src/web/web_painter_glow.rs @@ -10,7 +10,6 @@ use super::web_painter::WebPainter; pub(crate) struct WebPainterGlow { canvas: HtmlCanvasElement, - canvas_id: String, painter: egui_glow::Painter, } @@ -20,20 +19,17 @@ impl WebPainterGlow { } pub async fn new(canvas_id: &str, options: &WebOptions) -> Result { - let canvas = super::canvas_element_or_die(canvas_id); + let canvas = super::get_canvas_element_by_id_or_die(canvas_id); let (gl, shader_prefix) = init_glow_context_from_canvas(&canvas, options.webgl_context_option)?; + #[allow(clippy::arc_with_non_send_sync)] let gl = std::sync::Arc::new(gl); let painter = egui_glow::Painter::new(gl, shader_prefix, None) .map_err(|err| format!("Error starting glow painter: {err}"))?; - Ok(Self { - canvas, - canvas_id: canvas_id.to_owned(), - painter, - }) + Ok(Self { canvas, painter }) } } @@ -42,8 +38,8 @@ impl WebPainter for WebPainterGlow { self.painter.max_texture_side() } - fn canvas_id(&self) -> &str { - &self.canvas_id + fn canvas(&self) -> &HtmlCanvasElement { + &self.canvas } fn paint_and_update_textures( diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index 83510dc4f37..de5ba601111 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -1,9 +1,13 @@ use std::sync::Arc; +use raw_window_handle::{ + DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, + RawWindowHandle, WebDisplayHandle, WebWindowHandle, WindowHandle, +}; use wasm_bindgen::JsValue; use web_sys::HtmlCanvasElement; -use egui_wgpu::{renderer::ScreenDescriptor, RenderState, SurfaceErrorAction}; +use egui_wgpu::{RenderState, SurfaceErrorAction}; use crate::WebOptions; @@ -12,25 +16,32 @@ use super::web_painter::WebPainter; struct EguiWebWindow(u32); #[allow(unsafe_code)] -unsafe impl raw_window_handle::HasRawWindowHandle for EguiWebWindow { - fn raw_window_handle(&self) -> raw_window_handle::RawWindowHandle { - let mut window_handle = raw_window_handle::WebWindowHandle::empty(); - window_handle.id = self.0; - raw_window_handle::RawWindowHandle::Web(window_handle) +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)] -unsafe impl raw_window_handle::HasRawDisplayHandle for EguiWebWindow { - fn raw_display_handle(&self) -> raw_window_handle::RawDisplayHandle { - raw_window_handle::RawDisplayHandle::Web(raw_window_handle::WebDisplayHandle::empty()) +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, - canvas_id: String, - surface: wgpu::Surface, + surface: wgpu::Surface<'static>, surface_configuration: wgpu::SurfaceConfiguration, render_state: Option, on_surface_error: Arc SurfaceErrorAction>, @@ -75,49 +86,107 @@ impl WebPainterWgpu { pub async fn new(canvas_id: &str, options: &WebOptions) -> Result { log::debug!("Creating wgpu painter"); - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: options.wgpu_options.supported_backends, - dx12_shader_compiler: Default::default(), - }); + let mut backends = options.wgpu_options.supported_backends; - let canvas = super::canvas_element_or_die(canvas_id); + // 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." + ); - let surface = if false { - instance.create_surface_from_canvas(canvas.clone()) - } else { - // Workaround for https://github.com/gfx-rs/wgpu/issues/3710: - // Don't use `create_surface_from_canvas`, but `create_surface` instead! - let raw_window = EguiWebWindow(egui::util::hash(("egui on wgpu", canvas_id)) as u32); - canvas.set_attribute("data-raw-handle", &raw_window.0.to_string()); - - #[allow(unsafe_code)] - unsafe { - instance.create_surface(&raw_window) + // 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()); + } } } - .map_err(|err| format!("failed to create wgpu surface: {err}"))?; + + 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."); + + 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 { + backends, + ..Default::default() + }); + } else { + return Err( + "Failed to create WebGPU adapter and WebGL was not enabled.".to_owned() + ); + } + } + } + + let canvas = super::get_canvas_element_by_id_or_die(canvas_id); + let surface = instance + .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone())) + .map_err(|err| format!("failed to create wgpu surface: {err}"))?; let depth_format = egui_wgpu::depth_format_from_bits(options.depth_buffer, 0); + let render_state = RenderState::create(&options.wgpu_options, &instance, &surface, depth_format, 1) .await .map_err(|err| err.to_string())?; let surface_configuration = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: render_state.target_format, - width: 0, - height: 0, present_mode: options.wgpu_options.present_mode, - alpha_mode: wgpu::CompositeAlphaMode::Auto, 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")? }; log::debug!("wgpu painter initialized."); Ok(Self { canvas, - canvas_id: canvas_id.to_owned(), render_state: Some(render_state), surface, surface_configuration, @@ -129,8 +198,8 @@ impl WebPainterWgpu { } impl WebPainter for WebPainterWgpu { - fn canvas_id(&self) -> &str { - &self.canvas_id + fn canvas(&self) -> &HtmlCanvasElement { + &self.canvas } fn max_texture_side(&self) -> usize { @@ -148,9 +217,7 @@ impl WebPainter for WebPainterWgpu { ) -> Result<(), JsValue> { let size_in_pixels = [self.canvas.width(), self.canvas.height()]; - let render_state = if let Some(render_state) = &self.render_state { - render_state - } else { + let Some(render_state) = &self.render_state else { return Err(JsValue::from_str( "Can't paint, wgpu renderer was already disposed", )); @@ -164,7 +231,7 @@ impl WebPainter for WebPainterWgpu { }); // Upload all resources for the GPU. - let screen_descriptor = ScreenDescriptor { + let screen_descriptor = egui_wgpu::ScreenDescriptor { size_in_pixels, pixels_per_point, }; @@ -210,8 +277,7 @@ impl WebPainter for WebPainterWgpu { let frame = match self.surface.get_current_texture() { Ok(frame) => frame, - #[allow(clippy::single_match_else)] - Err(e) => match (*self.on_surface_error)(e) { + Err(err) => match (*self.on_surface_error)(err) { SurfaceErrorAction::RecreateSurface => { self.surface .configure(&render_state.device, &self.surface_configuration); @@ -239,7 +305,7 @@ impl WebPainter for WebPainterWgpu { b: clear_color[2] as f64, a: clear_color[3] as f64, }), - store: true, + store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| { @@ -247,12 +313,16 @@ impl WebPainter for WebPainterWgpu { view, depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), - store: false, + // It is very unlikely that the depth buffer is needed after egui finished rendering + // so no need to store it. (this can improve performance on tiling GPUs like mobile chips or Apple Silicon) + store: wgpu::StoreOp::Discard, }), stencil_ops: None, } }), label: Some("egui_render"), + occlusion_query_set: None, + timestamp_writes: None, }); renderer.render(&mut render_pass, clipped_primitives, &screen_descriptor); @@ -269,11 +339,9 @@ impl WebPainter for WebPainterWgpu { } // Submit the commands: both the main buffer and user-defined ones. - render_state.queue.submit( - user_cmd_bufs - .into_iter() - .chain(std::iter::once(encoder.finish())), - ); + render_state + .queue + .submit(user_cmd_bufs.into_iter().chain([encoder.finish()])); if let Some(frame) = frame { frame.present(); diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs index 2c039544591..f39fc0dcace 100644 --- a/crates/eframe/src/web/web_runner.rs +++ b/crates/eframe/src/web/web_runner.rs @@ -1,4 +1,7 @@ -use std::{cell::RefCell, rc::Rc}; +use std::{ + cell::{Cell, RefCell}, + rc::Rc, +}; use wasm_bindgen::prelude::*; @@ -24,10 +27,13 @@ pub struct WebRunner { /// They have to be in a separate `Rc` so that we don't need to pass them to /// the panic handler, since they aren't `Send`. events_to_unsubscribe: Rc>>, + + /// Used in `destroy` to cancel a pending frame. + request_animation_frame_id: Cell>, } impl WebRunner { - // Will install a panic handler that will catch and log any panics + /// Will install a panic handler that will catch and log any panics #[allow(clippy::new_without_default)] pub fn new() -> Self { #[cfg(not(web_sys_unstable_apis))] @@ -41,6 +47,7 @@ impl WebRunner { panic_handler, runner: Rc::new(RefCell::new(None)), events_to_unsubscribe: Rc::new(RefCell::new(Default::default())), + request_animation_frame_id: Cell::new(None), } } @@ -58,8 +65,7 @@ impl WebRunner { let follow_system_theme = web_options.follow_system_theme; - let mut runner = AppRunner::new(canvas_id, web_options, app_creator).await?; - runner.warm_up(); + let runner = AppRunner::new(canvas_id, web_options, app_creator).await?; self.runner.replace(Some(runner)); { @@ -72,7 +78,7 @@ impl WebRunner { events::install_color_scheme_change_event(self)?; } - events::request_animation_frame(self.clone())?; + self.request_animation_frame()?; } Ok(()) @@ -96,15 +102,24 @@ impl WebRunner { log::debug!("Unsubscribing from {} events", events_to_unsubscribe.len()); for x in events_to_unsubscribe { if let Err(err) = x.unsubscribe() { - log::warn!("Failed to unsubscribe from event: {err:?}"); + log::warn!( + "Failed to unsubscribe from event: {}", + super::string_from_js_value(&err) + ); } } } } + /// Shut down eframe and clean up resources. pub fn destroy(&self) { self.unsubscribe_from_all_events(); + if let Some(id) = self.request_animation_frame_id.get() { + let window = web_sys::window().unwrap(); + window.cancel_animation_frame(id).ok(); + } + if let Some(runner) = self.runner.replace(None) { runner.destroy(); } @@ -176,6 +191,18 @@ impl WebRunner { Ok(()) } + + pub(crate) fn request_animation_frame(&self) -> Result<(), wasm_bindgen::JsValue> { + let window = web_sys::window().unwrap(); + let closure = Closure::once({ + let runner_ref = self.clone(); + move || events::paint_and_schedule(&runner_ref) + }); + let id = window.request_animation_frame(closure.as_ref().unchecked_ref())?; + self.request_animation_frame_id.set(Some(id)); + closure.forget(); // We must forget it, or else the callback is canceled on drop + Ok(()) + } } // ---------------------------------------------------------------------------- @@ -202,14 +229,14 @@ enum EventToUnsubscribe { impl EventToUnsubscribe { pub fn unsubscribe(self) -> Result<(), JsValue> { match self { - EventToUnsubscribe::TargetEvent(handle) => { + Self::TargetEvent(handle) => { handle.target.remove_event_listener_with_callback( handle.event_name.as_str(), handle.closure.as_ref().unchecked_ref(), )?; Ok(()) } - EventToUnsubscribe::IntervalHandle(handle) => { + Self::IntervalHandle(handle) => { let window = web_sys::window().unwrap(); window.clear_interval_with_handle(handle.handle); Ok(()) diff --git a/crates/egui-wgpu/CHANGELOG.md b/crates/egui-wgpu/CHANGELOG.md index d7cbee4d11e..56cecde1cf2 100644 --- a/crates/egui-wgpu/CHANGELOG.md +++ b/crates/egui-wgpu/CHANGELOG.md @@ -3,7 +3,47 @@ All notable changes to the `egui-wgpu` integration will be noted in this file. This file is updated upon each release. -Changes since the last release can be found by running the `scripts/generate_changelog.py` script. +Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. + + +## 0.26.2 - 2024-02-14 +* Nothing new + + +## 0.26.1 - 2024-02-11 +* Improve panic message in egui-wgpu when failing to create buffers [#3986](https://github.com/emilk/egui/pull/3986) + + +## 0.26.0 - 2024-02-05 +* Update wgpu to 0.19 [#3824](https://github.com/emilk/egui/pull/3824) +* Add `WgpuConfiguration::desired_maximum_frame_latency` [#3874](https://github.com/emilk/egui/pull/3874) +* Disable the default features of `wgpu` [#3875](https://github.com/emilk/egui/pull/3875) +* If WebGPU fails, re-try adapter creation with WebGL [#3895](https://github.com/emilk/egui/pull/3895) (thanks [@Wumpf](https://github.com/Wumpf)!) +* Delay call to `get_current_texture` (possible small performance win) [#3914](https://github.com/emilk/egui/pull/3914) +* Add `x11` and `wayland` features [#3909](https://github.com/emilk/egui/pull/3909) (thanks [@YgorSouza](https://github.com/YgorSouza)!) +* Pass `ScreenDescriptor` to `egui_wgpu::CallbackTrait::prepare` [#3960](https://github.com/emilk/egui/pull/3960) (thanks [@StratusFearMe21](https://github.com/StratusFearMe21)!) +* Make `egui_wgpu::renderer` a private module [#3979](https://github.com/emilk/egui/pull/3979) + + +## 0.25.0 - 2024-01-08 +* Only call wgpu paint callback if viewport is positive [#3778](https://github.com/emilk/egui/pull/3778) (thanks [@msparkles](https://github.com/msparkles)!) + + +## 0.24.1 - 2023-11-30 +* Add a few `puffin` profile scopes + + +## 0.24.0 - 2023-11-23 +* Updated to wgpu 0.18 [#3505](https://github.com/emilk/egui/pull/3505) (thanks [@Wumpf](https://github.com/Wumpf)!) +* Update MSRV to Rust 1.72 [#3595](https://github.com/emilk/egui/pull/3595) +* Properly clamp and round viewport values, preventing rare warnings [#3604](https://github.com/emilk/egui/pull/3604) (thanks [@Wumpf](https://github.com/Wumpf)!) + + +## 0.23.0 - 2023-09-27 +* Update to `wgpu` 0.17.0 [#3170](https://github.com/emilk/egui/pull/3170) (thanks [@Aaron1011](https://github.com/Aaron1011)!) +* Improved wgpu callbacks [#3253](https://github.com/emilk/egui/pull/3253) (thanks [@Wumpf](https://github.com/Wumpf)!) +* Fix depth texture init with multisampling [#3207](https://github.com/emilk/egui/pull/3207) (thanks [@mauliu](https://github.com/mauliu)!) +* Fix panic on wgpu GL backend due to new screenshot capability [#3078](https://github.com/emilk/egui/pull/3078) (thanks [@amfaber](https://github.com/amfaber)!) ## 0.22.0 - 2023-05-23 diff --git a/crates/egui-wgpu/Cargo.toml b/crates/egui-wgpu/Cargo.toml index 187b05613ff..09e48e07643 100644 --- a/crates/egui-wgpu/Cargo.toml +++ b/crates/egui-wgpu/Cargo.toml @@ -1,16 +1,16 @@ [package] name = "egui-wgpu" -version = "0.22.0" +version.workspace = true description = "Bindings for using egui natively using the wgpu library" authors = [ "Nils Hasenbanck ", "embotech ", "Emil Ernerfeldt ", ] -edition = "2021" -rust-version = "1.67" +edition.workspace = true +rust-version.workspace = true homepage = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu" -license = "MIT OR Apache-2.0" +license.workspace = true readme = "README.md" repository = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu" categories = ["gui", "game-development"] @@ -28,30 +28,39 @@ all-features = true [features] +default = [] + ## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. puffin = ["dep:puffin"] -## Enable [`winit`](https://docs.rs/winit) integration. +## Enable [`winit`](https://docs.rs/winit) integration. On Linux, requires either `wayland` or `x11` winit = ["dep:winit"] +## Enables Wayland support for winit. +wayland = ["winit?/wayland"] + +## Enables x11 support for winit. +x11 = ["winit?/x11"] + [dependencies] -epaint = { version = "0.22.0", path = "../epaint", default-features = false, features = [ - "bytemuck", -] } +egui = { workspace = true, default-features = false } +epaint = { workspace = true, default-features = false, features = ["bytemuck"] } bytemuck = "1.7" -log = { version = "0.4", features = ["std"] } +document-features.workspace = true +log.workspace = true thiserror.workspace = true type-map = "0.5.0" -wgpu.workspace = true +web-time.workspace = true +wgpu = { workspace = true, features = ["wgsl"] } -#! ### Optional dependencies -## Enable this when generating docs. -document-features = { version = "0.2", optional = true } +# Optional dependencies: -winit = { version = "0.28", default-features = false, optional = true } +winit = { workspace = true, optional = true, default-features = false, features = [ + "rwh_06", +] } # Native: [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -puffin = { version = "0.16", optional = true } +puffin = { workspace = true, optional = true } diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 7d0a3506cf4..78def51dbb0 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -1,7 +1,19 @@ //! This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [wgpu](https://crates.io/crates/wgpu). //! +//! If you're targeting WebGL you also need to turn on the +//! `webgl` feature of the `wgpu` crate: +//! +//! ```toml +//! # Enable both WebGL and WebGPU backends on web. +//! wgpu = { version = "*", features = ["webgpu", "webgl"] } +//! ``` +//! +//! You can control whether WebGL or WebGPU will be picked at runtime by setting +//! [`WgpuConfiguration::supported_backends`]. +//! The default is to prefer WebGPU and fall back on WebGL. +//! //! ## Feature flags -#![cfg_attr(feature = "document-features", doc = document_features::document_features!())] +#![doc = document_features::document_features!()] //! #![allow(unsafe_code)] @@ -9,9 +21,9 @@ pub use wgpu; /// Low-level painting of [`egui`](https://github.com/emilk/egui) on [`wgpu`]. -pub mod renderer; -pub use renderer::Renderer; -pub use renderer::{Callback, CallbackResources, CallbackTrait}; +mod renderer; + +pub use renderer::*; /// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`]. #[cfg(feature = "winit")] @@ -21,6 +33,7 @@ use std::sync::Arc; use epaint::mutex::RwLock; +/// An error produced by egui-wgpu. #[derive(thiserror::Error, Debug)] pub enum WgpuError { #[error("Failed to create wgpu adapter, no suitable adapter found.")] @@ -34,6 +47,10 @@ pub enum WgpuError { #[error(transparent)] CreateSurfaceError(#[from] wgpu::CreateSurfaceError), + + #[cfg(feature = "winit")] + #[error(transparent)] + HandleError(#[from] ::winit::raw_window_handle::HandleError), } /// Access to the render state for egui. @@ -42,6 +59,13 @@ pub struct RenderState { /// Wgpu adapter used for rendering. pub adapter: Arc, + /// All the available adapters. + /// + /// This is not available on web. + /// On web, we always select WebGPU is available, then fall back to WebGL if not. + #[cfg(not(target_arch = "wasm32"))] + pub available_adapters: Arc<[wgpu::Adapter]>, + /// Wgpu device used for rendering, created from the adapter. pub device: Arc, @@ -63,30 +87,89 @@ impl RenderState { pub async fn create( config: &WgpuConfiguration, instance: &wgpu::Instance, - surface: &wgpu::Surface, + surface: &wgpu::Surface<'static>, depth_format: Option, msaa_samples: u32, ) -> Result { - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: config.power_preference, - compatible_surface: Some(surface), - force_fallback_adapter: false, - }) - .await - .ok_or(WgpuError::NoSuitableAdapterFound)?; - - let target_format = - crate::preferred_framebuffer_format(&surface.get_capabilities(&adapter).formats)?; - - let (device, queue) = adapter - .request_device(&(*config.device_descriptor)(&adapter), None) - .await?; + crate::profile_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) + ); + } + + 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()) + ); + } + + let capabilities = { + crate::profile_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, depth_format, msaa_samples); - Ok(RenderState { + Ok(Self { adapter: Arc::new(adapter), + #[cfg(not(target_arch = "wasm32"))] + available_adapters: available_adapters.into(), device: Arc::new(device), queue: Arc::new(queue), target_format, @@ -95,6 +178,24 @@ impl RenderState { } } +#[cfg(not(target_arch = "wasm32"))] +fn describe_adapters(adapters: &[wgpu::Adapter]) -> String { + if adapters.is_empty() { + "(none)".to_owned() + } else if adapters.len() == 1 { + adapter_info_summary(&adapters[0].get_info()) + } else { + let mut list_string = String::new(); + for adapter in adapters { + if !list_string.is_empty() { + list_string += ", "; + } + list_string += &format!("{{{}}}", adapter_info_summary(&adapter.get_info())); + } + list_string + } +} + /// Specifies which action should be taken as consequence of a [`wgpu::SurfaceError`] pub enum SurfaceErrorAction { /// Do nothing and skip the current frame. @@ -105,9 +206,20 @@ pub enum SurfaceErrorAction { } /// 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) + /// 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`. pub supported_backends: wgpu::Backends, /// Configuration passed on device request, given an adapter @@ -116,6 +228,15 @@ pub struct WgpuConfiguration { /// Present mode used for the primary surface. pub present_mode: wgpu::PresentMode, + /// Desired maximum number of frames that the presentation engine should queue in advance. + /// + /// Use `1` for low-latency, and `2` for high-throughput. + /// + /// See [`wgpu::SurfaceConfiguration::desired_maximum_frame_latency`] for details. + /// + /// `None` = `wgpu` default. + pub desired_maximum_frame_latency: Option, + /// Power preference for the adapter. pub power_preference: wgpu::PowerPreference, @@ -123,13 +244,36 @@ pub struct WgpuConfiguration { pub on_surface_error: Arc SurfaceErrorAction>, } +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, + 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) + .finish_non_exhaustive() + } +} + 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 a wgpu feature flag) + // (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() @@ -139,8 +283,8 @@ impl Default for WgpuConfiguration { wgpu::DeviceDescriptor { label: Some("egui wgpu device"), - features: wgpu::Features::default(), - limits: wgpu::Limits { + 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, @@ -148,7 +292,11 @@ impl Default for WgpuConfiguration { }, } }), + present_mode: wgpu::PresentMode::AutoVsync, + + desired_maximum_frame_latency: None, + power_preference: wgpu::util::power_preference_from_env() .unwrap_or(wgpu::PowerPreference::HighPerformance), @@ -183,7 +331,7 @@ pub fn preferred_framebuffer_format( } formats - .get(0) + .first() .copied() .ok_or(WgpuError::NoSurfaceFormatsAvailable) } @@ -203,22 +351,71 @@ pub fn depth_format_from_bits(depth_buffer: u8, stencil_buffer: u8) -> Option { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] - puffin::profile_function!($($arg)*); - }; +/// A human-readable summary about an adapter +pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String { + let wgpu::AdapterInfo { + name, + vendor, + device, + device_type, + driver, + driver_info, + backend, + } = &info; + + // Example values: + // > name: "llvmpipe (LLVM 16.0.6, 256 bits)", device_type: Cpu, backend: Vulkan, driver: "llvmpipe", driver_info: "Mesa 23.1.6-arch1.4 (LLVM 16.0.6)" + // > name: "Apple M1 Pro", device_type: IntegratedGpu, backend: Metal, driver: "", driver_info: "" + // > name: "ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)", device_type: IntegratedGpu, backend: Gl, driver: "", driver_info: "" + + let mut summary = format!("backend: {backend:?}, device_type: {device_type:?}"); + + if !name.is_empty() { + summary += &format!(", name: {name:?}"); + } + if !driver.is_empty() { + summary += &format!(", driver: {driver:?}"); + } + if !driver_info.is_empty() { + summary += &format!(", driver_info: {driver_info:?}"); + } + if *vendor != 0 { + // TODO(emilk): decode using https://github.com/gfx-rs/wgpu/blob/767ac03245ee937d3dc552edc13fe7ab0a860eec/wgpu-hal/src/auxil/mod.rs#L7 + summary += &format!(", vendor: 0x{vendor:04X}"); + } + if *device != 0 { + summary += &format!(", device: 0x{device:02X}"); + } + + summary } -pub(crate) use profile_function; -/// Profiling macro for feature "puffin" -macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] - puffin::profile_scope!($($arg)*); - }; +// --------------------------------------------------------------------------- + +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; } -pub(crate) use profile_scope; + +#[allow(unused_imports)] +pub(crate) use profiling_scopes::*; diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index 94fb9530754..b028af605bd 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -4,15 +4,16 @@ use std::{borrow::Cow, num::NonZeroU64, ops::Range}; use epaint::{ahash::HashMap, emath::NumExt, PaintCallbackInfo, Primitive, Vertex}; -use wgpu; use wgpu::util::DeviceExt as _; -// Only implements Send + Sync on wasm32 in order to allow storing wgpu resources on the type map. -#[cfg(not(target_arch = "wasm32"))] +/// You can use this for storage when implementing [`CallbackTrait`]. pub type CallbackResources = type_map::concurrent::TypeMap; -#[cfg(target_arch = "wasm32")] -pub type CallbackResources = type_map::TypeMap; +/// You can use this to do custom `wgpu` rendering in an egui app. +/// +/// Implement [`CallbackTrait`] and call [`Callback::new_paint_callback`]. +/// +/// This can be turned into a [`epaint::PaintCallback`] and [`epaint::Shape`]. pub struct Callback(Box); impl Callback { @@ -77,6 +78,7 @@ pub trait CallbackTrait: Send + Sync { &self, _device: &wgpu::Device, _queue: &wgpu::Queue, + _screen_descriptor: &ScreenDescriptor, _egui_encoder: &mut wgpu::CommandEncoder, _callback_resources: &mut CallbackResources, ) -> Vec { @@ -189,7 +191,10 @@ impl Renderer { label: Some("egui"), source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("egui.wgsl"))), }; - let module = device.create_shader_module(shader); + let module = { + crate::profile_scope!("create_shader_module"); + device.create_shader_module(shader) + }; let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("egui_uniform_buffer"), @@ -200,7 +205,8 @@ impl Renderer { usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, }); - let uniform_bind_group_layout = + let uniform_bind_group_layout = { + crate::profile_scope!("create_bind_group_layout"); device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("egui_uniform_bind_group_layout"), entries: &[wgpu::BindGroupLayoutEntry { @@ -213,22 +219,27 @@ impl Renderer { }, count: None, }], - }); + }) + }; - let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("egui_uniform_bind_group"), - layout: &uniform_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &uniform_buffer, - offset: 0, - size: None, - }), - }], - }); + let uniform_bind_group = { + crate::profile_scope!("create_bind_group"); + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("egui_uniform_bind_group"), + layout: &uniform_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &uniform_buffer, + offset: 0, + size: None, + }), + }], + }) + }; - let texture_bind_group_layout = + let texture_bind_group_layout = { + crate::profile_scope!("create_bind_group_layout"); device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("egui_texture_bind_group_layout"), entries: &[ @@ -249,7 +260,8 @@ impl Renderer { count: None, }, ], - }); + }) + }; let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("egui_pipeline_layout"), @@ -265,64 +277,68 @@ impl Renderer { bias: wgpu::DepthBiasState::default(), }); - let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("egui_pipeline"), - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - entry_point: "vs_main", - module: &module, - buffers: &[wgpu::VertexBufferLayout { - array_stride: 5 * 4, - step_mode: wgpu::VertexStepMode::Vertex, - // 0: vec2 position - // 1: vec2 texture coordinates - // 2: uint color - attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32], - }], - }, - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, - unclipped_depth: false, - conservative: false, - cull_mode: None, - front_face: wgpu::FrontFace::default(), - polygon_mode: wgpu::PolygonMode::default(), - strip_index_format: None, - }, - depth_stencil, - multisample: wgpu::MultisampleState { - alpha_to_coverage_enabled: false, - count: msaa_samples, - mask: !0, - }, - - fragment: Some(wgpu::FragmentState { - module: &module, - entry_point: 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 + let pipeline = { + crate::profile_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", + module: &module, + buffers: &[wgpu::VertexBufferLayout { + array_stride: 5 * 4, + step_mode: wgpu::VertexStepMode::Vertex, + // 0: vec2 position + // 1: vec2 texture coordinates + // 2: uint color + attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32], + }], + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + unclipped_depth: false, + conservative: false, + cull_mode: None, + front_face: wgpu::FrontFace::default(), + polygon_mode: wgpu::PolygonMode::default(), + strip_index_format: None, + }, + depth_stencil, + multisample: wgpu::MultisampleState { + alpha_to_coverage_enabled: false, + count: msaa_samples, + mask: !0, }, - targets: &[Some(wgpu::ColorTargetState { - format: output_color_format, - blend: Some(wgpu::BlendState { - color: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::One, - dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, - operation: wgpu::BlendOperation::Add, - }, - alpha: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::OneMinusDstAlpha, - dst_factor: wgpu::BlendFactor::One, - operation: wgpu::BlendOperation::Add, - }, - }), - write_mask: wgpu::ColorWrites::ALL, - })], - }), - multiview: None, - }); + + fragment: Some(wgpu::FragmentState { + module: &module, + entry_point: 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 { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::OneMinusDstAlpha, + dst_factor: wgpu::BlendFactor::One, + operation: wgpu::BlendOperation::Add, + }, + }), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + multiview: None, + } + ) + }; const VERTEX_BUFFER_START_CAPACITY: wgpu::BufferAddress = (std::mem::size_of::() * 1024) as _; @@ -435,51 +451,42 @@ impl Renderer { } } Primitive::Callback(callback) => { - let cbfn = if let Some(c) = callback.callback.downcast_ref::() { - c - } else { + let Some(cbfn) = callback.callback.downcast_ref::() else { // We already warned in the `prepare` callback continue; }; - if callback.rect.is_positive() { + let info = PaintCallbackInfo { + viewport: callback.rect, + clip_rect: *clip_rect, + pixels_per_point, + screen_size_px: size_in_pixels, + }; + + let viewport_px = info.viewport_in_pixels(); + if viewport_px.width_px > 0 && viewport_px.height_px > 0 { crate::profile_scope!("callback"); needs_reset = true; - { - // We're setting a default viewport for the render pass as a - // courtesy for the user, so that they don't have to think about - // it in the simple case where they just want to fill the whole - // paint area. - // - // The user still has the possibility of setting their own custom - // viewport during the paint callback, effectively overriding this - // one. - - let min = (callback.rect.min.to_vec2() * pixels_per_point).round(); - let max = (callback.rect.max.to_vec2() * pixels_per_point).round(); - - render_pass.set_viewport( - min.x, - min.y, - max.x - min.x, - max.y - min.y, - 0.0, - 1.0, - ); - } - - cbfn.0.paint( - PaintCallbackInfo { - viewport: callback.rect, - clip_rect: *clip_rect, - pixels_per_point, - screen_size_px: size_in_pixels, - }, - render_pass, - &self.callback_resources, + // We're setting a default viewport for the render pass as a + // courtesy for the user, so that they don't have to think about + // it in the simple case where they just want to fill the whole + // paint area. + // + // The user still has the possibility of setting their own custom + // viewport during the paint callback, effectively overriding this + // one. + render_pass.set_viewport( + viewport_px.left_px as f32, + viewport_px.top_px as f32, + viewport_px.width_px as f32, + viewport_px.height_px as f32, + 0.0, + 1.0, ); + + cbfn.0.paint(info, render_pass, &self.callback_resources); } } } @@ -522,12 +529,14 @@ impl Renderer { image.pixels.len(), "Mismatch between texture size and texel count" ); - Cow::Owned(image.srgba_pixels(None).collect::>()) + crate::profile_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"); queue.write_texture( wgpu::ImageCopyTexture { texture, @@ -565,16 +574,19 @@ impl Renderer { // Use same label for all resources associated with this texture id (no point in retyping the type) let label_str = format!("egui_texid_{id:?}"); let label = Some(label_str.as_str()); - let texture = device.create_texture(&wgpu::TextureDescriptor { - label, - size, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8UnormSrgb, // Minspec for wgpu WebGL emulation is WebGL2, so this should always be supported. - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, - view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb], - }); + let texture = { + crate::profile_scope!("create_texture"); + device.create_texture(&wgpu::TextureDescriptor { + label, + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, // Minspec for wgpu WebGL emulation is WebGL2, so this should always be supported. + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb], + }) + }; let sampler = self .samplers .entry(image_delta.options) @@ -607,9 +619,8 @@ impl Renderer { /// Get the WGPU texture and bind group associated to a texture that has been allocated by egui. /// - /// This could be used by custom paint hooks to render images that have been added through with - /// [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html) - /// or [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture). + /// This could be used by custom paint hooks to render images that have been added through + /// [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture). pub fn texture( &self, id: &epaint::TextureId, @@ -803,9 +814,10 @@ impl Renderer { }; if index_count > 0 { - crate::profile_scope!("indices"); + crate::profile_scope!("indices", index_count.to_string()); self.index_buffer.slices.clear(); + let required_index_buffer_size = (std::mem::size_of::() * index_count) as u64; if self.index_buffer.capacity < required_index_buffer_size { // Resize index buffer if needed. @@ -814,15 +826,18 @@ impl Renderer { self.index_buffer.buffer = create_index_buffer(device, self.index_buffer.capacity); } - let mut index_buffer_staging = queue - .write_buffer_with( - &self.index_buffer.buffer, - 0, - NonZeroU64::new(required_index_buffer_size).unwrap(), - ) - .expect("Failed to create staging buffer for index data"); + let index_buffer_staging = queue.write_buffer_with( + &self.index_buffer.buffer, + 0, + NonZeroU64::new(required_index_buffer_size).unwrap(), + ); + + let Some(mut index_buffer_staging) = index_buffer_staging else { + panic!("Failed to create staging buffer for index data. Index count: {index_count}. Required index buffer size: {required_index_buffer_size}. Actual size {} and capacity: {} (bytes)", self.index_buffer.buffer.size(), self.index_buffer.capacity); + }; + let mut index_offset = 0; - for epaint::ClippedPrimitive { primitive, .. } in paint_jobs.iter() { + for epaint::ClippedPrimitive { primitive, .. } in paint_jobs { match primitive { Primitive::Mesh(mesh) => { let size = mesh.indices.len() * std::mem::size_of::(); @@ -837,9 +852,10 @@ impl Renderer { } } if vertex_count > 0 { - crate::profile_scope!("vertices"); + crate::profile_scope!("vertices", vertex_count.to_string()); self.vertex_buffer.slices.clear(); + let required_vertex_buffer_size = (std::mem::size_of::() * vertex_count) as u64; if self.vertex_buffer.capacity < required_vertex_buffer_size { // Resize vertex buffer if needed. @@ -849,15 +865,18 @@ impl Renderer { create_vertex_buffer(device, self.vertex_buffer.capacity); } - let mut vertex_buffer_staging = queue - .write_buffer_with( - &self.vertex_buffer.buffer, - 0, - NonZeroU64::new(required_vertex_buffer_size).unwrap(), - ) - .expect("Failed to create staging buffer for vertex data"); + let vertex_buffer_staging = queue.write_buffer_with( + &self.vertex_buffer.buffer, + 0, + NonZeroU64::new(required_vertex_buffer_size).unwrap(), + ); + + let Some(mut vertex_buffer_staging) = vertex_buffer_staging else { + panic!("Failed to create staging buffer for vertex data. Vertex count: {vertex_count}. Required vertex buffer size: {required_vertex_buffer_size}. Actual size {} and capacity: {} (bytes)", self.vertex_buffer.buffer.size(), self.vertex_buffer.capacity); + }; + let mut vertex_offset = 0; - for epaint::ClippedPrimitive { primitive, .. } in paint_jobs.iter() { + for epaint::ClippedPrimitive { primitive, .. } in paint_jobs { match primitive { Primitive::Mesh(mesh) => { let size = mesh.vertices.len() * std::mem::size_of::(); @@ -879,6 +898,7 @@ impl Renderer { user_cmd_bufs.extend(callback.prepare( device, queue, + screen_descriptor, encoder, &mut self.callback_resources, )); @@ -912,12 +932,19 @@ fn create_sampler( epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest, epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear, }; + let address_mode = match options.wrap_mode { + epaint::textures::TextureWrapMode::ClampToEdge => wgpu::AddressMode::ClampToEdge, + epaint::textures::TextureWrapMode::Repeat => wgpu::AddressMode::Repeat, + epaint::textures::TextureWrapMode::MirroredRepeat => wgpu::AddressMode::MirrorRepeat, + }; device.create_sampler(&wgpu::SamplerDescriptor { label: Some(&format!( "egui sampler (mag: {mag_filter:?}, min {min_filter:?})" )), mag_filter, min_filter, + address_mode_u: address_mode, + address_mode_v: address_mode, ..Default::default() }) } @@ -979,9 +1006,6 @@ impl ScissorRect { } } -// Wgpu objects contain references to the JS heap on the web, therefore they are not Send/Sync. -// It follows that egui_wgpu::Renderer can not be Send/Sync either when building with wasm. -#[cfg(not(target_arch = "wasm32"))] #[test] fn renderer_impl_send_sync() { fn assert_send_sync() {} diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 171d5f08eff..07c917155aa 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -1,9 +1,11 @@ -use std::sync::Arc; +use std::{num::NonZeroU32, sync::Arc}; + +use egui::{ViewportId, ViewportIdMap, ViewportIdSet}; use crate::{renderer, RenderState, SurfaceErrorAction, WgpuConfiguration}; struct SurfaceState { - surface: wgpu::Surface, + surface: wgpu::Surface<'static>, alpha_mode: wgpu::CompositeAlphaMode, width: u32, height: u32, @@ -71,19 +73,23 @@ impl BufferPadding { /// Everything you need to paint egui with [`wgpu`] on [`winit`]. /// -/// Alternatively you can use [`crate::renderer`] directly. +/// Alternatively you can use [`crate::Renderer`] directly. +/// +/// NOTE: all egui viewports share the same painter. pub struct Painter { configuration: WgpuConfiguration, msaa_samples: u32, support_transparent_backbuffer: bool, depth_format: Option, - depth_texture_view: Option, - msaa_texture_view: Option, screen_capture_state: Option, instance: wgpu::Instance, render_state: Option, - surface_state: Option, + + // Per viewport/window: + depth_texture_view: ViewportIdMap, + msaa_texture_view: ViewportIdMap, + surfaces: ViewportIdMap, } impl Painter { @@ -107,7 +113,7 @@ impl Painter { ) -> Self { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends: configuration.supported_backends, - dx12_shader_compiler: Default::default(), + ..Default::default() }); Self { @@ -115,13 +121,14 @@ impl Painter { msaa_samples, support_transparent_backbuffer, depth_format, - depth_texture_view: None, screen_capture_state: None, instance, render_state: None, - surface_state: None, - msaa_texture_view: None, + + depth_texture_view: Default::default(), + surfaces: Default::default(), + msaa_texture_view: Default::default(), } } @@ -135,25 +142,38 @@ impl Painter { fn configure_surface( surface_state: &SurfaceState, render_state: &RenderState, - present_mode: wgpu::PresentMode, + 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 }; - surface_state.surface.configure( - &render_state.device, - &wgpu::SurfaceConfiguration { - usage, - format: render_state.target_format, - width: surface_state.width, - height: surface_state.height, - present_mode, - alpha_mode: surface_state.alpha_mode, - view_formats: vec![render_state.target_format], - }, - ); + + let width = surface_state.width; + let height = surface_state.height; + + let mut surf_config = wgpu::SurfaceConfiguration { + usage, + format: render_state.target_format, + present_mode: config.present_mode, + alpha_mode: surface_state.alpha_mode, + view_formats: vec![render_state.target_format], + ..surface_state + .surface + .get_default_config(&render_state.adapter, width, height) + .expect("The surface isn't supported by this adapter") + }; + + if let Some(desired_maximum_frame_latency) = config.desired_maximum_frame_latency { + surf_config.desired_maximum_frame_latency = desired_maximum_frame_latency; + } + + surface_state + .surface + .configure(&render_state.device, &surf_config); } /// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`] @@ -175,75 +195,115 @@ impl Painter { /// [`set_window`](Self::set_window) may be called with `Some(window)` as soon as you have a /// valid [`winit::window::Window`]. /// - /// # Safety - /// - /// The raw Window handle associated with the given `window` must be a valid object to create a - /// surface upon and must remain valid for the lifetime of the created surface. (The surface may - /// be cleared by passing `None`). - /// /// # Errors /// If the provided wgpu configuration does not match an available device. pub async fn set_window( &mut self, - window: Option<&winit::window::Window>, + viewport_id: ViewportId, + window: Option>, ) -> Result<(), crate::WgpuError> { - match window { - Some(window) => { - let surface = unsafe { self.instance.create_surface(&window)? }; - - let render_state = if let Some(render_state) = &self.render_state { - render_state - } else { - let render_state = RenderState::create( - &self.configuration, - &self.instance, - &surface, - self.depth_format, - self.msaa_samples, - ) - .await?; - self.render_state.get_or_insert(render_state) - }; - - let alpha_mode = if self.support_transparent_backbuffer { - let supported_alpha_modes = - surface.get_capabilities(&render_state.adapter).alpha_modes; - - // Prefer pre multiplied over post multiplied! - if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) { - wgpu::CompositeAlphaMode::PreMultiplied - } else if supported_alpha_modes - .contains(&wgpu::CompositeAlphaMode::PostMultiplied) - { - wgpu::CompositeAlphaMode::PostMultiplied - } else { - log::warn!("Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency."); - wgpu::CompositeAlphaMode::Auto - } - } else { - wgpu::CompositeAlphaMode::Auto - }; - - let supports_screenshot = - !matches!(render_state.adapter.get_info().backend, wgpu::Backend::Gl); + crate::profile_scope!("Painter::set_window"); // profile_function gives bad names for async functions - let size = window.inner_size(); - self.surface_state = Some(SurfaceState { - surface, - width: size.width, - height: size.height, - alpha_mode, - supports_screenshot, - }); - self.resize_and_generate_depth_texture_view_and_msaa_view(size.width, size.height); + if let Some(window) = window { + let size = window.inner_size(); + if self.surfaces.get(&viewport_id).is_none() { + let surface = self.instance.create_surface(window)?; + self.add_surface(surface, viewport_id, size).await?; } - None => { - self.surface_state = None; + } else { + log::warn!("No window - clearing all surfaces"); + self.surfaces.clear(); + } + Ok(()) + } + + /// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`] without taking ownership of the window. + /// + /// Like [`set_window`](Self::set_window) except: + /// + /// # Safety + /// The user is responsible for ensuring that the window is alive for as long as it is set. + pub async unsafe fn set_window_unsafe( + &mut self, + 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 + + if let Some(window) = window { + let size = window.inner_size(); + if self.surfaces.get(&viewport_id).is_none() { + let surface = unsafe { + self.instance + .create_surface_unsafe(wgpu::SurfaceTargetUnsafe::from_window(&window)?)? + }; + self.add_surface(surface, viewport_id, size).await?; } + } else { + log::warn!("No window - clearing all surfaces"); + self.surfaces.clear(); } Ok(()) } + async fn add_surface( + &mut self, + surface: wgpu::Surface<'static>, + viewport_id: ViewportId, + size: winit::dpi::PhysicalSize, + ) -> Result<(), crate::WgpuError> { + let render_state = if let Some(render_state) = &self.render_state { + render_state + } else { + let render_state = RenderState::create( + &self.configuration, + &self.instance, + &surface, + self.depth_format, + self.msaa_samples, + ) + .await?; + self.render_state.get_or_insert(render_state) + }; + let alpha_mode = if self.support_transparent_backbuffer { + let supported_alpha_modes = surface.get_capabilities(&render_state.adapter).alpha_modes; + + // Prefer pre multiplied over post multiplied! + if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) { + wgpu::CompositeAlphaMode::PreMultiplied + } else if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) { + wgpu::CompositeAlphaMode::PostMultiplied + } else { + log::warn!("Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency."); + wgpu::CompositeAlphaMode::Auto + } + } else { + wgpu::CompositeAlphaMode::Auto + }; + let supports_screenshot = + !matches!(render_state.adapter.get_info().backend, wgpu::Backend::Gl); + self.surfaces.insert( + viewport_id, + SurfaceState { + surface, + width: size.width, + height: size.height, + alpha_mode, + supports_screenshot, + }, + ); + let Some(width) = NonZeroU32::new(size.width) else { + log::debug!("The window width was zero; skipping generate textures"); + return Ok(()); + }; + let Some(height) = NonZeroU32::new(size.height) else { + log::debug!("The window height was zero; skipping generate textures"); + return Ok(()); + }; + self.resize_and_generate_depth_texture_view_and_msaa_view(viewport_id, width, height); + Ok(()) + } + /// Returns the maximum texture dimension supported if known /// /// This API will only return a known dimension after `set_window()` has been called @@ -257,50 +317,61 @@ impl Painter { fn resize_and_generate_depth_texture_view_and_msaa_view( &mut self, - width_in_pixels: u32, - height_in_pixels: u32, + viewport_id: ViewportId, + width_in_pixels: NonZeroU32, + height_in_pixels: NonZeroU32, ) { + crate::profile_function!(); + + let width = width_in_pixels.get(); + let height = height_in_pixels.get(); + let render_state = self.render_state.as_ref().unwrap(); - let surface_state = self.surface_state.as_mut().unwrap(); + let surface_state = self.surfaces.get_mut(&viewport_id).unwrap(); - surface_state.width = width_in_pixels; - surface_state.height = height_in_pixels; + surface_state.width = width; + surface_state.height = height; - Self::configure_surface(surface_state, render_state, self.configuration.present_mode); + Self::configure_surface(surface_state, render_state, &self.configuration); - self.depth_texture_view = self.depth_format.map(|depth_format| { - render_state - .device - .create_texture(&wgpu::TextureDescriptor { - label: Some("egui_depth_texture"), - size: wgpu::Extent3d { - width: width_in_pixels, - height: height_in_pixels, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: self.msaa_samples, - dimension: wgpu::TextureDimension::D2, - format: depth_format, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT - | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[depth_format], - }) - .create_view(&wgpu::TextureViewDescriptor::default()) - }); + if let Some(depth_format) = self.depth_format { + self.depth_texture_view.insert( + viewport_id, + render_state + .device + .create_texture(&wgpu::TextureDescriptor { + label: Some("egui_depth_texture"), + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: self.msaa_samples, + dimension: wgpu::TextureDimension::D2, + format: depth_format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[depth_format], + }) + .create_view(&wgpu::TextureViewDescriptor::default()), + ); + } - self.msaa_texture_view = (self.msaa_samples > 1) + if let Some(render_state) = (self.msaa_samples > 1) .then_some(self.render_state.as_ref()) .flatten() - .map(|render_state| { - let texture_format = render_state.target_format; + { + let texture_format = render_state.target_format; + self.msaa_texture_view.insert( + viewport_id, render_state .device .create_texture(&wgpu::TextureDescriptor { label: Some("egui_msaa_texture"), size: wgpu::Extent3d { - width: width_in_pixels, - height: height_in_pixels, + width, + height, depth_or_array_layers: 1, }, mip_level_count: 1, @@ -310,13 +381,22 @@ impl Painter { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, view_formats: &[texture_format], }) - .create_view(&wgpu::TextureViewDescriptor::default()) - }); + .create_view(&wgpu::TextureViewDescriptor::default()), + ); + }; } - pub fn on_window_resized(&mut self, width_in_pixels: u32, height_in_pixels: u32) { - if self.surface_state.is_some() { + pub fn on_window_resized( + &mut self, + viewport_id: ViewportId, + width_in_pixels: NonZeroU32, + height_in_pixels: NonZeroU32, + ) { + crate::profile_function!(); + + if self.surfaces.contains_key(&viewport_id) { self.resize_and_generate_depth_texture_view_and_msaa_view( + viewport_id, width_in_pixels, height_in_pixels, ); @@ -424,42 +504,28 @@ impl Painter { }) } - // Returns a vector with the frame's pixel data if it was requested. + /// Returns two things: + /// + /// The approximate number of seconds spent on vsync-waiting (if any), + /// and the captures captured screenshot if it was requested. pub fn paint_and_update_textures( &mut self, + viewport_id: ViewportId, pixels_per_point: f32, clear_color: [f32; 4], clipped_primitives: &[epaint::ClippedPrimitive], textures_delta: &epaint::textures::TexturesDelta, capture: bool, - ) -> Option { + ) -> (f32, Option) { crate::profile_function!(); - let render_state = self.render_state.as_mut()?; - let surface_state = self.surface_state.as_ref()?; + let mut vsync_sec = 0.0; - let output_frame = { - crate::profile_scope!("get_current_texture"); - // This is what vsync-waiting happens, at least on Mac. - surface_state.surface.get_current_texture() + let Some(render_state) = self.render_state.as_mut() else { + return (vsync_sec, None); }; - - let output_frame = match output_frame { - Ok(frame) => frame, - #[allow(clippy::single_match_else)] - Err(e) => match (*self.configuration.on_surface_error)(e) { - SurfaceErrorAction::RecreateSurface => { - Self::configure_surface( - surface_state, - render_state, - self.configuration.present_mode, - ); - return None; - } - SurfaceErrorAction::SkipFrame => { - return None; - } - }, + let Some(surface_state) = self.surfaces.get(&viewport_id) else { + return (vsync_sec, None); }; let mut encoder = @@ -504,6 +570,28 @@ impl Painter { } }; + let output_frame = { + crate::profile_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(); + vsync_sec += start.elapsed().as_secs_f32(); + output_frame + }; + + let output_frame = match output_frame { + Ok(frame) => frame, + Err(err) => match (*self.configuration.on_surface_error)(err) { + SurfaceErrorAction::RecreateSurface => { + Self::configure_surface(surface_state, render_state, &self.configuration); + return (vsync_sec, None); + } + SurfaceErrorAction::SkipFrame => { + return (vsync_sec, None); + } + }, + }; + { let renderer = render_state.renderer.read(); let frame_view = if capture { @@ -513,8 +601,11 @@ impl Painter { render_state, ); self.screen_capture_state - .as_ref()? - .texture + .as_ref() + .map_or_else( + || &output_frame.texture, + |capture_state| &capture_state.texture, + ) .create_view(&wgpu::TextureViewDescriptor::default()) } else { output_frame @@ -523,13 +614,14 @@ impl Painter { }; let (view, resolve_target) = (self.msaa_samples > 1) - .then_some(self.msaa_texture_view.as_ref()) + .then_some(self.msaa_texture_view.get(&viewport_id)) .flatten() .map_or((&frame_view, None), |texture_view| { (texture_view, Some(&frame_view)) }); let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("egui_render"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view, resolve_target, @@ -540,20 +632,23 @@ impl Painter { b: clear_color[2] as f64, a: clear_color[3] as f64, }), - store: true, + store: wgpu::StoreOp::Store, }, })], - depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| { + depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map(|view| { wgpu::RenderPassDepthStencilAttachment { view, depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), - store: true, + // It is very unlikely that the depth buffer is needed after egui finished rendering + // so no need to store it. (this can improve performance on tiling GPUs like mobile chips or Apple Silicon) + store: wgpu::StoreOp::Discard, }), stencil_ops: None, } }), - label: Some("egui_render"), + timestamp_writes: None, + occlusion_query_set: None, }); renderer.render(&mut render_pass, clipped_primitives, &screen_descriptor); @@ -574,23 +669,41 @@ impl Painter { // Submit the commands: both the main buffer and user-defined ones. { crate::profile_scope!("Queue::submit"); + // wgpu doesn't document where vsync can happen. Maybe here? + let start = web_time::Instant::now(); render_state .queue - .submit(user_cmd_bufs.into_iter().chain(std::iter::once(encoded))); + .submit(user_cmd_bufs.into_iter().chain([encoded])); + vsync_sec += start.elapsed().as_secs_f32(); }; let screenshot = if capture { - let screen_capture_state = self.screen_capture_state.as_ref()?; - Self::read_screen_rgba(screen_capture_state, render_state, &output_frame) + self.screen_capture_state + .as_ref() + .and_then(|screen_capture_state| { + Self::read_screen_rgba(screen_capture_state, render_state, &output_frame) + }) } else { None }; { crate::profile_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(); } - screenshot + + (vsync_sec, screenshot) + } + + pub fn gc_viewports(&mut self, active_viewports: &ViewportIdSet) { + self.surfaces.retain(|id, _| active_viewports.contains(id)); + self.depth_texture_view + .retain(|id, _| active_viewports.contains(id)); + self.msaa_texture_view + .retain(|id, _| active_viewports.contains(id)); } #[allow(clippy::unused_self)] diff --git a/crates/egui-winit/CHANGELOG.md b/crates/egui-winit/CHANGELOG.md index d181b99b454..95961377e68 100644 --- a/crates/egui-winit/CHANGELOG.md +++ b/crates/egui-winit/CHANGELOG.md @@ -2,7 +2,43 @@ All notable changes to the `egui-winit` integration will be noted in this file. This file is updated upon each release. -Changes since the last release can be found by running the `scripts/generate_changelog.py` script. +Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. + + +## 0.26.2 - 2024-02-14 +* Update memoffset to 0.9.0, arboard to 3.3.1, and remove egui_glow's needless dependency on pure_glow's deps [#4036](https://github.com/emilk/egui/pull/4036) (thanks [@Nopey](https://github.com/Nopey)!) + + +## 0.26.1 - 2024-02-11 +* Nothing new + + +## 0.26.0 - 2024-02-05 +* Don't consume clipboard shortcuts [#3812](https://github.com/emilk/egui/pull/3812) (thanks [@Dinnerbone](https://github.com/Dinnerbone)!) +* Make the `clipboard_text` and `allow_ime` state public [#3724](https://github.com/emilk/egui/pull/3724) (thanks [@tosti007](https://github.com/tosti007)!) + + +## 0.25.0 - 2024-01-08 +* Update to winit 0.29 [#3649](https://github.com/emilk/egui/pull/3649) (thanks [@fornwall](https://github.com/fornwall)!) +* Fix: Let `accesskit` process window events [#3733](https://github.com/emilk/egui/pull/3733) (thanks [@DataTriny](https://github.com/DataTriny)!) +* Simplify `egui_winit::State` [#3678](https://github.com/emilk/egui/pull/3678) + + +## 0.24.1 - 2023-11-30 +* Don't treat `WindowEvent::CloseRequested` as consumed [#3627](https://github.com/emilk/egui/pull/3627) (thanks [@Aaron1011](https://github.com/Aaron1011)!) +* Fix windowing problems when using the `x11` feature on Linux [#3643](https://github.com/emilk/egui/pull/3643) + + +## 0.24.0 - 2023-11-23 +* Update MSRV to Rust 1.72 [#3595](https://github.com/emilk/egui/pull/3595) +* Some breaking changes required for multi-viewport support + + +## 0.23.0 - 2023-09-27 +* Only show on-screen-keyboard and IME when editing text [#3362](https://github.com/emilk/egui/pull/3362) (thanks [@Barugon](https://github.com/Barugon)!) +* Replace `instant` with `web_time` [#3296](https://github.com/emilk/egui/pull/3296) +* Allow users to opt-out of default `winit` features [#3228](https://github.com/emilk/egui/pull/3228) +* Recognize numpad enter/plus/minus [#3285](https://github.com/emilk/egui/pull/3285) ## 0.22.0 - 2023-05-23 diff --git a/crates/egui-winit/Cargo.toml b/crates/egui-winit/Cargo.toml index 2d6833d9198..fdff0fcdcbf 100644 --- a/crates/egui-winit/Cargo.toml +++ b/crates/egui-winit/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "egui-winit" -version = "0.22.0" +version.workspace = true authors = ["Emil Ernerfeldt "] description = "Bindings for using egui with winit" -edition = "2021" -rust-version = "1.67" +edition.workspace = true +rust-version.workspace = true homepage = "https://github.com/emilk/egui/tree/master/crates/egui-winit" -license = "MIT OR Apache-2.0" +license.workspace = true readme = "README.md" repository = "https://github.com/emilk/egui/tree/master/crates/egui-winit" categories = ["gui", "game-development"] @@ -23,6 +23,15 @@ default = ["clipboard", "links", "wayland", "winit/default", "x11"] ## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/). accesskit = ["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 +# rely on Winit to pull in a suitable version (unlike most Rust crates, any version conflicts won't link) +# - It's also important that we don't impose an android-activity backend by taking this choice away from applications. +## Enable the `game-activity` backend via Winit on Android +android-game-activity = ["winit/android-game-activity"] +## Enable the `native-activity` backend via Winit on Android +android-native-activity = ["winit/android-native-activity"] + ## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`egui::epaint::Vertex`], [`egui::Vec2`] etc to `&[u8]`. bytemuck = ["egui/bytemuck"] @@ -34,58 +43,38 @@ clipboard = ["arboard", "smithay-clipboard"] links = ["webbrowser"] ## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -puffin = ["dep:puffin"] +puffin = ["dep:puffin", "egui/puffin"] ## Allow serialization of [`WindowSettings`] using [`serde`](https://docs.rs/serde). serde = ["egui/serde", "dep:serde"] ## Enables Wayland support. -wayland = ["winit/wayland"] +wayland = ["winit/wayland", "bytemuck"] ## Enables compiling for x11. -x11 = ["winit/x11"] - -# 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 -# rely on Winit to pull in a suitable version (unlike most Rust crates, any version conflicts won't link) -# - It's also important that we don't impose an android-activity backend by taking this choice away from applications. - -## Enable the `native-activity` backend via Winit on Android -android-native-activity = ["winit/android-native-activity"] -## Enable the `game-activity` backend via Winit on Android -android-game-activity = ["winit/android-game-activity"] +x11 = ["winit/x11", "bytemuck"] [dependencies] -egui = { version = "0.22.0", path = "../egui", default-features = false, features = [ - "log", -] } -log = { version = "0.4", features = ["std"] } -winit = { version = "0.28", default-features = false } -raw-window-handle = "0.5.0" +egui = { workspace = true, default-features = false, features = ["log"] } +log.workspace = true +raw-window-handle.workspace = true +web-time.workspace = true +winit = { workspace = true, default-features = false, features = ["rwh_06"] } #! ### Optional dependencies # feature accesskit -accesskit_winit = { version = "0.14.0", optional = true } +accesskit_winit = { version = "0.16.0", optional = true } ## Enable this when generating docs. -document-features = { version = "0.2", optional = true } +document-features = { workspace = true, optional = true } -puffin = { version = "0.16", optional = true } +puffin = { workspace = true, optional = true } serde = { version = "1.0", optional = true, features = ["derive"] } - webbrowser = { version = "0.8.3", optional = true } -[target.'cfg(not(target_arch="wasm32"))'.dependencies] -instant = { version = "0.1" } - -[target.'cfg(target_arch="wasm32")'.dependencies] -instant = { version = "0.1", features = [ - "wasm-bindgen", -] } # We use instant so we can (maybe) compile for web - [target.'cfg(any(target_os="linux", target_os="dragonfly", target_os="freebsd", target_os="netbsd", target_os="openbsd"))'.dependencies] -smithay-clipboard = { version = "0.6.3", optional = true } +smithay-clipboard = { version = "0.7.0", optional = true } [target.'cfg(not(target_os = "android"))'.dependencies] -arboard = { version = "3.2", optional = true, default-features = false } +arboard = { version = "3.3", optional = true, default-features = false } diff --git a/crates/egui-winit/src/clipboard.rs b/crates/egui-winit/src/clipboard.rs index 71d913dbbf8..c86a9e19d5b 100644 --- a/crates/egui-winit/src/clipboard.rs +++ b/crates/egui-winit/src/clipboard.rs @@ -1,4 +1,4 @@ -use raw_window_handle::HasRawDisplayHandle; +use raw_window_handle::RawDisplayHandle; /// Handles interfacing with the OS clipboard. /// @@ -26,11 +26,7 @@ pub struct Clipboard { impl Clipboard { /// Construct a new instance - /// - /// # Safety - /// - /// The returned `Clipboard` must not outlive the input `_display_target`. - pub fn new(_display_target: &dyn HasRawDisplayHandle) -> Self { + pub fn new(_raw_display_handle: Option) -> Self { Self { #[cfg(all(feature = "arboard", not(target_os = "android")))] arboard: init_arboard(), @@ -45,7 +41,7 @@ impl Clipboard { ), feature = "smithay-clipboard" ))] - smithay: init_smithay_clipboard(_display_target), + smithay: init_smithay_clipboard(_raw_display_handle), clipboard: Default::default(), } @@ -116,7 +112,9 @@ impl Clipboard { #[cfg(all(feature = "arboard", not(target_os = "android")))] fn init_arboard() -> Option { - log::debug!("Initializing arboard clipboard…"); + crate::profile_function!(); + + log::trace!("Initializing arboard clipboard…"); match arboard::Clipboard::new() { Ok(clipboard) => Some(clipboard), Err(err) => { @@ -137,13 +135,14 @@ fn init_arboard() -> Option { feature = "smithay-clipboard" ))] fn init_smithay_clipboard( - _display_target: &dyn HasRawDisplayHandle, + raw_display_handle: Option, ) -> Option { - use raw_window_handle::RawDisplayHandle; - if let RawDisplayHandle::Wayland(display) = _display_target.raw_display_handle() { - log::debug!("Initializing smithay clipboard…"); + crate::profile_function!(); + + if let Some(RawDisplayHandle::Wayland(display)) = raw_display_handle { + log::trace!("Initializing smithay clipboard…"); #[allow(unsafe_code)] - Some(unsafe { smithay_clipboard::Clipboard::new(display.display) }) + Some(unsafe { smithay_clipboard::Clipboard::new(display.display.as_ptr()) }) } else { #[cfg(feature = "wayland")] log::debug!("Cannot init smithay clipboard without a Wayland display handle"); diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 15e3bb1a795..418c39ece32 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -14,6 +14,7 @@ pub use accesskit_winit; pub use egui; #[cfg(feature = "accesskit")] use egui::accesskit; +use egui::{Pos2, Rect, Vec2, ViewportBuilder, ViewportCommand, ViewportId, ViewportInfo}; pub use winit; pub mod clipboard; @@ -21,20 +22,33 @@ mod window_settings; pub use window_settings::WindowSettings; -use raw_window_handle::HasRawDisplayHandle; +use raw_window_handle::HasDisplayHandle; -pub fn native_pixels_per_point(window: &winit::window::Window) -> f32 { - window.scale_factor() as f32 -} +#[allow(unused_imports)] +pub(crate) use profiling_scopes::*; -pub fn screen_size_in_pixels(window: &winit::window::Window) -> egui::Vec2 { +use winit::{ + dpi::{PhysicalPosition, PhysicalSize}, + event_loop::EventLoopWindowTarget, + window::{CursorGrabMode, Window, WindowButtons, WindowLevel}, +}; + +pub fn screen_size_in_pixels(window: &Window) -> egui::Vec2 { let size = window.inner_size(); egui::vec2(size.width as f32, size.height as f32) } +/// Calculate the `pixels_per_point` for a given window, given the current egui zoom factor +pub fn pixels_per_point(egui_ctx: &egui::Context, window: &Window) -> f32 { + let native_pixels_per_point = window.scale_factor() as f32; + let egui_zoom_factor = egui_ctx.zoom_factor(); + egui_zoom_factor * native_pixels_per_point +} + // ---------------------------------------------------------------------------- #[must_use] +#[derive(Clone, Copy, Debug, Default)] pub struct EventResponse { /// If true, egui consumed this event, i.e. wants exclusive use of this event /// (e.g. a mouse click on an egui window, or entering text into a text field). @@ -51,17 +65,20 @@ pub struct EventResponse { // ---------------------------------------------------------------------------- -/// Handles the integration between egui and winit. +/// Handles the integration between egui and a winit Window. +/// +/// Instantiate one of these per viewport/window. pub struct State { - start_time: instant::Instant, + /// Shared clone. + egui_ctx: egui::Context, + + viewport_id: ViewportId, + start_time: web_time::Instant, egui_input: egui::RawInput, pointer_pos_in_points: Option, any_pointer_button_down: bool, current_cursor_icon: Option, - /// What egui uses. - current_pixels_per_point: f32, - clipboard: clipboard::Clipboard, /// If `true`, mouse inputs will be treated as touches. @@ -80,29 +97,38 @@ pub struct State { #[cfg(feature = "accesskit")] accesskit: Option, + + allow_ime: bool, } impl State { /// Construct a new instance - /// - /// # Safety - /// - /// The returned `State` must not outlive the input `display_target`. - pub fn new(display_target: &dyn HasRawDisplayHandle) -> Self { + pub fn new( + egui_ctx: egui::Context, + viewport_id: ViewportId, + display_target: &dyn HasDisplayHandle, + native_pixels_per_point: Option, + max_texture_side: Option, + ) -> Self { + crate::profile_function!(); + let egui_input = egui::RawInput { focused: false, // winit will tell us when we have focus ..Default::default() }; - Self { - start_time: instant::Instant::now(), + let mut slf = Self { + egui_ctx, + viewport_id, + start_time: web_time::Instant::now(), egui_input, pointer_pos_in_points: None, any_pointer_button_down: false, current_cursor_icon: None, - current_pixels_per_point: 1.0, - clipboard: clipboard::Clipboard::new(display_target), + clipboard: clipboard::Clipboard::new( + display_target.display_handle().ok().map(|h| h.as_raw()), + ), simulate_touch_screen: false, pointer_touch_id: None, @@ -111,16 +137,30 @@ impl State { #[cfg(feature = "accesskit")] accesskit: None, + + allow_ime: false, + }; + + slf.egui_input + .viewports + .entry(ViewportId::ROOT) + .or_default() + .native_pixels_per_point = native_pixels_per_point; + + if let Some(max_texture_side) = max_texture_side { + slf.set_max_texture_side(max_texture_side); } + slf } #[cfg(feature = "accesskit")] pub fn init_accesskit + Send>( &mut self, - window: &winit::window::Window, + window: &Window, event_loop_proxy: winit::event_loop::EventLoopProxy, initial_tree_update_factory: impl 'static + FnOnce() -> accesskit::TreeUpdate + Send, ) { + crate::profile_function!(); self.accesskit = Some(accesskit_winit::Adapter::new( window, initial_tree_update_factory, @@ -134,37 +174,54 @@ impl State { self.egui_input.max_texture_side = Some(max_texture_side); } - /// Call this when a new native Window is created for rendering to initialize the `pixels_per_point` - /// for that window. - /// - /// In particular, on Android it is necessary to call this after each `Resumed` lifecycle - /// event, each time a new native window is created. - /// - /// Once this has been initialized for a new window then this state will be maintained by handling - /// [`winit::event::WindowEvent::ScaleFactorChanged`] events. - pub fn set_pixels_per_point(&mut self, pixels_per_point: f32) { - self.egui_input.pixels_per_point = Some(pixels_per_point); - self.current_pixels_per_point = pixels_per_point; + /// Fetches text from the clipboard and returns it. + pub fn clipboard_text(&mut self) -> Option { + self.clipboard.get() + } + + /// Places the text onto the clipboard. + pub fn set_clipboard_text(&mut self, text: String) { + self.clipboard.set(text); + } + + /// Returns [`false`] or the last value that [`Window::set_ime_allowed()`] was called with, used for debouncing. + pub fn allow_ime(&self) -> bool { + self.allow_ime + } + + /// Set the last value that [`Window::set_ime_allowed()`] was called with. + pub fn set_allow_ime(&mut self, allow: bool) { + self.allow_ime = allow; } - /// The number of physical pixels per logical point, - /// as configured on the current egui context (see [`egui::Context::pixels_per_point`]). #[inline] - pub fn pixels_per_point(&self) -> f32 { - self.current_pixels_per_point + pub fn egui_ctx(&self) -> &egui::Context { + &self.egui_ctx } /// The current input state. - /// This is changed by [`Self::on_event`] and cleared by [`Self::take_egui_input`]. + /// This is changed by [`Self::on_window_event`] and cleared by [`Self::take_egui_input`]. #[inline] pub fn egui_input(&self) -> &egui::RawInput { &self.egui_input } + /// The current input state. + /// This is changed by [`Self::on_window_event`] and cleared by [`Self::take_egui_input`]. + #[inline] + pub fn egui_input_mut(&mut self) -> &mut egui::RawInput { + &mut self.egui_input + } + /// Prepare for a new frame by extracting the accumulated input, + /// /// as well as setting [the time](egui::RawInput::time) and [screen rectangle](egui::RawInput::screen_rect). - pub fn take_egui_input(&mut self, window: &winit::window::Window) -> egui::RawInput { - let pixels_per_point = self.pixels_per_point(); + /// + /// You need to set [`egui::RawInput::viewports`] yourself though. + /// 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!(); self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64()); @@ -172,16 +229,21 @@ impl State { // See: https://github.com/rust-windowing/winit/issues/208 // This solves an issue where egui window positions would be changed when minimizing on Windows. let screen_size_in_pixels = screen_size_in_pixels(window); - let screen_size_in_points = screen_size_in_pixels / pixels_per_point; - self.egui_input.screen_rect = - if screen_size_in_points.x > 0.0 && screen_size_in_points.y > 0.0 { - Some(egui::Rect::from_min_size( - egui::Pos2::ZERO, - screen_size_in_points, - )) - } else { - None - }; + let screen_size_in_points = + screen_size_in_pixels / pixels_per_point(&self.egui_ctx, window); + + self.egui_input.screen_rect = (screen_size_in_points.x > 0.0 + && screen_size_in_points.y > 0.0) + .then(|| Rect::from_min_size(Pos2::ZERO, screen_size_in_points)); + + // Tell egui which viewport is now active: + self.egui_input.viewport_id = self.viewport_id; + + self.egui_input + .viewports + .entry(self.viewport_id) + .or_default() + .native_pixels_per_point = Some(window.scale_factor() as f32); self.egui_input.take() } @@ -189,17 +251,29 @@ impl State { /// Call this when there is a new event. /// /// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`]. - pub fn on_event( + pub fn on_window_event( &mut self, - egui_ctx: &egui::Context, - event: &winit::event::WindowEvent<'_>, + window: &Window, + event: &winit::event::WindowEvent, ) -> EventResponse { + crate::profile_function!(short_window_event_description(event)); + + #[cfg(feature = "accesskit")] + if let Some(accesskit) = &self.accesskit { + accesskit.process_event(window, event); + } + use winit::event::WindowEvent; match event { WindowEvent::ScaleFactorChanged { scale_factor, .. } => { - let pixels_per_point = *scale_factor as f32; - self.egui_input.pixels_per_point = Some(pixels_per_point); - self.current_pixels_per_point = pixels_per_point; + let native_pixels_per_point = *scale_factor as f32; + + self.egui_input + .viewports + .entry(self.viewport_id) + .or_default() + .native_pixels_per_point = Some(native_pixels_per_point); + EventResponse { repaint: true, consumed: false, @@ -209,21 +283,21 @@ impl State { self.on_mouse_button_input(*state, *button); EventResponse { repaint: true, - consumed: egui_ctx.wants_pointer_input(), + consumed: self.egui_ctx.wants_pointer_input(), } } WindowEvent::MouseWheel { delta, .. } => { - self.on_mouse_wheel(*delta); + self.on_mouse_wheel(window, *delta); EventResponse { repaint: true, - consumed: egui_ctx.wants_pointer_input(), + consumed: self.egui_ctx.wants_pointer_input(), } } WindowEvent::CursorMoved { position, .. } => { - self.on_cursor_moved(*position); + self.on_cursor_moved(window, *position); EventResponse { repaint: true, - consumed: egui_ctx.is_using_pointer(), + consumed: self.egui_ctx.is_using_pointer(), } } WindowEvent::CursorLeft { .. } => { @@ -236,37 +310,19 @@ impl State { } // WindowEvent::TouchpadPressure {device_id, pressure, stage, .. } => {} // TODO WindowEvent::Touch(touch) => { - self.on_touch(touch); + self.on_touch(window, touch); let consumed = match touch.phase { winit::event::TouchPhase::Started | winit::event::TouchPhase::Ended - | winit::event::TouchPhase::Cancelled => egui_ctx.wants_pointer_input(), - winit::event::TouchPhase::Moved => egui_ctx.is_using_pointer(), + | winit::event::TouchPhase::Cancelled => self.egui_ctx.wants_pointer_input(), + winit::event::TouchPhase::Moved => self.egui_ctx.is_using_pointer(), }; EventResponse { repaint: true, consumed, } } - WindowEvent::ReceivedCharacter(ch) => { - // On Mac we get here when the user presses Cmd-C (copy), ctrl-W, etc. - // We need to ignore these characters that are side-effects of commands. - let is_mac_cmd = cfg!(target_os = "macos") - && (self.egui_input.modifiers.ctrl || self.egui_input.modifiers.mac_cmd); - let consumed = if is_printable_char(*ch) && !is_mac_cmd { - self.egui_input - .events - .push(egui::Event::Text(ch.to_string())); - egui_ctx.wants_keyboard_input() - } else { - false - }; - EventResponse { - repaint: true, - consumed, - } - } 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. @@ -289,7 +345,7 @@ impl State { .events .push(egui::Event::CompositionEnd(text.clone())); } - winit::event::Ime::Preedit(text, ..) => { + winit::event::Ime::Preedit(text, Some(_)) => { if !self.input_method_editor_started { self.input_method_editor_started = true; self.egui_input.events.push(egui::Event::CompositionStart); @@ -298,18 +354,21 @@ impl State { .events .push(egui::Event::CompositionUpdate(text.clone())); } + winit::event::Ime::Preedit(_, None) => {} }; EventResponse { repaint: true, - consumed: egui_ctx.wants_keyboard_input(), + consumed: self.egui_ctx.wants_keyboard_input(), } } - WindowEvent::KeyboardInput { input, .. } => { - self.on_keyboard_input(input); + WindowEvent::KeyboardInput { event, .. } => { + self.on_keyboard_input(event); + // When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes. - let consumed = egui_ctx.wants_keyboard_input() - || input.virtual_keycode == Some(winit::event::VirtualKeyCode::Tab); + let consumed = self.egui_ctx.wants_keyboard_input() + || event.logical_key + == winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab); EventResponse { repaint: true, consumed, @@ -317,9 +376,6 @@ impl State { } WindowEvent::Focused(focused) => { self.egui_input.focused = *focused; - // We will not be given a KeyboardInput event when the modifiers are released while - // the window does not have focus. Unset all modifier state to be safe. - self.egui_input.modifiers = egui::Modifiers::default(); self.egui_input .events .push(egui::Event::WindowFocused(*focused)); @@ -357,15 +413,23 @@ impl State { } } WindowEvent::ModifiersChanged(state) => { - self.egui_input.modifiers.alt = state.alt(); - self.egui_input.modifiers.ctrl = state.ctrl(); - self.egui_input.modifiers.shift = state.shift(); - self.egui_input.modifiers.mac_cmd = cfg!(target_os = "macos") && state.logo(); + let state = state.state(); + + let alt = state.alt_key(); + let ctrl = state.control_key(); + let shift = state.shift_key(); + let super_ = state.super_key(); + + self.egui_input.modifiers.alt = alt; + self.egui_input.modifiers.ctrl = ctrl; + self.egui_input.modifiers.shift = shift; + self.egui_input.modifiers.mac_cmd = cfg!(target_os = "macos") && super_; self.egui_input.modifiers.command = if cfg!(target_os = "macos") { - state.logo() + super_ } else { - state.ctrl() + ctrl }; + EventResponse { repaint: true, consumed: false, @@ -373,20 +437,22 @@ impl State { } // Things that may require repaint: - WindowEvent::CloseRequested + WindowEvent::RedrawRequested | WindowEvent::CursorEntered { .. } | WindowEvent::Destroyed | WindowEvent::Occluded(_) | WindowEvent::Resized(_) + | WindowEvent::Moved(_) | WindowEvent::ThemeChanged(_) - | WindowEvent::TouchpadPressure { .. } => EventResponse { + | WindowEvent::TouchpadPressure { .. } + | WindowEvent::CloseRequested => EventResponse { repaint: true, consumed: false, }, // Things we completely ignore: - WindowEvent::AxisMotion { .. } - | WindowEvent::Moved(_) + WindowEvent::ActivationTokenDone { .. } + | WindowEvent::AxisMotion { .. } | WindowEvent::SmartMagnify { .. } | WindowEvent::TouchpadRotate { .. } => EventResponse { repaint: false, @@ -400,12 +466,19 @@ impl State { self.egui_input.events.push(egui::Event::Zoom(zoom_factor)); EventResponse { repaint: true, - consumed: egui_ctx.wants_pointer_input(), + consumed: self.egui_ctx.wants_pointer_input(), } } } } + pub fn on_mouse_motion(&mut self, delta: (f64, f64)) { + self.egui_input.events.push(egui::Event::MouseMoved(Vec2 { + x: delta.0 as f32, + y: delta.1 as f32, + })); + } + /// Call this when there is a new [`accesskit::ActionRequest`]. /// /// The result can be found in [`Self::egui_input`] and be extracted with [`Self::take_egui_input`]. @@ -461,10 +534,16 @@ impl State { } } - fn on_cursor_moved(&mut self, pos_in_pixels: winit::dpi::PhysicalPosition) { + fn on_cursor_moved( + &mut self, + window: &Window, + pos_in_pixels: winit::dpi::PhysicalPosition, + ) { + let pixels_per_point = pixels_per_point(&self.egui_ctx, window); + let pos_in_points = egui::pos2( - pos_in_pixels.x as f32 / self.pixels_per_point(), - pos_in_pixels.y as f32 / self.pixels_per_point(), + pos_in_pixels.x as f32 / pixels_per_point, + pos_in_pixels.y as f32 / pixels_per_point, ); self.pointer_pos_in_points = Some(pos_in_points); @@ -489,7 +568,9 @@ impl State { } } - fn on_touch(&mut self, touch: &winit::event::Touch) { + fn on_touch(&mut self, window: &Window, touch: &winit::event::Touch) { + let pixels_per_point = pixels_per_point(&self.egui_ctx, window); + // Emit touch event self.egui_input.events.push(egui::Event::Touch { device_id: egui::TouchDeviceId(egui::epaint::util::hash(touch.device_id)), @@ -501,8 +582,8 @@ impl State { winit::event::TouchPhase::Cancelled => egui::TouchPhase::Cancel, }, pos: egui::pos2( - touch.location.x as f32 / self.pixels_per_point(), - touch.location.y as f32 / self.pixels_per_point(), + touch.location.x as f32 / pixels_per_point, + touch.location.y as f32 / pixels_per_point, ), force: match touch.force { Some(winit::event::Force::Normalized(force)) => Some(force as f32), @@ -522,14 +603,14 @@ impl State { winit::event::TouchPhase::Started => { self.pointer_touch_id = Some(touch.id); // First move the pointer to the right location - self.on_cursor_moved(touch.location); + self.on_cursor_moved(window, touch.location); self.on_mouse_button_input( winit::event::ElementState::Pressed, winit::event::MouseButton::Left, ); } winit::event::TouchPhase::Moved => { - self.on_cursor_moved(touch.location); + self.on_cursor_moved(window, touch.location); } winit::event::TouchPhase::Ended => { self.pointer_touch_id = None; @@ -551,7 +632,9 @@ impl State { } } - fn on_mouse_wheel(&mut self, delta: winit::event::MouseScrollDelta) { + fn on_mouse_wheel(&mut self, window: &Window, delta: winit::event::MouseScrollDelta) { + let pixels_per_point = pixels_per_point(&self.egui_ctx, window); + { let (unit, delta) = match delta { winit::event::MouseScrollDelta::LineDelta(x, y) => { @@ -562,7 +645,7 @@ impl State { y, }) => ( egui::MouseWheelUnit::Point, - egui::vec2(x as f32, y as f32) / self.pixels_per_point(), + egui::vec2(x as f32, y as f32) / pixels_per_point, ), }; let modifiers = self.egui_input.modifiers; @@ -578,7 +661,7 @@ impl State { egui::vec2(x, y) * points_per_scroll_line } winit::event::MouseScrollDelta::PixelDelta(delta) => { - egui::vec2(delta.x as f32, delta.y as f32) / self.pixels_per_point() + egui::vec2(delta.x as f32, delta.y as f32) / pixels_per_point } }; @@ -597,34 +680,95 @@ impl State { } } - fn on_keyboard_input(&mut self, input: &winit::event::KeyboardInput) { - if let Some(keycode) = input.virtual_keycode { - let pressed = input.state == winit::event::ElementState::Pressed; + fn on_keyboard_input(&mut self, event: &winit::event::KeyEvent) { + let winit::event::KeyEvent { + // Represents the position of a key independent of the currently active layout. + // + // It also uniquely identifies the physical key (i.e. it's mostly synonymous with a scancode). + // The most prevalent use case for this is games. For example the default keys for the player + // to move around might be the W, A, S, and D keys on a US layout. The position of these keys + // is more important than their label, so they should map to Z, Q, S, and D on an "AZERTY" + // layout. (This value is `KeyCode::KeyW` for the Z key on an AZERTY layout.) + physical_key, + + // Represents the results of a keymap, i.e. what character a certain key press represents. + // When telling users "Press Ctrl-F to find", this is where we should + // look for the "F" key, because they may have a dvorak layout on + // a qwerty keyboard, and so the logical "F" character may not be located on the physical `KeyCode::KeyF` position. + logical_key, + + text, + + state, + + location: _, // e.g. is it on the numpad? + repeat: _, // egui will figure this out for us + .. + } = event; + + let pressed = *state == winit::event::ElementState::Pressed; + + let physical_key = if let winit::keyboard::PhysicalKey::Code(keycode) = *physical_key { + key_from_key_code(keycode) + } else { + None + }; + + let logical_key = key_from_winit_key(logical_key); + // Helpful logging to enable when adding new key support + log::trace!( + "logical {:?} -> {:?}, physical {:?} -> {:?}", + event.logical_key, + logical_key, + event.physical_key, + physical_key + ); + + if let Some(logical_key) = logical_key { if pressed { - // VirtualKeyCode::Paste etc in winit are broken/untrustworthy, - // so we detect these things manually: - if is_cut_command(self.egui_input.modifiers, keycode) { + if is_cut_command(self.egui_input.modifiers, logical_key) { self.egui_input.events.push(egui::Event::Cut); - } else if is_copy_command(self.egui_input.modifiers, keycode) { + return; + } else if is_copy_command(self.egui_input.modifiers, logical_key) { self.egui_input.events.push(egui::Event::Copy); - } else if is_paste_command(self.egui_input.modifiers, keycode) { + return; + } else if is_paste_command(self.egui_input.modifiers, logical_key) { if let Some(contents) = self.clipboard.get() { let contents = contents.replace("\r\n", "\n"); if !contents.is_empty() { self.egui_input.events.push(egui::Event::Paste(contents)); } } + return; } } - if let Some(key) = translate_virtual_key_code(keycode) { - self.egui_input.events.push(egui::Event::Key { - key, - pressed, - repeat: false, // egui will fill this in for us! - modifiers: self.egui_input.modifiers, - }); + self.egui_input.events.push(egui::Event::Key { + key: logical_key, + physical_key, + pressed, + repeat: false, // egui will fill this in for us! + modifiers: self.egui_input.modifiers, + }); + } + + if let Some(text) = &text { + // Make sure there is text, and that it is not control characters + // (e.g. delete is sent as "\u{f728}" on macOS). + if !text.is_empty() && text.chars().all(is_printable_char) { + // On some platforms we get here when the user presses Cmd-C (copy), ctrl-W, etc. + // We need to ignore these characters that are side-effects of commands. + // Also make sure the key is pressed (not released). On Linux, text might + // contain some data even when the key is released. + let is_cmd = self.egui_input.modifiers.ctrl + || self.egui_input.modifiers.command + || self.egui_input.modifiers.mac_cmd; + if pressed && !is_cmd { + self.egui_input + .events + .push(egui::Event::Text(text.to_string())); + } } } } @@ -639,21 +783,21 @@ impl State { /// * pub fn handle_platform_output( &mut self, - window: &winit::window::Window, - egui_ctx: &egui::Context, + window: &Window, platform_output: egui::PlatformOutput, ) { + crate::profile_function!(); + let egui::PlatformOutput { cursor_icon, open_url, copied_text, - events: _, // handled above + events: _, // handled elsewhere mutable_text_under_cursor: _, // only used in eframe web - text_cursor_pos, + ime, #[cfg(feature = "accesskit")] accesskit_update, } = platform_output; - self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI self.set_cursor_icon(window, cursor_icon); @@ -665,19 +809,39 @@ impl State { self.clipboard.set(copied_text); } - if let Some(egui::Pos2 { x, y }) = text_cursor_pos { - window.set_ime_position(winit::dpi::LogicalPosition { x, y }); + let allow_ime = ime.is_some(); + if self.allow_ime != allow_ime { + self.allow_ime = allow_ime; + crate::profile_scope!("set_ime_allowed"); + window.set_ime_allowed(allow_ime); + } + + if let Some(ime) = ime { + let rect = ime.rect; + let pixels_per_point = pixels_per_point(&self.egui_ctx, window); + crate::profile_scope!("set_ime_cursor_area"); + window.set_ime_cursor_area( + winit::dpi::PhysicalPosition { + x: pixels_per_point * rect.min.x, + y: pixels_per_point * rect.min.y, + }, + winit::dpi::PhysicalSize { + width: pixels_per_point * rect.width(), + height: pixels_per_point * rect.height(), + }, + ); } #[cfg(feature = "accesskit")] if let Some(accesskit) = self.accesskit.as_ref() { if let Some(update) = accesskit_update { + crate::profile_scope!("accesskit"); accesskit.update_if_active(|| update); } } } - fn set_cursor_icon(&mut self, window: &winit::window::Window, cursor_icon: egui::CursorIcon) { + fn set_cursor_icon(&mut self, window: &Window, cursor_icon: egui::CursorIcon) { if self.current_cursor_icon == Some(cursor_icon) { // Prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing. // On other platforms: just early-out to save CPU. @@ -701,6 +865,97 @@ impl State { } } +/// Update the given viewport info with the current state of the window. +/// +/// Call before [`State::take_egui_input`]. +pub fn update_viewport_info( + viewport_info: &mut ViewportInfo, + egui_ctx: &egui::Context, + window: &Window, +) { + crate::profile_function!(); + + let pixels_per_point = pixels_per_point(egui_ctx, window); + + let has_a_position = match window.is_minimized() { + None | Some(true) => false, + Some(false) => true, + }; + + let inner_pos_px = if has_a_position { + window + .inner_position() + .map(|pos| Pos2::new(pos.x as f32, pos.y as f32)) + .ok() + } else { + None + }; + + let outer_pos_px = if has_a_position { + window + .outer_position() + .map(|pos| Pos2::new(pos.x as f32, pos.y as f32)) + .ok() + } else { + None + }; + + let inner_size_px = if has_a_position { + let size = window.inner_size(); + Some(Vec2::new(size.width as f32, size.height as f32)) + } else { + None + }; + + let outer_size_px = if has_a_position { + let size = window.outer_size(); + Some(Vec2::new(size.width as f32, size.height as f32)) + } else { + None + }; + + let inner_rect_px = if let (Some(pos), Some(size)) = (inner_pos_px, inner_size_px) { + Some(Rect::from_min_size(pos, size)) + } else { + None + }; + + let outer_rect_px = if let (Some(pos), Some(size)) = (outer_pos_px, outer_size_px) { + Some(Rect::from_min_size(pos, size)) + } else { + None + }; + + let inner_rect = inner_rect_px.map(|r| r / pixels_per_point); + let outer_rect = outer_rect_px.map(|r| r / pixels_per_point); + + let monitor_size = { + crate::profile_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)) + } else { + None + } + }; + + viewport_info.focused = Some(window.has_focus()); + viewport_info.fullscreen = Some(window.fullscreen().is_some()); + viewport_info.inner_rect = inner_rect; + viewport_info.monitor_size = monitor_size; + viewport_info.native_pixels_per_point = Some(window.scale_factor() as f32); + viewport_info.outer_rect = outer_rect; + viewport_info.title = Some(window.title()); + + if cfg!(target_os = "windows") { + // It's tempting to do this, but it leads to a deadlock on Mac when running + // `cargo run -p custom_window_frame`. + // See https://github.com/emilk/egui/issues/3494 + viewport_info.maximized = Some(window.is_maximized()); + viewport_info.minimized = Some(window.is_minimized().unwrap_or(false)); + } +} + fn open_url_in_browser(_url: &str) { #[cfg(feature = "webbrowser")] if let Err(err) = webbrowser::open(_url) { @@ -725,25 +980,22 @@ fn is_printable_char(chr: char) -> bool { !is_in_private_use_area && !chr.is_ascii_control() } -fn is_cut_command(modifiers: egui::Modifiers, keycode: winit::event::VirtualKeyCode) -> bool { - (modifiers.command && keycode == winit::event::VirtualKeyCode::X) - || (cfg!(target_os = "windows") - && modifiers.shift - && keycode == winit::event::VirtualKeyCode::Delete) +fn is_cut_command(modifiers: egui::Modifiers, keycode: egui::Key) -> bool { + keycode == egui::Key::Cut + || (modifiers.command && keycode == egui::Key::X) + || (cfg!(target_os = "windows") && modifiers.shift && keycode == egui::Key::Delete) } -fn is_copy_command(modifiers: egui::Modifiers, keycode: winit::event::VirtualKeyCode) -> bool { - (modifiers.command && keycode == winit::event::VirtualKeyCode::C) - || (cfg!(target_os = "windows") - && modifiers.ctrl - && keycode == winit::event::VirtualKeyCode::Insert) +fn is_copy_command(modifiers: egui::Modifiers, keycode: egui::Key) -> bool { + keycode == egui::Key::Copy + || (modifiers.command && keycode == egui::Key::C) + || (cfg!(target_os = "windows") && modifiers.ctrl && keycode == egui::Key::Insert) } -fn is_paste_command(modifiers: egui::Modifiers, keycode: winit::event::VirtualKeyCode) -> bool { - (modifiers.command && keycode == winit::event::VirtualKeyCode::V) - || (cfg!(target_os = "windows") - && modifiers.shift - && keycode == winit::event::VirtualKeyCode::Insert) +fn is_paste_command(modifiers: egui::Modifiers, keycode: egui::Key) -> bool { + keycode == egui::Key::Paste + || (modifiers.command && keycode == egui::Key::V) + || (cfg!(target_os = "windows") && modifiers.shift && keycode == egui::Key::Insert) } fn translate_mouse_button(button: winit::event::MouseButton) -> Option { @@ -751,98 +1003,201 @@ fn translate_mouse_button(button: winit::event::MouseButton) -> Option Some(egui::PointerButton::Primary), winit::event::MouseButton::Right => Some(egui::PointerButton::Secondary), winit::event::MouseButton::Middle => Some(egui::PointerButton::Middle), - winit::event::MouseButton::Other(1) => Some(egui::PointerButton::Extra1), - winit::event::MouseButton::Other(2) => Some(egui::PointerButton::Extra2), + winit::event::MouseButton::Back => Some(egui::PointerButton::Extra1), + winit::event::MouseButton::Forward => Some(egui::PointerButton::Extra2), winit::event::MouseButton::Other(_) => None, } } -fn translate_virtual_key_code(key: winit::event::VirtualKeyCode) -> Option { +fn key_from_winit_key(key: &winit::keyboard::Key) -> Option { + match key { + winit::keyboard::Key::Named(named_key) => key_from_named_key(*named_key), + winit::keyboard::Key::Character(str) => egui::Key::from_name(str.as_str()), + winit::keyboard::Key::Unidentified(_) | winit::keyboard::Key::Dead(_) => None, + } +} + +fn key_from_named_key(named_key: winit::keyboard::NamedKey) -> Option { + use egui::Key; + use winit::keyboard::NamedKey; + + Some(match named_key { + NamedKey::Enter => Key::Enter, + NamedKey::Tab => Key::Tab, + NamedKey::ArrowDown => Key::ArrowDown, + NamedKey::ArrowLeft => Key::ArrowLeft, + NamedKey::ArrowRight => Key::ArrowRight, + NamedKey::ArrowUp => Key::ArrowUp, + NamedKey::End => Key::End, + NamedKey::Home => Key::Home, + NamedKey::PageDown => Key::PageDown, + NamedKey::PageUp => Key::PageUp, + NamedKey::Backspace => Key::Backspace, + NamedKey::Delete => Key::Delete, + NamedKey::Insert => Key::Insert, + NamedKey::Escape => Key::Escape, + NamedKey::Cut => Key::Cut, + NamedKey::Copy => Key::Copy, + NamedKey::Paste => Key::Paste, + + NamedKey::Space => Key::Space, + + NamedKey::F1 => Key::F1, + NamedKey::F2 => Key::F2, + NamedKey::F3 => Key::F3, + NamedKey::F4 => Key::F4, + NamedKey::F5 => Key::F5, + NamedKey::F6 => Key::F6, + NamedKey::F7 => Key::F7, + NamedKey::F8 => Key::F8, + NamedKey::F9 => Key::F9, + NamedKey::F10 => Key::F10, + NamedKey::F11 => Key::F11, + NamedKey::F12 => Key::F12, + NamedKey::F13 => Key::F13, + NamedKey::F14 => Key::F14, + NamedKey::F15 => Key::F15, + NamedKey::F16 => Key::F16, + NamedKey::F17 => Key::F17, + NamedKey::F18 => Key::F18, + NamedKey::F19 => Key::F19, + NamedKey::F20 => Key::F20, + NamedKey::F21 => Key::F21, + NamedKey::F22 => Key::F22, + NamedKey::F23 => Key::F23, + NamedKey::F24 => Key::F24, + NamedKey::F25 => Key::F25, + NamedKey::F26 => Key::F26, + NamedKey::F27 => Key::F27, + NamedKey::F28 => Key::F28, + NamedKey::F29 => Key::F29, + NamedKey::F30 => Key::F30, + NamedKey::F31 => Key::F31, + NamedKey::F32 => Key::F32, + NamedKey::F33 => Key::F33, + NamedKey::F34 => Key::F34, + NamedKey::F35 => Key::F35, + _ => { + log::trace!("Unknown key: {named_key:?}"); + return None; + } + }) +} + +fn key_from_key_code(key: winit::keyboard::KeyCode) -> Option { use egui::Key; - use winit::event::VirtualKeyCode; + use winit::keyboard::KeyCode; Some(match key { - VirtualKeyCode::Down => Key::ArrowDown, - VirtualKeyCode::Left => Key::ArrowLeft, - VirtualKeyCode::Right => Key::ArrowRight, - VirtualKeyCode::Up => Key::ArrowUp, - - VirtualKeyCode::Escape => Key::Escape, - VirtualKeyCode::Tab => Key::Tab, - VirtualKeyCode::Back => Key::Backspace, - VirtualKeyCode::Return => Key::Enter, - VirtualKeyCode::Space => Key::Space, - - VirtualKeyCode::Insert => Key::Insert, - VirtualKeyCode::Delete => Key::Delete, - VirtualKeyCode::Home => Key::Home, - VirtualKeyCode::End => Key::End, - VirtualKeyCode::PageUp => Key::PageUp, - VirtualKeyCode::PageDown => Key::PageDown, - - VirtualKeyCode::Minus => Key::Minus, - // Using Mac the key with the Plus sign on it is reported as the Equals key - // (with both English and Swedish keyboard). - VirtualKeyCode::Equals => Key::PlusEquals, - - VirtualKeyCode::Key0 | VirtualKeyCode::Numpad0 => Key::Num0, - VirtualKeyCode::Key1 | VirtualKeyCode::Numpad1 => Key::Num1, - VirtualKeyCode::Key2 | VirtualKeyCode::Numpad2 => Key::Num2, - VirtualKeyCode::Key3 | VirtualKeyCode::Numpad3 => Key::Num3, - VirtualKeyCode::Key4 | VirtualKeyCode::Numpad4 => Key::Num4, - VirtualKeyCode::Key5 | VirtualKeyCode::Numpad5 => Key::Num5, - VirtualKeyCode::Key6 | VirtualKeyCode::Numpad6 => Key::Num6, - VirtualKeyCode::Key7 | VirtualKeyCode::Numpad7 => Key::Num7, - VirtualKeyCode::Key8 | VirtualKeyCode::Numpad8 => Key::Num8, - VirtualKeyCode::Key9 | VirtualKeyCode::Numpad9 => Key::Num9, - - VirtualKeyCode::A => Key::A, - VirtualKeyCode::B => Key::B, - VirtualKeyCode::C => Key::C, - VirtualKeyCode::D => Key::D, - VirtualKeyCode::E => Key::E, - VirtualKeyCode::F => Key::F, - VirtualKeyCode::G => Key::G, - VirtualKeyCode::H => Key::H, - VirtualKeyCode::I => Key::I, - VirtualKeyCode::J => Key::J, - VirtualKeyCode::K => Key::K, - VirtualKeyCode::L => Key::L, - VirtualKeyCode::M => Key::M, - VirtualKeyCode::N => Key::N, - VirtualKeyCode::O => Key::O, - VirtualKeyCode::P => Key::P, - VirtualKeyCode::Q => Key::Q, - VirtualKeyCode::R => Key::R, - VirtualKeyCode::S => Key::S, - VirtualKeyCode::T => Key::T, - VirtualKeyCode::U => Key::U, - VirtualKeyCode::V => Key::V, - VirtualKeyCode::W => Key::W, - VirtualKeyCode::X => Key::X, - VirtualKeyCode::Y => Key::Y, - VirtualKeyCode::Z => Key::Z, - - VirtualKeyCode::F1 => Key::F1, - VirtualKeyCode::F2 => Key::F2, - VirtualKeyCode::F3 => Key::F3, - VirtualKeyCode::F4 => Key::F4, - VirtualKeyCode::F5 => Key::F5, - VirtualKeyCode::F6 => Key::F6, - VirtualKeyCode::F7 => Key::F7, - VirtualKeyCode::F8 => Key::F8, - VirtualKeyCode::F9 => Key::F9, - VirtualKeyCode::F10 => Key::F10, - VirtualKeyCode::F11 => Key::F11, - VirtualKeyCode::F12 => Key::F12, - VirtualKeyCode::F13 => Key::F13, - VirtualKeyCode::F14 => Key::F14, - VirtualKeyCode::F15 => Key::F15, - VirtualKeyCode::F16 => Key::F16, - VirtualKeyCode::F17 => Key::F17, - VirtualKeyCode::F18 => Key::F18, - VirtualKeyCode::F19 => Key::F19, - VirtualKeyCode::F20 => Key::F20, + KeyCode::ArrowDown => Key::ArrowDown, + KeyCode::ArrowLeft => Key::ArrowLeft, + KeyCode::ArrowRight => Key::ArrowRight, + KeyCode::ArrowUp => Key::ArrowUp, + + KeyCode::Escape => Key::Escape, + KeyCode::Tab => Key::Tab, + KeyCode::Backspace => Key::Backspace, + KeyCode::Enter | KeyCode::NumpadEnter => Key::Enter, + + KeyCode::Insert => Key::Insert, + KeyCode::Delete => Key::Delete, + KeyCode::Home => Key::Home, + KeyCode::End => Key::End, + KeyCode::PageUp => Key::PageUp, + KeyCode::PageDown => Key::PageDown, + + // Punctuation + KeyCode::Space => Key::Space, + KeyCode::Comma => Key::Comma, + KeyCode::Period => Key::Period, + // KeyCode::Colon => Key::Colon, // NOTE: there is no physical colon key on an american keyboard + KeyCode::Semicolon => Key::Semicolon, + KeyCode::Backslash => Key::Backslash, + KeyCode::Slash | KeyCode::NumpadDivide => Key::Slash, + KeyCode::BracketLeft => Key::OpenBracket, + KeyCode::BracketRight => Key::CloseBracket, + KeyCode::Backquote => Key::Backtick, + + KeyCode::Cut => Key::Cut, + KeyCode::Copy => Key::Copy, + KeyCode::Paste => Key::Paste, + KeyCode::Minus | KeyCode::NumpadSubtract => Key::Minus, + KeyCode::NumpadAdd => Key::Plus, + KeyCode::Equal => Key::Equals, + + KeyCode::Digit0 | KeyCode::Numpad0 => Key::Num0, + KeyCode::Digit1 | KeyCode::Numpad1 => Key::Num1, + KeyCode::Digit2 | KeyCode::Numpad2 => Key::Num2, + KeyCode::Digit3 | KeyCode::Numpad3 => Key::Num3, + KeyCode::Digit4 | KeyCode::Numpad4 => Key::Num4, + KeyCode::Digit5 | KeyCode::Numpad5 => Key::Num5, + KeyCode::Digit6 | KeyCode::Numpad6 => Key::Num6, + KeyCode::Digit7 | KeyCode::Numpad7 => Key::Num7, + KeyCode::Digit8 | KeyCode::Numpad8 => Key::Num8, + KeyCode::Digit9 | KeyCode::Numpad9 => Key::Num9, + + KeyCode::KeyA => Key::A, + KeyCode::KeyB => Key::B, + KeyCode::KeyC => Key::C, + KeyCode::KeyD => Key::D, + KeyCode::KeyE => Key::E, + KeyCode::KeyF => Key::F, + KeyCode::KeyG => Key::G, + KeyCode::KeyH => Key::H, + KeyCode::KeyI => Key::I, + KeyCode::KeyJ => Key::J, + KeyCode::KeyK => Key::K, + KeyCode::KeyL => Key::L, + KeyCode::KeyM => Key::M, + KeyCode::KeyN => Key::N, + KeyCode::KeyO => Key::O, + KeyCode::KeyP => Key::P, + KeyCode::KeyQ => Key::Q, + KeyCode::KeyR => Key::R, + KeyCode::KeyS => Key::S, + KeyCode::KeyT => Key::T, + KeyCode::KeyU => Key::U, + KeyCode::KeyV => Key::V, + KeyCode::KeyW => Key::W, + KeyCode::KeyX => Key::X, + KeyCode::KeyY => Key::Y, + KeyCode::KeyZ => Key::Z, + + KeyCode::F1 => Key::F1, + KeyCode::F2 => Key::F2, + KeyCode::F3 => Key::F3, + KeyCode::F4 => Key::F4, + KeyCode::F5 => Key::F5, + KeyCode::F6 => Key::F6, + KeyCode::F7 => Key::F7, + KeyCode::F8 => Key::F8, + KeyCode::F9 => Key::F9, + KeyCode::F10 => Key::F10, + KeyCode::F11 => Key::F11, + KeyCode::F12 => Key::F12, + KeyCode::F13 => Key::F13, + KeyCode::F14 => Key::F14, + KeyCode::F15 => Key::F15, + KeyCode::F16 => Key::F16, + KeyCode::F17 => Key::F17, + KeyCode::F18 => Key::F18, + KeyCode::F19 => Key::F19, + KeyCode::F20 => Key::F20, + KeyCode::F21 => Key::F21, + KeyCode::F22 => Key::F22, + KeyCode::F23 => Key::F23, + KeyCode::F24 => Key::F24, + KeyCode::F25 => Key::F25, + KeyCode::F26 => Key::F26, + KeyCode::F27 => Key::F27, + KeyCode::F28 => Key::F28, + KeyCode::F29 => Key::F29, + KeyCode::F30 => Key::F30, + KeyCode::F31 => Key::F31, + KeyCode::F32 => Key::F32, + KeyCode::F33 => Key::F33, + KeyCode::F34 => Key::F34, + KeyCode::F35 => Key::F35, _ => { return None; @@ -867,7 +1222,7 @@ fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option Some(winit::window::CursorIcon::Move), egui::CursorIcon::NoDrop => Some(winit::window::CursorIcon::NoDrop), egui::CursorIcon::NotAllowed => Some(winit::window::CursorIcon::NotAllowed), - egui::CursorIcon::PointingHand => Some(winit::window::CursorIcon::Hand), + egui::CursorIcon::PointingHand => Some(winit::window::CursorIcon::Pointer), egui::CursorIcon::Progress => Some(winit::window::CursorIcon::Progress), egui::CursorIcon::ResizeHorizontal => Some(winit::window::CursorIcon::EwResize), @@ -894,28 +1249,577 @@ fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option { - #[cfg(feature = "puffin")] - puffin::profile_function!($($arg)*); - }; +pub fn process_viewport_commands( + egui_ctx: &egui::Context, + info: &mut ViewportInfo, + commands: impl IntoIterator, + window: &Window, + is_viewport_focused: bool, + screenshot_requested: &mut bool, +) { + for command in commands { + process_viewport_command( + egui_ctx, + window, + command, + info, + is_viewport_focused, + screenshot_requested, + ); + } } -#[allow(unused_imports)] -pub(crate) use profile_function; - -/// Profiling macro for feature "puffin" -#[allow(unused_macros)] -macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - puffin::profile_scope!($($arg)*); +fn process_viewport_command( + egui_ctx: &egui::Context, + window: &Window, + command: ViewportCommand, + info: &mut ViewportInfo, + is_viewport_focused: bool, + screenshot_requested: &mut bool, +) { + crate::profile_function!(); + + use winit::window::ResizeDirection; + + log::trace!("Processing ViewportCommand::{command:?}"); + + let pixels_per_point = pixels_per_point(egui_ctx, window); + + match command { + ViewportCommand::Close => { + info.events.push(egui::ViewportEvent::Close); + } + ViewportCommand::CancelClose => { + // Need to be handled elsewhere + } + ViewportCommand::StartDrag => { + // If `is_viewport_focused` is not checked on x11 the input will be permanently taken until the app is killed! + + // TODO: check that the left mouse-button was pressed down recently, + // or we will have bugs on Windows. + // See https://github.com/emilk/egui/pull/1108 + if is_viewport_focused { + if let Err(err) = window.drag_window() { + log::warn!("{command:?}: {err}"); + } + } + } + ViewportCommand::InnerSize(size) => { + let width_px = pixels_per_point * size.x.max(1.0); + let height_px = pixels_per_point * size.y.max(1.0); + if window + .request_inner_size(PhysicalSize::new(width_px, height_px)) + .is_some() + { + log::debug!("ViewportCommand::InnerSize ignored by winit"); + } + } + ViewportCommand::BeginResize(direction) => { + if let Err(err) = window.drag_resize_window(match direction { + egui::viewport::ResizeDirection::North => ResizeDirection::North, + egui::viewport::ResizeDirection::South => ResizeDirection::South, + egui::viewport::ResizeDirection::East => ResizeDirection::East, + egui::viewport::ResizeDirection::West => ResizeDirection::West, + egui::viewport::ResizeDirection::NorthEast => ResizeDirection::NorthEast, + egui::viewport::ResizeDirection::SouthEast => ResizeDirection::SouthEast, + egui::viewport::ResizeDirection::NorthWest => ResizeDirection::NorthWest, + egui::viewport::ResizeDirection::SouthWest => ResizeDirection::SouthWest, + }) { + log::warn!("{command:?}: {err}"); + } + } + ViewportCommand::Title(title) => { + window.set_title(&title); + } + ViewportCommand::Transparent(v) => window.set_transparent(v), + ViewportCommand::Visible(v) => window.set_visible(v), + ViewportCommand::OuterPosition(pos) => { + window.set_outer_position(PhysicalPosition::new( + pixels_per_point * pos.x, + pixels_per_point * pos.y, + )); + } + ViewportCommand::MinInnerSize(s) => { + window.set_min_inner_size((s.is_finite() && s != Vec2::ZERO).then_some( + PhysicalSize::new(pixels_per_point * s.x, pixels_per_point * s.y), + )); + } + ViewportCommand::MaxInnerSize(s) => { + window.set_max_inner_size((s.is_finite() && s != Vec2::INFINITY).then_some( + PhysicalSize::new(pixels_per_point * s.x, pixels_per_point * s.y), + )); + } + ViewportCommand::ResizeIncrements(s) => { + window.set_resize_increments( + s.map(|s| PhysicalSize::new(pixels_per_point * s.x, pixels_per_point * s.y)), + ); + } + ViewportCommand::Resizable(v) => window.set_resizable(v), + ViewportCommand::EnableButtons { + close, + minimized, + maximize, + } => window.set_enabled_buttons( + if close { + WindowButtons::CLOSE + } else { + WindowButtons::empty() + } | if minimized { + WindowButtons::MINIMIZE + } else { + WindowButtons::empty() + } | if maximize { + WindowButtons::MAXIMIZE + } else { + WindowButtons::empty() + }, + ), + ViewportCommand::Minimized(v) => { + window.set_minimized(v); + info.minimized = Some(v); + } + ViewportCommand::Maximized(v) => { + window.set_maximized(v); + info.maximized = Some(v); + } + ViewportCommand::Fullscreen(v) => { + window.set_fullscreen(v.then_some(winit::window::Fullscreen::Borderless(None))); + } + ViewportCommand::Decorations(v) => window.set_decorations(v), + ViewportCommand::WindowLevel(l) => window.set_window_level(match l { + egui::viewport::WindowLevel::AlwaysOnBottom => WindowLevel::AlwaysOnBottom, + egui::viewport::WindowLevel::AlwaysOnTop => WindowLevel::AlwaysOnTop, + egui::viewport::WindowLevel::Normal => WindowLevel::Normal, + }), + ViewportCommand::Icon(icon) => { + let winit_icon = icon.and_then(|icon| to_winit_icon(&icon)); + window.set_window_icon(winit_icon); + } + ViewportCommand::IMERect(rect) => { + window.set_ime_cursor_area( + PhysicalPosition::new(pixels_per_point * rect.min.x, pixels_per_point * rect.min.y), + PhysicalSize::new( + pixels_per_point * rect.size().x, + pixels_per_point * rect.size().y, + ), + ); + } + ViewportCommand::IMEAllowed(v) => window.set_ime_allowed(v), + ViewportCommand::IMEPurpose(p) => window.set_ime_purpose(match p { + egui::viewport::IMEPurpose::Password => winit::window::ImePurpose::Password, + egui::viewport::IMEPurpose::Terminal => winit::window::ImePurpose::Terminal, + egui::viewport::IMEPurpose::Normal => winit::window::ImePurpose::Normal, + }), + ViewportCommand::Focus => { + if !window.has_focus() { + window.focus_window(); + } + } + ViewportCommand::RequestUserAttention(a) => { + window.request_user_attention(match a { + egui::UserAttentionType::Reset => None, + egui::UserAttentionType::Critical => { + Some(winit::window::UserAttentionType::Critical) + } + egui::UserAttentionType::Informational => { + Some(winit::window::UserAttentionType::Informational) + } + }); + } + ViewportCommand::SetTheme(t) => window.set_theme(match t { + egui::SystemTheme::Light => Some(winit::window::Theme::Light), + egui::SystemTheme::Dark => Some(winit::window::Theme::Dark), + egui::SystemTheme::SystemDefault => None, + }), + ViewportCommand::ContentProtected(v) => window.set_content_protected(v), + ViewportCommand::CursorPosition(pos) => { + if let Err(err) = window.set_cursor_position(PhysicalPosition::new( + pixels_per_point * pos.x, + pixels_per_point * pos.y, + )) { + log::warn!("{command:?}: {err}"); + } + } + ViewportCommand::CursorGrab(o) => { + if let Err(err) = window.set_cursor_grab(match o { + egui::viewport::CursorGrab::None => CursorGrabMode::None, + egui::viewport::CursorGrab::Confined => CursorGrabMode::Confined, + egui::viewport::CursorGrab::Locked => CursorGrabMode::Locked, + }) { + log::warn!("{command:?}: {err}"); + } + } + ViewportCommand::CursorVisible(v) => window.set_cursor_visible(v), + ViewportCommand::MousePassthrough(passthrough) => { + if let Err(err) = window.set_cursor_hittest(!passthrough) { + log::warn!("{command:?}: {err}"); + } + } + ViewportCommand::Screenshot => { + *screenshot_requested = true; + } + } +} + +/// Build and intitlaize a window. +/// +/// Wrapper around `create_winit_window_builder` and `apply_viewport_builder_to_window`. +pub fn create_window( + egui_ctx: &egui::Context, + event_loop: &EventLoopWindowTarget, + viewport_builder: &ViewportBuilder, +) -> Result { + crate::profile_function!(); + + let window_builder = + create_winit_window_builder(egui_ctx, event_loop, viewport_builder.clone()); + let window = { + crate::profile_scope!("WindowBuilder::build"); + window_builder.build(event_loop)? }; + apply_viewport_builder_to_window(egui_ctx, &window, viewport_builder); + Ok(window) } -#[allow(unused_imports)] -pub(crate) use profile_scope; +pub fn create_winit_window_builder( + egui_ctx: &egui::Context, + event_loop: &EventLoopWindowTarget, + viewport_builder: ViewportBuilder, +) -> winit::window::WindowBuilder { + crate::profile_function!(); + + // 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. + // We don't know what monitor the window will appear on though, but + // we'll try to fix that after the window is created in the vall to `apply_viewport_builder_to_window`. + let native_pixels_per_point = event_loop + .primary_monitor() + .or_else(|| event_loop.available_monitors().next()) + .map_or_else( + || { + log::debug!("Failed to find a monitor - assuming native_pixels_per_point of 1.0"); + 1.0 + }, + |m| m.scale_factor() as f32, + ); + let zoom_factor = egui_ctx.zoom_factor(); + let pixels_per_point = zoom_factor * native_pixels_per_point; + + let ViewportBuilder { + title, + position, + inner_size, + min_inner_size, + max_inner_size, + fullscreen, + maximized, + resizable, + transparent, + decorations, + icon, + active, + visible, + close_button, + minimize_button, + maximize_button, + window_level, + + // macOS: + fullsize_content_view: _fullsize_content_view, + title_shown: _title_shown, + titlebar_buttons_shown: _titlebar_buttons_shown, + titlebar_shown: _titlebar_shown, + + // Windows: + drag_and_drop: _drag_and_drop, + taskbar: _taskbar, + + // wayland: + app_id: _app_id, + + // x11 + window_type: _window_type, + + mouse_passthrough: _, // handled in `apply_viewport_builder_to_window` + } = viewport_builder; + + let mut window_builder = winit::window::WindowBuilder::new() + .with_title(title.unwrap_or_else(|| "egui window".to_owned())) + .with_transparent(transparent.unwrap_or(false)) + .with_decorations(decorations.unwrap_or(true)) + .with_resizable(resizable.unwrap_or(true)) + .with_visible(visible.unwrap_or(true)) + .with_maximized(maximized.unwrap_or(false)) + .with_window_level(match window_level.unwrap_or_default() { + egui::viewport::WindowLevel::AlwaysOnBottom => WindowLevel::AlwaysOnBottom, + egui::viewport::WindowLevel::AlwaysOnTop => WindowLevel::AlwaysOnTop, + egui::viewport::WindowLevel::Normal => WindowLevel::Normal, + }) + .with_fullscreen( + fullscreen.and_then(|e| e.then_some(winit::window::Fullscreen::Borderless(None))), + ) + .with_enabled_buttons({ + let mut buttons = WindowButtons::empty(); + if minimize_button.unwrap_or(true) { + buttons |= WindowButtons::MINIMIZE; + } + if maximize_button.unwrap_or(true) { + buttons |= WindowButtons::MAXIMIZE; + } + if close_button.unwrap_or(true) { + buttons |= WindowButtons::CLOSE; + } + buttons + }) + .with_active(active.unwrap_or(true)); + + if let Some(size) = inner_size { + window_builder = window_builder.with_inner_size(PhysicalSize::new( + pixels_per_point * size.x, + pixels_per_point * size.y, + )); + } + + if let Some(size) = min_inner_size { + window_builder = window_builder.with_min_inner_size(PhysicalSize::new( + pixels_per_point * size.x, + pixels_per_point * size.y, + )); + } + + if let Some(size) = max_inner_size { + window_builder = window_builder.with_max_inner_size(PhysicalSize::new( + pixels_per_point * size.x, + pixels_per_point * size.y, + )); + } + + if let Some(pos) = position { + window_builder = window_builder.with_position(PhysicalPosition::new( + pixels_per_point * pos.x, + pixels_per_point * pos.y, + )); + } + + if let Some(icon) = icon { + let winit_icon = to_winit_icon(&icon); + window_builder = window_builder.with_window_icon(winit_icon); + } + + #[cfg(all(feature = "wayland", target_os = "linux"))] + if let Some(app_id) = _app_id { + use winit::platform::wayland::WindowBuilderExtWayland as _; + window_builder = window_builder.with_name(app_id, ""); + } + + #[cfg(all(feature = "x11", target_os = "linux"))] + { + if let Some(window_type) = _window_type { + use winit::platform::x11::WindowBuilderExtX11 as _; + use winit::platform::x11::XWindowType; + window_builder = window_builder.with_x11_window_type(vec![match window_type { + egui::X11WindowType::Normal => XWindowType::Normal, + egui::X11WindowType::Utility => XWindowType::Utility, + egui::X11WindowType::Dock => XWindowType::Dock, + egui::X11WindowType::Desktop => XWindowType::Desktop, + egui::X11WindowType::Toolbar => XWindowType::Toolbar, + egui::X11WindowType::Menu => XWindowType::Menu, + egui::X11WindowType::Splash => XWindowType::Splash, + egui::X11WindowType::Dialog => XWindowType::Dialog, + egui::X11WindowType::DropdownMenu => XWindowType::DropdownMenu, + egui::X11WindowType::PopupMenu => XWindowType::PopupMenu, + egui::X11WindowType::Tooltip => XWindowType::Tooltip, + egui::X11WindowType::Notification => XWindowType::Notification, + egui::X11WindowType::Combo => XWindowType::Combo, + egui::X11WindowType::Dnd => XWindowType::Dnd, + }]); + } + } + + #[cfg(target_os = "windows")] + { + use winit::platform::windows::WindowBuilderExtWindows as _; + if let Some(enable) = _drag_and_drop { + window_builder = window_builder.with_drag_and_drop(enable); + } + if let Some(show) = _taskbar { + window_builder = window_builder.with_skip_taskbar(!show); + } + } + + #[cfg(target_os = "macos")] + { + use winit::platform::macos::WindowBuilderExtMacOS as _; + window_builder = window_builder + .with_title_hidden(!_title_shown.unwrap_or(true)) + .with_titlebar_buttons_hidden(!_titlebar_buttons_shown.unwrap_or(true)) + .with_titlebar_transparent(!_titlebar_shown.unwrap_or(true)) + .with_fullsize_content_view(_fullsize_content_view.unwrap_or(false)); + } + + window_builder +} + +fn to_winit_icon(icon: &egui::IconData) -> Option { + if icon.is_empty() { + None + } else { + crate::profile_function!(); + match winit::window::Icon::from_rgba(icon.rgba.clone(), icon.width, icon.height) { + Ok(winit_icon) => Some(winit_icon), + Err(err) => { + log::warn!("Invalid IconData: {err}"); + None + } + } + } +} + +/// Applies what `create_winit_window_builder` couldn't +pub fn apply_viewport_builder_to_window( + egui_ctx: &egui::Context, + window: &Window, + builder: &ViewportBuilder, +) { + if let Some(mouse_passthrough) = builder.mouse_passthrough { + if let Err(err) = window.set_cursor_hittest(!mouse_passthrough) { + log::warn!("set_cursor_hittest failed: {err}"); + } + } + + { + // In `create_winit_window_builder` we didn't know + // on what monitor the window would appear, so we didn't know + // how to translate egui ui point to native physical pixels. + // Now we do know: + + let pixels_per_point = pixels_per_point(egui_ctx, window); + + if let Some(size) = builder.inner_size { + if window + .request_inner_size(PhysicalSize::new( + pixels_per_point * size.x, + pixels_per_point * size.y, + )) + .is_some() + { + log::debug!("Failed to set window size"); + } + } + if let Some(size) = builder.min_inner_size { + window.set_min_inner_size(Some(PhysicalSize::new( + pixels_per_point * size.x, + pixels_per_point * size.y, + ))); + } + if let Some(size) = builder.max_inner_size { + window.set_max_inner_size(Some(PhysicalSize::new( + pixels_per_point * size.x, + pixels_per_point * size.y, + ))); + } + if let Some(pos) = builder.position { + let pos = PhysicalPosition::new(pixels_per_point * pos.x, pixels_per_point * pos.y); + window.set_outer_position(pos); + } + } +} + +// --------------------------------------------------------------------------- + +/// Short and fast description of an event. +/// Useful for logging and profiling. +pub fn short_generic_event_description(event: &winit::event::Event) -> &'static str { + use winit::event::{DeviceEvent, Event, StartCause}; + + match event { + Event::AboutToWait => "Event::AboutToWait", + Event::LoopExiting => "Event::LoopExiting", + Event::Suspended => "Event::Suspended", + Event::Resumed => "Event::Resumed", + Event::MemoryWarning => "Event::MemoryWarning", + Event::UserEvent(_) => "UserEvent", + Event::DeviceEvent { event, .. } => match event { + DeviceEvent::Added { .. } => "DeviceEvent::Added", + DeviceEvent::Removed { .. } => "DeviceEvent::Removed", + DeviceEvent::MouseMotion { .. } => "DeviceEvent::MouseMotion", + DeviceEvent::MouseWheel { .. } => "DeviceEvent::MouseWheel", + DeviceEvent::Motion { .. } => "DeviceEvent::Motion", + DeviceEvent::Button { .. } => "DeviceEvent::Button", + DeviceEvent::Key { .. } => "DeviceEvent::Key", + }, + Event::NewEvents(start_cause) => match start_cause { + StartCause::ResumeTimeReached { .. } => "NewEvents::ResumeTimeReached", + StartCause::WaitCancelled { .. } => "NewEvents::WaitCancelled", + StartCause::Poll => "NewEvents::Poll", + StartCause::Init => "NewEvents::Init", + }, + Event::WindowEvent { event, .. } => short_window_event_description(event), + } +} + +/// Short and fast description of an event. +/// Useful for logging and profiling. +pub fn short_window_event_description(event: &winit::event::WindowEvent) -> &'static str { + use winit::event::WindowEvent; + + match event { + WindowEvent::ActivationTokenDone { .. } => "WindowEvent::ActivationTokenDone", + WindowEvent::Resized { .. } => "WindowEvent::Resized", + WindowEvent::Moved { .. } => "WindowEvent::Moved", + WindowEvent::CloseRequested { .. } => "WindowEvent::CloseRequested", + WindowEvent::Destroyed { .. } => "WindowEvent::Destroyed", + WindowEvent::DroppedFile { .. } => "WindowEvent::DroppedFile", + WindowEvent::HoveredFile { .. } => "WindowEvent::HoveredFile", + WindowEvent::HoveredFileCancelled { .. } => "WindowEvent::HoveredFileCancelled", + WindowEvent::Focused { .. } => "WindowEvent::Focused", + WindowEvent::KeyboardInput { .. } => "WindowEvent::KeyboardInput", + WindowEvent::ModifiersChanged { .. } => "WindowEvent::ModifiersChanged", + WindowEvent::Ime { .. } => "WindowEvent::Ime", + WindowEvent::CursorMoved { .. } => "WindowEvent::CursorMoved", + WindowEvent::CursorEntered { .. } => "WindowEvent::CursorEntered", + WindowEvent::CursorLeft { .. } => "WindowEvent::CursorLeft", + WindowEvent::MouseWheel { .. } => "WindowEvent::MouseWheel", + WindowEvent::MouseInput { .. } => "WindowEvent::MouseInput", + WindowEvent::TouchpadMagnify { .. } => "WindowEvent::TouchpadMagnify", + WindowEvent::RedrawRequested { .. } => "WindowEvent::RedrawRequested", + WindowEvent::SmartMagnify { .. } => "WindowEvent::SmartMagnify", + WindowEvent::TouchpadRotate { .. } => "WindowEvent::TouchpadRotate", + WindowEvent::TouchpadPressure { .. } => "WindowEvent::TouchpadPressure", + WindowEvent::AxisMotion { .. } => "WindowEvent::AxisMotion", + WindowEvent::Touch { .. } => "WindowEvent::Touch", + WindowEvent::ScaleFactorChanged { .. } => "WindowEvent::ScaleFactorChanged", + WindowEvent::ThemeChanged { .. } => "WindowEvent::ThemeChanged", + WindowEvent::Occluded { .. } => "WindowEvent::Occluded", + } +} + +// --------------------------------------------------------------------------- + +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 5d685765eb6..c59a0f451ce 100644 --- a/crates/egui-winit/src/window_settings.rs +++ b/crates/egui-winit/src/window_settings.rs @@ -1,3 +1,5 @@ +use egui::ViewportBuilder; + /// Can be used to store native window settings (position and size). #[derive(Clone, Copy, Debug, Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -16,8 +18,10 @@ pub struct WindowSettings { } impl WindowSettings { - pub fn from_display(window: &winit::window::Window) -> Self { - let inner_size_points = window.inner_size().to_logical::(window.scale_factor()); + pub fn from_window(egui_zoom_factor: f32, window: &winit::window::Window) -> Self { + let inner_size_points = window + .inner_size() + .to_logical::(egui_zoom_factor as f64 * window.scale_factor()); let inner_position_pixels = window .inner_position() @@ -46,10 +50,12 @@ impl WindowSettings { self.inner_size_points } - pub fn initialize_window_builder( + pub fn initialize_viewport_builder( &self, - mut window: winit::window::WindowBuilder, - ) -> winit::window::WindowBuilder { + mut viewport_builder: ViewportBuilder, + ) -> ViewportBuilder { + crate::profile_function!(); + // `WindowBuilder::with_position` expects inner position in Macos, and outer position elsewhere // See [`winit::window::WindowBuilder::with_position`] for details. let pos_px = if cfg!(target_os = "macos") { @@ -57,26 +63,17 @@ impl WindowSettings { } else { self.outer_position_pixels }; - if let Some(pos_px) = pos_px { - window = window.with_position(winit::dpi::PhysicalPosition { - x: pos_px.x as f64, - y: pos_px.y as f64, - }); + if let Some(pos) = pos_px { + viewport_builder = viewport_builder.with_position(pos); } if let Some(inner_size_points) = self.inner_size_points { - window - .with_inner_size(winit::dpi::LogicalSize { - width: inner_size_points.x as f64, - height: inner_size_points.y as f64, - }) - .with_fullscreen( - self.fullscreen - .then_some(winit::window::Fullscreen::Borderless(None)), - ) - } else { - window + viewport_builder = viewport_builder + .with_inner_size(inner_size_points) + .with_fullscreen(self.fullscreen); } + + viewport_builder } pub fn initialize_window(&self, window: &winit::window::Window) { @@ -105,6 +102,7 @@ impl WindowSettings { pub fn clamp_position_to_monitors( &mut self, + egui_zoom_factor: f32, event_loop: &winit::event_loop::EventLoopWindowTarget, ) { // If the app last ran on two monitors and only one is now connected, then @@ -116,36 +114,39 @@ impl WindowSettings { return; } - let Some(inner_size_points) = self.inner_size_points else { return; }; + let Some(inner_size_points) = self.inner_size_points else { + return; + }; if let Some(pos_px) = &mut self.inner_position_pixels { - clamp_pos_to_monitors(event_loop, inner_size_points, pos_px); + clamp_pos_to_monitors(egui_zoom_factor, event_loop, inner_size_points, pos_px); } if let Some(pos_px) = &mut self.outer_position_pixels { - clamp_pos_to_monitors(event_loop, inner_size_points, pos_px); + clamp_pos_to_monitors(egui_zoom_factor, event_loop, inner_size_points, pos_px); } } } fn clamp_pos_to_monitors( + egui_zoom_factor: f32, event_loop: &winit::event_loop::EventLoopWindowTarget, window_size_pts: egui::Vec2, position_px: &mut egui::Pos2, ) { + crate::profile_function!(); + let monitors = event_loop.available_monitors(); // default to primary monitor, in case the correct monitor was disconnected. - let mut active_monitor = if let Some(active_monitor) = event_loop + let Some(mut active_monitor) = event_loop .primary_monitor() .or_else(|| event_loop.available_monitors().next()) - { - active_monitor - } else { + else { return; // no monitors 🤷 }; for monitor in monitors { - let window_size_px = window_size_pts * (monitor.scale_factor() as f32); + let window_size_px = window_size_pts * (egui_zoom_factor * monitor.scale_factor() as f32); let monitor_x_range = (monitor.position().x - window_size_px.x as i32) ..(monitor.position().x + monitor.size().width as i32); let monitor_y_range = (monitor.position().y - window_size_px.y as i32) @@ -158,10 +159,14 @@ fn clamp_pos_to_monitors( } } - let mut window_size_px = window_size_pts * (active_monitor.scale_factor() as f32); + let mut window_size_px = + window_size_pts * (egui_zoom_factor * active_monitor.scale_factor() as f32); // Add size of title bar. This is 32 px by default in Win 10/11. if cfg!(target_os = "windows") { - window_size_px += egui::Vec2::new(0.0, 32.0 * active_monitor.scale_factor() as f32); + window_size_px += egui::Vec2::new( + 0.0, + 32.0 * egui_zoom_factor * active_monitor.scale_factor() as f32, + ); } let monitor_position = egui::Pos2::new( active_monitor.position().x as f32, diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index 615195836cf..347dbeda7cb 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "egui" -version = "0.22.0" +version.workspace = true authors = ["Emil Ernerfeldt "] description = "An easy-to-use immediate mode GUI that runs on both web and native" -edition = "2021" -rust-version = "1.67" +edition.workspace = true +rust-version.workspace = true homepage = "https://github.com/emilk/egui" -license = "MIT OR Apache-2.0" +license.workspace = true readme = "../../README.md" repository = "https://github.com/emilk/egui" categories = ["gui", "game-development"] @@ -22,9 +22,18 @@ all-features = true [features] default = ["default_fonts"] +## Exposes detailed accessibility implementation required by platform +## accessibility APIs. Also requires support in the egui integration. +accesskit = ["dep:accesskit"] + ## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`epaint::Vertex`], [`emath::Vec2`] etc to `&[u8]`. bytemuck = ["epaint/bytemuck"] +## Show a debug-ui on hover including the stacktrace to the hovered item. +## This is very useful in finding the code that creates a part of the UI. +## Does not work on web. +callstack = ["dep:backtrace"] + ## [`cint`](https://docs.rs/cint) enables interoperability with other color libraries. cint = ["epaint/cint"] @@ -54,6 +63,16 @@ 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). +## +## This can help performance for graphics-intense applications. +rayon = ["epaint/rayon"] + ## Allow serialization using [`serde`](https://docs.rs/serde). serde = ["dep:serde", "epaint/serde", "accesskit?/serde"] @@ -62,22 +81,20 @@ unity = ["epaint/unity"] [dependencies] -epaint = { version = "0.22.0", path = "../epaint", default-features = false } +epaint = { workspace = true, default-features = false } -ahash = { version = "0.8.1", default-features = false, features = [ - "no-rng", # we don't need DOS-protection, so we let users opt-in to it instead - "std", -] } -nohash-hasher = "0.2" +ahash.workspace = true +nohash-hasher.workspace = true #! ### Optional dependencies -## Exposes detailed accessibility implementation required by platform -## accessibility APIs. Also requires support in the egui integration. -accesskit = { version = "0.11", optional = true } +accesskit = { version = "0.12", optional = true } + +backtrace = { workspace = true, optional = true } ## Enable this when generating docs. -document-features = { version = "0.2", optional = true } +document-features = { workspace = true, optional = true } -log = { version = "0.4", optional = true, features = ["std"] } +log = { workspace = true, optional = true } +puffin = { workspace = true, optional = true } ron = { version = "0.8", optional = true } serde = { version = "1", optional = true, features = ["derive", "rc"] } diff --git a/crates/egui/assets/ferris.png b/crates/egui/assets/ferris.png new file mode 100644 index 00000000000..8741baa19d0 Binary files /dev/null and b/crates/egui/assets/ferris.png differ diff --git a/crates/egui/src/animation_manager.rs b/crates/egui/src/animation_manager.rs index be181507962..fee5ad5ea8c 100644 --- a/crates/egui/src/animation_manager.rs +++ b/crates/egui/src/animation_manager.rs @@ -1,4 +1,7 @@ -use crate::{emath::remap_clamp, Id, IdMap, InputState}; +use crate::{ + emath::{remap_clamp, NumExt as _}, + Id, IdMap, InputState, +}; #[derive(Clone, Default)] pub(crate) struct AnimationManager { @@ -8,10 +11,8 @@ pub(crate) struct AnimationManager { #[derive(Clone, Debug)] struct BoolAnim { - value: bool, - - /// when did `value` last toggle? - toggle_time: f64, + last_value: f32, + last_tick: f64, } #[derive(Clone, Debug)] @@ -25,7 +26,7 @@ struct ValueAnim { } impl AnimationManager { - /// See `Context::animate_bool` for documentation + /// See [`crate::Context::animate_bool`] for documentation pub fn animate_bool( &mut self, input: &InputState, @@ -33,38 +34,33 @@ impl AnimationManager { id: Id, value: bool, ) -> f32 { + let (start, end) = if value { (0.0, 1.0) } else { (1.0, 0.0) }; match self.bools.get_mut(&id) { None => { self.bools.insert( id, BoolAnim { - value, - toggle_time: -f64::INFINITY, // long time ago + last_value: end, + last_tick: input.time - input.stable_dt as f64, }, ); - if value { - 1.0 - } else { - 0.0 - } + end } Some(anim) => { - if anim.value != value { - anim.value = value; - anim.toggle_time = input.time; - } - - let time_since_toggle = (input.time - anim.toggle_time) as f32; - - // On the frame we toggle we don't want to return the old value, - // so we extrapolate forwards: - let time_since_toggle = time_since_toggle + input.predicted_dt; - - if value { - remap_clamp(time_since_toggle, 0.0..=animation_time, 0.0..=1.0) + let BoolAnim { + last_value, + last_tick, + } = anim; + let current_time = input.time; + let elapsed = ((current_time - *last_tick) as f32).at_most(input.stable_dt); + let new_value = *last_value + (end - start) * elapsed / animation_time; + *last_value = if new_value.is_finite() { + new_value.clamp(0.0, 1.0) } else { - remap_clamp(time_since_toggle, 0.0..=animation_time, 1.0..=0.0) - } + end + }; + *last_tick = current_time; + *last_value } } } diff --git a/crates/egui/src/callstack.rs b/crates/egui/src/callstack.rs new file mode 100644 index 00000000000..fac5ac5936e --- /dev/null +++ b/crates/egui/src/callstack.rs @@ -0,0 +1,186 @@ +#[derive(Clone)] +struct Frame { + /// `_main` is usually as the deepest depth. + depth: usize, + name: String, + file_and_line: String, +} + +/// Capture a callstack, skipping the frames that are not interesting. +/// +/// In particular: slips everything before `egui::Context::run`, +/// and skipping all frames in the `egui::` namespace. +pub fn capture() -> String { + let mut frames = vec![]; + let mut depth = 0; + + backtrace::trace(|frame| { + // Resolve this instruction pointer to a symbol name + backtrace::resolve_frame(frame, |symbol| { + let mut file_and_line = symbol.filename().map(shorten_source_file_path); + + if let Some(file_and_line) = &mut file_and_line { + if let Some(line_nr) = symbol.lineno() { + file_and_line.push_str(&format!(":{line_nr}")); + } + } + let file_and_line = file_and_line.unwrap_or_default(); + + let name = symbol + .name() + .map(|name| name.to_string()) + .unwrap_or_default(); + + frames.push(Frame { + depth, + name, + file_and_line, + }); + }); + + depth += 1; // note: we can resolve multiple symbols on the same frame. + + true // keep going to the next frame + }); + + if frames.is_empty() { + return Default::default(); + } + + // Inclusive: + let mut min_depth = 0; + let mut max_depth = frames.len() - 1; + + for frame in &frames { + if frame.name.starts_with("egui::callstack::capture") { + min_depth = frame.depth + 1; + } + if frame.name.starts_with("egui::context::Context::run") { + max_depth = frame.depth; + } + } + + // Remove frames that are uninteresting: + frames.retain(|frame| { + // Keep some special frames to give the user a sense of chronology: + if frame.name == "main" + || frame.name == "_main" + || frame.name.starts_with("egui::context::Context::run") + || frame.name.starts_with("eframe::run_native") + { + return true; + } + + if frame.depth < min_depth || max_depth < frame.depth { + return false; + } + + // Remove stuff that isn't user calls: + let skip_prefixes = [ + // "backtrace::", // not needed, since we cut at at egui::callstack::capture + "egui::", + "", + "egui_plot::", + "egui_extras::", + "core::ptr::drop_in_place::", + "eframe::", + "core::ops::function::FnOnce::call_once", + " as core::ops::function::FnOnce>::call_once", + ]; + for prefix in skip_prefixes { + if frame.name.starts_with(prefix) { + return false; + } + } + true + }); + + frames.reverse(); // main on top, i.e. chronological order. Same as Python. + + let mut deepest_depth = 0; + let mut widest_file_line = 0; + for frame in &frames { + deepest_depth = frame.depth.max(deepest_depth); + widest_file_line = frame.file_and_line.len().max(widest_file_line); + } + + let widest_depth = deepest_depth.to_string().len(); + + let mut formatted = String::new(); + + if !frames.is_empty() { + let mut last_depth = frames[0].depth; + + for frame in &frames { + let Frame { + depth, + name, + file_and_line, + } = frame; + + if frame.depth + 1 < last_depth || last_depth + 1 < frame.depth { + // Show that some frames were elided + formatted.push_str(&format!("{:widest_depth$} …\n", "")); + } + + formatted.push_str(&format!( + "{depth:widest_depth$}: {file_and_line:widest_file_line$} {name}\n" + )); + + last_depth = frame.depth; + } + } + + formatted +} + +/// Shorten a path to a Rust source file from a callstack. +/// +/// Example input: +/// * `/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs` +/// * `crates/rerun/src/main.rs` +/// * `/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs` +fn shorten_source_file_path(path: &std::path::Path) -> String { + // Look for `src` and strip everything up to it. + + let components: Vec<_> = path.iter().map(|path| path.to_string_lossy()).collect(); + + let mut src_idx = None; + for (i, c) in components.iter().enumerate() { + if c == "src" { + src_idx = Some(i); + } + } + + // Look for the last `src`: + if let Some(src_idx) = src_idx { + // Before `src` comes the name of the crate - let's include that: + let first_index = src_idx.saturating_sub(1); + + let mut output = components[first_index].to_string(); + for component in &components[first_index + 1..] { + output.push('/'); + output.push_str(component); + } + output + } else { + // No `src` directory found - weird! + path.display().to_string() + } +} + +#[test] +fn test_shorten_path() { + for (before, after) in [ + ("/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs", "tokio-1.24.1/src/runtime/runtime.rs"), + ("crates/rerun/src/main.rs", "rerun/src/main.rs"), + ("/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs", "core/src/ops/function.rs"), + ("/weird/path/file.rs", "/weird/path/file.rs"), + ] + { + use std::str::FromStr as _; + let before = std::path::PathBuf::from_str(before).unwrap(); + assert_eq!(shorten_source_file_path(&before), after); + } +} diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index f493c20cded..c66be433966 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -48,7 +48,7 @@ impl State { /// /// ``` /// # egui::__run_test_ctx(|ctx| { -/// egui::Area::new("my_area") +/// egui::Area::new(egui::Id::new("my_area")) /// .fixed_pos(egui::pos2(32.0, 32.0)) /// .show(ctx, |ui| { /// ui.label("Floating text!"); @@ -65,31 +65,36 @@ pub struct Area { interactable: bool, enabled: bool, constrain: bool, + constrain_rect: Option, order: Order, default_pos: Option, pivot: Align2, anchor: Option<(Align2, Vec2)>, new_pos: Option, - drag_bounds: Option, } impl Area { - pub fn new(id: impl Into) -> Self { + /// The `id` must be globally unique. + pub fn new(id: Id) -> Self { Self { - id: id.into(), + id, movable: true, interactable: true, constrain: false, + constrain_rect: None, enabled: true, order: Order::Middle, default_pos: None, new_pos: None, pivot: Align2::LEFT_TOP, anchor: None, - drag_bounds: None, } } + /// Let's you change the `id` that you assigned in [`Self::new`]. + /// + /// The `id` must be globally unique. + #[inline] pub fn id(mut self, id: Id) -> Self { self.id = id; self @@ -103,12 +108,14 @@ impl Area { /// and widgets will be shown grayed out. /// You won't be able to move the window. /// Default: `true`. + #[inline] pub fn enabled(mut self, enabled: bool) -> Self { self.enabled = enabled; self } /// moveable by dragging the area? + #[inline] pub fn movable(mut self, movable: bool) -> Self { self.movable = movable; self.interactable |= movable; @@ -125,6 +132,7 @@ impl Area { /// If false, clicks goes straight through to what is behind us. /// Good for tooltips etc. + #[inline] pub fn interactable(mut self, interactable: bool) -> Self { self.interactable = interactable; self.movable &= interactable; @@ -132,17 +140,20 @@ impl Area { } /// `order(Order::Foreground)` for an Area that should always be on top + #[inline] pub fn order(mut self, order: Order) -> Self { self.order = order; self } + #[inline] pub fn default_pos(mut self, default_pos: impl Into) -> Self { self.default_pos = Some(default_pos.into()); self } /// Positions the window and prevents it from being moved + #[inline] pub fn fixed_pos(mut self, fixed_pos: impl Into) -> Self { self.new_pos = Some(fixed_pos.into()); self.movable = false; @@ -150,11 +161,22 @@ impl Area { } /// Constrains this area to the screen bounds. + #[inline] pub fn constrain(mut self, constrain: bool) -> Self { self.constrain = constrain; self } + /// Constrain the movement of the window to the given rectangle. + /// + /// For instance: `.constrain_to(ctx.screen_rect())`. + #[inline] + pub fn constrain_to(mut self, constrain_rect: Rect) -> Self { + self.constrain = true; + self.constrain_rect = Some(constrain_rect); + self + } + /// Where the "root" of the area is. /// /// For instance, if you set this to [`Align2::RIGHT_TOP`] @@ -162,12 +184,14 @@ impl Area { /// corner of the area. /// /// Default: [`Align2::LEFT_TOP`]. + #[inline] pub fn pivot(mut self, pivot: Align2) -> Self { self.pivot = pivot; self } /// Positions the window but you can still move it. + #[inline] pub fn current_pos(mut self, current_pos: impl Into) -> Self { self.new_pos = Some(current_pos.into()); self @@ -184,17 +208,12 @@ impl Area { /// Anchoring also makes the window immovable. /// /// It is an error to set both an anchor and a position. + #[inline] pub fn anchor(mut self, align: Align2, offset: impl Into) -> Self { self.anchor = Some((align, offset.into())); self.movable(false) } - /// Constrain the area up to which the window can be dragged. - pub fn drag_bounds(mut self, bounds: Rect) -> Self { - self.drag_bounds = Some(bounds); - self - } - pub(crate) fn get_pivot(&self) -> Align2 { if let Some((pivot, _)) = self.anchor { pivot @@ -209,7 +228,8 @@ pub(crate) struct Prepared { state: State, move_response: Response, enabled: bool, - drag_bounds: Option, + constrain: bool, + constrain_rect: Option, /// We always make windows invisible the first frame to hide "first-frame-jitters". /// @@ -233,7 +253,7 @@ impl Area { } pub(crate) fn begin(self, ctx: &Context) -> Prepared { - let Area { + let Self { id, movable, order, @@ -243,13 +263,19 @@ impl Area { new_pos, pivot, anchor, - drag_bounds, constrain, + constrain_rect, } = self; let layer_id = LayerId::new(order, id); - let state = ctx.memory(|mem| mem.areas.get(id).copied()); + let state = ctx + .memory(|mem| mem.areas().get(id).copied()) + .map(|mut state| { + // override the saved state with the correct value + state.pivot = pivot; + state + }); let is_new = state.is_none(); if is_new { ctx.request_repaint(); // if we don't know the previous size we are likely drawing the area in the wrong place @@ -271,64 +297,59 @@ impl Area { } // interact right away to prevent frame-delay - let move_response = { + let mut move_response = { let interact_id = layer_id.id.with("move"); let sense = if movable { - Sense::click_and_drag() + Sense::drag() } else if interactable { Sense::click() // allow clicks to bring to front } else { Sense::hover() }; - let move_response = ctx.interact( - Rect::EVERYTHING, - ctx.style().spacing.item_spacing, + let move_response = ctx.create_widget(WidgetRect { + id: interact_id, layer_id, - interact_id, - state.rect(), + rect: state.rect(), + interact_rect: state.rect(), sense, enabled, - ); - - // Important check - don't try to move e.g. a combobox popup! - if movable { - if move_response.dragged() { - state.pivot_pos += ctx.input(|i| i.pointer.delta()); - } + }); - state.set_left_top_pos( - ctx.constrain_window_rect_to_area(state.rect(), drag_bounds) - .min, - ); + if movable && move_response.dragged() { + state.pivot_pos += move_response.drag_delta(); } if (move_response.dragged() || move_response.clicked()) || pointer_pressed_on_area(ctx, layer_id) - || !ctx.memory(|m| m.areas.visible_last_frame(&layer_id)) + || !ctx.memory(|m| m.areas().visible_last_frame(&layer_id)) { - ctx.memory_mut(|m| m.areas.move_to_top(layer_id)); + ctx.memory_mut(|m| m.areas_mut().move_to_top(layer_id)); ctx.request_repaint(); } move_response }; - state.set_left_top_pos(ctx.round_pos_to_pixels(state.left_top_pos())); - if constrain { state.set_left_top_pos( - ctx.constrain_window_rect_to_area(state.rect(), drag_bounds) - .left_top(), + ctx.constrain_window_rect_to_area(state.rect(), constrain_rect) + .min, ); } + state.set_left_top_pos(ctx.round_pos_to_pixels(state.left_top_pos())); + + // Update responsbe with posisbly moved/constrained rect: + move_response = move_response.with_new_rect(state.rect()); + Prepared { layer_id, state, move_response, enabled, - drag_bounds, + constrain, + constrain_rect, temporarily_invisible: is_new, } } @@ -347,7 +368,7 @@ impl Area { } let layer_id = LayerId::new(self.order, self.id); - let area_rect = ctx.memory(|mem| mem.areas.get(self.id).map(|area| area.rect())); + let area_rect = ctx.memory(|mem| mem.areas().get(self.id).map(|area| area.rect())); if let Some(area_rect) = area_rect { let clip_rect = ctx.available_rect(); let painter = Painter::new(ctx.clone(), layer_id, clip_rect); @@ -371,15 +392,19 @@ impl Prepared { &mut self.state } - pub(crate) fn drag_bounds(&self) -> Option { - self.drag_bounds + pub(crate) fn constrain(&self) -> bool { + self.constrain + } + + pub(crate) fn constrain_rect(&self) -> Option { + self.constrain_rect } pub(crate) fn content_ui(&self, ctx: &Context) -> Ui { let screen_rect = ctx.screen_rect(); - let bounds = if let Some(bounds) = self.drag_bounds { - bounds.intersect(screen_rect) // protect against infinite bounds + let constrain_rect = if let Some(constrain_rect) = self.constrain_rect { + constrain_rect.intersect(screen_rect) // protect against infinite bounds } else { let central_area = ctx.available_rect(); @@ -393,17 +418,17 @@ impl Prepared { let max_rect = Rect::from_min_max( self.state.left_top_pos(), - bounds + constrain_rect .max .at_least(self.state.left_top_pos() + Vec2::splat(32.0)), ); - let shadow_radius = ctx.style().visuals.window_shadow.extrusion; // hacky + let shadow_radius = ctx.style().visuals.window_shadow.margin().sum().max_elem(); // hacky let clip_rect_margin = ctx.style().visuals.clip_rect_margin.max(shadow_radius); - let clip_rect = Rect::from_min_max(self.state.left_top_pos(), bounds.max) + let clip_rect = Rect::from_min_max(self.state.left_top_pos(), constrain_rect.max) .expand(clip_rect_margin) - .intersect(bounds); + .intersect(constrain_rect); let mut ui = Ui::new( ctx.clone(), @@ -419,18 +444,19 @@ impl Prepared { #[allow(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`. pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response { - let Prepared { + let Self { layer_id, mut state, move_response, enabled: _, - drag_bounds: _, + constrain: _, + constrain_rect: _, temporarily_invisible: _, } = self; state.size = content_ui.min_size(); - ctx.memory_mut(|m| m.areas.set_state(layer_id, state)); + ctx.memory_mut(|m| m.areas_mut().set_state(layer_id, state)); move_response } @@ -447,7 +473,7 @@ fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool { fn automatic_area_position(ctx: &Context) -> Pos2 { let mut existing: Vec = ctx.memory(|mem| { - mem.areas + mem.areas() .visible_windows() .into_iter() .map(State::rect) diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index 5d12dc79432..6fb7662af22 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -45,7 +45,7 @@ impl CollapsingState { } pub fn load_with_default_open(ctx: &Context, id: Id, default_open: bool) -> Self { - Self::load(ctx, id).unwrap_or(CollapsingState { + Self::load(ctx, id).unwrap_or(Self { id, state: InnerState { open: default_open, @@ -272,6 +272,18 @@ pub struct HeaderResponse<'ui, HeaderRet> { } impl<'ui, HeaderRet> HeaderResponse<'ui, HeaderRet> { + pub fn is_open(&self) -> bool { + self.state.is_open() + } + + pub fn set_open(&mut self, open: bool) { + self.state.set_open(open); + } + + pub fn toggle(&mut self) { + self.state.toggle(self.ui); + } + /// Returns the response of the collapsing button, the custom header, and the custom body. pub fn body( mut self, @@ -390,6 +402,7 @@ impl CollapsingHeader { /// By default, the [`CollapsingHeader`] is collapsed. /// Call `.default_open(true)` to change this. + #[inline] pub fn default_open(mut self, open: bool) -> Self { self.default_open = open; self @@ -400,6 +413,7 @@ impl CollapsingHeader { /// Calling `.open(Some(false))` will make the collapsing header close this frame (or stay closed). /// /// Calling `.open(None)` has no effect (default). + #[inline] pub fn open(mut self, open: Option) -> Self { self.open = open; self @@ -407,6 +421,7 @@ impl CollapsingHeader { /// Explicitly set the source of the [`Id`] of this widget, instead of using title label. /// This is useful if the title label is dynamic or not unique. + #[inline] pub fn id_source(mut self, id_source: impl Hash) -> Self { self.id_source = Id::new(id_source); self @@ -415,39 +430,12 @@ impl CollapsingHeader { /// If you set this to `false`, the [`CollapsingHeader`] will be grayed out and un-clickable. /// /// This is a convenience for [`Ui::set_enabled`]. + #[inline] pub fn enabled(mut self, enabled: bool) -> Self { self.enabled = enabled; self } - /// Can the [`CollapsingHeader`] be selected by clicking it? Default: `false`. - #[deprecated = "Use the more powerful egui::collapsing_header::CollapsingState::show_header"] // Deprecated in 2022-04-28, before egui 0.18 - pub fn selectable(mut self, selectable: bool) -> Self { - self.selectable = selectable; - self - } - - /// If you set this to 'true', the [`CollapsingHeader`] will be shown as selected. - /// - /// Example: - /// ``` - /// # egui::__run_test_ui(|ui| { - /// let mut selected = false; - /// let response = egui::CollapsingHeader::new("Select and open me") - /// .selectable(true) - /// .selected(selected) - /// .show(ui, |ui| ui.label("Body")); - /// if response.header_response.clicked() { - /// selected = true; - /// } - /// # }); - /// ``` - #[deprecated = "Use the more powerful egui::collapsing_header::CollapsingState::show_header"] // Deprecated in 2022-04-28, before egui 0.18 - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - /// Should the [`CollapsingHeader`] show a background behind it? Default: `false`. /// /// To show it behind all [`CollapsingHeader`] you can just use: @@ -456,6 +444,7 @@ impl CollapsingHeader { /// ui.visuals_mut().collapsing_header_frame = true; /// # }); /// ``` + #[inline] pub fn show_background(mut self, show_background: bool) -> Self { self.show_background = show_background; self @@ -478,6 +467,7 @@ impl CollapsingHeader { /// .show(ui, |ui| { ui.label("Hi!"); }); /// # }); /// ``` + #[inline] pub fn icon(mut self, icon_fn: impl FnOnce(&mut Ui, f32, &Response) + 'static) -> Self { self.icon = Some(Box::new(icon_fn)); self @@ -517,22 +507,22 @@ impl CollapsingHeader { let text_pos = available.min + vec2(ui.spacing().indent, 0.0); let wrap_width = available.right() - text_pos.x; let wrap = Some(false); - let text = text.into_galley(ui, wrap, wrap_width, TextStyle::Button); - let text_max_x = text_pos.x + text.size().x; + let galley = text.into_galley(ui, wrap, wrap_width, TextStyle::Button); + let text_max_x = text_pos.x + galley.size().x; let mut desired_width = text_max_x + button_padding.x - available.left(); if ui.visuals().collapsing_header_frame { desired_width = desired_width.max(available.width()); // fill full width } - let mut desired_size = vec2(desired_width, text.size().y + 2.0 * button_padding.y); + let mut desired_size = vec2(desired_width, galley.size().y + 2.0 * button_padding.y); desired_size = desired_size.at_least(ui.spacing().interact_size); let (_, rect) = ui.allocate_space(desired_size); let mut header_response = ui.interact(rect, id, Sense::click()); let text_pos = pos2( text_pos.x, - header_response.rect.center().y - text.size().y / 2.0, + header_response.rect.center().y - galley.size().y / 2.0, ); let mut state = CollapsingState::load_with_default_open(ui.ctx(), id, default_open); @@ -547,7 +537,7 @@ impl CollapsingHeader { } header_response - .widget_info(|| WidgetInfo::labeled(WidgetType::CollapsingHeader, text.text())); + .widget_info(|| WidgetInfo::labeled(WidgetType::CollapsingHeader, galley.text())); let openness = state.openness(ui.ctx()); @@ -585,7 +575,7 @@ impl CollapsingHeader { } } - text.paint_with_visuals(ui.painter(), text_pos, &visuals); + ui.painter().galley(text_pos, galley, visuals.text_color()); } Prepared { diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index e29b12b4a2c..828ee140aa5 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -2,6 +2,9 @@ use epaint::Shape; use crate::{style::WidgetVisuals, *}; +#[allow(unused_imports)] // Documentation +use crate::style::Spacing; + /// Indicate whether or not a popup will be shown above or below the box. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum AboveOrBelow { @@ -35,6 +38,7 @@ pub struct ComboBox { label: Option, selected_text: WidgetText, width: Option, + height: Option, icon: Option, wrap_enabled: bool, } @@ -47,6 +51,7 @@ impl ComboBox { label: Some(label.into()), selected_text: Default::default(), width: None, + height: None, icon: None, wrap_enabled: false, } @@ -60,6 +65,7 @@ impl ComboBox { label: Some(label), selected_text: Default::default(), width: None, + height: None, icon: None, wrap_enabled: false, } @@ -72,18 +78,32 @@ impl ComboBox { label: Default::default(), selected_text: Default::default(), width: None, + height: None, icon: None, wrap_enabled: false, } } /// Set the outer width of the button and menu. + /// + /// Default is [`Spacing::combo_width`]. + #[inline] pub fn width(mut self, width: f32) -> Self { self.width = Some(width); self } + /// Set the maximum outer height of the menu. + /// + /// Default is [`Spacing::combo_height`]. + #[inline] + pub fn height(mut self, height: f32) -> Self { + self.height = Some(height); + self + } + /// What we show as the currently selected value + #[inline] pub fn selected_text(mut self, selected_text: impl Into) -> Self { self.selected_text = selected_text.into(); self @@ -129,6 +149,7 @@ impl ComboBox { } /// Controls whether text wrap is used for the selected text + #[inline] pub fn wrap(mut self, wrap: bool) -> Self { self.wrap_enabled = wrap; self @@ -155,6 +176,7 @@ impl ComboBox { label, selected_text, width, + height, icon, wrap_enabled, } = self; @@ -169,7 +191,7 @@ impl ComboBox { menu_contents, icon, wrap_enabled, - width, + (width, height), ); if let Some(label) = label { ir.response @@ -238,13 +260,13 @@ fn combo_box_dyn<'c, R>( menu_contents: Box R + 'c>, icon: Option, wrap_enabled: bool, - width: Option, + (width, height): (Option, Option), ) -> InnerResponse> { let popup_id = button_id.with("popup"); let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id)); - let popup_height = ui.memory(|m| m.areas.get(popup_id).map_or(100.0, |state| state.size.y)); + let popup_height = ui.memory(|m| m.areas().get(popup_id).map_or(100.0, |state| state.size.y)); let above_or_below = if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height @@ -324,13 +346,17 @@ fn combo_box_dyn<'c, R>( } let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect); - galley.paint_with_visuals(ui.painter(), text_rect.min, visuals); + ui.painter() + .galley(text_rect.min, galley, visuals.text_color()); } }); if button_response.clicked() { ui.memory_mut(|mem| mem.toggle_popup(popup_id)); } + + let height = height.unwrap_or_else(|| ui.spacing().combo_height); + let inner = crate::popup::popup_above_or_below_widget( ui, popup_id, @@ -338,8 +364,16 @@ fn combo_box_dyn<'c, R>( above_or_below, |ui| { ScrollArea::vertical() - .max_height(ui.spacing().combo_height) - .show(ui, menu_contents) + .max_height(height) + .show(ui, |ui| { + // Often the button is very narrow, which means this popup + // is also very narrow. Having wrapping on would therefore + // result in labels that wrap very early. + // Instead, we turn it off by default so that the labels + // expand the width of the menu. + ui.style_mut().wrap = Some(false); + menu_contents(ui) + }) .inner }, ); diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index 6ce5cbc408e..92113f4dd49 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -1,6 +1,6 @@ //! Frame container -use crate::{layers::ShapeIdx, style::Margin, *}; +use crate::{layers::ShapeIdx, *}; use epaint::*; /// Add a background, frame and/or margin to a rectangular background of a [`Ui`]. @@ -14,6 +14,43 @@ use epaint::*; /// }); /// # }); /// ``` +/// +/// ## Dynamic color +/// If you want to change the color of the frame based on the response of +/// the widget, you needs to break it up into multiple steps: +/// +/// ``` +/// # egui::__run_test_ui(|ui| { +/// let mut frame = egui::Frame::default().inner_margin(4.0).begin(ui); +/// { +/// let response = frame.content_ui.label("Inside the frame"); +/// if response.hovered() { +/// frame.frame.fill = egui::Color32::RED; +/// } +/// } +/// frame.end(ui); // Will "close" the frame. +/// # }); +/// ``` +/// +/// You can also respond to the hovering of the frame itself: +/// +/// ``` +/// # egui::__run_test_ui(|ui| { +/// let mut frame = egui::Frame::default().inner_margin(4.0).begin(ui); +/// { +/// frame.content_ui.label("Inside the frame"); +/// frame.content_ui.label("This too"); +/// } +/// let response = frame.allocate_space(ui); +/// if response.hovered() { +/// frame.frame.fill = egui::Color32::RED; +/// } +/// frame.paint(ui); +/// # }); +/// ``` +/// +/// Note that you cannot change the margins after calling `begin`. +#[doc(alias = "border")] #[derive(Clone, Copy, Debug, Default, PartialEq)] #[must_use = "You should call .show()"] pub struct Frame { @@ -127,8 +164,8 @@ impl Frame { } #[inline] - pub fn stroke(mut self, stroke: Stroke) -> Self { - self.stroke = stroke; + pub fn stroke(mut self, stroke: impl Into) -> Self { + self.stroke = stroke.into(); self } @@ -152,18 +189,13 @@ impl Frame { self } - #[deprecated = "Renamed inner_margin in egui 0.18"] - #[inline] - pub fn margin(self, margin: impl Into) -> Self { - self.inner_margin(margin) - } - #[inline] pub fn shadow(mut self, shadow: Shadow) -> Self { self.shadow = shadow; self } + #[inline] pub fn multiply_with_opacity(mut self, opacity: f32) -> Self { self.fill = self.fill.linear_multiply(opacity); self.stroke.color = self.stroke.color.linear_multiply(opacity); @@ -183,12 +215,26 @@ impl Frame { // ---------------------------------------------------------------------------- pub struct Prepared { + /// The frame that was prepared. + /// + /// The margin has already been read and used, + /// but the rest of the fields may be modified. pub frame: Frame, + + /// This is where we will insert the frame shape so it ends up behind the content. where_to_put_background: ShapeIdx, + + /// Add your widgets to this UI so it ends up within the frame. pub content_ui: Ui, } impl Frame { + /// Begin a dynamically colored frame. + /// + /// This is a more advanced API. + /// Usually you want to use [`Self::show`] instead. + /// + /// See docs for [`Frame`] for an example. pub fn begin(self, ui: &mut Ui) -> Prepared { let where_to_put_background = ui.painter().add(Shape::Noop); let outer_rect_bounds = ui.available_rect_before_wrap(); @@ -210,6 +256,7 @@ impl Frame { } } + /// Show the given ui surrounded by this frame. pub fn show(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse { self.show_dyn(ui, Box::new(add_contents)) } @@ -225,6 +272,9 @@ impl Frame { InnerResponse::new(ret, response) } + /// Paint this frame as a shape. + /// + /// The margin is ignored. pub fn paint(&self, outer_rect: Rect) -> Shape { let Self { inner_margin: _, @@ -248,30 +298,37 @@ impl Frame { } impl Prepared { - fn paint_rect(&self) -> Rect { - self.frame - .inner_margin - .expand_rect(self.content_ui.min_rect()) - } - fn content_with_margin(&self) -> Rect { (self.frame.inner_margin + self.frame.outer_margin).expand_rect(self.content_ui.min_rect()) } - pub fn end(self, ui: &mut Ui) -> Response { - let paint_rect = self.paint_rect(); + /// Allocate the the space that was used by [`Self::content_ui`]. + /// + /// This MUST be called, or the parent ui will not know how much space this widget used. + /// + /// This can be called before or after [`Self::paint`]. + pub fn allocate_space(&self, ui: &mut Ui) -> Response { + ui.allocate_rect(self.content_with_margin(), Sense::hover()) + } - let Prepared { - frame, - where_to_put_background, - .. - } = self; + /// Paint the frame. + /// + /// This can be called before or after [`Self::allocate_space`]. + pub fn paint(&self, ui: &Ui) { + let paint_rect = self + .frame + .inner_margin + .expand_rect(self.content_ui.min_rect()); if ui.is_rect_visible(paint_rect) { - let shape = frame.paint(paint_rect); - ui.painter().set(where_to_put_background, shape); + let shape = self.frame.paint(paint_rect); + ui.painter().set(self.where_to_put_background, shape); } + } - ui.allocate_rect(self.content_with_margin(), Sense::hover()) + /// Convenience for calling [`Self::allocate_space`] and [`Self::paint`]. + pub fn end(self, ui: &mut Ui) -> Response { + self.paint(ui); + self.allocate_space(ui) } } diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index f5267e0f0c7..11e69b8289c 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -51,22 +51,22 @@ pub enum Side { impl Side { fn opposite(self) -> Self { match self { - Side::Left => Self::Right, - Side::Right => Self::Left, + Self::Left => Self::Right, + Self::Right => Self::Left, } } fn set_rect_width(self, rect: &mut Rect, width: f32) { match self { - Side::Left => rect.max.x = rect.min.x + width, - Side::Right => rect.min.x = rect.max.x - width, + Self::Left => rect.max.x = rect.min.x + width, + Self::Right => rect.min.x = rect.max.x - width, } } fn side_x(self, rect: Rect) -> f32 { match self { - Side::Left => rect.left(), - Side::Right => rect.right(), + Self::Left => rect.left(), + Self::Right => rect.right(), } } } @@ -135,6 +135,7 @@ impl SidePanel { /// * A [`Separator`]. /// * A [`TextEdit`]. /// * … + #[inline] pub fn resizable(mut self, resizable: bool) -> Self { self.resizable = resizable; self @@ -143,12 +144,14 @@ impl SidePanel { /// Show a separator line, even when not interacting with it? /// /// Default: `true`. + #[inline] pub fn show_separator_line(mut self, show_separator_line: bool) -> Self { self.show_separator_line = show_separator_line; self } /// The initial wrapping width of the [`SidePanel`]. + #[inline] pub fn default_width(mut self, default_width: f32) -> Self { self.default_width = default_width; self.width_range = Rangef::new( @@ -159,18 +162,21 @@ impl SidePanel { } /// Minimum width of the panel. + #[inline] pub fn min_width(mut self, min_width: f32) -> Self { self.width_range = Rangef::new(min_width, self.width_range.max.at_least(min_width)); self } /// Maximum width of the panel. + #[inline] pub fn max_width(mut self, max_width: f32) -> Self { self.width_range = Rangef::new(self.width_range.min.at_most(max_width), max_width); self } /// The allowable width range for the panel. + #[inline] pub fn width_range(mut self, width_range: impl Into) -> Self { let width_range = width_range.into(); self.default_width = clamp_to_range(self.default_width, width_range); @@ -179,6 +185,7 @@ impl SidePanel { } /// Enforce this exact width. + #[inline] pub fn exact_width(mut self, width: f32) -> Self { self.default_width = width; self.width_range = Rangef::point(width); @@ -186,6 +193,7 @@ impl SidePanel { } /// Change the background color, margins, etc. + #[inline] pub fn frame(mut self, frame: Frame) -> Self { self.frame = Some(frame); self @@ -230,40 +238,22 @@ impl SidePanel { ui.ctx().check_for_id_clash(id, panel_rect, "SidePanel"); } + let resize_id = id.with("__resize"); let mut resize_hover = false; let mut is_resizing = false; if resizable { - let resize_id = id.with("__resize"); - if let Some(pointer) = ui.ctx().pointer_latest_pos() { - let we_are_on_top = ui - .ctx() - .layer_id_at(pointer) - .map_or(true, |top_layer_id| top_layer_id == ui.layer_id()); - - let resize_x = side.opposite().side_x(panel_rect); - let mouse_over_resize_line = we_are_on_top - && panel_rect.y_range().contains(pointer.y) - && (resize_x - pointer.x).abs() - <= ui.style().interaction.resize_grab_radius_side; - - if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down()) - && mouse_over_resize_line - { - ui.memory_mut(|mem| mem.set_dragged_id(resize_id)); - } - is_resizing = ui.memory(|mem| mem.is_being_dragged(resize_id)); - if is_resizing { - let width = (pointer.x - side.side_x(panel_rect)).abs(); - let width = clamp_to_range(width, width_range).at_most(available_rect.width()); - side.set_rect_width(&mut panel_rect, width); - } + // First we read the resize interaction results, to avoid frame latency in the resize: + if let Some(resize_response) = ui.ctx().read_response(resize_id) { + resize_hover = resize_response.hovered(); + is_resizing = resize_response.dragged(); - let dragging_something_else = - ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed()); - resize_hover = mouse_over_resize_line && !dragging_something_else; - - if resize_hover || is_resizing { - ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal); + if is_resizing { + if let Some(pointer) = resize_response.interact_pointer_pos() { + let width = (pointer.x - side.side_x(panel_rect)).abs(); + let width = + clamp_to_range(width, width_range).at_most(available_rect.width()); + side.set_rect_width(&mut panel_rect, width); + } } } } @@ -293,6 +283,22 @@ impl SidePanel { } ui.expand_to_include_rect(rect); + if resizable { + // Now we do the actual resize interaction, on top of all the contents. + // Otherwise its input could be eaten by the contents, e.g. a + // `ScrollArea` on either side of the panel boundary. + let resize_x = side.opposite().side_x(panel_rect); + let resize_rect = Rect::from_x_y_ranges(resize_x..=resize_x, panel_rect.y_range()) + .expand2(vec2(ui.style().interaction.resize_grab_radius_side, 0.0)); + let resize_response = ui.interact(resize_rect, resize_id, Sense::drag()); + resize_hover = resize_response.hovered(); + is_resizing = resize_response.dragged(); + } + + if resize_hover || is_resizing { + ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal); + } + PanelState { rect }.store(ui.ctx(), id); { @@ -498,22 +504,22 @@ pub enum TopBottomSide { impl TopBottomSide { fn opposite(self) -> Self { match self { - TopBottomSide::Top => Self::Bottom, - TopBottomSide::Bottom => Self::Top, + Self::Top => Self::Bottom, + Self::Bottom => Self::Top, } } fn set_rect_height(self, rect: &mut Rect, height: f32) { match self { - TopBottomSide::Top => rect.max.y = rect.min.y + height, - TopBottomSide::Bottom => rect.min.y = rect.max.y - height, + Self::Top => rect.max.y = rect.min.y + height, + Self::Bottom => rect.min.y = rect.max.y - height, } } fn side_y(self, rect: Rect) -> f32 { match self { - TopBottomSide::Top => rect.top(), - TopBottomSide::Bottom => rect.bottom(), + Self::Top => rect.top(), + Self::Bottom => rect.bottom(), } } } @@ -582,6 +588,7 @@ impl TopBottomPanel { /// * A [`Separator`]. /// * A [`TextEdit`]. /// * … + #[inline] pub fn resizable(mut self, resizable: bool) -> Self { self.resizable = resizable; self @@ -590,13 +597,15 @@ impl TopBottomPanel { /// Show a separator line, even when not interacting with it? /// /// Default: `true`. + #[inline] pub fn show_separator_line(mut self, show_separator_line: bool) -> Self { self.show_separator_line = show_separator_line; self } - /// The initial height of the [`SidePanel`]. + /// The initial height of the [`TopBottomPanel`]. /// Defaults to [`style::Spacing::interact_size`].y. + #[inline] pub fn default_height(mut self, default_height: f32) -> Self { self.default_height = Some(default_height); self.height_range = Rangef::new( @@ -607,18 +616,21 @@ impl TopBottomPanel { } /// Minimum height of the panel. + #[inline] pub fn min_height(mut self, min_height: f32) -> Self { self.height_range = Rangef::new(min_height, self.height_range.max.at_least(min_height)); self } /// Maximum height of the panel. + #[inline] pub fn max_height(mut self, max_height: f32) -> Self { self.height_range = Rangef::new(self.height_range.min.at_most(max_height), max_height); self } /// The allowable height range for the panel. + #[inline] pub fn height_range(mut self, height_range: impl Into) -> Self { let height_range = height_range.into(); self.default_height = self @@ -629,6 +641,7 @@ impl TopBottomPanel { } /// Enforce this exact height. + #[inline] pub fn exact_height(mut self, height: f32) -> Self { self.default_height = Some(height); self.height_range = Rangef::point(height); @@ -636,6 +649,7 @@ impl TopBottomPanel { } /// Change the background color, margins, etc. + #[inline] pub fn frame(mut self, frame: Frame) -> Self { self.frame = Some(frame); self @@ -682,42 +696,22 @@ impl TopBottomPanel { .check_for_id_clash(id, panel_rect, "TopBottomPanel"); } + let resize_id = id.with("__resize"); let mut resize_hover = false; let mut is_resizing = false; if resizable { - let resize_id = id.with("__resize"); - let latest_pos = ui.input(|i| i.pointer.latest_pos()); - if let Some(pointer) = latest_pos { - let we_are_on_top = ui - .ctx() - .layer_id_at(pointer) - .map_or(true, |top_layer_id| top_layer_id == ui.layer_id()); - - let resize_y = side.opposite().side_y(panel_rect); - let mouse_over_resize_line = we_are_on_top - && panel_rect.x_range().contains(pointer.x) - && (resize_y - pointer.y).abs() - <= ui.style().interaction.resize_grab_radius_side; - - if ui.input(|i| i.pointer.any_pressed() && i.pointer.any_down()) - && mouse_over_resize_line - { - ui.memory_mut(|mem| mem.interaction.drag_id = Some(resize_id)); - } - is_resizing = ui.memory(|mem| mem.interaction.drag_id == Some(resize_id)); - if is_resizing { - let height = (pointer.y - side.side_y(panel_rect)).abs(); - let height = - clamp_to_range(height, height_range).at_most(available_rect.height()); - side.set_rect_height(&mut panel_rect, height); - } + // First we read the resize interaction results, to avoid frame latency in the resize: + if let Some(resize_response) = ui.ctx().read_response(resize_id) { + resize_hover = resize_response.hovered(); + is_resizing = resize_response.dragged(); - let dragging_something_else = - ui.input(|i| i.pointer.any_down() || i.pointer.any_pressed()); - resize_hover = mouse_over_resize_line && !dragging_something_else; - - if resize_hover || is_resizing { - ui.ctx().set_cursor_icon(CursorIcon::ResizeVertical); + if is_resizing { + if let Some(pointer) = resize_response.interact_pointer_pos() { + let height = (pointer.y - side.side_y(panel_rect)).abs(); + let height = + clamp_to_range(height, height_range).at_most(available_rect.height()); + side.set_rect_height(&mut panel_rect, height); + } } } } @@ -747,6 +741,23 @@ impl TopBottomPanel { } ui.expand_to_include_rect(rect); + if resizable { + // Now we do the actual resize interaction, on top of all the contents. + // Otherwise its input could be eaten by the contents, e.g. a + // `ScrollArea` on either side of the panel boundary. + + let resize_y = side.opposite().side_y(panel_rect); + let resize_rect = Rect::from_x_y_ranges(panel_rect.x_range(), resize_y..=resize_y) + .expand2(vec2(0.0, ui.style().interaction.resize_grab_radius_side)); + let resize_response = ui.interact(resize_rect, resize_id, Sense::drag()); + resize_hover = resize_response.hovered(); + is_resizing = resize_response.dragged(); + } + + if resize_hover || is_resizing { + ui.ctx().set_cursor_icon(CursorIcon::ResizeVertical); + } + PanelState { rect }.store(ui.ctx(), id); { @@ -994,6 +1005,7 @@ pub struct CentralPanel { impl CentralPanel { /// Change the background color, margins, etc. + #[inline] pub fn frame(mut self, frame: Frame) -> Self { self.frame = Some(frame); self @@ -1045,7 +1057,7 @@ impl CentralPanel { ) -> InnerResponse { let available_rect = ctx.available_rect(); let layer_id = LayerId::background(); - let id = Id::new("central_panel"); + let id = Id::new((ctx.viewport_id(), "central_panel")); let clip_rect = ctx.screen_rect(); let mut panel_ui = Ui::new(ctx.clone(), layer_id, id, available_rect, clip_rect); diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index e4ad1b1305f..99165149f1d 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -13,11 +13,11 @@ pub(crate) struct TooltipState { impl TooltipState { pub fn load(ctx: &Context) -> Option { - ctx.data_mut(|d| d.get_temp(Id::null())) + ctx.data_mut(|d| d.get_temp(Id::NULL)) } fn store(self, ctx: &Context) { - ctx.data_mut(|d| d.insert_temp(Id::null(), self)); + ctx.data_mut(|d| d.insert_temp(Id::NULL, self)); } fn individual_tooltip_size(&self, common_id: Id, index: usize) -> Option { @@ -260,9 +260,8 @@ fn show_tooltip_area_dyn<'c, R>( Area::new(area_id) .order(Order::Tooltip) .fixed_pos(window_pos) - .constrain(true) + .constrain_to(ctx.screen_rect()) .interactable(false) - .drag_bounds(ctx.screen_rect()) .show(ctx, |ui| { Frame::popup(&ctx.style()) .show(ui, |ui| { @@ -281,7 +280,7 @@ pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool { if *individual_id == tooltip_id { let area_id = common_id.with(count); let layer_id = LayerId::new(Order::Tooltip, area_id); - if ctx.memory(|mem| mem.areas.visible_last_frame(&layer_id)) { + if ctx.memory(|mem| mem.areas().visible_last_frame(&layer_id)) { return true; } } @@ -352,8 +351,6 @@ pub fn popup_above_or_below_widget( .fixed_pos(pos) .pivot(pivot) .show(ui.ctx(), |ui| { - // Note: we use a separate clip-rect for this area, so the popup can be outside the parent. - // See https://github.com/emilk/egui/issues/825 let frame = Frame::popup(ui.style()); let frame_margin = frame.total_margin(); frame diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 2f71a57c08b..e2654ca5a6c 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -34,7 +34,7 @@ pub struct Resize { id_source: Option, /// If false, we are no enabled - resizable: bool, + resizable: Vec2b, pub(crate) min_size: Vec2, pub(crate) max_size: Vec2, @@ -49,7 +49,7 @@ impl Default for Resize { Self { id: None, id_source: None, - resizable: true, + resizable: Vec2b::TRUE, min_size: Vec2::splat(16.0), max_size: Vec2::splat(f32::INFINITY), default_size: vec2(320.0, 128.0), // TODO(emilk): preferred size of [`Resize`] area. @@ -60,12 +60,14 @@ impl Default for Resize { impl Resize { /// Assign an explicit and globally unique id. + #[inline] pub fn id(mut self, id: Id) -> Self { self.id = Some(id); self } /// A source for the unique [`Id`], e.g. `.id_source("second_resize_area")` or `.id_source(loop_index)`. + #[inline] pub fn id_source(mut self, id_source: impl std::hash::Hash) -> Self { self.id_source = Some(Id::new(id_source)); self @@ -77,6 +79,7 @@ impl Resize { /// * if the contents is text, this will decide where we break long lines. /// * if the contents is a canvas, this decides the width of it, /// * if the contents is some buttons, this is ignored and we will auto-size. + #[inline] pub fn default_width(mut self, width: f32) -> Self { self.default_size.x = width; self @@ -89,51 +92,73 @@ impl Resize { /// * if the contents is a canvas, this decides the height of it, /// * if the contents is text and buttons, then the `default_height` is ignored /// and the height is picked automatically.. + #[inline] pub fn default_height(mut self, height: f32) -> Self { self.default_size.y = height; self } + #[inline] pub fn default_size(mut self, default_size: impl Into) -> Self { self.default_size = default_size.into(); self } /// Won't shrink to smaller than this + #[inline] pub fn min_size(mut self, min_size: impl Into) -> Self { self.min_size = min_size.into(); self } /// Won't shrink to smaller than this + #[inline] pub fn min_width(mut self, min_width: f32) -> Self { self.min_size.x = min_width; self } /// Won't shrink to smaller than this + #[inline] pub fn min_height(mut self, min_height: f32) -> Self { self.min_size.y = min_height; self } /// Won't expand to larger than this + #[inline] pub fn max_size(mut self, max_size: impl Into) -> Self { self.max_size = max_size.into(); self } + /// Won't expand to larger than this + #[inline] + pub fn max_width(mut self, max_width: f32) -> Self { + self.max_size.x = max_width; + self + } + + /// Won't expand to larger than this + #[inline] + pub fn max_height(mut self, max_height: f32) -> Self { + self.max_size.y = max_height; + self + } + /// Can you resize it with the mouse? /// /// Note that a window can still auto-resize. /// /// Default is `true`. - pub fn resizable(mut self, resizable: bool) -> Self { - self.resizable = resizable; + #[inline] + pub fn resizable(mut self, resizable: impl Into) -> Self { + self.resizable = resizable.into(); self } - pub fn is_resizable(&self) -> bool { + #[inline] + pub fn is_resizable(&self) -> Vec2b { self.resizable } @@ -145,15 +170,17 @@ impl Resize { .resizable(false) } + #[inline] pub fn fixed_size(mut self, size: impl Into) -> Self { let size = size.into(); self.default_size = size; self.min_size = size; self.max_size = size; - self.resizable = false; + self.resizable = Vec2b::FALSE; self } + #[inline] pub fn with_stroke(mut self, with_stroke: bool) -> Self { self.with_stroke = with_stroke; self @@ -162,8 +189,8 @@ impl Resize { struct Prepared { id: Id, + corner_id: Option, state: State, - corner_response: Option, content_ui: Ui, } @@ -200,22 +227,17 @@ impl Resize { let mut user_requested_size = state.requested_size.take(); - let corner_response = if self.resizable { - // Resize-corner: - let corner_size = Vec2::splat(ui.visuals().resize_corner_size); - let corner_rect = - Rect::from_min_size(position + state.desired_size - corner_size, corner_size); - let corner_response = ui.interact(corner_rect, id.with("corner"), Sense::drag()); + let corner_id = self.resizable.any().then(|| id.with("__resize_corner")); - if let Some(pointer_pos) = corner_response.interact_pointer_pos() { - user_requested_size = - Some(pointer_pos - position + 0.5 * corner_response.rect.size()); + if let Some(corner_id) = corner_id { + if let Some(corner_response) = ui.ctx().read_response(corner_id) { + if let Some(pointer_pos) = corner_response.interact_pointer_pos() { + // Respond to the interaction early to avoid frame delay. + user_requested_size = + Some(pointer_pos - position + 0.5 * corner_response.rect.size()); + } } - - Some(corner_response) - } else { - None - }; + } if let Some(user_requested_size) = user_requested_size { state.desired_size = user_requested_size; @@ -253,8 +275,8 @@ impl Resize { Prepared { id, + corner_id, state, - corner_response, content_ui, } } @@ -269,8 +291,8 @@ impl Resize { fn end(self, ui: &mut Ui, prepared: Prepared) { let Prepared { id, + corner_id, mut state, - corner_response, content_ui, } = prepared; @@ -278,19 +300,36 @@ impl Resize { // ------------------------------ - let size = if self.with_stroke || self.resizable { - // We show how large we are, - // so we must follow the contents: + let mut size = state.last_content_size; + for d in 0..2 { + if self.with_stroke || self.resizable[d] { + // We show how large we are, + // so we must follow the contents: - state.desired_size = state.desired_size.max(state.last_content_size); + state.desired_size[d] = state.desired_size[d].max(state.last_content_size[d]); + + // We are as large as we look + size[d] = state.desired_size[d]; + } else { + // Probably a window. + size[d] = state.last_content_size[d]; + } + } + ui.advance_cursor_after_rect(Rect::from_min_size(content_ui.min_rect().min, size)); - // We are as large as we look - state.desired_size + // ------------------------------ + + let corner_response = if let Some(corner_id) = corner_id { + // We do the corner interaction last to place it on top of the content: + let corner_size = Vec2::splat(ui.visuals().resize_corner_size); + let corner_rect = Rect::from_min_size( + content_ui.min_rect().left_top() + size - corner_size, + corner_size, + ); + Some(ui.interact(corner_rect, corner_id, Sense::drag())) } else { - // Probably a window. - state.last_content_size + None }; - ui.advance_cursor_after_rect(Rect::from_min_size(content_ui.min_rect().min, size)); // ------------------------------ @@ -314,6 +353,7 @@ impl Resize { state.store(ui.ctx(), id); + #[cfg(debug_assertions)] if ui.ctx().style().debug.show_resize { ui.ctx().debug_painter().debug_rect( Rect::from_min_size(content_ui.min_rect().left_top(), state.desired_size), @@ -331,15 +371,24 @@ impl Resize { use epaint::Stroke; -pub fn paint_resize_corner(ui: &mut Ui, response: &Response) { +pub fn paint_resize_corner(ui: &Ui, response: &Response) { let stroke = ui.style().interact(response).fg_stroke; - paint_resize_corner_with_style(ui, &response.rect, stroke, Align2::RIGHT_BOTTOM); + paint_resize_corner_with_style(ui, &response.rect, stroke.color, Align2::RIGHT_BOTTOM); } -pub fn paint_resize_corner_with_style(ui: &mut Ui, rect: &Rect, stroke: Stroke, corner: Align2) { +pub fn paint_resize_corner_with_style( + ui: &Ui, + rect: &Rect, + color: impl Into, + corner: Align2, +) { let painter = ui.painter(); let cp = painter.round_pos_to_pixels(corner.pos_in_rect(rect)); let mut w = 2.0; + let stroke = Stroke { + width: 1.0, // Set width to 1.0 to prevent overlapping + color: color.into(), + }; while w <= rect.width() && w <= rect.height() { painter.line_segment( diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 8bd8142cf1e..c35cca34225 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -1,12 +1,14 @@ -//! Coordinate system names: -//! * content: size of contents (generally large; that's why we want scroll bars) -//! * outer: size of scroll area including scroll bar(s) -//! * inner: excluding scroll bar(s). The area we clip the contents to. - #![allow(clippy::needless_range_loop)] use crate::*; +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +struct ScrollTarget { + animation_time_span: (f64, f64), + target_offset: f32, +} + #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] @@ -14,11 +16,17 @@ pub struct State { /// Positive offset means scrolling down/right pub offset: Vec2, + /// If set, quickly but smoothly scroll to this target offset. + offset_target: [Option; 2], + /// Were the scroll bars visible last frame? - show_scroll: [bool; 2], + show_scroll: Vec2b, /// The content were to large to fit large frame. - content_is_too_large: [bool; 2], + content_is_too_large: Vec2b, + + /// Did the user interact (hover or drag) the scroll bars last frame? + scroll_bar_interaction: Vec2b, /// Momentum, used for kinetic scrolling #[cfg_attr(feature = "serde", serde(skip))] @@ -30,18 +38,20 @@ pub struct State { /// 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) /// it remains false until the scroll touches the end position, which reenables stickiness. - scroll_stuck_to_end: [bool; 2], + scroll_stuck_to_end: Vec2b, } impl Default for State { fn default() -> Self { Self { offset: Vec2::ZERO, - show_scroll: [false; 2], - content_is_too_large: [false; 2], + offset_target: Default::default(), + show_scroll: Vec2b::FALSE, + content_is_too_large: Vec2b::FALSE, + scroll_bar_interaction: Vec2b::FALSE, vel: Vec2::ZERO, scroll_start_offset_from_top_left: [None; 2], - scroll_stuck_to_end: [true; 2], + scroll_stuck_to_end: Vec2b::TRUE, } } } @@ -54,6 +64,11 @@ impl State { pub fn store(self, ctx: &Context, id: Id) { ctx.data_mut(|d| d.insert_persisted(id, self)); } + + /// Get the current kinetic scrolling velocity. + pub fn velocity(&self) -> Vec2 { + self.vel + } } pub struct ScrollAreaOutput { @@ -75,15 +90,61 @@ pub struct ScrollAreaOutput { } /// Indicate whether the horizontal and vertical scroll bars must be always visible, hidden or visible when needed. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum ScrollBarVisibility { - AlwaysVisible, - VisibleWhenNeeded, + /// Hide scroll bar even if they are needed. + /// + /// You can still scroll, with the scroll-wheel + /// and by dragging the contents, but there is no + /// visual indication of how far you have scrolled. AlwaysHidden, + + /// Show scroll bars only when the content size exceeds the container, + /// i.e. when there is any need to scroll. + /// + /// This is the default. + VisibleWhenNeeded, + + /// Always show the scroll bar, even if the contents fit in the container + /// and there is no need to scroll. + AlwaysVisible, +} + +impl Default for ScrollBarVisibility { + #[inline] + fn default() -> Self { + Self::VisibleWhenNeeded + } +} + +impl ScrollBarVisibility { + pub const ALL: [Self; 3] = [ + Self::AlwaysHidden, + Self::VisibleWhenNeeded, + Self::AlwaysVisible, + ]; } /// Add vertical and/or horizontal scrolling to a contained [`Ui`]. /// +/// By default, scroll bars only show up when needed, i.e. when the contents +/// is larger than the container. +/// This is controlled by [`Self::scroll_bar_visibility`]. +/// +/// There are two flavors of scroll areas: solid and floating. +/// Solid scroll bars use up space, reducing the amount of space available +/// to the contents. Floating scroll bars float on top of the contents, covering it. +/// You can change the scroll style by changing the [`crate::style::Spacing::scroll`]. +/// +/// ### Coordinate system +/// * content: size of contents (generally large; that's why we want scroll bars) +/// * outer: size of scroll area including scroll bar(s) +/// * inner: excluding scroll bar(s). The area we clip the contents to. +/// +/// If the floating scroll bars settings is turned on then `inner == outer`. +/// +/// ## Example /// ``` /// # egui::__run_test_ui(|ui| { /// egui::ScrollArea::vertical().show(ui, |ui| { @@ -96,9 +157,10 @@ pub enum ScrollBarVisibility { #[derive(Clone, Debug)] #[must_use = "You should call .show()"] pub struct ScrollArea { - /// Do we have horizontal/vertical scrolling? - has_bar: [bool; 2], - auto_shrink: [bool; 2], + /// Do we have horizontal/vertical scrolling enabled? + scroll_enabled: Vec2b, + + auto_shrink: Vec2b, max_size: Vec2, min_scrolled_size: Vec2, scroll_bar_visibility: ScrollBarVisibility, @@ -113,46 +175,50 @@ pub struct ScrollArea { /// If true for vertical or horizontal the scroll wheel will stick to the /// end position until user manually changes position. It will become true /// again once scroll handle makes contact with end. - stick_to_end: [bool; 2], + stick_to_end: Vec2b, } impl ScrollArea { /// Create a horizontal scroll area. + #[inline] pub fn horizontal() -> Self { Self::new([true, false]) } /// Create a vertical scroll area. + #[inline] pub fn vertical() -> Self { Self::new([false, true]) } /// Create a bi-directional (horizontal and vertical) scroll area. + #[inline] pub fn both() -> Self { Self::new([true, true]) } /// Create a scroll area where both direction of scrolling is disabled. /// It's unclear why you would want to do this. + #[inline] pub fn neither() -> Self { Self::new([false, false]) } /// Create a scroll area where you decide which axis has scrolling enabled. /// For instance, `ScrollArea::new([true, false])` enables horizontal scrolling. - pub fn new(has_bar: [bool; 2]) -> Self { + pub fn new(scroll_enabled: impl Into) -> Self { Self { - has_bar, - auto_shrink: [true; 2], + scroll_enabled: scroll_enabled.into(), + auto_shrink: Vec2b::TRUE, max_size: Vec2::INFINITY, min_scrolled_size: Vec2::splat(64.0), - scroll_bar_visibility: ScrollBarVisibility::VisibleWhenNeeded, + scroll_bar_visibility: Default::default(), id_source: None, offset_x: None, offset_y: None, scrolling_enabled: true, drag_to_scroll: true, - stick_to_end: [false; 2], + stick_to_end: Vec2b::FALSE, } } @@ -161,6 +227,7 @@ impl ScrollArea { /// Use `f32::INFINITY` if you want the scroll area to expand to fit the surrounding [`Ui`] (default). /// /// See also [`Self::auto_shrink`]. + #[inline] pub fn max_width(mut self, max_width: f32) -> Self { self.max_size.x = max_width; self @@ -171,6 +238,7 @@ impl ScrollArea { /// Use `f32::INFINITY` if you want the scroll area to expand to fit the surrounding [`Ui`] (default). /// /// See also [`Self::auto_shrink`]. + #[inline] pub fn max_height(mut self, max_height: f32) -> Self { self.max_size.y = max_height; self @@ -182,6 +250,7 @@ impl ScrollArea { /// (and so we don't require scroll bars). /// /// Default: `64.0`. + #[inline] pub fn min_scrolled_width(mut self, min_scrolled_width: f32) -> Self { self.min_scrolled_size.x = min_scrolled_width; self @@ -193,6 +262,7 @@ impl ScrollArea { /// (and so we don't require scroll bars). /// /// Default: `64.0`. + #[inline] pub fn min_scrolled_height(mut self, min_scrolled_height: f32) -> Self { self.min_scrolled_size.y = min_scrolled_height; self @@ -201,12 +271,14 @@ impl ScrollArea { /// Set the visibility of both horizontal and vertical scroll bars. /// /// With `ScrollBarVisibility::VisibleWhenNeeded` (default), the scroll bar will be visible only when needed. + #[inline] pub fn scroll_bar_visibility(mut self, scroll_bar_visibility: ScrollBarVisibility) -> Self { self.scroll_bar_visibility = scroll_bar_visibility; self } /// A source for the unique [`Id`], e.g. `.id_source("second_scroll_area")` or `.id_source(loop_index)`. + #[inline] pub fn id_source(mut self, id_source: impl std::hash::Hash) -> Self { self.id_source = Some(Id::new(id_source)); self @@ -219,6 +291,7 @@ impl ScrollArea { /// See also: [`Self::vertical_scroll_offset`], [`Self::horizontal_scroll_offset`], /// [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and /// [`Response::scroll_to_me`](crate::Response::scroll_to_me) + #[inline] pub fn scroll_offset(mut self, offset: Vec2) -> Self { self.offset_x = Some(offset.x); self.offset_y = Some(offset.y); @@ -231,6 +304,7 @@ impl ScrollArea { /// /// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and /// [`Response::scroll_to_me`](crate::Response::scroll_to_me) + #[inline] pub fn vertical_scroll_offset(mut self, offset: f32) -> Self { self.offset_y = Some(offset); self @@ -242,26 +316,30 @@ impl ScrollArea { /// /// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and /// [`Response::scroll_to_me`](crate::Response::scroll_to_me) + #[inline] pub fn horizontal_scroll_offset(mut self, offset: f32) -> Self { self.offset_x = Some(offset); self } /// Turn on/off scrolling on the horizontal axis. + #[inline] pub fn hscroll(mut self, hscroll: bool) -> Self { - self.has_bar[0] = hscroll; + self.scroll_enabled[0] = hscroll; self } /// Turn on/off scrolling on the vertical axis. + #[inline] pub fn vscroll(mut self, vscroll: bool) -> Self { - self.has_bar[1] = vscroll; + self.scroll_enabled[1] = vscroll; self } /// Turn on/off scrolling on the horizontal/vertical axes. - pub fn scroll2(mut self, has_bar: [bool; 2]) -> Self { - self.has_bar = has_bar; + #[inline] + pub fn scroll2(mut self, scroll_enabled: impl Into) -> Self { + self.scroll_enabled = scroll_enabled.into(); self } @@ -274,6 +352,7 @@ impl ScrollArea { /// is typing text in a [`TextEdit`] widget contained within the scroll area. /// /// This controls both scrolling directions. + #[inline] pub fn enable_scrolling(mut self, enable: bool) -> Self { self.scrolling_enabled = enable; self @@ -286,6 +365,7 @@ impl ScrollArea { /// If `true`, the [`ScrollArea`] will sense drags. /// /// Default: `true`. + #[inline] pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self { self.drag_to_scroll = drag_to_scroll; self @@ -296,14 +376,16 @@ impl ScrollArea { /// * If `true`, egui will add blank space outside the scroll area. /// * If `false`, egui will add blank space inside the scroll area. /// - /// Default: `[true; 2]`. - pub fn auto_shrink(mut self, auto_shrink: [bool; 2]) -> Self { - self.auto_shrink = auto_shrink; + /// Default: `true`. + #[inline] + pub fn auto_shrink(mut self, auto_shrink: impl Into) -> Self { + self.auto_shrink = auto_shrink.into(); self } - pub(crate) fn has_any_bar(&self) -> bool { - self.has_bar[0] || self.has_bar[1] + /// Is any scrolling enabled? + pub(crate) fn is_any_scroll_enabled(&self) -> bool { + self.scroll_enabled[0] || self.scroll_enabled[1] } /// The scroll handle will stick to the rightmost position even while the content size @@ -312,6 +394,7 @@ impl ScrollArea { /// it will remain focused on whatever content viewport the user left it on. If the scroll /// handle is dragged all the way to the right it will again become stuck and remain there /// until manually pulled from the end position. + #[inline] pub fn stick_to_right(mut self, stick: bool) -> Self { self.stick_to_end[0] = stick; self @@ -323,6 +406,7 @@ impl ScrollArea { /// it will remain focused on whatever content viewport the user left it on. If the scroll /// handle is dragged to the bottom it will again become stuck and remain there until manually /// pulled from the end position. + #[inline] pub fn stick_to_bottom(mut self, stick: bool) -> Self { self.stick_to_end[1] = stick; self @@ -332,11 +416,24 @@ impl ScrollArea { struct Prepared { id: Id, state: State, - has_bar: [bool; 2], - auto_shrink: [bool; 2], + + auto_shrink: Vec2b, + + /// Does this `ScrollArea` have horizontal/vertical scrolling enabled? + scroll_enabled: Vec2b, + + /// Smoothly interpolated boolean of whether or not to show the scroll bars. + show_bars_factor: Vec2, /// How much horizontal and vertical space are used up by the /// width of the vertical bar, and the height of the horizontal bar? + /// + /// This is always zero for floating scroll bars. + /// + /// Note that this is a `yx` swizzling of [`Self::show_bars_factor`] + /// times the maximum bar with. + /// That's because horizontal scroll uses up vertical space, + /// and vice versa. current_bar_use: Vec2, scroll_bar_visibility: ScrollBarVisibility, @@ -351,13 +448,13 @@ struct Prepared { viewport: Rect, scrolling_enabled: bool, - stick_to_end: [bool; 2], + stick_to_end: Vec2b, } impl ScrollArea { fn begin(self, ui: &mut Ui) -> Prepared { let Self { - has_bar, + scroll_enabled, auto_shrink, max_size, min_scrolled_size, @@ -374,7 +471,7 @@ impl ScrollArea { let id_source = id_source.unwrap_or_else(|| Id::new("scroll_area")); let id = ui.make_persistent_id(id_source); - ui.ctx().check_for_id_clash( + ctx.check_for_id_clash( id, Rect::from_min_size(ui.available_rect_before_wrap().min, Vec2::ZERO), "ScrollArea", @@ -384,25 +481,18 @@ impl ScrollArea { state.offset.x = offset_x.unwrap_or(state.offset.x); state.offset.y = offset_y.unwrap_or(state.offset.y); - let max_scroll_bar_width = max_scroll_bar_width_with_margin(ui); - - let current_hscroll_bar_height = if !has_bar[0] { - 0.0 - } else if scroll_bar_visibility == ScrollBarVisibility::AlwaysVisible { - max_scroll_bar_width - } else { - max_scroll_bar_width * ui.ctx().animate_bool(id.with("h"), state.show_scroll[0]) + let show_bars: Vec2b = match scroll_bar_visibility { + ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE, + ScrollBarVisibility::VisibleWhenNeeded => state.show_scroll, + ScrollBarVisibility::AlwaysVisible => scroll_enabled, }; - let current_vscroll_bar_width = if !has_bar[1] { - 0.0 - } else if scroll_bar_visibility == ScrollBarVisibility::AlwaysVisible { - max_scroll_bar_width - } else { - max_scroll_bar_width * ui.ctx().animate_bool(id.with("v"), state.show_scroll[1]) - }; + let show_bars_factor = Vec2::new( + ctx.animate_bool(id.with("h"), show_bars[0]), + ctx.animate_bool(id.with("v"), show_bars[1]), + ); - let current_bar_use = vec2(current_vscroll_bar_width, current_hscroll_bar_height); + let current_bar_use = show_bars_factor.yx() * ui.spacing().scroll.allocated_width(); let available_outer = ui.available_rect_before_wrap(); @@ -416,7 +506,7 @@ impl ScrollArea { // one shouldn't collapse into nothingness. // See https://github.com/emilk/egui/issues/1097 for d in 0..2 { - if has_bar[d] { + if scroll_enabled[d] { inner_size[d] = inner_size[d].max(min_scrolled_size[d]); } } @@ -433,7 +523,7 @@ impl ScrollArea { } else { // Tell the inner Ui to use as much space as possible, we can scroll to see it! for d in 0..2 { - if has_bar[d] { + if scroll_enabled[d] { content_max_size[d] = f32::INFINITY; } } @@ -445,21 +535,13 @@ impl ScrollArea { { // Clip the content, but only when we really need to: let clip_rect_margin = ui.visuals().clip_rect_margin; - let scroll_bar_inner_margin = ui.spacing().scroll_bar_inner_margin; let mut content_clip_rect = ui.clip_rect(); for d in 0..2 { - if has_bar[d] { + if scroll_enabled[d] { if state.content_is_too_large[d] { content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin; content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin; } - - if state.show_scroll[d] { - // Make sure content doesn't cover scroll bars - let tiny_gap = 1.0; - content_clip_rect.max[1 - d] = - inner_rect.max[1 - d] + scroll_bar_inner_margin - tiny_gap; - } } else { // Nice handling of forced resizing beyond the possible: content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d]; @@ -471,6 +553,7 @@ impl ScrollArea { } let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size); + let dt = ui.input(|i| i.stable_dt).at_most(0.1); if (scrolling_enabled && drag_to_scroll) && (state.content_is_too_large[0] || state.content_is_too_large[1]) @@ -482,30 +565,64 @@ impl ScrollArea { if content_response.dragged() { for d in 0..2 { - if has_bar[d] { + 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 { - let stop_speed = 20.0; // Pixels per second. - let friction_coeff = 1000.0; // Pixels per second squared. - let dt = ui.input(|i| i.unstable_dt); + for d in 0..2 { + // Kinetic scrolling + let stop_speed = 20.0; // Pixels per second. + let friction_coeff = 1000.0; // Pixels per second squared. - let friction = friction_coeff * dt; - if friction > state.vel.length() || state.vel.length() < stop_speed { - state.vel = Vec2::ZERO; + let friction = friction_coeff * dt; + if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed { + state.vel[d] = 0.0; + } else { + state.vel[d] -= friction * state.vel[d].signum(); + // Offset has an inverted coordinate system compared to + // the velocity, so we subtract it instead of adding it + state.offset[d] -= state.vel[d] * dt; + ctx.request_repaint(); + } + } + } + } + + // Scroll with an animation if we have a target offset (that hasn't been cleared by the code + // above). + for d in 0..2 { + if let Some(scroll_target) = state.offset_target[d] { + state.vel[d] = 0.0; + + if (state.offset[d] - scroll_target.target_offset).abs() < 1.0 { + // Arrived + state.offset[d] = scroll_target.target_offset; + state.offset_target[d] = None; } else { - state.vel -= friction * state.vel.normalized(); - // Offset has an inverted coordinate system compared to - // the velocity, so we subtract it instead of adding it - state.offset -= state.vel * dt; - ui.ctx().request_repaint(); + // Move towards target + let t = emath::interpolation_factor( + scroll_target.animation_time_span, + ui.input(|i| i.time), + dt, + emath::ease_in_ease_out, + ); + if t < 1.0 { + state.offset[d] = + emath::lerp(state.offset[d]..=scroll_target.target_offset, t); + ctx.request_repaint(); + } else { + // Arrived + state.offset[d] = scroll_target.target_offset; + state.offset_target[d] = None; + } } } } @@ -513,8 +630,9 @@ impl ScrollArea { Prepared { id, state, - has_bar, auto_shrink, + scroll_enabled, + show_bars_factor, current_bar_use, scroll_bar_visibility, inner_rect, @@ -620,13 +738,14 @@ impl ScrollArea { impl Prepared { /// Returns content size and state fn end(self, ui: &mut Ui) -> (Vec2, State) { - let Prepared { + let Self { id, mut state, inner_rect, - has_bar, auto_shrink, - mut current_bar_use, + scroll_enabled, + mut show_bars_factor, + current_bar_use, scroll_bar_visibility, content_ui, viewport: _, @@ -637,16 +756,18 @@ impl Prepared { let content_size = content_ui.min_size(); for d in 0..2 { - if has_bar[d] { - // We take the scroll target so only this ScrollArea will use it: - let scroll_target = content_ui - .ctx() - .frame_state_mut(|state| state.scroll_target[d].take()); - if let Some((scroll, align)) = scroll_target { + // We always take both scroll targets regardless of which scroll axes are enabled. This + // is to avoid them leaking to other scroll areas. + let scroll_target = content_ui + .ctx() + .frame_state_mut(|state| state.scroll_target[d].take()); + + if scroll_enabled[d] { + if let Some((target_range, align)) = scroll_target { let min = content_ui.min_rect().min[d]; let clip_rect = content_ui.clip_rect(); let visible_range = min..=min + clip_rect.size()[d]; - let (start, end) = (scroll.min, scroll.max); + let (start, end) = (target_range.min, target_range.max); let clip_start = clip_rect.min[d]; let clip_end = clip_rect.max[d]; let mut spacing = ui.spacing().item_spacing[d]; @@ -655,7 +776,7 @@ impl Prepared { let center_factor = align.to_factor(); let offset = - lerp(scroll, center_factor) - lerp(visible_range, center_factor); + lerp(target_range, center_factor) - lerp(visible_range, center_factor); // Depending on the alignment we need to add or subtract the spacing spacing *= remap(center_factor, 0.0..=1.0, -1.0..=1.0); @@ -671,7 +792,24 @@ impl Prepared { }; if delta != 0.0 { - state.offset[d] += delta; + let target_offset = state.offset[d] + delta; + + if let Some(animation) = &mut state.offset_target[d] { + // For instance: the user is continuously calling `ui.scroll_to_cursor`, + // so we don't want to reset the animation, but perhaps update the target: + animation.target_offset = target_offset; + } else { + // The further we scroll, the more time we take. + // TODO(emilk): let users configure this in `Style`. + let now = ui.input(|i| i.time); + let points_per_second = 1000.0; + let animation_duration = + (delta.abs() / points_per_second).clamp(0.1, 0.3); + state.offset_target[d] = Some(ScrollTarget { + animation_time_span: (now, now + animation_duration as f64), + target_offset, + }); + } ui.ctx().request_repaint(); } } @@ -683,7 +821,7 @@ impl Prepared { let mut inner_size = inner_rect.size(); for d in 0..2 { - inner_size[d] = match (has_bar[d], auto_shrink[d]) { + inner_size[d] = match (scroll_enabled[d], auto_shrink[d]) { (true, true) => inner_size[d].min(content_size[d]), // shrink scroll area if content is small (true, false) => inner_size[d], // let scroll area be larger than content; fill with blank space (false, true) => content_size[d], // Follow the content (expand/contract to fit it). @@ -696,64 +834,117 @@ impl Prepared { let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use); - let content_is_too_large = [ - content_size.x > inner_rect.width(), - content_size.y > inner_rect.height(), - ]; + let content_is_too_large = Vec2b::new( + scroll_enabled[0] && inner_rect.width() < content_size.x, + scroll_enabled[1] && inner_rect.height() < content_size.y, + ); let max_offset = content_size - inner_rect.size(); - if scrolling_enabled && ui.rect_contains_pointer(outer_rect) { + let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect); + if scrolling_enabled && is_hovering_outer_rect { + let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction + && scroll_enabled[0] != scroll_enabled[1]; for d in 0..2 { - if has_bar[d] { - let scroll_delta = ui.ctx().frame_state(|fs| fs.scroll_delta); + if scroll_enabled[d] { + let scroll_delta = ui.ctx().input_mut(|input| { + if always_scroll_enabled_direction { + // no bidirectional scrolling; allow horizontal scrolling without pressing shift + input.smooth_scroll_delta[0] + input.smooth_scroll_delta[1] + } else { + input.smooth_scroll_delta[d] + } + }); - let scrolling_up = state.offset[d] > 0.0 && scroll_delta[d] > 0.0; - let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta[d] < 0.0; + let scrolling_up = state.offset[d] > 0.0 && scroll_delta > 0.0; + let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta < 0.0; if scrolling_up || scrolling_down { - state.offset[d] -= scroll_delta[d]; - // Clear scroll delta so no parent scroll will use it. - ui.ctx().frame_state_mut(|fs| fs.scroll_delta[d] = 0.0); + state.offset[d] -= scroll_delta; + + // Clear scroll delta so no parent scroll will use it: + ui.ctx().input_mut(|input| { + if always_scroll_enabled_direction { + input.smooth_scroll_delta[0] = 0.0; + input.smooth_scroll_delta[1] = 0.0; + } else { + input.smooth_scroll_delta[d] = 0.0; + } + }); + state.scroll_stuck_to_end[d] = false; + state.offset_target[d] = None; } } } } let show_scroll_this_frame = match scroll_bar_visibility { - ScrollBarVisibility::AlwaysVisible => [true, true], - ScrollBarVisibility::VisibleWhenNeeded => { - [content_is_too_large[0], content_is_too_large[1]] - } - ScrollBarVisibility::AlwaysHidden => [false, false], + ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE, + ScrollBarVisibility::VisibleWhenNeeded => content_is_too_large, + ScrollBarVisibility::AlwaysVisible => scroll_enabled, }; - let max_scroll_bar_width = max_scroll_bar_width_with_margin(ui); - // Avoid frame delay; start showing scroll bar right away: - if show_scroll_this_frame[0] && current_bar_use.y <= 0.0 { - current_bar_use.y = max_scroll_bar_width * ui.ctx().animate_bool(id.with("h"), true); + if show_scroll_this_frame[0] && show_bars_factor.x <= 0.0 { + show_bars_factor.x = ui.ctx().animate_bool(id.with("h"), true); } - if show_scroll_this_frame[1] && current_bar_use.x <= 0.0 { - current_bar_use.x = max_scroll_bar_width * ui.ctx().animate_bool(id.with("v"), true); + if show_scroll_this_frame[1] && show_bars_factor.y <= 0.0 { + show_bars_factor.y = ui.ctx().animate_bool(id.with("v"), true); } + let scroll_style = ui.spacing().scroll; + + // Paint the bars: for d in 0..2 { - let animation_t = current_bar_use[1 - d] / max_scroll_bar_width; + // maybe force increase in offset to keep scroll stuck to end position + if stick_to_end[d] && state.scroll_stuck_to_end[d] { + state.offset[d] = content_size[d] - inner_rect.size()[d]; + } - if animation_t == 0.0 { + let show_factor = show_bars_factor[d]; + if show_factor == 0.0 { + state.scroll_bar_interaction[d] = false; continue; } - // margin on either side of the scroll bar - let inner_margin = animation_t * ui.spacing().scroll_bar_inner_margin; - let outer_margin = animation_t * ui.spacing().scroll_bar_outer_margin; - let mut min_cross = inner_rect.max[1 - d] + inner_margin; // left of vertical scroll (d == 1) - let mut max_cross = outer_rect.max[1 - d] - outer_margin; // right of vertical scroll (d == 1) - let min_main = inner_rect.min[d]; // top of vertical scroll (d == 1) - let max_main = inner_rect.max[d]; // bottom of vertical scroll (d == 1) + // left/right of a horizontal scroll (d==1) + // top/bottom of vertical scroll (d == 1) + let main_range = Rangef::new(inner_rect.min[d], inner_rect.max[d]); + + // Margin on either side of the scroll bar: + let inner_margin = show_factor * scroll_style.bar_inner_margin; + let outer_margin = show_factor * scroll_style.bar_outer_margin; + + // top/bottom of a horizontal scroll (d==0). + // left/rigth of a vertical scroll (d==1). + let mut cross = if scroll_style.floating { + let max_bar_rect = if d == 0 { + outer_rect.with_min_y(outer_rect.max.y - scroll_style.allocated_width()) + } else { + outer_rect.with_min_x(outer_rect.max.x - scroll_style.allocated_width()) + }; + let is_hovering_bar_area = is_hovering_outer_rect + && ui.rect_contains_pointer(max_bar_rect) + || state.scroll_bar_interaction[d]; + let is_hovering_bar_area_t = ui + .ctx() + .animate_bool(id.with((d, "bar_hover")), is_hovering_bar_area); + let width = show_factor + * lerp( + scroll_style.floating_width..=scroll_style.bar_width, + is_hovering_bar_area_t, + ); + + let max_cross = outer_rect.max[1 - d] - outer_margin; + let min_cross = max_cross - width; + Rangef::new(min_cross, max_cross) + } else { + let min_cross = inner_rect.max[1 - d] + inner_margin; + let max_cross = outer_rect.max[1 - d] - outer_margin; + Rangef::new(min_cross, max_cross) + }; - if ui.clip_rect().max[1 - d] < max_cross + outer_margin { + if ui.clip_rect().max[1 - d] < cross.max + outer_margin { // Move the scrollbar so it is visible. This is needed in some cases. // For instance: // * When we have a vertical-only scroll area in a top level panel, @@ -763,41 +954,35 @@ impl Prepared { // is outside the clip rectangle. // Really this should use the tighter clip_rect that ignores clip_rect_margin, but we don't store that. // clip_rect_margin is quite a hack. It would be nice to get rid of it. - let width = max_cross - min_cross; - max_cross = ui.clip_rect().max[1 - d] - outer_margin; - min_cross = max_cross - width; + let width = cross.max - cross.min; + cross.max = ui.clip_rect().max[1 - d] - outer_margin; + cross.min = cross.max - width; } let outer_scroll_rect = if d == 0 { Rect::from_min_max( - pos2(inner_rect.left(), min_cross), - pos2(inner_rect.right(), max_cross), + pos2(inner_rect.left(), cross.min), + pos2(inner_rect.right(), cross.max), ) } else { Rect::from_min_max( - pos2(min_cross, inner_rect.top()), - pos2(max_cross, inner_rect.bottom()), + pos2(cross.min, inner_rect.top()), + pos2(cross.max, inner_rect.bottom()), ) }; - // maybe force increase in offset to keep scroll stuck to end position - if stick_to_end[d] && state.scroll_stuck_to_end[d] { - state.offset[d] = content_size[d] - inner_rect.size()[d]; - } - - let from_content = - |content| remap_clamp(content, 0.0..=content_size[d], min_main..=max_main); + let from_content = |content| remap_clamp(content, 0.0..=content_size[d], main_range); let handle_rect = if d == 0 { Rect::from_min_max( - pos2(from_content(state.offset.x), min_cross), - pos2(from_content(state.offset.x + inner_rect.width()), max_cross), + pos2(from_content(state.offset.x), cross.min), + pos2(from_content(state.offset.x + inner_rect.width()), cross.max), ) } else { Rect::from_min_max( - pos2(min_cross, from_content(state.offset.y)), + pos2(cross.min, from_content(state.offset.y)), pos2( - max_cross, + cross.max, from_content(state.offset.y + inner_rect.height()), ), ) @@ -811,25 +996,28 @@ impl Prepared { }; let response = ui.interact(outer_scroll_rect, interact_id, sense); + state.scroll_bar_interaction[d] = response.hovered() || response.dragged(); + if let Some(pointer_pos) = response.interact_pointer_pos() { let scroll_start_offset_from_top_left = state.scroll_start_offset_from_top_left[d] .get_or_insert_with(|| { if handle_rect.contains(pointer_pos) { pointer_pos[d] - handle_rect.min[d] } else { - let handle_top_pos_at_bottom = max_main - handle_rect.size()[d]; + let handle_top_pos_at_bottom = main_range.max - handle_rect.size()[d]; // Calculate the new handle top position, centering the handle on the mouse. let new_handle_top_pos = (pointer_pos[d] - handle_rect.size()[d] / 2.0) - .clamp(min_main, handle_top_pos_at_bottom); + .clamp(main_range.min, handle_top_pos_at_bottom); pointer_pos[d] - new_handle_top_pos } }); let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left; - state.offset[d] = remap(new_handle_top, min_main..=max_main, 0.0..=content_size[d]); + state.offset[d] = remap(new_handle_top, main_range, 0.0..=content_size[d]); // some manual action taken, scroll not stuck state.scroll_stuck_to_end[d] = false; + state.offset_target[d] = None; } else { state.scroll_start_offset_from_top_left[d] = None; } @@ -846,19 +1034,19 @@ impl Prepared { // Avoid frame-delay by calculating a new handle rect: let mut handle_rect = if d == 0 { Rect::from_min_max( - pos2(from_content(state.offset.x), min_cross), - pos2(from_content(state.offset.x + inner_rect.width()), max_cross), + pos2(from_content(state.offset.x), cross.min), + pos2(from_content(state.offset.x + inner_rect.width()), cross.max), ) } else { Rect::from_min_max( - pos2(min_cross, from_content(state.offset.y)), + pos2(cross.min, from_content(state.offset.y)), pos2( - max_cross, + cross.max, from_content(state.offset.y + inner_rect.height()), ), ) }; - let min_handle_size = ui.spacing().scroll_handle_min_length; + let min_handle_size = scroll_style.handle_min_length; if handle_rect.size()[d] < min_handle_size { handle_rect = Rect::from_center_size( handle_rect.center(), @@ -871,21 +1059,76 @@ impl Prepared { } let visuals = if scrolling_enabled { - ui.style().interact(&response) + // Pick visuals based on interaction with the handle. + // Remember that the response is for the whole scroll bar! + let is_hovering_handle = response.hovered() + && ui.input(|i| { + i.pointer + .latest_pos() + .map_or(false, |p| handle_rect.contains(p)) + }); + let visuals = ui.visuals(); + if response.is_pointer_button_down_on() { + &visuals.widgets.active + } else if is_hovering_handle { + &visuals.widgets.hovered + } else { + &visuals.widgets.inactive + } } else { - &ui.style().visuals.widgets.inactive + &ui.visuals().widgets.inactive }; + let handle_opacity = if scroll_style.floating { + if response.hovered() || response.dragged() { + scroll_style.interact_handle_opacity + } else { + let is_hovering_outer_rect_t = ui.ctx().animate_bool( + id.with((d, "is_hovering_outer_rect")), + is_hovering_outer_rect, + ); + lerp( + scroll_style.dormant_handle_opacity + ..=scroll_style.active_handle_opacity, + is_hovering_outer_rect_t, + ) + } + } else { + 1.0 + }; + + let background_opacity = if scroll_style.floating { + if response.hovered() || response.dragged() { + scroll_style.interact_background_opacity + } else if is_hovering_outer_rect { + scroll_style.active_background_opacity + } else { + scroll_style.dormant_background_opacity + } + } else { + 1.0 + }; + + let handle_color = if scroll_style.foreground_color { + visuals.fg_stroke.color + } else { + visuals.bg_fill + }; + + // Background: ui.painter().add(epaint::Shape::rect_filled( outer_scroll_rect, visuals.rounding, - ui.visuals().extreme_bg_color, + ui.visuals() + .extreme_bg_color + .gamma_multiply(background_opacity), )); + // Handle: ui.painter().add(epaint::Shape::rect_filled( handle_rect, visuals.rounding, - visuals.bg_fill, + handle_color.gamma_multiply(handle_opacity), )); } } @@ -905,12 +1148,12 @@ impl Prepared { // Only has an effect if stick_to_end is enabled but we save in // state anyway so that entering sticky mode at an arbitrary time // has appropriate effect. - state.scroll_stuck_to_end = [ + state.scroll_stuck_to_end = Vec2b::new( (state.offset[0] == available_offset[0]) - || (self.stick_to_end[0] && available_offset[0] < 0.), + || (self.stick_to_end[0] && available_offset[0] < 0.0), (state.offset[1] == available_offset[1]) - || (self.stick_to_end[1] && available_offset[1] < 0.), - ]; + || (self.stick_to_end[1] && available_offset[1] < 0.0), + ); state.show_scroll = show_scroll_this_frame; state.content_is_too_large = content_is_too_large; @@ -920,10 +1163,3 @@ impl Prepared { (content_size, state) } } - -/// Width of a vertical scrollbar, or height of a horizontal scroll bar -fn max_scroll_bar_width_with_margin(ui: &Ui) -> f32 { - ui.spacing().scroll_bar_inner_margin - + ui.spacing().scroll_bar_width - + ui.spacing().scroll_bar_outer_margin -} diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index ca4f6190d24..af6f6b16c31 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -1,7 +1,9 @@ // WARNING: the code in here is horrible. It is a behemoth that needs breaking up into simpler parts. +use std::sync::Arc; + use crate::collapsing_header::CollapsingState; -use crate::{widget_text::WidgetTextGalley, *}; +use crate::*; use epaint::*; use super::*; @@ -24,6 +26,9 @@ use super::*; /// ``` /// /// The previous rectangle used by this window can be obtained through [`crate::Memory::area_rect()`]. +/// +/// Note that this is NOT a native OS window. +/// To create a new native OS window, use [`crate::Context::show_viewport_deferred`]. #[must_use = "You should call .show()"] pub struct Window<'open> { title: WidgetText, @@ -43,7 +48,7 @@ impl<'open> Window<'open> { /// If you need a changing title, you must call `window.id(…)` with a fixed id. pub fn new(title: impl Into) -> Self { let title = title.into().fallback_text_style(TextStyle::Heading); - let area = Area::new(Id::new(title.text())); + let area = Area::new(Id::new(title.text())).constrain(true); Self { title, open: None, @@ -61,6 +66,7 @@ impl<'open> Window<'open> { } /// Assign a unique id to the Window. Required if the title changes, or is shared with another window. + #[inline] pub fn id(mut self, id: Id) -> Self { self.area = self.area.id(id); self @@ -71,24 +77,28 @@ impl<'open> Window<'open> { /// * If `*open == false`, the window will not be visible. /// * If `*open == true`, the window will have a close button. /// * If the close button is pressed, `*open` will be set to `false`. + #[inline] pub fn open(mut self, open: &'open mut bool) -> Self { self.open = Some(open); self } /// If `false` the window will be grayed out and non-interactive. + #[inline] pub fn enabled(mut self, enabled: bool) -> Self { self.area = self.area.enabled(enabled); self } /// If `false` the window will be non-interactive. + #[inline] pub fn interactable(mut self, interactable: bool) -> Self { self.area = self.area.interactable(interactable); self } /// If `false` the window will be immovable. + #[inline] pub fn movable(mut self, movable: bool) -> Self { self.area = self.area.movable(movable); self @@ -96,6 +106,7 @@ impl<'open> Window<'open> { /// Usage: `Window::new(…).mutate(|w| w.resize = w.resize.auto_expand_width(true))` // TODO(emilk): I'm not sure this is a good interface for this. + #[inline] pub fn mutate(mut self, mutate: impl Fn(&mut Self)) -> Self { mutate(&mut self); self @@ -103,54 +114,103 @@ impl<'open> Window<'open> { /// Usage: `Window::new(…).resize(|r| r.auto_expand_width(true))` // TODO(emilk): I'm not sure this is a good interface for this. + #[inline] pub fn resize(mut self, mutate: impl Fn(Resize) -> Resize) -> Self { self.resize = mutate(self.resize); self } /// Change the background color, margins, etc. + #[inline] pub fn frame(mut self, frame: Frame) -> Self { self.frame = Some(frame); self } /// Set minimum width of the window. + #[inline] pub fn min_width(mut self, min_width: f32) -> Self { self.resize = self.resize.min_width(min_width); self } /// Set minimum height of the window. + #[inline] pub fn min_height(mut self, min_height: f32) -> Self { self.resize = self.resize.min_height(min_height); self } + /// Set minimum size of the window, equivalent to calling both `min_width` and `min_height`. + #[inline] + pub fn min_size(mut self, min_size: impl Into) -> Self { + self.resize = self.resize.min_size(min_size); + self + } + + /// Set maximum width of the window. + #[inline] + pub fn max_width(mut self, max_width: f32) -> Self { + self.resize = self.resize.max_width(max_width); + self + } + + /// Set maximum height of the window. + #[inline] + pub fn max_height(mut self, max_height: f32) -> Self { + self.resize = self.resize.max_height(max_height); + self + } + + /// Set maximum size of the window, equivalent to calling both `max_width` and `max_height`. + #[inline] + pub fn max_size(mut self, max_size: impl Into) -> Self { + self.resize = self.resize.max_size(max_size); + self + } + /// Set current position of the window. /// If the window is movable it is up to you to keep track of where it moved to! + #[inline] pub fn current_pos(mut self, current_pos: impl Into) -> Self { self.area = self.area.current_pos(current_pos); self } /// Set initial position of the window. + #[inline] pub fn default_pos(mut self, default_pos: impl Into) -> Self { self.area = self.area.default_pos(default_pos); self } /// Sets the window position and prevents it from being dragged around. + #[inline] pub fn fixed_pos(mut self, pos: impl Into) -> Self { self.area = self.area.fixed_pos(pos); self } /// Constrains this window to the screen bounds. + /// + /// To change the area to constrain to, use [`Self::constrain_to`]. + /// + /// Default: `true`. + #[inline] pub fn constrain(mut self, constrain: bool) -> Self { self.area = self.area.constrain(constrain); self } + /// Constrain the movement of the window to the given rectangle. + /// + /// For instance: `.constrain_to(ctx.screen_rect())`. + #[inline] + pub fn constrain_to(mut self, constrain_rect: Rect) -> Self { + self.area = self.area.constrain_to(constrain_rect); + self + } + /// Where the "root" of the window is. /// /// For instance, if you set this to [`Align2::RIGHT_TOP`] @@ -158,6 +218,7 @@ impl<'open> Window<'open> { /// corner of the window. /// /// Default: [`Align2::LEFT_TOP`]. + #[inline] pub fn pivot(mut self, pivot: Align2) -> Self { self.area = self.area.pivot(pivot); self @@ -174,36 +235,42 @@ impl<'open> Window<'open> { /// Anchoring also makes the window immovable. /// /// It is an error to set both an anchor and a position. + #[inline] pub fn anchor(mut self, align: Align2, offset: impl Into) -> Self { self.area = self.area.anchor(align, offset); self } /// Set initial collapsed state of the window + #[inline] pub fn default_open(mut self, default_open: bool) -> Self { self.default_open = default_open; self } /// Set initial size of the window. + #[inline] pub fn default_size(mut self, default_size: impl Into) -> Self { self.resize = self.resize.default_size(default_size); self } /// Set initial width of the window. + #[inline] pub fn default_width(mut self, default_width: f32) -> Self { self.resize = self.resize.default_width(default_width); self } /// Set initial height of the window. + #[inline] pub fn default_height(mut self, default_height: f32) -> Self { self.resize = self.resize.default_height(default_height); self } /// Sets the window size and prevents it from being resized by dragging its edges. + #[inline] pub fn fixed_size(mut self, size: impl Into) -> Self { self.resize = self.resize.fixed_size(size); self @@ -223,13 +290,20 @@ impl<'open> Window<'open> { /// /// Note that even if you set this to `false` the window may still auto-resize. /// + /// You can set the window to only be resizable in one direction by using + /// e.g. `[true, false]` as the argument, + /// making the window only resizable in the x-direction. + /// /// Default is `true`. - pub fn resizable(mut self, resizable: bool) -> Self { + #[inline] + pub fn resizable(mut self, resizable: impl Into) -> Self { + let resizable = resizable.into(); self.resize = self.resize.resizable(resizable); self } /// Can the window be collapsed by clicking on its title? + #[inline] pub fn collapsible(mut self, collapsible: bool) -> Self { self.collapsible = collapsible; self @@ -237,6 +311,7 @@ impl<'open> Window<'open> { /// Show title bar on top of the window? /// If `false`, the window will not be collapsible nor have a close-button. + #[inline] pub fn title_bar(mut self, title_bar: bool) -> Self { self.with_title_bar = title_bar; self @@ -245,6 +320,7 @@ impl<'open> Window<'open> { /// Not resizable, just takes the size of its contents. /// Also disabled scrolling. /// Text will not wrap, but will instead make your window width expand. + #[inline] pub fn auto_sized(mut self) -> Self { self.resize = self.resize.auto_sized(); self.scroll = ScrollArea::neither(); @@ -252,18 +328,21 @@ impl<'open> Window<'open> { } /// Enable/disable horizontal/vertical scrolling. `false` by default. - pub fn scroll2(mut self, scroll: [bool; 2]) -> Self { + #[inline] + pub fn scroll2(mut self, scroll: impl Into) -> Self { self.scroll = self.scroll.scroll2(scroll); self } /// Enable/disable horizontal scrolling. `false` by default. + #[inline] pub fn hscroll(mut self, hscroll: bool) -> Self { self.scroll = self.scroll.hscroll(hscroll); self } /// Enable/disable vertical scrolling. `false` by default. + #[inline] pub fn vscroll(mut self, vscroll: bool) -> Self { self.scroll = self.scroll.vscroll(vscroll); self @@ -272,16 +351,11 @@ impl<'open> Window<'open> { /// Enable/disable scrolling on the window by dragging with the pointer. `true` by default. /// /// See [`ScrollArea::drag_to_scroll`] for more. + #[inline] pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self { self.scroll = self.scroll.drag_to_scroll(drag_to_scroll); self } - - /// Constrain the area up to which the window can be dragged. - pub fn drag_bounds(mut self, bounds: Rect) -> Self { - self.area = self.area.drag_bounds(bounds); - self - } } impl<'open> Window<'open> { @@ -313,11 +387,18 @@ impl<'open> Window<'open> { with_title_bar, } = self; - let frame = frame.unwrap_or_else(|| Frame::window(&ctx.style())); + let header_color = + frame.map_or_else(|| ctx.style().visuals.widgets.open.weak_bg_fill, |f| f.fill); + let mut window_frame = frame.unwrap_or_else(|| Frame::window(&ctx.style())); + // Keep the original inner margin for later use + let window_margin = window_frame.inner_margin; + let border_padding = window_frame.stroke.width / 2.0; + // Add border padding to the inner margin to prevent it from covering the contents + window_frame.inner_margin += border_padding; let is_explicitly_closed = matches!(open, Some(false)); let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible()); - area.show_open_close_animation(ctx, &frame, is_open); + area.show_open_close_animation(ctx, &window_frame, is_open); if !is_open { return None; @@ -332,58 +413,68 @@ impl<'open> Window<'open> { let is_collapsed = with_title_bar && !collapsing.is_open(); let possible = PossibleInteractions::new(&area, &resize, is_collapsed); - let area = area.movable(false); // We move it manually, or the area will move the window when we want to resize it - let resize = resize.resizable(false); // We move it manually + let resize = resize.resizable(false); // We resize it manually let mut resize = resize.id(resize_id); + let on_top = Some(area_layer_id) == ctx.top_layer_id(); let mut area = area.begin(ctx); - let title_content_spacing = 2.0 * ctx.style().spacing.item_spacing.y; - - // First interact (move etc) to avoid frame delay: - let last_frame_outer_rect = area.state().rect(); - let interaction = if possible.movable || possible.resizable() { - window_interaction( - ctx, - possible, - area_layer_id, - area_id.with("frame_resize"), - last_frame_outer_rect, - ) - .and_then(|window_interaction| { - // Calculate roughly how much larger the window size is compared to the inner rect - let title_bar_height = if with_title_bar { - let style = ctx.style(); - ctx.fonts(|f| title.font_height(f, &style)) + title_content_spacing - } else { - 0.0 - }; - let margins = frame.outer_margin.sum() - + frame.inner_margin.sum() - + vec2(0.0, title_bar_height); - - interact( - window_interaction, - ctx, - margins, - area_layer_id, - &mut area, - resize_id, - ) - }) + // 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(); + let spacing = window_margin.top + window_margin.bottom; + let height = ctx.fonts(|f| title.font_height(f, &style)) + spacing; + window_frame.rounding.ne = window_frame.rounding.ne.clamp(0.0, height / 2.0); + window_frame.rounding.nw = window_frame.rounding.nw.clamp(0.0, height / 2.0); + (height, spacing) } else { - None + (0.0, 0.0) }; - let hover_interaction = resize_hover(ctx, possible, area_layer_id, last_frame_outer_rect); + + { + // Prevent window from becoming larger than the constraint rect and/or screen rect. + let screen_rect = ctx.screen_rect(); + let max_rect = area.constrain_rect().unwrap_or(screen_rect); + let max_width = max_rect.width(); + let max_height = max_rect.height() - title_bar_height; + resize.max_size.x = resize.max_size.x.min(max_width); + resize.max_size.y = resize.max_size.y.min(max_height); + } + + // 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 margins = window_frame.outer_margin.sum() + + window_frame.inner_margin.sum() + + vec2(0.0, title_bar_height); + + resize_response( + resize_interaction, + ctx, + margins, + area_layer_id, + &mut area, + resize_id, + ); let mut area_content_ui = area.content_ui(ctx); let content_inner = { // BEGIN FRAME -------------------------------- - let frame_stroke = frame.stroke; - let mut frame = frame.begin(&mut area_content_ui); + 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 = show_title_bar( &mut frame.content_ui, @@ -398,14 +489,16 @@ impl<'open> Window<'open> { None }; - let (content_inner, content_response) = collapsing + // 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| { - resize.show(ui, |ui| { - if title_bar.is_some() { - ui.add_space(title_content_spacing); - } + // Restore item spacing for the content + ui.spacing_mut().item_spacing.y = item_spacing.y; - if scroll.has_any_bar() { + resize.show(ui, |ui| { + if scroll.is_any_scroll_enabled() { scroll.show(ui, add_contents).inner } else { add_contents(ui) @@ -415,14 +508,52 @@ impl<'open> Window<'open> { .map_or((None, None), |ir| (Some(ir.inner), Some(ir.response))); let outer_rect = frame.end(&mut area_content_ui).rect; - paint_resize_corner(&mut area_content_ui, &possible, outer_rect, frame_stroke); + paint_resize_corner( + &area_content_ui, + &possible, + outer_rect, + frame_stroke, + window_frame.rounding, + ); // END FRAME -------------------------------- if let Some(title_bar) = title_bar { + let mut title_rect = Rect::from_min_size( + outer_rect.min + vec2(border_padding, border_padding), + Vec2 { + x: outer_rect.size().x - border_padding * 2.0, + y: title_bar_height, + }, + ); + + title_rect = area_content_ui.painter().round_rect_to_pixels(title_rect); + + if on_top && area_content_ui.visuals().window_highlight_topmost { + let mut round = window_frame.rounding; + + // Eliminate the rounding gap between the title bar and the window frame + round -= border_padding; + + if !is_collapsed { + round.se = 0.0; + round.sw = 0.0; + } + + area_content_ui.painter().set( + *where_to_put_header_background, + RectShape::filled(title_rect, round, header_color), + ); + }; + + // Fix title bar separator line position + if let Some(response) = &mut content_response { + response.rect.min.y = outer_rect.min.y + title_bar_height + border_padding; + } + title_bar.ui( &mut area_content_ui, - outer_rect, + title_rect, &content_response, open, &mut collapsing, @@ -432,33 +563,11 @@ impl<'open> Window<'open> { collapsing.store(ctx); - if let Some(interaction) = interaction { - paint_frame_interaction( - &mut area_content_ui, - outer_rect, - interaction, - ctx.style().visuals.widgets.active, - ); - } else if let Some(hover_interaction) = hover_interaction { - if ctx.input(|i| i.pointer.has_pointer()) { - paint_frame_interaction( - &mut area_content_ui, - outer_rect, - hover_interaction, - ctx.style().visuals.widgets.hovered, - ); - } - } + paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction); + content_inner }; - { - let pos = ctx - .constrain_window_rect_to_area(area.state().rect(), area.drag_bounds()) - .left_top(); - area.state_mut().set_left_top_pos(pos); - } - let full_response = area.end(ctx, area_content_ui); let inner_response = InnerResponse { @@ -470,35 +579,59 @@ impl<'open> Window<'open> { } fn paint_resize_corner( - ui: &mut Ui, + ui: &Ui, possible: &PossibleInteractions, outer_rect: Rect, - stroke: Stroke, + stroke: impl Into, + rounding: impl Into, ) { - let corner = if possible.resize_right && possible.resize_bottom { - Align2::RIGHT_BOTTOM + let stroke = stroke.into(); + let rounding = rounding.into(); + let (corner, radius) = if possible.resize_right && possible.resize_bottom { + (Align2::RIGHT_BOTTOM, rounding.se) } else if possible.resize_left && possible.resize_bottom { - Align2::LEFT_BOTTOM + (Align2::LEFT_BOTTOM, rounding.sw) } else if possible.resize_left && possible.resize_top { - Align2::LEFT_TOP + (Align2::LEFT_TOP, rounding.nw) } else if possible.resize_right && possible.resize_top { - Align2::RIGHT_TOP + (Align2::RIGHT_TOP, rounding.ne) } else { - return; + // We're not in two directions, but it is still nice to tell the user + // we're resizable by painting the resize corner in the expected place + // (i.e. for windows only resizable in one direction): + if possible.resize_right || possible.resize_bottom { + (Align2::RIGHT_BOTTOM, rounding.se) + } else if possible.resize_left || possible.resize_bottom { + (Align2::LEFT_BOTTOM, rounding.sw) + } else if possible.resize_left || possible.resize_top { + (Align2::LEFT_TOP, rounding.nw) + } else if possible.resize_right || possible.resize_top { + (Align2::RIGHT_TOP, rounding.ne) + } else { + return; + } }; + // Adjust the corner offset to accommodate the stroke width and window rounding + let offset = if radius <= 2.0 && stroke.width < 2.0 { + 2.0 + } else { + // The corner offset is calculated to make the corner appear to be in the correct position + (2.0_f32.sqrt() * (1.0 + radius + stroke.width / 2.0) - radius) + * 45.0_f32.to_radians().cos() + }; let corner_size = Vec2::splat(ui.visuals().resize_corner_size); let corner_rect = corner.align_size_within_rect(corner_size, outer_rect); - let corner_rect = corner_rect.translate(-2.0 * corner.to_sign()); // move away from corner - crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke, corner); + let corner_rect = corner_rect.translate(-offset * corner.to_sign()); // move away from corner + crate::resize::paint_resize_corner_with_style(ui, &corner_rect, stroke.color, corner); } // ---------------------------------------------------------------------------- +/// Which sides can be resized? #[derive(Clone, Copy, Debug)] struct PossibleInteractions { - movable: bool, - // Which sides can we drag to resize? + // Which sides can we drag to resize or move? resize_left: bool, resize_right: bool, resize_top: bool, @@ -508,14 +641,15 @@ struct PossibleInteractions { impl PossibleInteractions { fn new(area: &Area, resize: &Resize, is_collapsed: bool) -> Self { let movable = area.is_enabled() && area.is_movable(); - let resizable = area.is_enabled() && resize.is_resizable() && !is_collapsed; + let resizable = resize + .is_resizable() + .and(area.is_enabled() && !is_collapsed); let pivot = area.get_pivot(); Self { - movable, - resize_left: resizable && (movable || pivot.x() != Align::LEFT), - resize_right: resizable && (movable || pivot.x() != Align::RIGHT), - resize_top: resizable && (movable || pivot.y() != Align::TOP), - resize_bottom: resizable && (movable || pivot.y() != Align::BOTTOM), + resize_left: resizable.x && (movable || pivot.x() != Align::LEFT), + resize_right: resizable.x && (movable || pivot.x() != Align::RIGHT), + resize_top: resizable.y && (movable || pivot.y() != Align::TOP), + resize_bottom: resizable.y && (movable || pivot.y() != Align::BOTTOM), } } @@ -524,248 +658,266 @@ impl PossibleInteractions { } } -/// Either a move or resize +/// Resizing the window edges. #[derive(Clone, Copy, Debug)] -pub(crate) struct WindowInteraction { - pub(crate) area_layer_id: LayerId, - pub(crate) start_rect: Rect, - pub(crate) left: bool, - pub(crate) right: bool, - pub(crate) top: bool, - pub(crate) bottom: bool, +struct ResizeInteraction { + start_rect: Rect, + left: SideResponse, + right: SideResponse, + top: SideResponse, + bottom: SideResponse, +} + +/// A minitature version of `Response`, for each side of the window. +#[derive(Clone, Copy, Debug, Default)] +struct SideResponse { + hover: bool, + drag: bool, +} + +impl SideResponse { + pub fn any(&self) -> bool { + self.hover || self.drag + } +} + +impl std::ops::BitOrAssign for SideResponse { + fn bitor_assign(&mut self, rhs: Self) { + *self = Self { + hover: self.hover || rhs.hover, + drag: self.drag || rhs.drag, + }; + } } -impl WindowInteraction { +impl ResizeInteraction { pub fn set_cursor(&self, ctx: &Context) { - if (self.left && self.top) || (self.right && self.bottom) { + let left = self.left.any(); + let right = self.right.any(); + let top = self.top.any(); + let bottom = self.bottom.any(); + + if (left && top) || (right && bottom) { ctx.set_cursor_icon(CursorIcon::ResizeNwSe); - } else if (self.right && self.top) || (self.left && self.bottom) { + } else if (right && top) || (left && bottom) { ctx.set_cursor_icon(CursorIcon::ResizeNeSw); - } else if self.left || self.right { + } else if left || right { ctx.set_cursor_icon(CursorIcon::ResizeHorizontal); - } else if self.bottom || self.top { + } else if bottom || top { ctx.set_cursor_icon(CursorIcon::ResizeVertical); } } - pub fn is_resize(&self) -> bool { - self.left || self.right || self.top || self.bottom + pub fn any_hovered(&self) -> bool { + self.left.hover || self.right.hover || self.top.hover || self.bottom.hover + } + + pub fn any_dragged(&self) -> bool { + self.left.drag || self.right.drag || self.top.drag || self.bottom.drag } } -fn interact( - window_interaction: WindowInteraction, +fn resize_response( + resize_interaction: ResizeInteraction, ctx: &Context, margins: Vec2, area_layer_id: LayerId, area: &mut area::Prepared, resize_id: Id, -) -> Option { - let new_rect = move_and_resize_window(ctx, &window_interaction)?; - let new_rect = ctx.round_rect_to_pixels(new_rect); +) { + let Some(new_rect) = move_and_resize_window(ctx, &resize_interaction) else { + return; + }; + let mut new_rect = ctx.round_rect_to_pixels(new_rect); - let new_rect = ctx.constrain_window_rect_to_area(new_rect, area.drag_bounds()); + if area.constrain() { + new_rect = ctx.constrain_window_rect_to_area(new_rect, area.constrain_rect()); + } // TODO(emilk): add this to a Window state instead as a command "move here next frame" area.state_mut().set_left_top_pos(new_rect.left_top()); - if window_interaction.is_resize() { + if resize_interaction.any_dragged() { if let Some(mut state) = resize::State::load(ctx, resize_id) { state.requested_size = Some(new_rect.size() - margins); state.store(ctx, resize_id); } } - ctx.memory_mut(|mem| mem.areas.move_to_top(area_layer_id)); - Some(window_interaction) + ctx.memory_mut(|mem| mem.areas_mut().move_to_top(area_layer_id)); } -fn move_and_resize_window(ctx: &Context, window_interaction: &WindowInteraction) -> Option { - window_interaction.set_cursor(ctx); - - // Only move/resize windows with primary mouse button: - if !ctx.input(|i| i.pointer.primary_down()) { +fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Option { + if !interaction.any_dragged() { return None; } let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?; - let mut rect = window_interaction.start_rect; // prevent drift + let mut rect = interaction.start_rect; // prevent drift - if window_interaction.is_resize() { - if window_interaction.left { - rect.min.x = ctx.round_to_pixel(pointer_pos.x); - } else if window_interaction.right { - rect.max.x = ctx.round_to_pixel(pointer_pos.x); - } + if interaction.left.drag { + rect.min.x = ctx.round_to_pixel(pointer_pos.x); + } else if interaction.right.drag { + rect.max.x = ctx.round_to_pixel(pointer_pos.x); + } - if window_interaction.top { - rect.min.y = ctx.round_to_pixel(pointer_pos.y); - } else if window_interaction.bottom { - rect.max.y = ctx.round_to_pixel(pointer_pos.y); - } - } else { - // Movement. - - // We do window interaction first (to avoid frame delay), - // but we want anything interactive in the window (e.g. slider) to steal - // the drag from us. It is therefor important not to move the window the first frame, - // but instead let other widgets to the steal. HACK. - if !ctx.input(|i| i.pointer.any_pressed()) { - let press_origin = ctx.input(|i| i.pointer.press_origin())?; - let delta = pointer_pos - press_origin; - rect = rect.translate(delta); - } + if interaction.top.drag { + rect.min.y = ctx.round_to_pixel(pointer_pos.y); + } else if interaction.bottom.drag { + rect.max.y = ctx.round_to_pixel(pointer_pos.y); } Some(rect) } -/// Returns `Some` if there is a move or resize -fn window_interaction( +fn resize_interaction( ctx: &Context, possible: PossibleInteractions, - area_layer_id: LayerId, - id: Id, + layer_id: LayerId, rect: Rect, -) -> Option { - { - let drag_id = ctx.memory(|mem| mem.interaction.drag_id); - - if drag_id.is_some() && drag_id != Some(id) { - return None; - } +) -> ResizeInteraction { + if !possible.resizable() { + return ResizeInteraction { + start_rect: rect, + left: Default::default(), + right: Default::default(), + top: Default::default(), + bottom: Default::default(), + }; } - let mut window_interaction = ctx.memory(|mem| mem.window_interaction); - - if window_interaction.is_none() { - if let Some(hover_window_interaction) = resize_hover(ctx, possible, area_layer_id, rect) { - hover_window_interaction.set_cursor(ctx); - if ctx.input(|i| i.pointer.any_pressed() && i.pointer.primary_down()) { - ctx.memory_mut(|mem| { - mem.interaction.drag_id = Some(id); - mem.interaction.drag_is_window = true; - window_interaction = Some(hover_window_interaction); - mem.window_interaction = window_interaction; - }); - } + let is_dragging = |rect, id| { + let response = ctx.create_widget(WidgetRect { + layer_id, + id, + rect, + interact_rect: rect, + sense: Sense::drag(), + enabled: true, + }); + SideResponse { + hover: response.hovered(), + drag: response.dragged(), } - } + }; - if let Some(window_interaction) = window_interaction { - let is_active = ctx.memory_mut(|mem| mem.interaction.drag_id == Some(id)); + let id = Id::new(layer_id).with("edge_drag"); - if is_active && window_interaction.area_layer_id == area_layer_id { - return Some(window_interaction); - } - } + let side_grab_radius = ctx.style().interaction.resize_grab_radius_side; + let corner_grab_radius = ctx.style().interaction.resize_grab_radius_corner; - None -} + let corner_rect = + |center: Pos2| Rect::from_center_size(center, Vec2::splat(2.0 * corner_grab_radius)); -fn resize_hover( - ctx: &Context, - possible: PossibleInteractions, - area_layer_id: LayerId, - rect: Rect, -) -> Option { - let pointer = ctx.input(|i| i.pointer.interact_pos())?; + // What are we dragging/hovering? + let [mut left, mut right, mut top, mut bottom] = [SideResponse::default(); 4]; - if ctx.input(|i| i.pointer.any_down() && !i.pointer.any_pressed()) { - return None; // already dragging (something) - } + // ---------------------------------------- + // Check sides first, so that corners are on top, covering the sides (i.e. corners have priority) - if let Some(top_layer_id) = ctx.layer_id_at(pointer) { - if top_layer_id != area_layer_id && top_layer_id.order != Order::Background { - return None; // Another window is on top here - } + if possible.resize_right { + let response = is_dragging( + Rect::from_min_max(rect.right_top(), rect.right_bottom()).expand(side_grab_radius), + id.with("right"), + ); + right |= response; + } + if possible.resize_left { + let response = is_dragging( + Rect::from_min_max(rect.left_top(), rect.left_bottom()).expand(side_grab_radius), + id.with("left"), + ); + left |= response; + } + if possible.resize_bottom { + let response = is_dragging( + Rect::from_min_max(rect.left_bottom(), rect.right_bottom()).expand(side_grab_radius), + id.with("bottom"), + ); + bottom |= response; + } + if possible.resize_top { + let response = is_dragging( + Rect::from_min_max(rect.left_top(), rect.right_top()).expand(side_grab_radius), + id.with("top"), + ); + top |= response; } - if ctx.memory(|mem| mem.interaction.drag_interest) { - // Another widget will become active if we drag here - return None; + // ---------------------------------------- + // Now check corners: + + if possible.resize_right && possible.resize_bottom { + let response = is_dragging(corner_rect(rect.right_bottom()), id.with("right_bottom")); + right |= response; + bottom |= response; } - let side_grab_radius = ctx.style().interaction.resize_grab_radius_side; - let corner_grab_radius = ctx.style().interaction.resize_grab_radius_corner; - if !rect.expand(side_grab_radius).contains(pointer) { - return None; + if possible.resize_right && possible.resize_top { + let response = is_dragging(corner_rect(rect.right_top()), id.with("right_top")); + right |= response; + top |= response; } - let mut left = possible.resize_left && (rect.left() - pointer.x).abs() <= side_grab_radius; - let mut right = possible.resize_right && (rect.right() - pointer.x).abs() <= side_grab_radius; - let mut top = possible.resize_top && (rect.top() - pointer.y).abs() <= side_grab_radius; - let mut bottom = - possible.resize_bottom && (rect.bottom() - pointer.y).abs() <= side_grab_radius; - - if possible.resize_right - && possible.resize_bottom - && rect.right_bottom().distance(pointer) < corner_grab_radius - { - right = true; - bottom = true; - } - if possible.resize_right - && possible.resize_top - && rect.right_top().distance(pointer) < corner_grab_radius - { - right = true; - top = true; - } - if possible.resize_left - && possible.resize_top - && rect.left_top().distance(pointer) < corner_grab_radius - { - left = true; - top = true; - } - if possible.resize_left - && possible.resize_bottom - && rect.left_bottom().distance(pointer) < corner_grab_radius - { - left = true; - bottom = true; - } - - let any_resize = left || right || top || bottom; - - if !any_resize && !possible.movable { - return None; + if possible.resize_left && possible.resize_bottom { + let response = is_dragging(corner_rect(rect.left_bottom()), id.with("left_bottom")); + left |= response; + bottom |= response; } - if any_resize || possible.movable { - Some(WindowInteraction { - area_layer_id, - start_rect: rect, - left, - right, - top, - bottom, - }) - } else { - None + if possible.resize_left && possible.resize_top { + let response = is_dragging(corner_rect(rect.left_top()), id.with("left_top")); + left |= response; + top |= response; } + + let interaction = ResizeInteraction { + start_rect: rect, + left, + right, + top, + bottom, + }; + interaction.set_cursor(ctx); + interaction } /// Fill in parts of the window frame when we resize by dragging that part -fn paint_frame_interaction( - ui: &mut Ui, - rect: Rect, - interaction: WindowInteraction, - visuals: style::WidgetVisuals, -) { +fn paint_frame_interaction(ui: &Ui, rect: Rect, interaction: ResizeInteraction) { use epaint::tessellator::path::add_circle_quadrant; + let visuals = if interaction.any_dragged() { + ui.style().visuals.widgets.active + } else if interaction.any_hovered() { + ui.style().visuals.widgets.hovered + } else { + return; + }; + + let [left, right, top, bottom]: [bool; 4]; + + if interaction.any_dragged() { + left = interaction.left.drag; + right = interaction.right.drag; + top = interaction.top.drag; + bottom = interaction.bottom.drag; + } else { + left = interaction.left.hover; + right = interaction.right.hover; + top = interaction.top.hover; + bottom = interaction.bottom.hover; + } + let rounding = ui.visuals().window_rounding; let Rect { min, max } = rect; let mut points = Vec::new(); - if interaction.right && !interaction.bottom && !interaction.top { + if right && !bottom && !top { points.push(pos2(max.x, min.y + rounding.ne)); points.push(pos2(max.x, max.y - rounding.se)); } - if interaction.right && interaction.bottom { + if right && bottom { points.push(pos2(max.x, min.y + rounding.ne)); points.push(pos2(max.x, max.y - rounding.se)); add_circle_quadrant( @@ -775,11 +927,11 @@ fn paint_frame_interaction( 0.0, ); } - if interaction.bottom { + if bottom { points.push(pos2(max.x - rounding.se, max.y)); points.push(pos2(min.x + rounding.sw, max.y)); } - if interaction.left && interaction.bottom { + if left && bottom { add_circle_quadrant( &mut points, pos2(min.x + rounding.sw, max.y - rounding.sw), @@ -787,11 +939,11 @@ fn paint_frame_interaction( 1.0, ); } - if interaction.left { + if left { points.push(pos2(min.x, max.y - rounding.sw)); points.push(pos2(min.x, min.y + rounding.nw)); } - if interaction.left && interaction.top { + if left && top { add_circle_quadrant( &mut points, pos2(min.x + rounding.nw, min.y + rounding.nw), @@ -799,11 +951,11 @@ fn paint_frame_interaction( 2.0, ); } - if interaction.top { + if top { points.push(pos2(min.x + rounding.nw, min.y)); points.push(pos2(max.x - rounding.ne, min.y)); } - if interaction.right && interaction.top { + if right && top { add_circle_quadrant( &mut points, pos2(max.x - rounding.ne, min.y + rounding.ne), @@ -823,7 +975,7 @@ struct TitleBar { id: Id, /// Prepared text in the title - title_galley: WidgetTextGalley, + title_galley: Arc, /// Size of the title bar in a collapsed state (if window is collapsible), /// which includes all necessary space for showing the expand button, the @@ -922,20 +1074,23 @@ impl TitleBar { let full_top_rect = Rect::from_x_y_ranges(self.rect.x_range(), self.min_rect.y_range()); let text_pos = emath::align::center_size_in_rect(self.title_galley.size(), full_top_rect).left_top(); - let text_pos = text_pos - self.title_galley.galley().rect.min.to_vec2(); + let text_pos = text_pos - self.title_galley.rect.min.to_vec2(); let text_pos = text_pos - 1.5 * Vec2::Y; // HACK: center on x-height of text (looks better) - self.title_galley.paint_with_fallback_color( - ui.painter(), + ui.painter().galley( text_pos, + self.title_galley.clone(), ui.visuals().text_color(), ); if let Some(content_response) = &content_response { // paint separator between title and content: - let y = content_response.rect.top() + ui.spacing().item_spacing.y * 0.5; + let y = content_response.rect.top(); // let y = lerp(self.rect.bottom()..=content_response.rect.top(), 0.5); let stroke = ui.visuals().widgets.noninteractive.bg_stroke; - ui.painter().hline(outer_rect.x_range(), y, stroke); + // Workaround: To prevent border infringement, + // the 0.1 value should ideally be calculated using TessellationOptions::feathering_size_in_pixels + let x_range = outer_rect.x_range().shrink(0.1); + ui.painter().hline(x_range, y, stroke); } // Don't cover the close- and collapse buttons: diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index d8d5a0f5af2..ea61cd7d034 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1,20 +1,38 @@ -// #![warn(missing_docs)] -use std::sync::Arc; +#![warn(missing_docs)] // Let's keep `Context` well-documented. + +use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration}; + +use epaint::{ + emath::TSTransform, mutex::*, stats::*, text::Fonts, util::OrderedFloat, TessellationOptions, *, +}; use crate::{ - animation_manager::AnimationManager, data::output::PlatformOutput, frame_state::FrameState, - input_state::*, layers::GraphicLayers, memory::Options, os::OperatingSystem, - output::FullOutput, util::IdTypeMap, TextureHandle, *, + animation_manager::AnimationManager, + data::output::PlatformOutput, + frame_state::FrameState, + input_state::*, + layers::GraphicLayers, + load::{Bytes, Loaders, SizedTexture}, + memory::Options, + os::OperatingSystem, + output::FullOutput, + util::IdTypeMap, + viewport::ViewportClass, + TextureHandle, ViewportCommand, *, }; -use epaint::{mutex::*, stats::*, text::Fonts, TessellationOptions, *}; + +use self::{hit_test::WidgetHits, interaction::InteractionSnapshot}; /// Information given to the backend about when it is time to repaint the ui. /// /// This is given in the callback set by [`Context::set_request_repaint_callback`]. #[derive(Clone, Copy, Debug)] pub struct RequestRepaintInfo { + /// This is used to specify what viewport that should repaint. + pub viewport_id: ViewportId, + /// Repaint after this duration. If zero, repaint as soon as possible. - pub after: std::time::Duration, + pub delay: Duration, /// The current frame number. /// @@ -25,6 +43,12 @@ pub struct RequestRepaintInfo { // ---------------------------------------------------------------------------- +thread_local! { + static IMMEDIATE_VIEWPORT_RENDERER: RefCell>> = Default::default(); +} + +// ---------------------------------------------------------------------------- + struct WrappedTextureManager(Arc>); impl Default for WrappedTextureManager { @@ -45,157 +69,419 @@ impl Default for WrappedTextureManager { // ---------------------------------------------------------------------------- -/// Logic related to repainting the ui. -struct Repaint { - /// The current frame number. - /// - /// Incremented at the end of each frame. - frame_nr: u64, - - /// The duration backend will poll for new events, before forcing another egui update - /// even if there's no new events. - /// - /// Also used to suppress multiple calls to the repaint callback during the same frame. - repaint_after: std::time::Duration, +/// Generic event callback. +pub type ContextCallback = Arc; - /// While positive, keep requesting repaints. Decrement at the end of each frame. - repaint_requests: u32, - request_repaint_callback: Option>, +#[derive(Clone)] +struct NamedContextCallback { + debug_name: &'static str, + callback: ContextCallback, +} - requested_repaint_last_frame: bool, +/// Callbacks that users can register +#[derive(Clone, Default)] +struct Plugins { + pub on_begin_frame: Vec, + pub on_end_frame: Vec, } -impl Default for Repaint { - fn default() -> Self { - Self { - frame_nr: 0, - repaint_after: std::time::Duration::from_millis(100), - // Start with painting an extra frame to compensate for some widgets - // that take two frames before they "settle": - repaint_requests: 1, - request_repaint_callback: None, - requested_repaint_last_frame: false, +impl Plugins { + fn call(ctx: &Context, _cb_name: &str, callbacks: &[NamedContextCallback]) { + crate::profile_scope!("plugins", _cb_name); + for NamedContextCallback { + debug_name: _name, + callback, + } in callbacks + { + crate::profile_scope!("plugin", _name); + (callback)(ctx); } } -} -impl Repaint { - fn request_repaint(&mut self) { - self.request_repaint_after(std::time::Duration::ZERO); + fn on_begin_frame(&self, ctx: &Context) { + Self::call(ctx, "on_begin_frame", &self.on_begin_frame); } - fn request_repaint_after(&mut self, after: std::time::Duration) { - if after == std::time::Duration::ZERO { - // Do a few extra frames to let things settle. - // This is a bit of a hack, and we don't support it for `repaint_after` callbacks yet. - self.repaint_requests = 2; - } + fn on_end_frame(&self, ctx: &Context) { + Self::call(ctx, "on_end_frame", &self.on_end_frame); + } +} + +// ---------------------------------------------------------------------------- + +/// Repaint-logic +impl ContextImpl { + /// This is where we update the repaint logic. + fn begin_frame_repaint_logic(&mut self, viewport_id: ViewportId) { + let viewport = self.viewports.entry(viewport_id).or_default(); + + std::mem::swap( + &mut viewport.repaint.prev_causes, + &mut viewport.repaint.causes, + ); + viewport.repaint.causes.clear(); - // We only re-call the callback if we get a lower duration, - // otherwise it's already been covered by the previous callback. - if after < self.repaint_after { - self.repaint_after = after; + viewport.repaint.prev_frame_paint_delay = viewport.repaint.repaint_delay; + if viewport.repaint.outstanding == 0 { + // We are repainting now, so we can wait a while for the next repaint. + viewport.repaint.repaint_delay = Duration::MAX; + } else { + viewport.repaint.repaint_delay = Duration::ZERO; + viewport.repaint.outstanding -= 1; if let Some(callback) = &self.request_repaint_callback { - let info = RequestRepaintInfo { - after, - current_frame_nr: self.frame_nr, - }; - (callback)(info); + (callback)(RequestRepaintInfo { + viewport_id, + delay: Duration::ZERO, + current_frame_nr: viewport.repaint.frame_nr, + }); } } } - fn start_frame(&mut self) { - // We are repainting; no need to reschedule a repaint unless the user asks for it again. - self.repaint_after = std::time::Duration::MAX; + fn request_repaint(&mut self, viewport_id: ViewportId, cause: RepaintCause) { + self.request_repaint_after(Duration::ZERO, viewport_id, cause); } - // returns how long to wait until repaint - fn end_frame(&mut self) -> std::time::Duration { - // if repaint_requests is greater than zero. just set the duration to zero for immediate - // repaint. if there's no repaint requests, then we can use the actual repaint_after instead. - let repaint_after = if self.repaint_requests > 0 { - self.repaint_requests -= 1; - std::time::Duration::ZERO + fn request_repaint_after( + &mut self, + delay: Duration, + viewport_id: ViewportId, + cause: RepaintCause, + ) { + let viewport = self.viewports.entry(viewport_id).or_default(); + + if delay == Duration::ZERO { + // Each request results in two repaints, just to give some things time to settle. + // This solves some corner-cases of missing repaints on frame-delayed responses. + viewport.repaint.outstanding = 1; } else { - self.repaint_after - }; - self.repaint_after = std::time::Duration::MAX; + // For non-zero delays, we only repaint once, because + // otherwise we would just schedule an immediate repaint _now_, + // which would then clear the delay and repaint again. + // Hovering a tooltip is a good example of a case where we want to repaint after a delay. + } + + viewport.repaint.causes.push(cause); - self.requested_repaint_last_frame = repaint_after.is_zero(); - self.frame_nr += 1; + // We save some CPU time by only calling the callback if we need to. + // If the new delay is greater or equal to the previous lowest, + // it means we have already called the callback, and don't need to do it again. + if delay < viewport.repaint.repaint_delay { + viewport.repaint.repaint_delay = delay; + + if let Some(callback) = &self.request_repaint_callback { + (callback)(RequestRepaintInfo { + viewport_id, + delay, + current_frame_nr: viewport.repaint.frame_nr, + }); + } + } + } + + #[must_use] + fn requested_immediate_repaint_prev_frame(&self, viewport_id: &ViewportId) -> bool { + self.viewports.get(viewport_id).map_or(false, |v| { + v.repaint.requested_immediate_repaint_prev_frame() + }) + } - repaint_after + #[must_use] + fn has_requested_repaint(&self, viewport_id: &ViewportId) -> bool { + self.viewports.get(viewport_id).map_or(false, |v| { + 0 < v.repaint.outstanding || v.repaint.repaint_delay < Duration::MAX + }) } } // ---------------------------------------------------------------------------- +/// State stored per viewport #[derive(Default)] -struct ContextImpl { - /// `None` until the start of the first frame. - fonts: Option, - memory: Memory, - animation_manager: AnimationManager, - tex_manager: WrappedTextureManager, +struct ViewportState { + /// The type of viewport. + /// + /// This will never be [`ViewportClass::Embedded`], + /// since those don't result in real viewports. + class: ViewportClass, - os: OperatingSystem, + /// The latest delta + builder: ViewportBuilder, + + /// The user-code that shows the GUI, used for deferred viewports. + /// + /// `None` for immediate viewports. + viewport_ui_cb: Option>, input: InputState, /// State that is collected during a frame and then cleared frame_state: FrameState, + /// Has this viewport been updated this frame? + used: bool, + + /// Written to during the frame. + widgets_this_frame: WidgetRects, + + /// Read + widgets_prev_frame: WidgetRects, + + /// State related to repaint scheduling. + repaint: ViewportRepaintInfo, + + // ---------------------- + // Updated at the start of the frame: + // + /// Which widgets are under the pointer? + hits: WidgetHits, + + /// What widgets are being interacted with this frame? + /// + /// Based on the widgets from last frame, and input in this frame. + interact_widgets: InteractionSnapshot, + + // ---------------------- // The output of a frame: + // graphics: GraphicLayers, + // Most of the things in `PlatformOutput` are not actually viewport dependent. output: PlatformOutput, + commands: Vec, +} + +/// What called [`Context::request_repaint`]? +#[derive(Clone, Debug)] +pub struct RepaintCause { + /// What file had the call that requested the repaint? + pub file: &'static str, + + /// What line number of the the call that requested the repaint? + pub line: u32, +} + +impl RepaintCause { + /// Capture the file and line number of the call site. + #[allow(clippy::new_without_default)] + #[track_caller] + pub fn new() -> Self { + let caller = Location::caller(); + Self { + file: caller.file(), + line: caller.line(), + } + } +} + +impl std::fmt::Display for RepaintCause { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.file, self.line) + } +} + +/// Per-viewport state related to repaint scheduling. +struct ViewportRepaintInfo { + /// Monotonically increasing counter. + frame_nr: u64, + + /// The duration which the backend will poll for new events + /// before forcing another egui update, even if there's no new events. + /// + /// Also used to suppress multiple calls to the repaint callback during the same frame. + /// + /// This is also returned in [`crate::ViewportOutput`]. + repaint_delay: Duration, + + /// While positive, keep requesting repaints. Decrement at the start of each frame. + outstanding: u8, + + /// What caused repaints during this frame? + causes: Vec, + + /// What triggered a repaint the previous frame? + /// (i.e: why are we updating now?) + prev_causes: Vec, + + /// What was the output of `repaint_delay` on the previous frame? + /// + /// If this was zero, we are repainting as quickly as possible + /// (as far as we know). + prev_frame_paint_delay: Duration, +} + +impl Default for ViewportRepaintInfo { + fn default() -> Self { + Self { + frame_nr: 0, + + // We haven't scheduled a repaint yet. + repaint_delay: Duration::MAX, + + // Let's run a couple of frames at the start, because why not. + outstanding: 1, + + causes: Default::default(), + prev_causes: Default::default(), + + prev_frame_paint_delay: Duration::MAX, + } + } +} + +impl ViewportRepaintInfo { + pub fn requested_immediate_repaint_prev_frame(&self) -> bool { + self.prev_frame_paint_delay == Duration::ZERO + } +} + +// ---------------------------------------------------------------------------- + +#[derive(Default)] +struct ContextImpl { + /// Since we could have multiple viewports across multiple monitors with + /// different `pixels_per_point`, we need a `Fonts` instance for each unique + /// `pixels_per_point`. + /// This is because the `Fonts` depend on `pixels_per_point` for the font atlas + /// as well as kerning, font sizes, etc. + fonts: std::collections::BTreeMap, Fonts>, + font_definitions: FontDefinitions, + + memory: Memory, + animation_manager: AnimationManager, + + plugins: Plugins, + + /// All viewports share the same texture manager and texture namespace. + /// + /// In all viewports, [`TextureId::default`] is special, and points to the font atlas. + /// The font-atlas texture _may_ be different across viewports, as they may have different + /// `pixels_per_point`, so we do special book-keeping for that. + /// See . + tex_manager: WrappedTextureManager, + + /// Set during the frame, becomes active at the start of the next frame. + new_zoom_factor: Option, + + os: OperatingSystem, + + /// How deeply nested are we? + viewport_stack: Vec, + + /// What is the last viewport rendered? + last_viewport: ViewportId, paint_stats: PaintStats, - repaint: Repaint, + request_repaint_callback: Option>, - /// Written to during the frame. - layer_rects_this_frame: ahash::HashMap>, + viewport_parents: ViewportIdMap, + viewports: ViewportIdMap, - /// Read - layer_rects_prev_frame: ahash::HashMap>, + embed_viewports: bool, #[cfg(feature = "accesskit")] is_accesskit_enabled: bool, #[cfg(feature = "accesskit")] accesskit_node_classes: accesskit::NodeClassSet, + + loaders: Arc, } impl ContextImpl { fn begin_frame_mut(&mut self, mut new_raw_input: RawInput) { - self.repaint.start_frame(); + let viewport_id = new_raw_input.viewport_id; + let parent_id = new_raw_input + .viewports + .get(&viewport_id) + .and_then(|v| v.parent) + .unwrap_or_default(); + let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent_id); + + let is_outermost_viewport = self.viewport_stack.is_empty(); // not necessarily root, just outermost immediate viewport + self.viewport_stack.push(ids); + + self.begin_frame_repaint_logic(viewport_id); + + let viewport = self.viewports.entry(viewport_id).or_default(); + + if is_outermost_viewport { + if let Some(new_zoom_factor) = self.new_zoom_factor.take() { + let ratio = self.memory.options.zoom_factor / new_zoom_factor; + self.memory.options.zoom_factor = new_zoom_factor; + + let input = &viewport.input; + // This is a bit hacky, but is required to avoid jitter: + let mut rect = input.screen_rect; + rect.min = (ratio * rect.min.to_vec2()).to_pos2(); + rect.max = (ratio * rect.max.to_vec2()).to_pos2(); + new_raw_input.screen_rect = Some(rect); + // We should really scale everything else in the input too, + // but the `screen_rect` is the most important part. + } + } + let native_pixels_per_point = new_raw_input + .viewport() + .native_pixels_per_point + .unwrap_or(1.0); + let pixels_per_point = self.memory.options.zoom_factor * native_pixels_per_point; - if let Some(new_pixels_per_point) = self.memory.new_pixels_per_point.take() { - new_raw_input.pixels_per_point = Some(new_pixels_per_point); + let all_viewport_ids: ViewportIdSet = self.all_viewport_ids(); - // This is a bit hacky, but is required to avoid jitter: - let ratio = self.input.pixels_per_point / new_pixels_per_point; - let mut rect = self.input.screen_rect; - rect.min = (ratio * rect.min.to_vec2()).to_pos2(); - rect.max = (ratio * rect.max.to_vec2()).to_pos2(); - new_raw_input.screen_rect = Some(rect); - } + let viewport = self.viewports.entry(self.viewport_id()).or_default(); - self.layer_rects_prev_frame = std::mem::take(&mut self.layer_rects_this_frame); + self.memory.begin_frame(&new_raw_input, &all_viewport_ids); - self.memory.begin_frame(&self.input, &new_raw_input); + viewport.input = std::mem::take(&mut viewport.input).begin_frame( + new_raw_input, + viewport.repaint.requested_immediate_repaint_prev_frame(), + pixels_per_point, + ); - self.input = std::mem::take(&mut self.input) - .begin_frame(new_raw_input, self.repaint.requested_repaint_last_frame); + let screen_rect = viewport.input.screen_rect; - self.frame_state.begin_frame(&self.input); + viewport.frame_state.begin_frame(screen_rect); - self.update_fonts_mut(); + { + let area_order = self.memory.areas().order_map(); + + let mut layers: Vec = viewport.widgets_prev_frame.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) + } + }); + + viewport.hits = if let Some(pos) = viewport.input.pointer.interact_pos() { + let interact_radius = self.memory.options.style.interaction.interact_radius; + + crate::hit_test::hit_test( + &viewport.widgets_prev_frame, + &layers, + &self.memory.layer_transforms, + pos, + interact_radius, + ) + } else { + WidgetHits::default() + }; + + viewport.interact_widgets = crate::interaction::interact( + &viewport.interact_widgets, + &viewport.widgets_prev_frame, + &viewport.hits, + &viewport.input, + self.memory.interaction_mut(), + ); + } // Ensure we register the background area so panels and background ui can catch clicks: - let screen_rect = self.input.screen_rect(); - self.memory.areas.set_state( + self.memory.areas_mut().set_state( LayerId::background(), containers::area::State { pivot_pos: screen_rect.left_top(), @@ -207,41 +493,66 @@ impl ContextImpl { #[cfg(feature = "accesskit")] if self.is_accesskit_enabled { + crate::profile_scope!("accesskit"); use crate::frame_state::AccessKitFrameState; let id = crate::accesskit_root_id(); let mut builder = accesskit::NodeBuilder::new(accesskit::Role::Window); - builder.set_transform(accesskit::Affine::scale( - self.input.pixels_per_point().into(), - )); + 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); - self.frame_state.accesskit_state = Some(AccessKitFrameState { + viewport.frame_state.accesskit_state = Some(AccessKitFrameState { node_builders, parent_stack: vec![id], }); } + + self.update_fonts_mut(); } /// Load fonts unless already loaded. fn update_fonts_mut(&mut self) { - let pixels_per_point = self.input.pixels_per_point(); - let max_texture_side = self.input.max_texture_side; + crate::profile_function!(); + + let input = &self.viewport().input; + let pixels_per_point = input.pixels_per_point(); + let max_texture_side = input.max_texture_side; if let Some(font_definitions) = self.memory.new_font_definitions.take() { - let fonts = Fonts::new(pixels_per_point, max_texture_side, font_definitions); - self.fonts = Some(fonts); + // New font definition loaded, so we need to reload all fonts. + self.fonts.clear(); + self.font_definitions = font_definitions; + #[cfg(feature = "log")] + log::debug!("Loading new font definitions"); } - let fonts = self.fonts.get_or_insert_with(|| { - let font_definitions = FontDefinitions::default(); - Fonts::new(pixels_per_point, max_texture_side, font_definitions) - }); + let mut is_new = false; + + let fonts = self + .fonts + .entry(pixels_per_point.into()) + .or_insert_with(|| { + #[cfg(feature = "log")] + log::trace!("Creating new Fonts for pixels_per_point={pixels_per_point}"); + + is_new = true; + crate::profile_scope!("Fonts::new"); + Fonts::new( + pixels_per_point, + max_texture_side, + self.font_definitions.clone(), + ) + }); - fonts.begin_frame(pixels_per_point, max_texture_side); + { + crate::profile_scope!("Fonts::begin_frame"); + fonts.begin_frame(pixels_per_point, max_texture_side); + } - if self.memory.options.preload_font_glyphs { + if is_new && self.memory.options.preload_font_glyphs { + crate::profile_scope!("preload_font_glyphs"); // Preload the most common characters for the most common fonts. - // This is not very important to do, but may a few GPU operations. + // 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() { fonts.lock().fonts.font(font_id).preload_common_characters(); } @@ -250,7 +561,12 @@ impl ContextImpl { #[cfg(feature = "accesskit")] fn accesskit_node_builder(&mut self, id: Id) -> &mut accesskit::NodeBuilder { - let state = self.frame_state.accesskit_state.as_mut().unwrap(); + let state = self + .viewport() + .frame_state + .accesskit_state + .as_mut() + .unwrap(); let builders = &mut state.node_builders; if let std::collections::hash_map::Entry::Vacant(entry) = builders.entry(id) { entry.insert(Default::default()); @@ -260,6 +576,45 @@ impl ContextImpl { } builders.get_mut(&id).unwrap() } + + fn pixels_per_point(&mut self) -> f32 { + self.viewport().input.pixels_per_point + } + + /// Return the `ViewportId` of the current viewport. + /// + /// For the root viewport this will return [`ViewportId::ROOT`]. + pub(crate) fn viewport_id(&self) -> ViewportId { + self.viewport_stack.last().copied().unwrap_or_default().this + } + + /// Return the `ViewportId` of his parent. + /// + /// For the root viewport this will return [`ViewportId::ROOT`]. + pub(crate) fn parent_viewport_id(&self) -> ViewportId { + let viewport_id = self.viewport_id(); + *self + .viewport_parents + .get(&viewport_id) + .unwrap_or(&ViewportId::ROOT) + } + + fn all_viewport_ids(&self) -> ViewportIdSet { + self.viewports + .keys() + .copied() + .chain([ViewportId::ROOT]) + .collect() + } + + /// The current active viewport + pub(crate) fn viewport(&mut self) -> &mut ViewportState { + self.viewports.entry(self.viewport_id()).or_default() + } + + fn viewport_for(&mut self, viewport_id: ViewportId) -> &mut ViewportState { + self.viewports.entry(viewport_id).or_default() + } } // ---------------------------------------------------------------------------- @@ -310,7 +665,7 @@ impl ContextImpl { /// }); /// }); /// handle_platform_output(full_output.platform_output); -/// let clipped_primitives = ctx.tessellate(full_output.shapes); // create triangles to paint +/// let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point); /// paint(full_output.textures_delta, clipped_primitives); /// } /// ``` @@ -324,24 +679,35 @@ impl std::fmt::Debug for Context { } impl std::cmp::PartialEq for Context { - fn eq(&self, other: &Context) -> bool { + fn eq(&self, other: &Self) -> bool { Arc::ptr_eq(&self.0, &other.0) } } impl Default for Context { fn default() -> Self { - Self(Arc::new(RwLock::new(ContextImpl::default()))) + let ctx_impl = ContextImpl { + embed_viewports: true, + ..Default::default() + }; + let ctx = Self(Arc::new(RwLock::new(ctx_impl))); + + // Register built-in plugins: + crate::debug_text::register(&ctx); + crate::text_selection::LabelSelectionState::register(&ctx); + crate::DragAndDrop::register(&ctx); + + ctx } } impl Context { - // Do read-only (shared access) transaction on Context + /// Do read-only (shared access) transaction on Context fn read(&self, reader: impl FnOnce(&ContextImpl) -> R) -> R { reader(&self.0.read()) } - // Do read-write (exclusive access) transaction on Context + /// Do read-write (exclusive access) transaction on Context fn write(&self, writer: impl FnOnce(&mut ContextImpl) -> R) -> R { writer(&mut self.0.write()) } @@ -369,7 +735,9 @@ impl Context { /// // handle full_output /// ``` #[must_use] - pub fn run(&self, new_input: RawInput, run_ui: impl FnOnce(&Context)) -> FullOutput { + pub fn run(&self, new_input: RawInput, run_ui: impl FnOnce(&Self)) -> FullOutput { + crate::profile_function!(); + self.begin_frame(new_input); run_ui(self); self.end_frame() @@ -393,6 +761,8 @@ impl Context { /// // handle full_output /// ``` pub fn begin_frame(&self, new_input: RawInput) { + crate::profile_function!(); + self.read(|ctx| ctx.plugins.clone()).on_begin_frame(self); self.write(|ctx| ctx.begin_frame_mut(new_input)); } } @@ -408,7 +778,7 @@ impl Context { /// ``` /// # let mut ctx = egui::Context::default(); /// ctx.input(|i| { - /// // ⚠️ Using `ctx` (even from other `Arc` reference) again here will lead to a dead-lock! + /// // ⚠️ Using `ctx` (even from other `Arc` reference) again here will lead to a deadlock! /// }); /// /// if let Some(pos) = ctx.input(|i| i.pointer.hover_pos()) { @@ -417,13 +787,25 @@ impl Context { /// ``` #[inline] pub fn input(&self, reader: impl FnOnce(&InputState) -> R) -> R { - self.read(move |ctx| reader(&ctx.input)) + self.write(move |ctx| reader(&ctx.viewport().input)) + } + + /// This will create a `InputState::default()` if there is no input state for that viewport + #[inline] + pub fn input_for(&self, id: ViewportId, reader: impl FnOnce(&InputState) -> R) -> R { + self.write(move |ctx| reader(&ctx.viewport_for(id).input)) } /// Read-write access to [`InputState`]. #[inline] pub fn input_mut(&self, writer: impl FnOnce(&mut InputState) -> R) -> R { - self.write(move |ctx| writer(&mut ctx.input)) + self.input_mut_for(self.viewport_id(), writer) + } + + /// This will create a `InputState::default()` if there is no input state for that viewport + #[inline] + pub fn input_mut_for(&self, id: ViewportId, writer: impl FnOnce(&mut InputState) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.viewport_for(id).input)) } /// Read-only access to [`Memory`]. @@ -452,8 +834,14 @@ impl Context { /// Read-write access to [`GraphicLayers`], where painted [`crate::Shape`]s are written to. #[inline] - pub(crate) fn graphics_mut(&self, writer: impl FnOnce(&mut GraphicLayers) -> R) -> R { - self.write(move |ctx| writer(&mut ctx.graphics)) + pub fn graphics_mut(&self, writer: impl FnOnce(&mut GraphicLayers) -> R) -> R { + self.write(move |ctx| writer(&mut ctx.viewport().graphics)) + } + + /// Read-only access to [`GraphicLayers`], where painted [`crate::Shape`]s are written to. + #[inline] + pub fn graphics(&self, reader: impl FnOnce(&GraphicLayers) -> R) -> R { + self.write(move |ctx| reader(&ctx.viewport().graphics)) } /// Read-only access to [`PlatformOutput`]. @@ -466,25 +854,25 @@ impl Context { /// ``` #[inline] pub fn output(&self, reader: impl FnOnce(&PlatformOutput) -> R) -> R { - self.read(move |ctx| reader(&ctx.output)) + self.write(move |ctx| reader(&ctx.viewport().output)) } /// Read-write access to [`PlatformOutput`]. #[inline] pub fn output_mut(&self, writer: impl FnOnce(&mut PlatformOutput) -> R) -> R { - self.write(move |ctx| writer(&mut ctx.output)) + self.write(move |ctx| writer(&mut ctx.viewport().output)) } /// Read-only access to [`FrameState`]. #[inline] pub(crate) fn frame_state(&self, reader: impl FnOnce(&FrameState) -> R) -> R { - self.read(move |ctx| reader(&ctx.frame_state)) + self.write(move |ctx| reader(&ctx.viewport().frame_state)) } /// Read-write access to [`FrameState`]. #[inline] pub(crate) fn frame_state_mut(&self, writer: impl FnOnce(&mut FrameState) -> R) -> R { - self.write(move |ctx| writer(&mut ctx.frame_state)) + self.write(move |ctx| writer(&mut ctx.viewport().frame_state)) } /// Read-only access to [`Fonts`]. @@ -493,21 +881,16 @@ impl Context { /// That's because since we don't know the proper `pixels_per_point` until then. #[inline] pub fn fonts(&self, reader: impl FnOnce(&Fonts) -> R) -> R { - self.read(move |ctx| { + self.write(move |ctx| { + let pixels_per_point = ctx.pixels_per_point(); reader( ctx.fonts - .as_ref() + .get(&pixels_per_point.into()) .expect("No fonts available until first call to Context::run()"), ) }) } - /// Read-write access to [`Fonts`]. - #[inline] - pub fn fonts_mut(&self, writer: impl FnOnce(&mut Option) -> R) -> R { - self.write(move |ctx| writer(&mut ctx.fonts)) - } - /// Read-only access to [`Options`]. #[inline] pub fn options(&self, reader: impl FnOnce(&Options) -> R) -> R { @@ -534,10 +917,6 @@ impl Context { ) -> R { self.write(move |ctx| writer(&mut ctx.memory.options.tessellation_options)) } -} - -impl Context { - // --------------------------------------------------------------------- /// If the given [`Id`] has been used previously the same frame at at different position, /// then an error will be printed on screen. @@ -559,19 +938,21 @@ impl Context { // it is ok to reuse the same ID for e.g. a frame around a widget, // or to check for interaction with the same widget twice: - if prev_rect.expand(0.1).contains_rect(new_rect) - || new_rect.expand(0.1).contains_rect(prev_rect) - { + let is_same_rect = prev_rect.expand(0.1).contains_rect(new_rect) + || new_rect.expand(0.1).contains_rect(prev_rect); + if is_same_rect { return; } let show_error = |widget_rect: Rect, text: String| { + let screen_rect = self.screen_rect(); + let text = format!("🔥 {text}"); let color = self.style().visuals.error_fg_color; let painter = self.debug_painter(); painter.rect_stroke(widget_rect, 0.0, (1.0, color)); - let below = widget_rect.bottom() + 32.0 < self.input(|i| i.screen_rect.bottom()); + let below = widget_rect.bottom() + 32.0 < screen_rect.bottom(); let text_rect = if below { painter.debug_text( @@ -621,244 +1002,197 @@ impl Context { // --------------------------------------------------------------------- - /// Use `ui.interact` instead + /// Create a widget and check for interaction. + /// + /// If this is not called, the widget doesn't exist. + /// + /// You should use [`Ui::interact`] instead. + /// + /// If the widget already exists, its state (sense, Rect, etc) will be updated. #[allow(clippy::too_many_arguments)] - pub(crate) fn interact( - &self, - clip_rect: Rect, - item_spacing: Vec2, - layer_id: LayerId, - id: Id, - rect: Rect, - sense: Sense, - enabled: bool, - ) -> Response { - let gap = 0.1; // Just to make sure we don't accidentally hover two things at once (a small eps should be sufficient). - - // Make it easier to click things: - let interact_rect = rect.expand2( - (0.5 * item_spacing - Vec2::splat(gap)) - .at_least(Vec2::splat(0.0)) - .at_most(Vec2::splat(5.0)), - ); + pub(crate) fn create_widget(&self, w: WidgetRect) -> Response { + // Remember this widget + self.write(|ctx| { + let viewport = ctx.viewport(); - // Respect clip rectangle when interacting - let interact_rect = clip_rect.intersect(interact_rect); - let mut hovered = self.rect_contains_pointer(layer_id, interact_rect); - - // This solves the problem of overlapping widgets. - // Whichever widget is added LAST (=on top) gets the input: - if interact_rect.is_positive() && sense.interactive() { - if self.style().debug.show_interactive_widgets { - Self::layer_painter(self, LayerId::debug()).rect( - interact_rect, - 0.0, - Color32::YELLOW.additive().linear_multiply(0.005), - Stroke::new(1.0, Color32::YELLOW.additive().linear_multiply(0.05)), - ); + // We add all widgets here, even non-interactive ones, + // because we need this list not only for checking for blocking widgets, + // but also to know when we have reached the widget we are checking for cover. + viewport.widgets_this_frame.insert(w.layer_id, w); + + if w.sense.focusable { + ctx.memory.interested_in_focus(w.id); } - let mut show_blocking_widget = None; + }); - self.write(|ctx| { - ctx.layer_rects_this_frame - .entry(layer_id) - .or_default() - .push((id, interact_rect)); - - if hovered { - let pointer_pos = ctx.input.pointer.interact_pos(); - if let Some(pointer_pos) = pointer_pos { - if let Some(rects) = ctx.layer_rects_prev_frame.get(&layer_id) { - for &(prev_id, prev_rect) in rects.iter().rev() { - if prev_id == id { - break; // there is no other interactive widget covering us at the pointer position. - } - if prev_rect.contains(pointer_pos) { - // Another interactive widget is covering us at the pointer position, - // so we aren't hovered. - - if ctx.memory.options.style.debug.show_blocking_widget { - // Store the rects to use them outside the write() call to - // avoid deadlock - show_blocking_widget = Some((interact_rect, prev_rect)); - } - - hovered = false; - break; - } - } - } - } - } - }); + if !w.enabled || !w.sense.focusable || !w.layer_id.allow_interaction() { + // Not interested or allowed input: + self.memory_mut(|mem| mem.surrender_focus(w.id)); + } - if let Some((interact_rect, prev_rect)) = show_blocking_widget { - Self::layer_painter(self, LayerId::debug()).debug_rect( - interact_rect, - Color32::GREEN, - "Covered", - ); - Self::layer_painter(self, LayerId::debug()).debug_rect( - prev_rect, - Color32::LIGHT_BLUE, - "On top", - ); - } + if w.sense.interactive() || w.sense.focusable { + self.check_for_id_clash(w.id, w.rect, "widget"); + } + + #[allow(clippy::let_and_return)] + let res = self.get_response(w); + + #[cfg(feature = "accesskit")] + if w.sense.focusable { + // Make sure anything that can receive focus has an AccessKit node. + // TODO(mwcampbell): For nodes that are filled from widget info, + // some information is written to the node twice. + self.accesskit_node_builder(w.id, |builder| res.fill_accesskit_node_common(builder)); } - self.interact_with_hovered(layer_id, id, rect, sense, enabled, hovered) + res } - /// You specify if a thing is hovered, and the function gives a [`Response`]. - pub(crate) fn interact_with_hovered( - &self, - layer_id: LayerId, - id: Id, - rect: Rect, - sense: Sense, - enabled: bool, - hovered: bool, - ) -> Response { - let hovered = hovered && enabled; // can't even hover disabled widgets + /// Read the response of some widget, which may be called _before_ creating the widget (!). + /// + /// This is because widget interaction happens at the start of the frame, using the previous frame's widgets. + /// + /// If the widget was not visible the previous frame (or this frame), this will return `None`. + pub fn read_response(&self, id: Id) -> Option { + self.write(|ctx| { + let viewport = ctx.viewport(); + viewport + .widgets_this_frame + .get(id) + .or_else(|| viewport.widgets_prev_frame.get(id)) + .copied() + }) + .map(|widget_rect| self.get_response(widget_rect)) + } + + /// Returns `true` if the widget with the given `Id` contains the pointer. + #[deprecated = "Use Response.contains_pointer or Context::read_response instead"] + pub fn widget_contains_pointer(&self, id: Id) -> bool { + self.read_response(id) + .map_or(false, |response| response.contains_pointer) + } + + /// Do all interaction for an existing widget, without (re-)registering it. + fn get_response(&self, widget_rect: WidgetRect) -> Response { + let WidgetRect { + id, + layer_id, + rect, + interact_rect, + sense, + enabled, + } = widget_rect; let highlighted = self.frame_state(|fs| fs.highlight_this_frame.contains(&id)); - let mut response = Response { + let mut res = Response { ctx: self.clone(), layer_id, id, rect, + interact_rect, sense, enabled, - hovered, + contains_pointer: false, + hovered: false, highlighted, - clicked: Default::default(), - double_clicked: Default::default(), - triple_clicked: Default::default(), + clicked: false, + fake_primary_click: false, + long_touched: false, + drag_started: false, dragged: false, - drag_released: false, + drag_stopped: false, is_pointer_button_down_on: false, interact_pointer_pos: None, - changed: false, // must be set by the widget itself + changed: false, }; - if !enabled || !sense.focusable || !layer_id.allow_interaction() { - // Not interested or allowed input: - self.memory_mut(|mem| mem.surrender_focus(id)); - return response; - } + let clicked_elsewhere = res.clicked_elsewhere(); - self.check_for_id_clash(id, rect, "widget"); + self.write(|ctx| { + let viewport = ctx.viewports.entry(ctx.viewport_id()).or_default(); - #[cfg(feature = "accesskit")] - if sense.focusable { - // Make sure anything that can receive focus has an AccessKit node. - // TODO(mwcampbell): For nodes that are filled from widget info, - // some information is written to the node twice. - self.accesskit_node_builder(id, |builder| response.fill_accesskit_node_common(builder)); - } + res.contains_pointer = viewport.interact_widgets.contains_pointer.contains(&id); - let clicked_elsewhere = response.clicked_elsewhere(); - self.write(|ctx| { + let input = &viewport.input; let memory = &mut ctx.memory; - let input = &mut ctx.input; - - if sense.focusable { - memory.interested_in_focus(id); - } - if sense.click - && memory.has_focus(response.id) + if enabled + && sense.click + && memory.has_focus(id) && (input.key_pressed(Key::Space) || input.key_pressed(Key::Enter)) { // Space/enter works like a primary click for e.g. selected buttons - response.clicked[PointerButton::Primary as usize] = true; + res.fake_primary_click = true; } #[cfg(feature = "accesskit")] + if enabled + && sense.click + && input.has_accesskit_action_request(id, accesskit::Action::Default) { - if sense.click - && input.has_accesskit_action_request(response.id, accesskit::Action::Default) - { - response.clicked[PointerButton::Primary as usize] = true; - } + res.fake_primary_click = true; } - if sense.click || sense.drag { - memory.interaction.click_interest |= hovered && sense.click; - memory.interaction.drag_interest |= hovered && sense.drag; - - response.dragged = memory.interaction.drag_id == Some(id); - response.is_pointer_button_down_on = - memory.interaction.click_id == Some(id) || response.dragged; - - for pointer_event in &input.pointer.pointer_events { - match pointer_event { - PointerEvent::Moved(_) => {} - PointerEvent::Pressed { .. } => { - if hovered { - if sense.click && memory.interaction.click_id.is_none() { - // potential start of a click - memory.interaction.click_id = Some(id); - response.is_pointer_button_down_on = true; - } - - // HACK: windows have low priority on dragging. - // This is so that if you drag a slider in a window, - // the slider will steal the drag away from the window. - // This is needed because we do window interaction first (to prevent frame delay), - // and then do content layout. - if sense.drag - && (memory.interaction.drag_id.is_none() - || memory.interaction.drag_is_window) - { - // potential start of a drag - memory.interaction.drag_id = Some(id); - memory.interaction.drag_is_window = false; - memory.window_interaction = None; // HACK: stop moving windows (if any) - response.is_pointer_button_down_on = true; - response.dragged = true; - } - } - } - PointerEvent::Released { click, button } => { - response.drag_released = response.dragged; - response.dragged = false; - - if hovered && response.is_pointer_button_down_on { - if let Some(click) = click { - let clicked = hovered && response.is_pointer_button_down_on; - response.clicked[*button as usize] = clicked; - response.double_clicked[*button as usize] = - clicked && click.is_double(); - response.triple_clicked[*button as usize] = - clicked && click.is_triple(); - } - } - } - } - } + if enabled && sense.click && Some(id) == viewport.interact_widgets.long_touched { + res.long_touched = true; } - if response.is_pointer_button_down_on { - response.interact_pointer_pos = input.pointer.interact_pos(); - } + let interaction = memory.interaction(); - if input.pointer.any_down() { - response.hovered &= response.is_pointer_button_down_on; // we don't hover widgets while interacting with *other* widgets - } + res.is_pointer_button_down_on = interaction.potential_click_id == Some(id) + || interaction.potential_drag_id == Some(id); - if memory.has_focus(response.id) && clicked_elsewhere { - memory.surrender_focus(id); + if res.enabled { + res.hovered = viewport.interact_widgets.hovered.contains(&id); + res.dragged = Some(id) == viewport.interact_widgets.dragged; + res.drag_started = Some(id) == viewport.interact_widgets.drag_started; + res.drag_stopped = Some(id) == viewport.interact_widgets.drag_stopped; } - if response.dragged() && !memory.has_focus(response.id) { - // e.g.: remove focus from a widget when you drag something else + let clicked = Some(id) == viewport.interact_widgets.clicked; + + for pointer_event in &input.pointer.pointer_events { + if let PointerEvent::Released { click, .. } = pointer_event { + if enabled && sense.click && clicked && click.is_some() { + res.clicked = true; + } + + res.is_pointer_button_down_on = false; + res.dragged = false; + } + } + + // is_pointer_button_down_on is false when released, but we want interact_pointer_pos + // to still work. + let is_interacted_with = + 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), + &mut res.interact_pointer_pos, + ) { + *pos = transform.inverse() * *pos; + } + } + + if input.pointer.any_down() && !is_interacted_with { + // We don't hover widgets while interacting with *other* widgets: + res.hovered = false; + } + + if clicked_elsewhere && memory.has_focus(id) { + memory.surrender_focus(id); + } + + if res.dragged() && !memory.has_focus(id) { + // e.g.: remove focus from a widget when you drag something else memory.stop_text_input(); } }); - response + res } /// Get a full-screen painter for a new or existing layer @@ -872,6 +1206,24 @@ impl Context { Self::layer_painter(self, LayerId::debug()) } + /// Print this text next to the cursor at the end of the frame. + /// + /// If you call this multiple times, the text will be appended. + /// + /// This only works if compiled with `debug_assertions`. + /// + /// ``` + /// # let ctx = egui::Context::default(); + /// # let state = true; + /// ctx.debug_text(format!("State: {state:?}")); + /// ``` + /// + /// This is just a convenience for calling [`crate::debug_text::print`]. + #[track_caller] + pub fn debug_text(&self, text: impl Into) { + crate::debug_text::print(self, text); + } + /// What operating system are we running on? /// /// When compiling natively, this is @@ -902,6 +1254,31 @@ impl Context { self.output_mut(|o| o.cursor_icon = cursor_icon); } + /// Open an URL in a browser. + /// + /// Equivalent to: + /// ``` + /// # let ctx = egui::Context::default(); + /// # let open_url = egui::OpenUrl::same_tab("http://www.example.com"); + /// ctx.output_mut(|o| o.open_url = Some(open_url)); + /// ``` + pub fn open_url(&self, open_url: crate::OpenUrl) { + self.output_mut(|o| o.open_url = Some(open_url)); + } + + /// Copy the given text to the system clipboard. + /// + /// Empty strings are ignored. + /// + /// Equivalent to: + /// ``` + /// # let ctx = egui::Context::default(); + /// ctx.output_mut(|o| o.copied_text = "Copy this".to_owned()); + /// ``` + pub fn copy_text(&self, text: String) { + self.output_mut(|o| o.copied_text = text); + } + /// Format the given shortcut in a human-readable way (e.g. `Ctrl+Shift+X`). /// /// Can be used to get the text for [`Button::shortcut_text`]. @@ -937,13 +1314,22 @@ impl Context { } } - /// The current frame number. + /// The current frame number for the current viewport. /// /// Starts at zero, and is incremented at the end of [`Self::run`] or by [`Self::end_frame`]. /// /// Between calls to [`Self::run`], this is the frame number of the coming frame. pub fn frame_nr(&self) -> u64 { - self.read(|ctx| ctx.repaint.frame_nr) + self.frame_nr_for(self.viewport_id()) + } + + /// The current frame number. + /// + /// Starts at zero, and is incremented at the end of [`Self::run`] or by [`Self::end_frame`]. + /// + /// Between calls to [`Self::run`], this is the frame number of the coming frame. + pub fn frame_nr_for(&self, id: ViewportId) -> u64 { + self.read(|ctx| ctx.viewports.get(&id).map_or(0, |v| v.repaint.frame_nr)) } /// Call this if there is need to repaint the UI, i.e. if you are showing an animation. @@ -951,12 +1337,34 @@ impl Context { /// If this is called at least once in a frame, then there will be another frame right after this. /// Call as many times as you wish, only one repaint will be issued. /// + /// To request repaint with a delay, use [`Self::request_repaint_after`]. + /// /// If called from outside the UI thread, the UI thread will wake up and run, /// provided the egui integration has set that up via [`Self::set_request_repaint_callback`] /// (this will work on `eframe`). + /// + /// This will repaint the current viewport. + #[track_caller] pub fn request_repaint(&self) { - // request two frames of repaint, just to cover some corner cases (frame delays): - self.write(|ctx| ctx.repaint.request_repaint()); + self.request_repaint_of(self.viewport_id()); + } + + /// Call this if there is need to repaint the UI, i.e. if you are showing an animation. + /// + /// If this is called at least once in a frame, then there will be another frame right after this. + /// Call as many times as you wish, only one repaint will be issued. + /// + /// To request repaint with a delay, use [`Self::request_repaint_after_for`]. + /// + /// If called from outside the UI thread, the UI thread will wake up and run, + /// provided the egui integration has set that up via [`Self::set_request_repaint_callback`] + /// (this will work on `eframe`). + /// + /// This will repaint the specified viewport. + #[track_caller] + pub fn request_repaint_of(&self, id: ViewportId) { + let cause = RepaintCause::new(); + self.write(|ctx| ctx.request_repaint(id, cause)); } /// Request repaint after at most the specified duration elapses. @@ -972,28 +1380,97 @@ impl Context { /// redraws when the app is not in focus. But sometimes the GUI of the app might become stale /// and outdated if it is not updated for too long. /// - /// Lets say, something like a stop watch widget that displays the time in seconds. You would waste + /// Let's say, something like a stopwatch widget that displays the time in seconds. You would waste /// resources repainting multiple times within the same second (when you have no input), /// just calculate the difference of duration between current time and next second change, /// and call this function, to make sure that you are displaying the latest updated time, but /// not wasting resources on needless repaints within the same second. /// - /// NOTE: only works if called before `Context::end_frame()`. to force egui to update, - /// use `Context::request_repaint()` instead. + /// ### Quirk: + /// Duration begins at the next frame. Let's say for example that it's a very inefficient app + /// and takes 500 milliseconds per frame at 2 fps. The widget / user might want a repaint in + /// next 500 milliseconds. Now, app takes 1000 ms per frame (1 fps) because the backend event + /// timeout takes 500 milliseconds AFTER the vsync swap buffer. + /// So, it's not that we are requesting repaint within X duration. We are rather timing out + /// during app idle time where we are not receiving any new input events. + /// + /// This repaints the current viewport + #[track_caller] + pub fn request_repaint_after(&self, duration: Duration) { + self.request_repaint_after_for(duration, self.viewport_id()); + } + + /// Request repaint after at most the specified duration elapses. + /// + /// The backend can chose to repaint sooner, for instance if some other code called + /// this method with a lower duration, or if new events arrived. + /// + /// The function can be multiple times, but only the *smallest* duration will be considered. + /// So, if the function is called two times with `1 second` and `2 seconds`, egui will repaint + /// after `1 second` + /// + /// This is primarily useful for applications who would like to save battery by avoiding wasted + /// redraws when the app is not in focus. But sometimes the GUI of the app might become stale + /// and outdated if it is not updated for too long. + /// + /// Let's say, something like a stopwatch widget that displays the time in seconds. You would waste + /// resources repainting multiple times within the same second (when you have no input), + /// just calculate the difference of duration between current time and next second change, + /// and call this function, to make sure that you are displaying the latest updated time, but + /// not wasting resources on needless repaints within the same second. /// /// ### Quirk: - /// Duration begins at the next frame. lets say for example that its a very inefficient app + /// Duration begins at the next frame. Let's say for example that it's a very inefficient app /// and takes 500 milliseconds per frame at 2 fps. The widget / user might want a repaint in /// next 500 milliseconds. Now, app takes 1000 ms per frame (1 fps) because the backend event /// timeout takes 500 milliseconds AFTER the vsync swap buffer. - /// So, its not that we are requesting repaint within X duration. We are rather timing out + /// So, it's not that we are requesting repaint within X duration. We are rather timing out /// during app idle time where we are not receiving any new input events. - pub fn request_repaint_after(&self, duration: std::time::Duration) { - // Maybe we can check if duration is ZERO, and call self.request_repaint()? - self.write(|ctx| ctx.repaint.request_repaint_after(duration)); + /// + /// This repaints the specified viewport + #[track_caller] + pub fn request_repaint_after_for(&self, duration: Duration, id: ViewportId) { + let cause = RepaintCause::new(); + self.write(|ctx| ctx.request_repaint_after(duration, id, cause)); + } + + /// Was a repaint requested last frame for the current viewport? + #[must_use] + pub fn requested_repaint_last_frame(&self) -> bool { + self.requested_repaint_last_frame_for(&self.viewport_id()) + } + + /// Was a repaint requested last frame for the given viewport? + #[must_use] + pub fn requested_repaint_last_frame_for(&self, viewport_id: &ViewportId) -> bool { + self.read(|ctx| ctx.requested_immediate_repaint_prev_frame(viewport_id)) + } + + /// Has a repaint been requested for the current viewport? + #[must_use] + pub fn has_requested_repaint(&self) -> bool { + self.has_requested_repaint_for(&self.viewport_id()) } - /// For integrations: this callback will be called when an egui user calls [`Self::request_repaint`]. + /// Has a repaint been requested for the given viewport? + #[must_use] + pub fn has_requested_repaint_for(&self, viewport_id: &ViewportId) -> bool { + self.read(|ctx| ctx.has_requested_repaint(viewport_id)) + } + + /// Why are we repainting? + /// + /// This can be helpful in debugging why egui is constantly repainting. + pub fn repaint_causes(&self) -> Vec { + self.read(|ctx| { + ctx.viewports + .get(&ctx.viewport_id()) + .map(|v| v.repaint.causes.clone()) + }) + .unwrap_or_default() + } + + /// For integrations: this callback will be called when an egui user calls [`Self::request_repaint`] or [`Self::request_repaint_after`]. /// /// This lets you wake up a sleeping UI thread. /// @@ -1003,9 +1480,40 @@ impl Context { callback: impl Fn(RequestRepaintInfo) + Send + Sync + 'static, ) { let callback = Box::new(callback); - self.write(|ctx| ctx.repaint.request_repaint_callback = Some(callback)); + self.write(|ctx| ctx.request_repaint_callback = Some(callback)); + } +} + +/// Callbacks +impl Context { + /// Call the given callback at the start of each frame + /// of each viewport. + /// + /// This can be used for egui _plugins_. + /// See [`crate::debug_text`] for an example. + pub fn on_begin_frame(&self, debug_name: &'static str, cb: ContextCallback) { + let named_cb = NamedContextCallback { + debug_name, + callback: cb, + }; + self.write(|ctx| ctx.plugins.on_begin_frame.push(named_cb)); + } + + /// Call the given callback at the end of each frame + /// of each viewport. + /// + /// This can be used for egui _plugins_. + /// See [`crate::debug_text`] for an example. + pub fn on_end_frame(&self, debug_name: &'static str, cb: ContextCallback) { + let named_cb = NamedContextCallback { + debug_name, + callback: cb, + }; + self.write(|ctx| ctx.plugins.on_end_frame.push(named_cb)); } +} +impl Context { /// Tell `egui` which fonts to use. /// /// The default `egui` fonts only support latin and cyrillic alphabets, @@ -1013,12 +1521,18 @@ impl Context { /// /// The new fonts will become active at the start of the next frame. pub fn set_fonts(&self, font_definitions: FontDefinitions) { - let update_fonts = self.fonts_mut(|fonts| { - if let Some(current_fonts) = fonts { + crate::profile_function!(); + + 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()) { // NOTE: this comparison is expensive since it checks TTF data for equality - current_fonts.lock().fonts.definitions() != &font_definitions - } else { - true + if current_fonts.lock().fonts.definitions() == &font_definitions { + update_fonts = false; // no need to update + } } }); @@ -1032,17 +1546,24 @@ impl Context { self.options(|opt| opt.style.clone()) } - /// The [`Style`] used by all new windows, panels etc. - /// - /// You can also use [`Ui::style_mut`] to change the style of a single [`Ui`]. + /// Mutate the [`Style`] used by all subsequent windows, panels etc. /// /// Example: /// ``` /// # let mut ctx = egui::Context::default(); - /// let mut style: egui::Style = (*ctx.style()).clone(); - /// style.spacing.item_spacing = egui::vec2(10.0, 20.0); - /// ctx.set_style(style); + /// ctx.style_mut(|style| { + /// style.spacing.item_spacing = egui::vec2(10.0, 20.0); + /// }); /// ``` + pub fn style_mut(&self, mutate_style: impl FnOnce(&mut Style)) { + self.options_mut(|opt| mutate_style(std::sync::Arc::make_mut(&mut opt.style))); + } + + /// The [`Style`] used by all new windows, panels etc. + /// + /// You can also change this using [`Self::style_mut]` + /// + /// You can use [`Ui::style_mut`] to change the style of a single [`Ui`]. pub fn set_style(&self, style: impl Into>) { self.options_mut(|opt| opt.style = style.into()); } @@ -1061,40 +1582,91 @@ impl Context { } /// The number of physical pixels for each logical point. + /// + /// This is calculated as [`Self::zoom_factor`] * [`Self::native_pixels_per_point`] #[inline(always)] pub fn pixels_per_point(&self) -> f32 { - self.input(|i| i.pixels_per_point()) + self.input(|i| i.pixels_per_point) } /// Set the number of physical pixels for each logical point. /// Will become active at the start of the next frame. /// - /// Note that this may be overwritten by input from the integration via [`RawInput::pixels_per_point`]. - /// For instance, when using `eframe` on web, the browsers native zoom level will always be used. + /// This will actually translate to a call to [`Self::set_zoom_factor`]. pub fn set_pixels_per_point(&self, pixels_per_point: f32) { if pixels_per_point != self.pixels_per_point() { - self.request_repaint(); - self.memory_mut(|mem| mem.new_pixels_per_point = Some(pixels_per_point)); + self.set_zoom_factor(pixels_per_point / self.native_pixels_per_point().unwrap_or(1.0)); } } + /// The number of physical pixels for each logical point on this monitor. + /// + /// This is given as input to egui via [`ViewportInfo::native_pixels_per_point`] + /// and cannot be changed. + #[inline(always)] + pub fn native_pixels_per_point(&self) -> Option { + self.input(|i| i.viewport().native_pixels_per_point) + } + + /// Global zoom factor of the UI. + /// + /// This is used to calculate the `pixels_per_point` + /// for the UI as `pixels_per_point = zoom_fator * native_pixels_per_point`. + /// + /// The default is 1.0. + /// Make larger to make everything larger. + #[inline(always)] + pub fn zoom_factor(&self) -> f32 { + self.options(|o| o.zoom_factor) + } + + /// Sets zoom factor of the UI. + /// Will become active at the start of the next frame. + /// + /// Note that calling this will not update [`Self::zoom_factor`] until the end of the frame. + /// + /// This is used to calculate the `pixels_per_point` + /// for the UI as `pixels_per_point = zoom_fator * native_pixels_per_point`. + /// + /// The default is 1.0. + /// Make larger to make everything larger. + /// + /// It is better to call this than modifying + /// [`Options::zoom_factor`]. + #[inline(always)] + pub fn set_zoom_factor(&self, zoom_factor: f32) { + let cause = RepaintCause::new(); + self.write(|ctx| { + if ctx.memory.options.zoom_factor != zoom_factor { + ctx.new_zoom_factor = Some(zoom_factor); + for viewport_id in ctx.all_viewport_ids() { + ctx.request_repaint(viewport_id, cause.clone()); + } + } + }); + } + /// Useful for pixel-perfect rendering + #[inline] pub(crate) fn round_to_pixel(&self, point: f32) -> f32 { let pixels_per_point = self.pixels_per_point(); (point * pixels_per_point).round() / pixels_per_point } /// Useful for pixel-perfect rendering + #[inline] pub(crate) fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 { pos2(self.round_to_pixel(pos.x), self.round_to_pixel(pos.y)) } /// Useful for pixel-perfect rendering + #[inline] pub(crate) fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 { vec2(self.round_to_pixel(vec.x), self.round_to_pixel(vec.y)) } /// Useful for pixel-perfect rendering + #[inline] pub(crate) fn round_rect_to_pixels(&self, rect: Rect) -> Rect { Rect { min: self.round_pos_to_pixels(rect.min), @@ -1104,9 +1676,16 @@ impl Context { /// Allocate a texture. /// + /// This is for advanced users. + /// Most users should use [`crate::Ui::image`] or [`Self::try_load_texture`] + /// instead. + /// /// In order to display an image you must convert it to a texture using this function. + /// The function will hand over the image data to the egui backend, which will + /// upload it to the GPU. /// - /// Make sure to only call this once for each image, i.e. NOT in your main GUI code. + /// ⚠️ Make sure to only call this ONCE for each image, i.e. NOT in your main GUI code. + /// The call is NOT immediate safe. /// /// The given name can be useful for later debugging, and will be visible if you call [`Self::texture_ui`]. /// @@ -1129,12 +1708,12 @@ impl Context { /// }); /// /// // Show the image: - /// ui.image(texture, texture.size_vec2()); + /// ui.image((texture.id(), texture.size_vec2())); /// } /// } /// ``` /// - /// Se also [`crate::ImageData`], [`crate::Ui::image`] and [`crate::ImageButton`]. + /// See also [`crate::ImageData`], [`crate::Ui::image`] and [`crate::Image`]. pub fn load_texture( &self, name: impl Into, @@ -1207,85 +1786,348 @@ impl Context { /// Call at the end of each frame. #[must_use] pub fn end_frame(&self) -> FullOutput { - if self.input(|i| i.wants_repaint()) { - self.request_repaint(); + crate::profile_function!(); + + if self.options(|o| o.zoom_with_keyboard) { + crate::gui_zoom::zoom_with_keyboard(self); } - let textures_delta = self.write(|ctx| { - ctx.memory.end_frame(&ctx.input, &ctx.frame_state.used_ids); + self.read(|ctx| ctx.plugins.clone()).on_end_frame(self); + + #[cfg(debug_assertions)] + self.debug_painting(); + + self.write(|ctx| ctx.end_frame()) + } - let font_image_delta = ctx.fonts.as_ref().unwrap().font_image_delta(); - if let Some(font_image_delta) = font_image_delta { - ctx.tex_manager - .0 - .write() - .set(TextureId::default(), font_image_delta); + #[cfg(debug_assertions)] + fn debug_painting(&self) { + let paint_widget = |widget: &WidgetRect, text: &str, color: Color32| { + let rect = widget.interact_rect; + if rect.is_positive() { + let painter = Painter::new(self.clone(), widget.layer_id, Rect::EVERYTHING); + painter.debug_rect(rect, color, text); } + }; - ctx.tex_manager.0.write().take_delta() - }); + let paint_widget_id = |id: Id, text: &str, color: Color32| { + if let Some(widget) = + self.write(|ctx| ctx.viewport().widgets_this_frame.get(id).cloned()) + { + paint_widget(&widget, text, color); + } + }; + + if self.style().debug.show_interactive_widgets { + // Show all interactive widgets: + let rects = self.write(|ctx| ctx.viewport().widgets_this_frame.clone()); + for (layer_id, rects) in rects.layers() { + let painter = Painter::new(self.clone(), *layer_id, Rect::EVERYTHING); + for rect in rects { + if rect.sense.interactive() { + let (color, text) = if rect.sense.click && rect.sense.drag { + (Color32::from_rgb(0x88, 0, 0x88), "click+drag") + } else if rect.sense.click { + (Color32::from_rgb(0x88, 0, 0), "click") + } else if rect.sense.drag { + (Color32::from_rgb(0, 0, 0x88), "drag") + } else { + continue; + // (Color32::from_rgb(0, 0, 0x88), "hover") + }; + painter.debug_rect(rect.interact_rect, color, text); + } + } + } + + // Show the ones actually interacted with: + { + let interact_widgets = self.write(|ctx| ctx.viewport().interact_widgets.clone()); + let InteractionSnapshot { + clicked, + long_touched: _, + drag_started: _, + dragged, + drag_stopped: _, + contains_pointer, + hovered, + } = interact_widgets; + + if false { + for widget in contains_pointer { + paint_widget_id(widget, "contains_pointer", Color32::BLUE); + } + } + if true { + for widget in hovered { + paint_widget_id(widget, "hovered", Color32::WHITE); + } + } + for &widget in &clicked { + paint_widget_id(widget, "clicked", Color32::RED); + } + for &widget in &dragged { + paint_widget_id(widget, "dragged", Color32::GREEN); + } + } + } + + if self.style().debug.show_widget_hits { + let hits = self.write(|ctx| ctx.viewport().hits.clone()); + let WidgetHits { + contains_pointer, + click, + drag, + } = hits; + + if false { + for widget in &contains_pointer { + paint_widget(widget, "contains_pointer", Color32::BLUE); + } + } + for widget in &click { + paint_widget(widget, "click", Color32::RED); + } + for widget in &drag { + paint_widget(widget, "drag", Color32::GREEN); + } + } + } +} + +impl ContextImpl { + fn end_frame(&mut self) -> FullOutput { + let ended_viewport_id = self.viewport_id(); + let viewport = self.viewports.entry(ended_viewport_id).or_default(); + let pixels_per_point = viewport.input.pixels_per_point; + + viewport.repaint.frame_nr += 1; + + self.memory.end_frame(&viewport.frame_state.used_ids); + + if let Some(fonts) = self.fonts.get(&pixels_per_point.into()) { + let tex_mngr = &mut self.tex_manager.0.write(); + if let Some(font_image_delta) = fonts.font_image_delta() { + // A partial font atlas update, e.g. a new glyph has been entered. + tex_mngr.set(TextureId::default(), font_image_delta); + } + + if 1 < self.fonts.len() { + // We have multiple different `pixels_per_point`, + // e.g. because we have many viewports spread across + // monitors with different DPI scaling. + // All viewports share the same texture namespace and renderer, + // so the all use `TextureId::default()` for the font texture. + // This is a problem. + // We solve this with a hack: we always upload the full font atlas + // every frame, for all viewports. + // This ensures it is up-to-date, solving + // 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"); + let full_delta = ImageDelta::full(fonts.image(), TextureAtlas::texture_options()); + tex_mngr.set(TextureId::default(), full_delta); + } + } + + // Inform the backend of all textures that have been updated (including font atlas). + let textures_delta = self.tex_manager.0.write().take_delta(); #[cfg_attr(not(feature = "accesskit"), allow(unused_mut))] - let mut platform_output: PlatformOutput = self.output_mut(|o| std::mem::take(o)); + let mut platform_output: PlatformOutput = std::mem::take(&mut viewport.output); #[cfg(feature = "accesskit")] { - let state = self.frame_state_mut(|fs| fs.accesskit_state.take()); + crate::profile_scope!("accesskit"); + let state = viewport.frame_state.accesskit_state.take(); if let Some(state) = state { - let has_focus = self.input(|i| i.raw.focused); let root_id = crate::accesskit_root_id().accesskit_id(); - let nodes = self.write(|ctx| { + let nodes = { state .node_builders .into_iter() .map(|(id, builder)| { ( id.accesskit_id(), - builder.build(&mut ctx.accesskit_node_classes), + builder.build(&mut self.accesskit_node_classes), ) }) .collect() - }); + }; + let focus_id = self + .memory + .focused() + .map_or(root_id, |id| id.accesskit_id()); platform_output.accesskit_update = Some(accesskit::TreeUpdate { nodes, tree: Some(accesskit::Tree::new(root_id)), - focus: has_focus.then(|| { - let focus_id = self.memory(|mem| mem.interaction.focus.id); - focus_id.map_or(root_id, |id| id.accesskit_id()) - }), + focus: focus_id, }); } } - let repaint_after = self.write(|ctx| ctx.repaint.end_frame()); - let shapes = self.drain_paint_lists(); + let shapes = viewport + .graphics + .drain(self.memory.areas().order(), &self.memory.layer_transforms); + + let mut repaint_needed = false; + + { + if self.memory.options.repaint_on_widget_change { + crate::profile_function!("compare-widget-rects"); + if viewport.widgets_prev_frame != viewport.widgets_this_frame { + repaint_needed = true; // Some widget has moved + } + } + + std::mem::swap( + &mut viewport.widgets_prev_frame, + &mut viewport.widgets_this_frame, + ); + viewport.widgets_this_frame.clear(); + } + + if repaint_needed || viewport.input.wants_repaint() { + self.request_repaint(ended_viewport_id, RepaintCause::new()); + } + + // ------------------- + + let all_viewport_ids = self.all_viewport_ids(); + + self.last_viewport = ended_viewport_id; + + self.viewports.retain(|&id, viewport| { + let parent = *self.viewport_parents.entry(id).or_default(); + + if !all_viewport_ids.contains(&parent) { + #[cfg(feature = "log")] + log::debug!( + "Removing viewport {:?} ({:?}): the parent is gone", + id, + viewport.builder.title + ); + + return false; + } + + let is_our_child = parent == ended_viewport_id && id != ViewportId::ROOT; + if is_our_child { + if !viewport.used { + #[cfg(feature = "log")] + log::debug!( + "Removing viewport {:?} ({:?}): it was never used this frame", + id, + viewport.builder.title + ); + + return false; // Only keep children that have been updated this frame + } + + viewport.used = false; // reset so we can check again next frame + } + + true + }); + + // If we are an immediate viewport, this will resume the previous viewport. + self.viewport_stack.pop(); + + // The last viewport is not necessarily the root viewport, + // just the top _immediate_ viewport. + let is_last = self.viewport_stack.is_empty(); + + let viewport_output = self + .viewports + .iter_mut() + .map(|(&id, viewport)| { + let parent = *self.viewport_parents.entry(id).or_default(); + let commands = if is_last { + // Let the primary immediate viewport handle the commands of its children too. + // This can make things easier for the backend, as otherwise we may get commands + // that affect a viewport while its egui logic is running. + std::mem::take(&mut viewport.commands) + } else { + vec![] + }; + + ( + id, + ViewportOutput { + parent, + class: viewport.class, + builder: viewport.builder.clone(), + viewport_ui_cb: viewport.viewport_ui_cb.clone(), + commands, + repaint_delay: viewport.repaint.repaint_delay, + }, + ) + }) + .collect(); + + if is_last { + // Remove dead viewports: + self.viewports.retain(|id, _| all_viewport_ids.contains(id)); + self.viewport_parents + .retain(|id, _| all_viewport_ids.contains(id)); + } else { + let viewport_id = self.viewport_id(); + self.memory.set_viewport_id(viewport_id); + } + + let active_pixels_per_point: std::collections::BTreeSet> = self + .viewports + .values() + .map(|v| v.input.pixels_per_point.into()) + .collect(); + self.fonts.retain(|pixels_per_point, _| { + if active_pixels_per_point.contains(pixels_per_point) { + true + } else { + #[cfg(feature = "log")] + log::trace!( + "Freeing Fonts with pixels_per_point={} because it is no longer needed", + pixels_per_point.into_inner() + ); + false + } + }); FullOutput { platform_output, - repaint_after, textures_delta, shapes, + pixels_per_point, + viewport_output, } } +} - fn drain_paint_lists(&self) -> Vec { - self.write(|ctx| ctx.graphics.drain(ctx.memory.areas.order()).collect()) - } - +impl Context { /// Tessellate the given shapes into triangle meshes. - pub fn tessellate(&self, shapes: Vec) -> Vec { + /// + /// `pixels_per_point` is used for feathering (anti-aliasing). + /// For this you can use [`FullOutput::pixels_per_point`], [`Self::pixels_per_point`], + /// or whatever is appropriate for your viewport. + pub fn tessellate( + &self, + shapes: Vec, + pixels_per_point: f32, + ) -> Vec { + crate::profile_function!(); + // 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 // it takes to tessellate them, so it is not a worth optimization. - // here we expect that we are the only user of context, since frame is ended self.write(|ctx| { - let pixels_per_point = ctx.input.pixels_per_point(); let tessellation_options = ctx.memory.options.tessellation_options; let texture_atlas = ctx .fonts - .as_ref() - .expect("tessellate called before first call to Context::run()") + .get(&pixels_per_point.into()) + .expect("tessellate called with a different pixels_per_point than the font atlas was created with. \ + You should use egui::FullOutput::pixels_per_point when tessellating.") .texture_atlas(); let (font_tex_size, prepared_discs) = { let atlas = texture_atlas.lock(); @@ -1293,13 +2135,16 @@ impl Context { }; let paint_stats = PaintStats::from_shapes(&shapes); - let clipped_primitives = tessellator::tessellate_shapes( - pixels_per_point, - tessellation_options, - font_tex_size, - prepared_discs, - shapes, - ); + let clipped_primitives = { + crate::profile_scope!("tessellator::tessellate_shapes"); + tessellator::Tessellator::new( + pixels_per_point, + tessellation_options, + font_tex_size, + prepared_discs, + ) + .tessellate_shapes(shapes) + }; ctx.paint_stats = paint_stats.with_clipped_primitives(&clipped_primitives); clipped_primitives }) @@ -1322,9 +2167,9 @@ impl Context { /// How much space is used by panels and windows. pub fn used_rect(&self) -> Rect { - self.read(|ctx| { - let mut used = ctx.frame_state.used_by_panels; - for window in ctx.memory.areas.visible_windows() { + self.write(|ctx| { + let mut used = ctx.viewport().frame_state.used_by_panels; + for window in ctx.memory.areas().visible_windows() { used = used.union(window.rect()); } used @@ -1373,12 +2218,12 @@ impl Context { /// /// NOTE: this will return `false` if the pointer is just hovering over an egui area. pub fn is_using_pointer(&self) -> bool { - self.memory(|m| m.interaction.is_using_pointer()) + self.memory(|m| m.interaction().is_using_pointer()) } /// If `true`, egui is currently listening on text input (e.g. typing text in a [`TextEdit`]). pub fn wants_keyboard_input(&self) -> bool { - self.memory(|m| m.interaction.focus.focused().is_some()) + self.memory(|m| m.focused().is_some()) } /// Highlight this widget, to make it look like it is hovered, even if it isn't. @@ -1389,6 +2234,14 @@ impl Context { pub fn highlight_widget(&self, id: Id) { self.frame_state_mut(|fs| fs.highlight_next_frame.insert(id)); } + + /// Is an egui context menu open? + pub fn is_context_menu_open(&self) -> bool { + self.data(|d| { + d.get_temp::(menu::CONTEXT_MENU_ID_STR.into()) + .map_or(false, |state| state.has_root()) + }) + } } // Ergonomic methods to forward some calls often used in 'if let' without holding the borrow @@ -1424,52 +2277,116 @@ impl Context { } impl Context { + /// Transform the graphics of the given layer. + /// + /// This will also affect input. + /// + /// This is a sticky setting, remembered from one frame to the next. + /// + /// Can be used to implement pan and zoom (see relevant demo). + /// + /// For a temporary transform, use [`Self::transform_layer_shapes`] instead. + 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) + } else { + m.layer_transforms.insert(layer_id, transform) + } + }); + } + /// Move all the graphics at the given layer. /// - /// Can be used to implement drag-and-drop (see relevant demo). + /// Is used to implement drag-and-drop preview. + /// + /// This only applied to the existing graphics at the layer, not to new graphics added later. + /// + /// For a persistent transform, use [`Self::set_transform_layer`] instead. + #[deprecated = "Use `transform_layer_shapes` instead"] pub fn translate_layer(&self, layer_id: LayerId, delta: Vec2) { if delta != Vec2::ZERO { - self.graphics_mut(|g| g.list(layer_id).translate(delta)); + let transform = emath::TSTransform::from_translation(delta); + self.transform_layer_shapes(layer_id, transform); + } + } + + /// Transform all the graphics at the given layer. + /// + /// Is used to implement drag-and-drop preview. + /// + /// This only applied to the existing graphics at the layer, not to new graphics added later. + /// + /// For a persistent transform, use [`Self::set_transform_layer`] instead. + pub fn transform_layer_shapes(&self, layer_id: LayerId, transform: TSTransform) { + if transform != TSTransform::IDENTITY { + self.graphics_mut(|g| g.entry(layer_id).transform(transform)); } } /// Top-most layer at the given position. pub fn layer_id_at(&self, pos: Pos2) -> Option { - self.memory(|mem| { - mem.layer_id_at(pos, mem.options.style.interaction.resize_grab_radius_side) - }) + self.memory(|mem| mem.layer_id_at(pos)) } /// Moves the given area to the top in its [`Order`]. /// /// [`Area`]:s and [`Window`]:s also do this automatically when being clicked on or interacted with. pub fn move_to_top(&self, layer_id: LayerId) { - self.memory_mut(|mem| mem.areas.move_to_top(layer_id)); + self.memory_mut(|mem| mem.areas_mut().move_to_top(layer_id)); + } + + /// Retrieve the [`LayerId`] of the top level windows. + pub fn top_layer_id(&self) -> Option { + self.memory(|mem| mem.areas().top_layer_id(Order::Middle)) } - pub(crate) fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool { - rect.is_positive() && { - let pointer_pos = self.input(|i| i.pointer.interact_pos()); - if let Some(pointer_pos) = pointer_pos { - rect.contains(pointer_pos) && self.layer_id_at(pointer_pos) == Some(layer_id) + /// Does the given rectangle contain the mouse pointer? + /// + /// Will return false if some other area is covering the given layer. + /// + /// The given rectangle is assumed to have been clipped by its parent clip rect. + /// + /// 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).cloned()) { + transform * rect } else { - false - } + rect + }; + if !rect.is_positive() { + return false; + } + + let pointer_pos = self.input(|i| i.pointer.interact_pos()); + let Some(pointer_pos) = pointer_pos else { + return false; + }; + + if !rect.contains(pointer_pos) { + return false; + } + + if self.layer_id_at(pointer_pos) != Some(layer_id) { + return false; } + + true } // --------------------------------------------------------------------- /// Whether or not to debug widget layout on hover. + #[cfg(debug_assertions)] pub fn debug_on_hover(&self) -> bool { self.options(|opt| opt.style.debug.debug_on_hover) } /// Turn on/off whether or not to debug widget layout on hover. + #[cfg(debug_assertions)] pub fn set_debug_on_hover(&self, debug_on_hover: bool) { - let mut style = self.options(|opt| (*opt.style).clone()); - style.debug.debug_on_hover = debug_on_hover; - self.set_style(style); + self.style_mut(|style| style.debug.debug_on_hover = debug_on_hover); } } @@ -1484,16 +2401,22 @@ impl Context { /// The function will call [`Self::request_repaint()`] when appropriate. /// /// The animation time is taken from [`Style::animation_time`]. + #[track_caller] // To track repaint cause pub fn animate_bool(&self, id: Id, value: bool) -> f32 { let animation_time = self.style().animation_time; self.animate_bool_with_time(id, value, animation_time) } /// Like [`Self::animate_bool`] but allows you to control the animation time. + #[track_caller] // To track repaint cause pub fn animate_bool_with_time(&self, id: Id, target_value: bool, animation_time: f32) -> f32 { let animated_value = self.write(|ctx| { - ctx.animation_manager - .animate_bool(&ctx.input, animation_time, id, target_value) + ctx.animation_manager.animate_bool( + &ctx.viewports.entry(ctx.viewport_id()).or_default().input, + animation_time, + id, + target_value, + ) }); let animation_in_progress = 0.0 < animated_value && animated_value < 1.0; if animation_in_progress { @@ -1506,10 +2429,15 @@ impl Context { /// /// At the first call the value is written to memory. /// When it is called with a new value, it linearly interpolates to it in the given time. + #[track_caller] // To track repaint cause pub fn animate_value_with_time(&self, id: Id, target_value: f32, animation_time: f32) -> f32 { let animated_value = self.write(|ctx| { - ctx.animation_manager - .animate_value(&ctx.input, animation_time, id, target_value) + ctx.animation_manager.animate_value( + &ctx.viewports.entry(ctx.viewport_id()).or_default().input, + animation_time, + id, + target_value, + ) }); let animation_in_progress = animated_value != target_value; if animation_in_progress { @@ -1526,31 +2454,21 @@ impl Context { } impl Context { + /// Show a ui for settings (style and tessellation options). pub fn settings_ui(&self, ui: &mut Ui) { - use crate::containers::*; + let prev_options = self.options(|o| o.clone()); + let mut options = prev_options.clone(); - CollapsingHeader::new("🎑 Style") - .default_open(true) - .show(ui, |ui| { - self.style_ui(ui); - }); + options.ui(ui); - CollapsingHeader::new("✒ Painting") - .default_open(true) - .show(ui, |ui| { - let prev_tessellation_options = self.tessellation_options(|o| *o); - let mut tessellation_options = prev_tessellation_options; - tessellation_options.ui(ui); - ui.vertical_centered(|ui| reset_button(ui, &mut tessellation_options)); - if tessellation_options != prev_tessellation_options { - self.tessellation_options_mut(move |o| *o = tessellation_options); - } - }); + if options != prev_options { + self.options_mut(move |o| *o = options); + } } + /// Show the state of egui, including its input and output. pub fn inspection_ui(&self, ui: &mut Ui) { use crate::containers::*; - crate::trace!(ui); ui.label(format!("Is using pointer: {}", self.is_using_pointer())) .on_hover_text( @@ -1565,7 +2483,7 @@ impl Context { .on_hover_text("Is egui currently listening for text input?"); ui.label(format!( "Keyboard focus widget: {}", - self.memory(|m| m.interaction.focus.focused()) + self.memory(|m| m.focused()) .as_ref() .map(Id::short_debug_format) .unwrap_or_default() @@ -1592,6 +2510,18 @@ impl Context { .on_hover_text("This is approximately the number of text strings on screen"); ui.add_space(16.0); + CollapsingHeader::new("🔃 Repaint Causes") + .default_open(false) + .show(ui, |ui| { + ui.set_min_height(120.0); + ui.label("What caused egui to repaint:"); + ui.add_space(8.0); + let causes = ui.ctx().repaint_causes(); + for cause in causes { + ui.label(cause.to_string()); + } + }); + CollapsingHeader::new("📥 Input") .default_open(false) .show(ui, |ui| { @@ -1618,6 +2548,22 @@ impl Context { let font_image_size = self.fonts(|f| f.font_image_size()); crate::introspection::font_texture_ui(ui, font_image_size); }); + + CollapsingHeader::new("Label text selection state") + .default_open(false) + .show(ui, |ui| { + ui.label(format!( + "{:#?}", + crate::text_selection::LabelSelectionState::load(ui.ctx()) + )); + }); + + CollapsingHeader::new("Interaction") + .default_open(false) + .show(ui, |ui| { + let interact_widgets = self.write(|ctx| ctx.viewport().interact_widgets.clone()); + interact_widgets.ui(ui); + }); } /// Show stats about the allocated textures. @@ -1658,14 +2604,15 @@ impl Context { let mut size = vec2(w as f32, h as f32); size *= (max_preview_size.x / size.x).min(1.0); size *= (max_preview_size.y / size.y).min(1.0); - ui.image(texture_id, size).on_hover_ui(|ui| { - // show larger on hover - let max_size = 0.5 * ui.ctx().screen_rect().size(); - let mut size = vec2(w as f32, h as f32); - size *= max_size.x / size.x.max(max_size.x); - size *= max_size.y / size.y.max(max_size.y); - ui.image(texture_id, size); - }); + ui.image(SizedTexture::new(texture_id, size)) + .on_hover_ui(|ui| { + // show larger on hover + let max_size = 0.5 * ui.ctx().screen_rect().size(); + let mut size = vec2(w as f32, h as f32); + size *= max_size.x / size.x.max(max_size.x); + size *= max_size.y / size.y.max(max_size.y); + ui.image(SizedTexture::new(texture_id, size)); + }); ui.label(format!("{w} x {h}")); ui.label(format!("{:.3} MB", meta.bytes_used() as f64 * 1e-6)); @@ -1677,6 +2624,7 @@ impl Context { }); } + /// Shows the contents of [`Self::memory`]. pub fn memory_ui(&self, ui: &mut crate::Ui) { if ui .button("Reset all") @@ -1694,20 +2642,20 @@ impl Context { ui.horizontal(|ui| { ui.label(format!( "{} areas (panels, windows, popups, …)", - self.memory(|mem| mem.areas.count()) + self.memory(|mem| mem.areas().count()) )); if ui.button("Reset").clicked() { - self.memory_mut(|mem| mem.areas = Default::default()); + 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"); - let layers_ids: Vec = self.memory(|mem| mem.areas.order().to_vec()); + let layers_ids: Vec = self.memory(|mem| mem.areas().order().to_vec()); for layer_id in layers_ids { - let area = self.memory(|mem| mem.areas.get(layer_id.id).copied()); + let area = self.memory(|mem| mem.areas().get(layer_id.id).copied()); if let Some(area) = area { - let is_visible = self.memory(|mem| mem.areas.is_visible(&layer_id)); + let is_visible = self.memory(|mem| mem.areas().is_visible(&layer_id)); if !is_visible { continue; } @@ -1770,13 +2718,14 @@ impl Context { ui.label("NOTE: the position of this window cannot be reset from within itself."); ui.collapsing("Interaction", |ui| { - let interaction = self.memory(|mem| mem.interaction.clone()); + let interaction = self.memory(|mem| mem.interaction().clone()); interaction.ui(ui); }); } } impl Context { + /// Edit the active [`Style`]. pub fn style_ui(&self, ui: &mut Ui) { let mut style: Style = (*self.style()).clone(); style.ui(ui); @@ -1792,6 +2741,8 @@ impl Context { /// the function is still called, but with no other effect. /// /// No locks are held while the given closure is called. + #[allow(clippy::unused_self)] + #[inline] pub fn with_accessibility_parent(&self, _id: Id, f: impl FnOnce()) { // TODO(emilk): this isn't thread-safe - another thread can call this function between the push/pop calls #[cfg(feature = "accesskit")] @@ -1827,7 +2778,8 @@ impl Context { writer: impl FnOnce(&mut accesskit::NodeBuilder) -> R, ) -> Option { self.write(|ctx| { - ctx.frame_state + ctx.viewport() + .frame_state .accesskit_state .is_some() .then(|| ctx.accesskit_node_builder(id)) @@ -1854,6 +2806,8 @@ impl Context { /// to get a full tree update after running [`Context::enable_accesskit`]. #[cfg(feature = "accesskit")] pub fn accesskit_placeholder_tree_update(&self) -> accesskit::TreeUpdate { + crate::profile_function!(); + use accesskit::{NodeBuilder, Role, Tree, TreeUpdate}; let root_id = crate::accesskit_root_id().accesskit_id(); @@ -1863,11 +2817,519 @@ impl Context { NodeBuilder::new(Role::Window).build(&mut ctx.accesskit_node_classes), )], tree: Some(Tree::new(root_id)), - focus: None, + focus: root_id, + }) + } +} + +/// ## Image loading +impl Context { + /// Associate some static bytes with a `uri`. + /// + /// The same `uri` may be passed to [`Ui::image`] later to load the bytes as an image. + /// + /// By convention, the `uri` should start with `bytes://`. + /// Following that convention will lead to better error messages. + pub fn include_bytes(&self, uri: impl Into>, bytes: impl Into) { + self.loaders().include.insert(uri, bytes); + } + + /// Returns `true` if the chain of bytes, image, or texture loaders + /// contains a loader with the given `id`. + pub fn is_loader_installed(&self, id: &str) -> bool { + let loaders = self.loaders(); + + loaders.bytes.lock().iter().any(|l| l.id() == id) + || loaders.image.lock().iter().any(|l| l.id() == id) + || loaders.texture.lock().iter().any(|l| l.id() == id) + } + + /// Add a new bytes loader. + /// + /// It will be tried first, before any already installed loaders. + /// + /// See [`load`] for more information. + pub fn add_bytes_loader(&self, loader: Arc) { + self.loaders().bytes.lock().push(loader); + } + + /// Add a new image loader. + /// + /// It will be tried first, before any already installed loaders. + /// + /// See [`load`] for more information. + pub fn add_image_loader(&self, loader: Arc) { + self.loaders().image.lock().push(loader); + } + + /// Add a new texture loader. + /// + /// It will be tried first, before any already installed loaders. + /// + /// See [`load`] for more information. + pub fn add_texture_loader(&self, loader: Arc) { + self.loaders().texture.lock().push(loader); + } + + /// Release all memory and textures related to the given image URI. + /// + /// If you attempt to load the image again, it will be reloaded from scratch. + pub fn forget_image(&self, uri: &str) { + use load::BytesLoader as _; + + crate::profile_function!(); + + let loaders = self.loaders(); + + loaders.include.forget(uri); + for loader in loaders.bytes.lock().iter() { + loader.forget(uri); + } + for loader in loaders.image.lock().iter() { + loader.forget(uri); + } + for loader in loaders.texture.lock().iter() { + loader.forget(uri); + } + } + + /// Release all memory and textures related to images used in [`Ui::image`] or [`Image`]. + /// + /// If you attempt to load any images again, they will be reloaded from scratch. + pub fn forget_all_images(&self) { + use load::BytesLoader as _; + + crate::profile_function!(); + + let loaders = self.loaders(); + + loaders.include.forget_all(); + for loader in loaders.bytes.lock().iter() { + loader.forget_all(); + } + for loader in loaders.image.lock().iter() { + loader.forget_all(); + } + for loader in loaders.texture.lock().iter() { + loader.forget_all(); + } + } + + /// Try loading the bytes from the given uri using any available bytes loaders. + /// + /// Loaders are expected to cache results, so that this call is immediate-mode safe. + /// + /// This calls the loaders one by one in the order in which they were registered. + /// If a loader returns [`LoadError::NotSupported`][not_supported], + /// then the next loader is called. This process repeats until all loaders have + /// been exhausted, at which point this returns [`LoadError::NotSupported`][not_supported]. + /// + /// # Errors + /// This may fail with: + /// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`. + /// - [`LoadError::Loading`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed. + /// + /// ⚠ May deadlock if called from within a `BytesLoader`! + /// + /// [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); + + let loaders = self.loaders(); + let bytes_loaders = loaders.bytes.lock(); + + // Try most recently added loaders first (hence `.rev()`) + for loader in bytes_loaders.iter().rev() { + match loader.load(self, uri) { + Err(load::LoadError::NotSupported) => continue, + result => return result, + } + } + + Err(load::LoadError::NoMatchingBytesLoader) + } + + /// Try loading the image from the given uri using any available image loaders. + /// + /// Loaders are expected to cache results, so that this call is immediate-mode safe. + /// + /// This calls the loaders one by one in the order in which they were registered. + /// If a loader returns [`LoadError::NotSupported`][not_supported], + /// then the next loader is called. This process repeats until all loaders have + /// been exhausted, at which point this returns [`LoadError::NotSupported`][not_supported]. + /// + /// # Errors + /// This may fail with: + /// - [`LoadError::NoImageLoaders`][no_image_loaders] if tbere are no registered image loaders. + /// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`. + /// - [`LoadError::Loading`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed. + /// + /// ⚠ May deadlock if called from within an `ImageLoader`! + /// + /// [no_image_loaders]: crate::load::LoadError::NoImageLoaders + /// [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); + + let loaders = self.loaders(); + let image_loaders = loaders.image.lock(); + if image_loaders.is_empty() { + return Err(load::LoadError::NoImageLoaders); + } + + // 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, + result => return result, + } + } + + Err(load::LoadError::NoMatchingImageLoader) + } + + /// Try loading the texture from the given uri using any available texture loaders. + /// + /// Loaders are expected to cache results, so that this call is immediate-mode safe. + /// + /// This calls the loaders one by one in the order in which they were registered. + /// If a loader returns [`LoadError::NotSupported`][not_supported], + /// then the next loader is called. This process repeats until all loaders have + /// been exhausted, at which point this returns [`LoadError::NotSupported`][not_supported]. + /// + /// # Errors + /// This may fail with: + /// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`. + /// - [`LoadError::Loading`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed. + /// + /// ⚠ May deadlock if called from within a `TextureLoader`! + /// + /// [not_supported]: crate::load::LoadError::NotSupported + /// [custom]: crate::load::LoadError::Loading + pub fn try_load_texture( + &self, + uri: &str, + texture_options: TextureOptions, + size_hint: load::SizeHint, + ) -> load::TextureLoadResult { + crate::profile_function!(uri); + + let loaders = self.loaders(); + let texture_loaders = loaders.texture.lock(); + + // Try most recently added loaders first (hence `.rev()`) + for loader in texture_loaders.iter().rev() { + match loader.load(self, uri, texture_options, size_hint) { + Err(load::LoadError::NotSupported) => continue, + result => return result, + } + } + + Err(load::LoadError::NoMatchingTextureLoader) + } + + /// The loaders of bytes, images, and textures. + pub fn loaders(&self) -> Arc { + crate::profile_function!(); + self.read(|this| this.loaders.clone()) + } +} + +/// ## Viewports +impl Context { + /// Return the `ViewportId` of the current viewport. + /// + /// If this is the root viewport, this will return [`ViewportId::ROOT`]. + /// + /// Don't use this outside of `Self::run`, or after `Self::end_frame`. + pub fn viewport_id(&self) -> ViewportId { + self.read(|ctx| ctx.viewport_id()) + } + + /// Return the `ViewportId` of his parent. + /// + /// If this is the root viewport, this will return [`ViewportId::ROOT`]. + /// + /// Don't use this outside of `Self::run`, or after `Self::end_frame`. + pub fn parent_viewport_id(&self) -> ViewportId { + self.read(|ctx| ctx.parent_viewport_id()) + } + + /// For integrations: Set this to render a sync viewport. + /// + /// This will only set the callback for the current thread, + /// which most likely should be the main thread. + /// + /// When an immediate viewport is created with [`Self::show_viewport_immediate`] it will be rendered by this function. + /// + /// When called, the integration needs to: + /// * Check if there already is a window for this viewport id, and if not open one + /// * Set the window attributes (position, size, …) based on [`ImmediateViewport::builder`]. + /// * Call [`Context::run`] with [`ImmediateViewport::viewport_ui_cb`]. + /// * Handle the output from [`Context::run`], including rendering + #[allow(clippy::unused_self)] + pub fn set_immediate_viewport_renderer( + callback: impl for<'a> Fn(&Self, ImmediateViewport<'a>) + 'static, + ) { + let callback = Box::new(callback); + IMMEDIATE_VIEWPORT_RENDERER.with(|render_sync| { + render_sync.replace(Some(callback)); + }); + } + + /// If `true`, [`Self::show_viewport_deferred`] and [`Self::show_viewport_immediate`] will + /// embed the new viewports inside the existing one, instead of spawning a new native window. + /// + /// `eframe` sets this to `false` on supported platforms, but the default value is `true`. + pub fn embed_viewports(&self) -> bool { + self.read(|ctx| ctx.embed_viewports) + } + + /// If `true`, [`Self::show_viewport_deferred`] and [`Self::show_viewport_immediate`] will + /// embed the new viewports inside the existing one, instead of spawning a new native window. + /// + /// `eframe` sets this to `false` on supported platforms, but the default value is `true`. + pub fn set_embed_viewports(&self, value: bool) { + self.write(|ctx| ctx.embed_viewports = value); + } + + /// Send a command to the current viewport. + /// + /// This lets you affect the current viewport, e.g. resizing the window. + pub fn send_viewport_cmd(&self, command: ViewportCommand) { + self.send_viewport_cmd_to(self.viewport_id(), command); + } + + /// Send a command to a specific viewport. + /// + /// This lets you affect another viewport, e.g. resizing its window. + pub fn send_viewport_cmd_to(&self, id: ViewportId, command: ViewportCommand) { + self.request_repaint_of(id); + + if command.requires_parent_repaint() { + self.request_repaint_of(self.parent_viewport_id()); + } + + self.write(|ctx| ctx.viewport_for(id).commands.push(command)); + } + + /// Show a deferred viewport, creating a new native window, if possible. + /// + /// The given id must be unique for each viewport. + /// + /// You need to call this each frame when the child viewport should exist. + /// + /// You can check if the user wants to close the viewport by checking the + /// [`crate::ViewportInfo::close_requested`] flags found in [`crate::InputState::viewport`]. + /// + /// The given callback will be called whenever the child viewport needs repainting, + /// e.g. on an event or when [`Self::request_repaint`] is called. + /// This means it may be called multiple times, for instance while the + /// parent viewport (the caller) is sleeping but the child viewport is animating. + /// + /// You will need to wrap your viewport state in an `Arc>` or `Arc>`. + /// When this is called again with the same id in `ViewportBuilder` the render function for that viewport will be updated. + /// + /// You can also use [`Self::show_viewport_immediate`], which uses a simpler `FnOnce` + /// with no need for `Send` or `Sync`. The downside is that it will require + /// the parent viewport (the caller) to repaint anytime the child is repainted, + /// and vice versa. + /// + /// If [`Context::embed_viewports`] is `true` (e.g. if the current egui + /// backend does not support multiple viewports), the given callback + /// will be called immediately, embedding the new viewport in the current one. + /// You can check this with the [`ViewportClass`] given in the callback. + /// If you find [`ViewportClass::Embedded`], you need to create a new [`crate::Window`] for you content. + /// + /// See [`crate::viewport`] for more information about viewports. + pub fn show_viewport_deferred( + &self, + new_viewport_id: ViewportId, + viewport_builder: ViewportBuilder, + viewport_ui_cb: impl Fn(&Self, ViewportClass) + Send + Sync + 'static, + ) { + crate::profile_function!(); + + if self.embed_viewports() { + viewport_ui_cb(self, ViewportClass::Embedded); + } else { + self.write(|ctx| { + ctx.viewport_parents + .insert(new_viewport_id, ctx.viewport_id()); + + let viewport = ctx.viewports.entry(new_viewport_id).or_default(); + viewport.class = ViewportClass::Deferred; + viewport.builder = viewport_builder; + viewport.used = true; + viewport.viewport_ui_cb = Some(Arc::new(move |ctx| { + (viewport_ui_cb)(ctx, ViewportClass::Deferred); + })); + }); + } + } + + /// Show an immediate viewport, creating a new native window, if possible. + /// + /// 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. + /// To avoid this, use [`Self::show_viewport_deferred`] instead. + /// + /// The given id must be unique for each viewport. + /// + /// You need to call this each frame when the child viewport should exist. + /// + /// You can check if the user wants to close the viewport by checking the + /// [`crate::ViewportInfo::close_requested`] flags found in [`crate::InputState::viewport`]. + /// + /// The given ui function will be called immediately. + /// This may only be called on the main thread. + /// This call will pause the current viewport and render the child viewport in its own window. + /// This means that the child viewport will not be repainted when the parent viewport is repainted, and vice versa. + /// + /// If [`Context::embed_viewports`] is `true` (e.g. if the current egui + /// backend does not support multiple viewports), the given callback + /// will be called immediately, embedding the new viewport in the current one. + /// You can check this with the [`ViewportClass`] given in the callback. + /// If you find [`ViewportClass::Embedded`], you need to create a new [`crate::Window`] for you content. + /// + /// See [`crate::viewport`] for more information about viewports. + pub fn show_viewport_immediate( + &self, + new_viewport_id: ViewportId, + builder: ViewportBuilder, + viewport_ui_cb: impl FnOnce(&Self, ViewportClass) -> T, + ) -> T { + crate::profile_function!(); + + if self.embed_viewports() { + return viewport_ui_cb(self, ViewportClass::Embedded); + } + + IMMEDIATE_VIEWPORT_RENDERER.with(|immediate_viewport_renderer| { + let immediate_viewport_renderer = immediate_viewport_renderer.borrow(); + let Some(immediate_viewport_renderer) = immediate_viewport_renderer.as_ref() else { + // This egui backend does not support multiple viewports. + return viewport_ui_cb(self, ViewportClass::Embedded); + }; + + let ids = self.write(|ctx| { + let parent_viewport_id = ctx.viewport_id(); + + ctx.viewport_parents + .insert(new_viewport_id, parent_viewport_id); + + let viewport = ctx.viewports.entry(new_viewport_id).or_default(); + viewport.builder = builder.clone(); + viewport.used = true; + viewport.viewport_ui_cb = None; // it is immediate + + ViewportIdPair::from_self_and_parent(new_viewport_id, parent_viewport_id) + }); + + let mut out = None; + { + let out = &mut out; + + let viewport = ImmediateViewport { + ids, + builder, + viewport_ui_cb: Box::new(move |context| { + *out = Some(viewport_ui_cb(context, ViewportClass::Immediate)); + }), + }; + + immediate_viewport_renderer(self, viewport); + } + + out.expect( + "egui backend is implemented incorrectly - the user callback was never called", + ) }) } } +/// ## Interaction +impl Context { + /// Read you what widgets are currently being interacted with. + pub fn interaction_snapshot(&self, reader: impl FnOnce(&InteractionSnapshot) -> R) -> R { + self.write(|w| reader(&w.viewport().interact_widgets)) + } + + /// The widget currently being dragged, if any. + /// + /// For widgets that sense both clicks and drags, this will + /// not be set until the mouse cursor has moved a certain distance. + /// + /// NOTE: if the widget was released this frame, this will be `None`. + /// Use [`Self::drag_stopped_id`] instead. + pub fn dragged_id(&self) -> Option { + self.interaction_snapshot(|i| i.dragged) + } + + /// Is this specific widget being dragged? + /// + /// A widget that sense both clicks and drags is only marked as "dragged" + /// when the mouse has moved a bit + /// + /// See also: [`crate::Response::dragged`]. + pub fn is_being_dragged(&self, id: Id) -> bool { + self.dragged_id() == Some(id) + } + + /// This widget just started being dragged this frame. + /// + /// The same widget should also be found in [`Self::dragged_id`]. + pub fn drag_started_id(&self) -> Option { + self.interaction_snapshot(|i| i.drag_started) + } + + /// This widget was being dragged, but was released this frame + pub fn drag_stopped_id(&self) -> Option { + self.interaction_snapshot(|i| i.drag_stopped) + } + + /// Set which widget is being dragged. + pub fn set_dragged_id(&self, id: Id) { + self.write(|ctx| { + let vp = ctx.viewport(); + let i = &mut vp.interact_widgets; + if i.dragged != Some(id) { + i.drag_stopped = i.dragged.or(i.drag_stopped); + i.dragged = Some(id); + i.drag_started = Some(id); + } + + ctx.memory.interaction_mut().potential_drag_id = Some(id); + }); + } + + /// Stop dragging any widget. + pub fn stop_dragging(&self) { + self.write(|ctx| { + let vp = ctx.viewport(); + let i = &mut vp.interact_widgets; + if i.dragged.is_some() { + i.drag_stopped = i.dragged; + i.dragged = None; + } + + ctx.memory.interaction_mut().potential_drag_id = None; + }); + } + + /// Is something else being dragged? + /// + /// Returns true if we are dragging something, but not the given widget. + #[inline(always)] + pub fn dragging_something_else(&self, not_this: Id) -> bool { + let dragged = self.dragged_id(); + dragged.is_some() && dragged != Some(not_this) + } +} + #[test] fn context_impl_send_sync() { fn assert_send_sync() {} diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index ed61aa216e1..f61b9312f89 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -1,6 +1,8 @@ //! The input needed by egui. -use crate::emath::*; +use epaint::ColorImage; + +use crate::{emath::*, Key, ViewportId, ViewportIdMap}; /// What the integrations provides to egui at the start of each frame. /// @@ -9,10 +11,19 @@ use crate::emath::*; /// You can check if `egui` is using the inputs using /// [`crate::Context::wants_pointer_input`] and [`crate::Context::wants_keyboard_input`]. /// -/// All coordinates are in points (logical pixels) with origin (0, 0) in the top left corner. +/// All coordinates are in points (logical pixels) with origin (0, 0) in the top left .corner. +/// +/// Ii "points" can be calculated from native physical pixels +/// using `pixels_per_point` = [`crate::Context::zoom_factor`] * `native_pixels_per_point`; #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct RawInput { + /// The id of the active viewport. + pub viewport_id: ViewportId, + + /// Information about all egui viewports. + pub viewports: ViewportIdMap, + /// Position and size of the area that egui should use, in points. /// Usually you would set this to /// @@ -23,11 +34,6 @@ pub struct RawInput { /// `None` will be treated as "same as last frame", with the default being a very big area. pub screen_rect: Option, - /// Also known as device pixel ratio, > 1 for high resolution screens. - /// If text looks blurry you probably forgot to set this. - /// Set this the first frame, whenever it changes, or just on every frame. - pub pixels_per_point: Option, - /// Maximum size of one side of the font texture. /// /// Ask your graphics drivers about this. This corresponds to `GL_MAX_TEXTURE_SIZE`. @@ -72,8 +78,9 @@ pub struct RawInput { impl Default for RawInput { fn default() -> Self { Self { + viewport_id: ViewportId::ROOT, + viewports: std::iter::once((ViewportId::ROOT, Default::default())).collect(), screen_rect: None, - pixels_per_point: None, max_texture_side: None, time: None, predicted_dt: 1.0 / 60.0, @@ -87,14 +94,21 @@ impl Default for RawInput { } impl RawInput { + /// Info about the active viewport + #[inline] + pub fn viewport(&self) -> &ViewportInfo { + self.viewports.get(&self.viewport_id).expect("Failed to find current viewport in egui RawInput. This is the fault of the egui backend") + } + /// Helper: move volatile (deltas and events), clone the rest. /// /// * [`Self::hovered_files`] is cloned. /// * [`Self::dropped_files`] is moved. - pub fn take(&mut self) -> RawInput { - RawInput { + pub fn take(&mut self) -> Self { + Self { + viewport_id: self.viewport_id, + viewports: self.viewports.clone(), screen_rect: self.screen_rect.take(), - pixels_per_point: self.pixels_per_point.take(), max_texture_side: self.max_texture_side.take(), time: self.time.take(), predicted_dt: self.predicted_dt, @@ -109,8 +123,9 @@ impl RawInput { /// Add on new input. pub fn append(&mut self, newer: Self) { let Self { + viewport_id: viewport_ids, + viewports, screen_rect, - pixels_per_point, max_texture_side, time, predicted_dt, @@ -121,8 +136,9 @@ impl RawInput { focused, } = newer; + self.viewport_id = viewport_ids; + self.viewports = viewports; self.screen_rect = screen_rect.or(self.screen_rect); - self.pixels_per_point = pixels_per_point.or(self.pixels_per_point); self.max_texture_side = max_texture_side.or(self.max_texture_side); self.time = time; // use latest time self.predicted_dt = predicted_dt; // use latest dt @@ -134,6 +150,164 @@ impl RawInput { } } +/// An input event from the backend into egui, about a specific [viewport](crate::viewport). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum ViewportEvent { + /// The user clicked the close-button on the window, or similar. + /// + /// If this is the root viewport, the application will exit + /// after this frame unless you send a + /// [`crate::ViewportCommand::CancelClose`] command. + /// + /// If this is not the root viewport, + /// it is up to the user to hide this viewport the next frame. + /// + /// This even will wake up both the child and parent viewport. + Close, +} + +/// Information about the current viewport, given as input each frame. +/// +/// `None` means "unknown". +/// +/// All units are in ui "points", which can be calculated from native physical pixels +/// using `pixels_per_point` = [`crate::Context::zoom_factor`] * `[Self::native_pixels_per_point`]; +#[derive(Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct ViewportInfo { + /// Parent viewport, if known. + pub parent: Option, + + /// Name of the viewport, if known. + pub title: Option, + + pub events: Vec, + + /// The OS native pixels-per-point. + /// + /// This should always be set, if known. + /// + /// On web this takes browser scaling into account, + /// and orresponds to [`window.devicePixelRatio`](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio) in JavaScript. + pub native_pixels_per_point: Option, + + /// Current monitor size in egui points. + pub monitor_size: Option, + + /// The inner rectangle of the native window, in monitor space and ui points scale. + /// + /// This is the content rectangle of the viewport. + pub inner_rect: Option, + + /// The outer rectangle of the native window, in monitor space and ui points scale. + /// + /// This is the content rectangle plus decoration chrome. + pub outer_rect: Option, + + /// Are we minimized? + pub minimized: Option, + + /// Are we maximized? + pub maximized: Option, + + /// Are we in fullscreen mode? + pub fullscreen: Option, + + /// Is the window focused and able to receive input? + /// + /// This should be the same as [`RawInput::focused`]. + pub focused: Option, +} + +impl ViewportInfo { + /// This viewport has been told to close. + /// + /// If this is the root viewport, the application will exit + /// after this frame unless you send a + /// [`crate::ViewportCommand::CancelClose`] command. + /// + /// If this is not the root viewport, + /// it is up to the user to hide this viewport the next frame. + pub fn close_requested(&self) -> bool { + self.events + .iter() + .any(|&event| event == ViewportEvent::Close) + } + + pub fn ui(&self, ui: &mut crate::Ui) { + let Self { + parent, + title, + events, + native_pixels_per_point, + monitor_size, + inner_rect, + outer_rect, + minimized, + maximized, + fullscreen, + focused, + } = self; + + crate::Grid::new("viewport_info").show(ui, |ui| { + ui.label("Parent:"); + ui.label(opt_as_str(parent)); + ui.end_row(); + + ui.label("Title:"); + ui.label(opt_as_str(title)); + ui.end_row(); + + ui.label("Events:"); + ui.label(format!("{events:?}")); + ui.end_row(); + + ui.label("Native pixels-per-point:"); + ui.label(opt_as_str(native_pixels_per_point)); + ui.end_row(); + + ui.label("Monitor size:"); + ui.label(opt_as_str(monitor_size)); + ui.end_row(); + + ui.label("Inner rect:"); + ui.label(opt_rect_as_string(inner_rect)); + ui.end_row(); + + ui.label("Outer rect:"); + ui.label(opt_rect_as_string(outer_rect)); + ui.end_row(); + + ui.label("Minimized:"); + ui.label(opt_as_str(minimized)); + ui.end_row(); + + ui.label("Maximized:"); + ui.label(opt_as_str(maximized)); + ui.end_row(); + + ui.label("Fullscreen:"); + ui.label(opt_as_str(fullscreen)); + ui.end_row(); + + ui.label("Focused:"); + ui.label(opt_as_str(focused)); + ui.end_row(); + + fn opt_rect_as_string(v: &Option) -> String { + v.as_ref().map_or(String::new(), |r| { + format!("Pos: {:?}, size: {:?}", r.min, r.size()) + }) + } + + fn opt_as_str(v: &Option) -> String { + v.as_ref().map_or(String::new(), |v| format!("{v:?}")) + } + }); + } +} + /// A file about to be dropped into egui. #[derive(Clone, Debug, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -155,6 +329,9 @@ pub struct DroppedFile { /// Name of the file. Set by the `eframe` web backend. pub name: String, + /// With the `eframe` web backend, this is set to the mime-type of the file (if available). + pub mime: String, + /// Set by the `eframe` web backend. pub last_modified: Option, @@ -184,8 +361,22 @@ pub enum Event { /// A key was pressed or released. Key { + /// The logical key, heeding the users keymap. + /// + /// For instance, if the user is using Dvorak keyboard layout, + /// this will take that into account. key: Key, + /// The physical key, corresponding to the actual position on the keyboard. + /// + /// This ignores keymaps, so it is not recommended to use this. + /// The only thing it makes sense for is things like games, + /// where e.g. the physical location of WSAD on QWERTY should always map to movement, + /// even if the user is using Dvorak or AZERTY. + /// + /// `eframe` does not (yet) implement this on web. + physical_key: Option, + /// Was it pressed or released? pressed: bool, @@ -205,6 +396,12 @@ pub enum Event { /// The mouse or touch moved to a new place. PointerMoved(Pos2), + /// The mouse moved, the units are unspecified. + /// Represents the actual movement of the mouse, without acceleration or clamped by screen edges. + /// `PointerMoved` and `MouseMoved` can be sent at the same time. + /// This event is optional. If the integration can not determine unfiltered motion it should not send this event. + MouseMoved(Vec2), + /// A mouse button was pressed or released (or a touch started or stopped). PointerButton { /// Where is the pointer? @@ -302,6 +499,12 @@ pub enum Event { /// An assistive technology (e.g. screen reader) requested an action. #[cfg(feature = "accesskit")] AccessKitActionRequest(accesskit::ActionRequest), + + /// The reply of a screenshot requested with [`crate::ViewportCommand::Screenshot`]. + Screenshot { + viewport_id: crate::ViewportId, + image: std::sync::Arc, + }, } /// Mouse button (or similar for touch input) @@ -389,15 +592,6 @@ impl Modifiers { command: false, }; - #[deprecated = "Use `Modifiers::ALT | Modifiers::SHIFT` instead"] - pub const ALT_SHIFT: Self = Self { - alt: true, - ctrl: false, - shift: true, - mac_cmd: false, - command: false, - }; - /// The Mac ⌘ Command key pub const MAC_CMD: Self = Self { alt: false, @@ -452,6 +646,11 @@ impl Modifiers { !self.is_none() } + #[inline] + pub fn all(&self) -> bool { + self.alt && self.ctrl && self.shift && self.command + } + /// Is shift the only pressed button? #[inline] pub fn shift_only(&self) -> bool { @@ -464,13 +663,68 @@ impl Modifiers { !self.alt && !self.shift && self.command } + /// Checks that the `ctrl/cmd` matches, and that the `shift/alt` of the argument is a subset + /// of the pressed ksey (`self`). + /// + /// This means that if the pattern has not set `shift`, then `self` can have `shift` set or not. + /// + /// The reason is that many logical keys require `shift` or `alt` on some keyboard layouts. + /// For instance, in order to press `+` on an English keyboard, you need to press `shift` and `=`, + /// but a Swedish keyboard has dedicated `+` key. + /// So if you want to make a [`KeyboardShortcut`] looking for `Cmd` + `+`, it makes sense + /// to ignore the shift key. + /// Similarly, the `Alt` key is sometimes used to type special characters. + /// + /// However, if the pattern (the argument) explicitly requires the `shift` or `alt` keys + /// to be pressed, then they must be pressed. + /// + /// # Example: + /// ``` + /// # use egui::Modifiers; + /// # let pressed_modifiers = Modifiers::default(); + /// if pressed_modifiers.matches(Modifiers::ALT | Modifiers::SHIFT) { + /// // Alt and Shift are pressed, and nothing else + /// } + /// ``` + /// + /// ## Behavior: + /// ``` + /// # use egui::Modifiers; + /// assert!(Modifiers::CTRL.matches_logically(Modifiers::CTRL)); + /// assert!(!Modifiers::CTRL.matches_logically(Modifiers::CTRL | Modifiers::SHIFT)); + /// assert!((Modifiers::CTRL | Modifiers::SHIFT).matches_logically(Modifiers::CTRL)); + /// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_logically(Modifiers::CTRL)); + /// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_logically(Modifiers::COMMAND)); + /// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches_logically(Modifiers::COMMAND)); + /// assert!(!Modifiers::COMMAND.matches_logically(Modifiers::MAC_CMD)); + /// ``` + pub fn matches_logically(&self, pattern: Self) -> bool { + if pattern.alt && !self.alt { + return false; + } + if pattern.shift && !self.shift { + return false; + } + + self.cmd_ctrl_matches(pattern) + } + /// Check for equality but with proper handling of [`Self::command`]. /// + /// `self` here are the currently pressed modifiers, + /// and the argument the pattern we are testing for. + /// + /// Note that this will require the `shift` and `alt` keys match, even though + /// these modifiers are sometimes required to produce some logical keys. + /// For instance, to press `+` on an English keyboard, you need to press `shift` and `=`, + /// but on a Swedish keyboard you can press the dedicated `+` key. + /// Therefore, you often want to use [`Self::matches_logically`] instead. + /// /// # Example: /// ``` /// # use egui::Modifiers; - /// # let current_modifiers = Modifiers::default(); - /// if current_modifiers.matches(Modifiers::ALT | Modifiers::SHIFT) { + /// # let pressed_modifiers = Modifiers::default(); + /// if pressed_modifiers.matches(Modifiers::ALT | Modifiers::SHIFT) { /// // Alt and Shift are pressed, and nothing else /// } /// ``` @@ -486,12 +740,28 @@ impl Modifiers { /// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches(Modifiers::COMMAND)); /// assert!(!Modifiers::COMMAND.matches(Modifiers::MAC_CMD)); /// ``` - pub fn matches(&self, pattern: Modifiers) -> bool { + pub fn matches_exact(&self, pattern: Self) -> bool { // alt and shift must always match the pattern: if pattern.alt != self.alt || pattern.shift != self.shift { return false; } + self.cmd_ctrl_matches(pattern) + } + + #[deprecated = "Renamed `matches_exact`, but maybe you want to use `matches_logically` instead"] + pub fn matches(&self, pattern: Self) -> bool { + self.matches_exact(pattern) + } + + /// Checks only cmd/ctrl, not alt/shift. + /// + /// `self` here are the currently pressed modifiers, + /// and the argument the pattern we are testing for. + /// + /// This takes care to properly handle the difference between + /// [`Self::ctrl`], [`Self::command`] and [`Self::mac_cmd`]. + pub fn cmd_ctrl_matches(&self, pattern: Self) -> bool { if pattern.mac_cmd { // Mac-specific match: if !self.mac_cmd { @@ -536,12 +806,12 @@ impl Modifiers { /// assert!((Modifiers::CTRL | Modifiers::SHIFT).contains(Modifiers::CTRL)); /// assert!(!Modifiers::CTRL.contains(Modifiers::CTRL | Modifiers::SHIFT)); /// ``` - pub fn contains(&self, query: Modifiers) -> bool { - if query == Modifiers::default() { + pub fn contains(&self, query: Self) -> bool { + if query == Self::default() { return true; } - let Modifiers { + let Self { alt, ctrl, shift, @@ -550,27 +820,27 @@ impl Modifiers { } = *self; if alt && query.alt { - return self.contains(Modifiers { + return self.contains(Self { alt: false, ..query }); } if shift && query.shift { - return self.contains(Modifiers { + return self.contains(Self { shift: false, ..query }); } if (ctrl || command) && (query.ctrl || query.command) { - return self.contains(Modifiers { + return self.contains(Self { command: false, ctrl: false, ..query }); } if (mac_cmd || command) && (query.mac_cmd || query.command) { - return self.contains(Modifiers { + return self.contains(Self { mac_cmd: false, command: false, ..query @@ -663,232 +933,24 @@ impl<'a> ModifierNames<'a> { // ---------------------------------------------------------------------------- -/// Keyboard keys. -/// -/// Includes all keys egui is interested in (such as `Home` and `End`) -/// plus a few that are useful for detecting keyboard shortcuts. -/// -/// Many keys are omitted because they are not always physical keys (depending on keyboard language), e.g. `;` and `§`, -/// and are therefore unsuitable as keyboard shortcuts if you want your app to be portable. -#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum Key { - ArrowDown, - ArrowLeft, - ArrowRight, - ArrowUp, - - Escape, - Tab, - Backspace, - Enter, - Space, - - Insert, - Delete, - Home, - End, - PageUp, - PageDown, - - /// The virtual keycode for the Minus key. - Minus, - - /// The virtual keycode for the Plus/Equals key. - PlusEquals, - - /// Either from the main row or from the numpad. - Num0, - - /// Either from the main row or from the numpad. - Num1, - - /// Either from the main row or from the numpad. - Num2, - - /// Either from the main row or from the numpad. - Num3, - - /// Either from the main row or from the numpad. - Num4, - - /// Either from the main row or from the numpad. - Num5, - - /// Either from the main row or from the numpad. - Num6, - - /// Either from the main row or from the numpad. - Num7, - - /// Either from the main row or from the numpad. - Num8, - - /// Either from the main row or from the numpad. - Num9, - - A, // Used for cmd+A (select All) - B, - C, // |CMD COPY| - D, // |CMD BOOKMARK| - E, // |CMD SEARCH| - F, // |CMD FIND firefox & chrome| - G, // |CMD FIND chrome| - H, // |CMD History| - I, // italics - J, // |CMD SEARCH firefox/DOWNLOAD chrome| - K, // Used for ctrl+K (delete text after cursor) - L, - M, - N, - O, // |CMD OPEN| - P, // |CMD PRINT| - Q, - R, // |CMD REFRESH| - S, // |CMD SAVE| - T, // |CMD TAB| - U, // Used for ctrl+U (delete text before cursor) - V, // |CMD PASTE| - W, // Used for ctrl+W (delete previous word) - X, // |CMD CUT| - Y, - Z, // |CMD UNDO| - - // The function keys: - F1, - F2, - F3, - F4, - F5, // |CMD REFRESH| - F6, - F7, - F8, - F9, - F10, - F11, - F12, - F13, - F14, - F15, - F16, - F17, - F18, - F19, - F20, -} - -impl Key { - /// Emoji or name representing the key - pub fn symbol_or_name(self) -> &'static str { - // TODO(emilk): add support for more unicode symbols (see for instance https://wincent.com/wiki/Unicode_representations_of_modifier_keys). - // Before we do we must first make sure they are supported in `Fonts` though, - // so perhaps this functions needs to take a `supports_character: impl Fn(char) -> bool` or something. - match self { - Key::ArrowDown => "⏷", - Key::ArrowLeft => "⏴", - Key::ArrowRight => "⏵", - Key::ArrowUp => "⏶", - Key::Minus => "-", - Key::PlusEquals => "+", - _ => self.name(), - } - } - - /// Human-readable English name. - pub fn name(self) -> &'static str { - match self { - Key::ArrowDown => "Down", - Key::ArrowLeft => "Left", - Key::ArrowRight => "Right", - Key::ArrowUp => "Up", - Key::Escape => "Escape", - Key::Tab => "Tab", - Key::Backspace => "Backspace", - Key::Enter => "Enter", - Key::Space => "Space", - Key::Insert => "Insert", - Key::Delete => "Delete", - Key::Home => "Home", - Key::End => "End", - Key::PageUp => "PageUp", - Key::PageDown => "PageDown", - Key::Minus => "Minus", - Key::PlusEquals => "Plus", - Key::Num0 => "0", - Key::Num1 => "1", - Key::Num2 => "2", - Key::Num3 => "3", - Key::Num4 => "4", - Key::Num5 => "5", - Key::Num6 => "6", - Key::Num7 => "7", - Key::Num8 => "8", - Key::Num9 => "9", - Key::A => "A", - Key::B => "B", - Key::C => "C", - Key::D => "D", - Key::E => "E", - Key::F => "F", - Key::G => "G", - Key::H => "H", - Key::I => "I", - Key::J => "J", - Key::K => "K", - Key::L => "L", - Key::M => "M", - Key::N => "N", - Key::O => "O", - Key::P => "P", - Key::Q => "Q", - Key::R => "R", - Key::S => "S", - Key::T => "T", - Key::U => "U", - Key::V => "V", - Key::W => "W", - Key::X => "X", - Key::Y => "Y", - Key::Z => "Z", - Key::F1 => "F1", - Key::F2 => "F2", - Key::F3 => "F3", - Key::F4 => "F4", - Key::F5 => "F5", - Key::F6 => "F6", - Key::F7 => "F7", - Key::F8 => "F8", - Key::F9 => "F9", - Key::F10 => "F10", - Key::F11 => "F11", - Key::F12 => "F12", - Key::F13 => "F13", - Key::F14 => "F14", - Key::F15 => "F15", - Key::F16 => "F16", - Key::F17 => "F17", - Key::F18 => "F18", - Key::F19 => "F19", - Key::F20 => "F20", - } - } -} - -// ---------------------------------------------------------------------------- - /// A keyboard shortcut, e.g. `Ctrl+Alt+W`. /// /// Can be used with [`crate::InputState::consume_shortcut`] /// and [`crate::Context::format_shortcut`]. #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct KeyboardShortcut { pub modifiers: Modifiers, - pub key: Key, + + pub logical_key: Key, } impl KeyboardShortcut { - pub const fn new(modifiers: Modifiers, key: Key) -> Self { - Self { modifiers, key } + pub const fn new(modifiers: Modifiers, logical_key: Key) -> Self { + Self { + modifiers, + logical_key, + } } pub fn format(&self, names: &ModifierNames<'_>, is_mac: bool) -> String { @@ -897,9 +959,9 @@ impl KeyboardShortcut { s += names.concat; } if names.is_short { - s += self.key.symbol_or_name(); + s += self.logical_key.symbol_or_name(); } else { - s += self.key.name(); + s += self.logical_key.name(); } s } @@ -925,8 +987,9 @@ fn format_kb_shortcut() { impl RawInput { pub fn ui(&self, ui: &mut crate::Ui) { let Self { + viewport_id, + viewports, screen_rect, - pixels_per_point, max_texture_side, time, predicted_dt, @@ -937,11 +1000,17 @@ impl RawInput { focused, } = self; + ui.label(format!("Active viwport: {viewport_id:?}")); + for (id, viewport) in viewports { + ui.group(|ui| { + ui.label(format!("Viewport {id:?}")); + ui.push_id(id, |ui| { + viewport.ui(ui); + }); + }); + } ui.label(format!("screen_rect: {screen_rect:?} points")); - ui.label(format!("pixels_per_point: {pixels_per_point:?}")) - .on_hover_text( - "Also called HDPI factor.\nNumber of physical pixels per each logical pixel.", - ); + ui.label(format!("max_texture_side: {max_texture_side:?}")); if let Some(time) = time { ui.label(format!("time: {time:.3} s")); @@ -1025,3 +1094,67 @@ impl From for TouchId { Self(id as u64) } } + +// ---------------------------------------------------------------------------- + +// TODO(emilk): generalize this to a proper event filter. +/// Controls which events that a focused widget will have exclusive access to. +/// +/// Currently this only controls a few special keyboard events, +/// but in the future this `struct` should be extended into a full callback thing. +/// +/// Any events not covered by the filter are given to the widget, but are not exclusive. +#[derive(Clone, Copy, Debug)] +pub struct EventFilter { + /// If `true`, pressing tab will act on the widget, + /// and NOT move focus away from the focused widget. + /// + /// Default: `false` + pub tab: bool, + + /// If `true`, pressing horizontal arrows will act on the + /// widget, and NOT move focus away from the focused widget. + /// + /// Default: `false` + pub horizontal_arrows: bool, + + /// If `true`, pressing vertical arrows will act on the + /// widget, and NOT move focus away from the focused widget. + /// + /// Default: `false` + pub vertical_arrows: bool, + + /// If `true`, pressing escape will act on the widget, + /// and NOT surrender focus from the focused widget. + /// + /// Default: `false` + pub escape: bool, +} + +#[allow(clippy::derivable_impls)] // let's be explicit +impl Default for EventFilter { + fn default() -> Self { + Self { + tab: false, + horizontal_arrows: false, + vertical_arrows: false, + escape: false, + } + } +} + +impl EventFilter { + pub fn matches(&self, event: &Event) -> bool { + if let Event::Key { key, .. } = event { + match key { + crate::Key::Tab => self.tab, + crate::Key::ArrowUp | crate::Key::ArrowDown => self.vertical_arrows, + crate::Key::ArrowRight | crate::Key::ArrowLeft => self.horizontal_arrows, + crate::Key::Escape => self.escape, + _ => true, + } + } else { + true + } + } +} diff --git a/crates/egui/src/data/key.rs b/crates/egui/src/data/key.rs new file mode 100644 index 00000000000..92e6b69dba2 --- /dev/null +++ b/crates/egui/src/data/key.rs @@ -0,0 +1,584 @@ +/// Keyboard keys. +/// +/// egui usually uses logical keys, i.e. after applying any user keymap. +// TODO(emilk): split into `LogicalKey` and `PhysicalKey` +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum Key { + ArrowDown, + ArrowLeft, + ArrowRight, + ArrowUp, + + Escape, + Tab, + Backspace, + Enter, + Space, + + Insert, + Delete, + Home, + End, + PageUp, + PageDown, + + Copy, + Cut, + Paste, + + // ---------------------------------------------- + // Punctuation: + /// `:` + Colon, + + /// `,` + Comma, + + /// '\\' + Backslash, + + /// '/' + Slash, + + /// '|', a vertical bar + Pipe, + + /// `?` + Questionmark, + + // '[' + OpenBracket, + + // ']' + CloseBracket, + + /// '`', also known as "backquote" or "grave" + Backtick, + + /// `-` + Minus, + + /// `.` + Period, + + /// `+` + Plus, + + /// `=` + Equals, + + /// `;` + Semicolon, + + // ---------------------------------------------- + // Digits: + /// Either from the main row or from the numpad. + Num0, + + /// Either from the main row or from the numpad. + Num1, + + /// Either from the main row or from the numpad. + Num2, + + /// Either from the main row or from the numpad. + Num3, + + /// Either from the main row or from the numpad. + Num4, + + /// Either from the main row or from the numpad. + Num5, + + /// Either from the main row or from the numpad. + Num6, + + /// Either from the main row or from the numpad. + Num7, + + /// Either from the main row or from the numpad. + Num8, + + /// Either from the main row or from the numpad. + Num9, + + // ---------------------------------------------- + // Letters: + A, // Used for cmd+A (select All) + B, + C, // |CMD COPY| + D, // |CMD BOOKMARK| + E, // |CMD SEARCH| + F, // |CMD FIND firefox & chrome| + G, // |CMD FIND chrome| + H, // |CMD History| + I, // italics + J, // |CMD SEARCH firefox/DOWNLOAD chrome| + K, // Used for ctrl+K (delete text after cursor) + L, + M, + N, + O, // |CMD OPEN| + P, // |CMD PRINT| + Q, + R, // |CMD REFRESH| + S, // |CMD SAVE| + T, // |CMD TAB| + U, // Used for ctrl+U (delete text before cursor) + V, // |CMD PASTE| + W, // Used for ctrl+W (delete previous word) + X, // |CMD CUT| + Y, + Z, // |CMD UNDO| + + // ---------------------------------------------- + // Function keys: + F1, + F2, + F3, + F4, + F5, // |CMD REFRESH| + F6, + F7, + F8, + F9, + F10, + F11, + F12, + F13, + F14, + F15, + F16, + F17, + F18, + F19, + F20, + F21, + F22, + F23, + F24, + F25, + F26, + F27, + F28, + F29, + F30, + F31, + F32, + F33, + F34, + F35, + // When adding keys, remember to also update `crates/egui-winit/src/lib.rs` + // and [`Self::ALL`]. + // Also: don't add keys last; add them to the group they best belong to. +} + +impl Key { + /// All egui keys + pub const ALL: &'static [Self] = &[ + Self::ArrowDown, + Self::ArrowLeft, + Self::ArrowRight, + Self::ArrowUp, + Self::Escape, + Self::Tab, + Self::Backspace, + Self::Enter, + Self::Insert, + Self::Delete, + Self::Home, + Self::End, + Self::PageUp, + Self::PageDown, + Self::Copy, + Self::Cut, + Self::Paste, + // Punctuation: + Self::Space, + Self::Colon, + Self::Comma, + Self::Minus, + Self::Period, + Self::Plus, + Self::Equals, + Self::Semicolon, + Self::OpenBracket, + Self::CloseBracket, + Self::Backtick, + Self::Backslash, + Self::Slash, + Self::Pipe, + Self::Questionmark, + // Digits: + Self::Num0, + Self::Num1, + Self::Num2, + Self::Num3, + Self::Num4, + Self::Num5, + Self::Num6, + Self::Num7, + Self::Num8, + Self::Num9, + // Letters: + Self::A, + Self::B, + Self::C, + Self::D, + Self::E, + Self::F, + Self::G, + Self::H, + Self::I, + Self::J, + Self::K, + Self::L, + Self::M, + Self::N, + Self::O, + Self::P, + Self::Q, + Self::R, + Self::S, + Self::T, + Self::U, + Self::V, + Self::W, + Self::X, + Self::Y, + Self::Z, + // Function keys: + Self::F1, + Self::F2, + Self::F3, + Self::F4, + Self::F5, + Self::F6, + Self::F7, + Self::F8, + Self::F9, + Self::F10, + Self::F11, + Self::F12, + Self::F13, + Self::F14, + Self::F15, + Self::F16, + Self::F17, + Self::F18, + Self::F19, + Self::F20, + Self::F21, + Self::F22, + Self::F23, + Self::F24, + Self::F25, + Self::F26, + Self::F27, + Self::F28, + Self::F29, + Self::F30, + Self::F31, + Self::F32, + Self::F33, + Self::F34, + Self::F35, + ]; + + /// Converts `"A"` to `Key::A`, `Space` to `Key::Space`, etc. + /// + /// Makes sense for logical keys. + /// + /// This will parse the output of both [`Self::name`] and [`Self::symbol_or_name`], + /// but will also parse single characters, so that both `"-"` and `"Minus"` will return `Key::Minus`. + /// + /// This should support both the names generated in a web browser, + /// and by winit. Please test on both with `eframe`. + pub fn from_name(key: &str) -> Option { + Some(match key { + "⏷" | "ArrowDown" | "Down" => Self::ArrowDown, + "⏴" | "ArrowLeft" | "Left" => Self::ArrowLeft, + "⏵" | "ArrowRight" | "Right" => Self::ArrowRight, + "⏶" | "ArrowUp" | "Up" => Self::ArrowUp, + + "Escape" | "Esc" => Self::Escape, + "Tab" => Self::Tab, + "Backspace" => Self::Backspace, + "Enter" | "Return" => Self::Enter, + + "Help" | "Insert" => Self::Insert, + "Delete" => Self::Delete, + "Home" => Self::Home, + "End" => Self::End, + "PageUp" => Self::PageUp, + "PageDown" => Self::PageDown, + + "Copy" => Self::Copy, + "Cut" => Self::Cut, + "Paste" => Self::Paste, + + " " | "Space" => Self::Space, + ":" | "Colon" => Self::Colon, + "," | "Comma" => Self::Comma, + "-" | "−" | "Minus" => Self::Minus, + "." | "Period" => Self::Period, + "+" | "Plus" => Self::Plus, + "=" | "Equal" | "Equals" | "NumpadEqual" => Self::Equals, + ";" | "Semicolon" => Self::Semicolon, + "\\" | "Backslash" => Self::Backslash, + "/" | "Slash" => Self::Slash, + "|" | "Pipe" => Self::Pipe, + "?" | "Questionmark" => Self::Questionmark, + "[" | "OpenBracket" => Self::OpenBracket, + "]" | "CloseBracket" => Self::CloseBracket, + "`" | "Backtick" | "Backquote" | "Grave" => Self::Backtick, + + "0" | "Digit0" | "Numpad0" => Self::Num0, + "1" | "Digit1" | "Numpad1" => Self::Num1, + "2" | "Digit2" | "Numpad2" => Self::Num2, + "3" | "Digit3" | "Numpad3" => Self::Num3, + "4" | "Digit4" | "Numpad4" => Self::Num4, + "5" | "Digit5" | "Numpad5" => Self::Num5, + "6" | "Digit6" | "Numpad6" => Self::Num6, + "7" | "Digit7" | "Numpad7" => Self::Num7, + "8" | "Digit8" | "Numpad8" => Self::Num8, + "9" | "Digit9" | "Numpad9" => Self::Num9, + + "a" | "A" => Self::A, + "b" | "B" => Self::B, + "c" | "C" => Self::C, + "d" | "D" => Self::D, + "e" | "E" => Self::E, + "f" | "F" => Self::F, + "g" | "G" => Self::G, + "h" | "H" => Self::H, + "i" | "I" => Self::I, + "j" | "J" => Self::J, + "k" | "K" => Self::K, + "l" | "L" => Self::L, + "m" | "M" => Self::M, + "n" | "N" => Self::N, + "o" | "O" => Self::O, + "p" | "P" => Self::P, + "q" | "Q" => Self::Q, + "r" | "R" => Self::R, + "s" | "S" => Self::S, + "t" | "T" => Self::T, + "u" | "U" => Self::U, + "v" | "V" => Self::V, + "w" | "W" => Self::W, + "x" | "X" => Self::X, + "y" | "Y" => Self::Y, + "z" | "Z" => Self::Z, + + "F1" => Self::F1, + "F2" => Self::F2, + "F3" => Self::F3, + "F4" => Self::F4, + "F5" => Self::F5, + "F6" => Self::F6, + "F7" => Self::F7, + "F8" => Self::F8, + "F9" => Self::F9, + "F10" => Self::F10, + "F11" => Self::F11, + "F12" => Self::F12, + "F13" => Self::F13, + "F14" => Self::F14, + "F15" => Self::F15, + "F16" => Self::F16, + "F17" => Self::F17, + "F18" => Self::F18, + "F19" => Self::F19, + "F20" => Self::F20, + "F21" => Self::F21, + "F22" => Self::F22, + "F23" => Self::F23, + "F24" => Self::F24, + "F25" => Self::F25, + "F26" => Self::F26, + "F27" => Self::F27, + "F28" => Self::F28, + "F29" => Self::F29, + "F30" => Self::F30, + "F31" => Self::F31, + "F32" => Self::F32, + "F33" => Self::F33, + "F34" => Self::F34, + "F35" => Self::F35, + + _ => return None, + }) + } + + /// Emoji or name representing the key + pub fn symbol_or_name(self) -> &'static str { + // TODO(emilk): add support for more unicode symbols (see for instance https://wincent.com/wiki/Unicode_representations_of_modifier_keys). + // Before we do we must first make sure they are supported in `Fonts` though, + // so perhaps this functions needs to take a `supports_character: impl Fn(char) -> bool` or something. + match self { + Self::ArrowDown => "⏷", + Self::ArrowLeft => "⏴", + Self::ArrowRight => "⏵", + Self::ArrowUp => "⏶", + + Self::Colon => ":", + Self::Comma => ",", + Self::Minus => crate::MINUS_CHAR_STR, + Self::Period => ".", + Self::Plus => "+", + Self::Equals => "=", + Self::Semicolon => ";", + Self::Backslash => "\\", + Self::Slash => "/", + Self::Pipe => "|", + Self::Questionmark => "?", + Self::OpenBracket => "[", + Self::CloseBracket => "]", + Self::Backtick => "`", + + _ => self.name(), + } + } + + /// Human-readable English name. + pub fn name(self) -> &'static str { + match self { + Self::ArrowDown => "Down", + Self::ArrowLeft => "Left", + Self::ArrowRight => "Right", + Self::ArrowUp => "Up", + + Self::Escape => "Escape", + Self::Tab => "Tab", + Self::Backspace => "Backspace", + Self::Enter => "Enter", + + Self::Insert => "Insert", + Self::Delete => "Delete", + Self::Home => "Home", + Self::End => "End", + Self::PageUp => "PageUp", + Self::PageDown => "PageDown", + + Self::Copy => "Copy", + Self::Cut => "Cut", + Self::Paste => "Paste", + + Self::Space => "Space", + Self::Colon => "Colon", + Self::Comma => "Comma", + Self::Minus => "Minus", + Self::Period => "Period", + Self::Plus => "Plus", + Self::Equals => "Equals", + Self::Semicolon => "Semicolon", + Self::Backslash => "Backslash", + Self::Slash => "Slash", + Self::Pipe => "Pipe", + Self::Questionmark => "Questionmark", + Self::OpenBracket => "OpenBracket", + Self::CloseBracket => "CloseBracket", + Self::Backtick => "Backtick", + + Self::Num0 => "0", + Self::Num1 => "1", + Self::Num2 => "2", + Self::Num3 => "3", + Self::Num4 => "4", + Self::Num5 => "5", + Self::Num6 => "6", + Self::Num7 => "7", + Self::Num8 => "8", + Self::Num9 => "9", + + Self::A => "A", + Self::B => "B", + Self::C => "C", + Self::D => "D", + Self::E => "E", + Self::F => "F", + Self::G => "G", + Self::H => "H", + Self::I => "I", + Self::J => "J", + Self::K => "K", + Self::L => "L", + Self::M => "M", + Self::N => "N", + Self::O => "O", + Self::P => "P", + Self::Q => "Q", + Self::R => "R", + Self::S => "S", + Self::T => "T", + Self::U => "U", + Self::V => "V", + Self::W => "W", + Self::X => "X", + Self::Y => "Y", + Self::Z => "Z", + Self::F1 => "F1", + Self::F2 => "F2", + Self::F3 => "F3", + Self::F4 => "F4", + Self::F5 => "F5", + Self::F6 => "F6", + Self::F7 => "F7", + Self::F8 => "F8", + Self::F9 => "F9", + Self::F10 => "F10", + Self::F11 => "F11", + Self::F12 => "F12", + Self::F13 => "F13", + Self::F14 => "F14", + Self::F15 => "F15", + Self::F16 => "F16", + Self::F17 => "F17", + Self::F18 => "F18", + Self::F19 => "F19", + Self::F20 => "F20", + Self::F21 => "F21", + Self::F22 => "F22", + Self::F23 => "F23", + Self::F24 => "F24", + Self::F25 => "F25", + Self::F26 => "F26", + Self::F27 => "F27", + Self::F28 => "F28", + Self::F29 => "F29", + Self::F30 => "F30", + Self::F31 => "F31", + Self::F32 => "F32", + Self::F33 => "F33", + Self::F34 => "F34", + Self::F35 => "F35", + } + } +} + +#[test] +fn test_key_from_name() { + assert_eq!( + Key::ALL.len(), + Key::F35 as usize + 1, + "Some keys are missing in Key::ALL" + ); + + for &key in Key::ALL { + let name = key.name(); + assert_eq!( + Key::from_name(name), + Some(key), + "Failed to roundtrip {key:?} from name {name:?}" + ); + + let symbol = key.symbol_or_name(); + assert_eq!( + Key::from_name(symbol), + Some(key), + "Failed to roundtrip {key:?} from symbol {symbol:?}" + ); + } +} diff --git a/crates/egui/src/data/mod.rs b/crates/egui/src/data/mod.rs index 5a4b3f1dc50..bfe1e8a327d 100644 --- a/crates/egui/src/data/mod.rs +++ b/crates/egui/src/data/mod.rs @@ -1,4 +1,7 @@ //! All the data sent between egui and the backend pub mod input; +mod key; pub mod output; + +pub use key::Key; diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index 156a3492849..d1db5fc2a41 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -1,35 +1,38 @@ //! All the data egui returns to the backend at the end of each frame. -use crate::WidgetType; +use crate::{ViewportIdMap, ViewportOutput, WidgetType}; /// What egui emits each frame from [`crate::Context::run`]. /// /// The backend should use this. -#[derive(Clone, Default, PartialEq)] +#[derive(Clone, Default)] pub struct FullOutput { /// Non-rendering related output. pub platform_output: PlatformOutput, - /// If `Duration::is_zero()`, egui is requesting immediate repaint (i.e. on the next frame). - /// - /// This happens for instance when there is an animation, or if a user has called `Context::request_repaint()`. - /// - /// If `Duration` is greater than zero, egui wants to be repainted at or before the specified - /// duration elapses. when in reactive mode, egui spends forever waiting for input and only then, - /// will it repaint itself. this can be used to make sure that backend will only wait for a - /// specified amount of time, and repaint egui without any new input. - pub repaint_after: std::time::Duration, - /// Texture changes since last frame (including the font texture). /// /// The backend needs to apply [`crate::TexturesDelta::set`] _before_ painting, /// and free any texture in [`crate::TexturesDelta::free`] _after_ painting. + /// + /// It is assumed that all egui viewports share the same painter and texture namespace. pub textures_delta: epaint::textures::TexturesDelta, /// What to paint. /// /// You can use [`crate::Context::tessellate`] to turn this into triangles. pub shapes: Vec, + + /// The number of physical pixels per logical ui point, for the viewport that was updated. + /// + /// You can pass this to [`crate::Context::tessellate`] together with [`Self::shapes`]. + pub pixels_per_point: f32, + + /// All the active viewports, including the root. + /// + /// It is up to the integration to spawn a native window for each viewport, + /// and to close any window that no longer has a viewport in this map. + pub viewport_output: ViewportIdMap, } impl FullOutput { @@ -37,18 +40,45 @@ impl FullOutput { pub fn append(&mut self, newer: Self) { let Self { platform_output, - repaint_after, textures_delta, shapes, + pixels_per_point, + viewport_output: viewports, } = newer; self.platform_output.append(platform_output); - self.repaint_after = repaint_after; // if the last frame doesn't need a repaint, then we don't need to repaint self.textures_delta.append(textures_delta); self.shapes = shapes; // Only paint the latest + self.pixels_per_point = pixels_per_point; // Use latest + + for (id, new_viewport) in viewports { + match self.viewport_output.entry(id) { + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(new_viewport); + } + std::collections::hash_map::Entry::Occupied(mut entry) => { + entry.get_mut().append(new_viewport); + } + } + } } } +/// Information about text being edited. +/// +/// Useful for IME. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct IMEOutput { + /// Where the [`crate::TextEdit`] is located on screen. + pub rect: crate::Rect, + + /// Where the primary cursor is. + /// + /// This is a very thin rectangle. + pub cursor_rect: crate::Rect, +} + /// The non-rendering part of what egui emits each frame. /// /// You can access (and modify) this with [`crate::Context::output`]. @@ -83,20 +113,19 @@ pub struct PlatformOutput { /// Use by `eframe` web to show/hide mobile keyboard and IME agent. pub mutable_text_under_cursor: bool, - /// Screen-space position of text edit cursor (used for IME). - pub text_cursor_pos: Option, + /// This is et if, and only if, the user is currently editing text. + /// + /// Useful for IME. + pub ime: Option, + /// The difference in the widget tree since last frame. + /// + /// NOTE: this needs to be per-viewport. #[cfg(feature = "accesskit")] pub accesskit_update: Option, } impl PlatformOutput { - /// Open the given url in a web browser. - /// If egui is running in a browser, the same tab will be reused. - pub fn open_url(&mut self, url: impl ToString) { - self.open_url = Some(OpenUrl::same_tab(url)); - } - /// This can be used by a text-to-speech system to describe the events (if any). pub fn events_description(&self) -> String { // only describe last event: @@ -123,7 +152,7 @@ impl PlatformOutput { copied_text, mut events, mutable_text_under_cursor, - text_cursor_pos, + ime, #[cfg(feature = "accesskit")] accesskit_update, } = newer; @@ -137,7 +166,7 @@ impl PlatformOutput { } self.events.append(&mut events); self.mutable_text_under_cursor = mutable_text_under_cursor; - self.text_cursor_pos = text_cursor_pos.or(self.text_cursor_pos); + self.ime = ime.or(self.ime); #[cfg(feature = "accesskit")] { @@ -156,6 +185,8 @@ impl PlatformOutput { } /// What URL to open, and how. +/// +/// Use with [`crate::Context::open_url`]. #[derive(Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct OpenUrl { @@ -191,6 +222,7 @@ impl OpenUrl { /// /// [user_attention_type]: https://docs.rs/winit/latest/winit/window/enum.UserAttentionType.html #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum UserAttentionType { /// Request an elevated amount of animations and flair for the window and the task bar or dock icon. Critical, @@ -331,42 +363,42 @@ pub enum CursorIcon { } impl CursorIcon { - pub const ALL: [CursorIcon; 35] = [ - CursorIcon::Default, - CursorIcon::None, - CursorIcon::ContextMenu, - CursorIcon::Help, - CursorIcon::PointingHand, - CursorIcon::Progress, - CursorIcon::Wait, - CursorIcon::Cell, - CursorIcon::Crosshair, - CursorIcon::Text, - CursorIcon::VerticalText, - CursorIcon::Alias, - CursorIcon::Copy, - CursorIcon::Move, - CursorIcon::NoDrop, - CursorIcon::NotAllowed, - CursorIcon::Grab, - CursorIcon::Grabbing, - CursorIcon::AllScroll, - CursorIcon::ResizeHorizontal, - CursorIcon::ResizeNeSw, - CursorIcon::ResizeNwSe, - CursorIcon::ResizeVertical, - CursorIcon::ResizeEast, - CursorIcon::ResizeSouthEast, - CursorIcon::ResizeSouth, - CursorIcon::ResizeSouthWest, - CursorIcon::ResizeWest, - CursorIcon::ResizeNorthWest, - CursorIcon::ResizeNorth, - CursorIcon::ResizeNorthEast, - CursorIcon::ResizeColumn, - CursorIcon::ResizeRow, - CursorIcon::ZoomIn, - CursorIcon::ZoomOut, + pub const ALL: [Self; 35] = [ + Self::Default, + Self::None, + Self::ContextMenu, + Self::Help, + Self::PointingHand, + Self::Progress, + Self::Wait, + Self::Cell, + Self::Crosshair, + Self::Text, + Self::VerticalText, + Self::Alias, + Self::Copy, + Self::Move, + Self::NoDrop, + Self::NotAllowed, + Self::Grab, + Self::Grabbing, + Self::AllScroll, + Self::ResizeHorizontal, + Self::ResizeNeSw, + Self::ResizeNwSe, + Self::ResizeVertical, + Self::ResizeEast, + Self::ResizeSouthEast, + Self::ResizeSouth, + Self::ResizeSouthWest, + Self::ResizeWest, + Self::ResizeNorthWest, + Self::ResizeNorth, + Self::ResizeNorthEast, + Self::ResizeColumn, + Self::ResizeRow, + Self::ZoomIn, + Self::ZoomOut, ]; } @@ -404,12 +436,12 @@ pub enum OutputEvent { impl OutputEvent { pub fn widget_info(&self) -> &WidgetInfo { match self { - OutputEvent::Clicked(info) - | OutputEvent::DoubleClicked(info) - | OutputEvent::TripleClicked(info) - | OutputEvent::FocusGained(info) - | OutputEvent::TextSelectionChanged(info) - | OutputEvent::ValueChanged(info) => info, + Self::Clicked(info) + | Self::DoubleClicked(info) + | Self::TripleClicked(info) + | Self::FocusGained(info) + | Self::TextSelectionChanged(info) + | Self::ValueChanged(info) => info, } } } @@ -601,6 +633,7 @@ impl WidgetInfo { WidgetType::ColorButton => "color button", WidgetType::ImageButton => "image button", WidgetType::CollapsingHeader => "collapsing header", + WidgetType::ProgressIndicator => "progress indicator", WidgetType::Label | WidgetType::Other => "", }; @@ -620,16 +653,15 @@ impl WidgetInfo { } if typ == &WidgetType::TextEdit { - let text; - if let Some(text_value) = text_value { + let text = if let Some(text_value) = text_value { if text_value.is_empty() { - text = "blank".into(); + "blank".into() } else { - text = text_value.to_string(); + text_value.to_string() } } else { - text = "blank".into(); - } + "blank".into() + }; description = format!("{text}: {description}"); } diff --git a/crates/egui/src/debug_text.rs b/crates/egui/src/debug_text.rs new file mode 100644 index 00000000000..b2551de9ddf --- /dev/null +++ b/crates/egui/src/debug_text.rs @@ -0,0 +1,135 @@ +//! This is an example of how to create a plugin for egui. +//! +//! A plugin usually consist of a struct that holds some state, +//! which is stored using [`Context::data_mut`]. +//! The plugin registers itself onto a specific [`Context`] +//! to get callbacks on certain events ([`Context::on_begin_frame`], [`Context::on_end_frame`]). + +use crate::*; + +/// Register this plugin on the given egui context, +/// so that it will be called every frame. +/// +/// This is a built-in plugin in egui, +/// meaning [`Context`] calls this from its `Default` implementation, +/// so this i marked as `pub(crate)`. +pub(crate) fn register(ctx: &Context) { + ctx.on_end_frame("debug_text", std::sync::Arc::new(State::end_frame)); +} + +/// Print this text next to the cursor at the end of the frame. +/// +/// If you call this multiple times, the text will be appended. +/// +/// This only works if compiled with `debug_assertions`. +/// +/// ``` +/// # let ctx = &egui::Context::default(); +/// # let state = true; +/// egui::debug_text::print(ctx, format!("State: {state:?}")); +/// ``` +#[track_caller] +pub fn print(ctx: &Context, text: impl Into) { + if !cfg!(debug_assertions) { + return; + } + + let location = std::panic::Location::caller(); + let location = format!("{}:{}", location.file(), location.line()); + ctx.data_mut(|data| { + // We use `Id::NULL` as the id, since we only have one instance of this plugin. + // We use the `temp` version instead of `persisted` since we don't want to + // persist state on disk when the egui app is closed. + let state = data.get_temp_mut_or_default::(Id::NULL); + state.entries.push(Entry { + location, + text: text.into(), + }); + }); +} + +#[derive(Clone)] +struct Entry { + location: String, + text: WidgetText, +} + +/// A plugin for easily showing debug-text on-screen. +/// +/// This is a built-in plugin in egui. +#[derive(Clone, Default)] +struct State { + // This gets re-filled every frame. + entries: Vec, +} + +impl State { + fn end_frame(ctx: &Context) { + let state = ctx.data_mut(|data| data.remove_temp::(Id::NULL)); + if let Some(state) = state { + state.paint(ctx); + } + } + + fn paint(self, ctx: &Context) { + let Self { entries } = self; + + if entries.is_empty() { + return; + } + + // Show debug-text next to the cursor. + let mut pos = ctx + .input(|i| i.pointer.latest_pos()) + .unwrap_or_else(|| ctx.screen_rect().center()) + + 8.0 * Vec2::Y; + + let painter = ctx.debug_painter(); + let where_to_put_background = painter.add(Shape::Noop); + + let mut bounding_rect = Rect::from_points(&[pos]); + + let color = Color32::GRAY; + let font_id = FontId::new(10.0, FontFamily::Proportional); + + for Entry { location, text } in entries { + { + // Paint location to left of `pos`: + let location_galley = + ctx.fonts(|f| f.layout(location, font_id.clone(), color, f32::INFINITY)); + let location_rect = + Align2::RIGHT_TOP.anchor_size(pos - 4.0 * Vec2::X, location_galley.size()); + painter.galley(location_rect.min, location_galley, color); + bounding_rect = bounding_rect.union(location_rect); + } + + { + // Paint `text` to right of `pos`: + let wrap = true; + let available_width = ctx.screen_rect().max.x - pos.x; + let galley = text.into_galley_impl( + ctx, + &ctx.style(), + wrap, + available_width, + font_id.clone().into(), + Align::TOP, + ); + let rect = Align2::LEFT_TOP.anchor_size(pos, galley.size()); + painter.galley(rect.min, galley, color); + bounding_rect = bounding_rect.union(rect); + } + + pos.y = bounding_rect.max.y + 4.0; + } + + painter.set( + where_to_put_background, + Shape::rect_filled( + bounding_rect.expand(4.0), + 2.0, + Color32::from_black_alpha(192), + ), + ); + } +} diff --git a/crates/egui/src/drag_and_drop.rs b/crates/egui/src/drag_and_drop.rs new file mode 100644 index 00000000000..b0c26ac8489 --- /dev/null +++ b/crates/egui/src/drag_and_drop.rs @@ -0,0 +1,125 @@ +use std::{any::Any, sync::Arc}; + +use crate::{Context, CursorIcon, Id}; + +/// Tracking of drag-and-drop payload. +/// +/// This is a low-level API. +/// +/// For a higher-level API, see: +/// - [`crate::Ui::dnd_drag_source`] +/// - [`crate::Ui::dnd_drop_zone`] +/// - [`crate::Response::dnd_set_drag_payload`] +/// - [`crate::Response::dnd_hover_payload`] +/// - [`crate::Response::dnd_release_payload`] +/// +/// See [this example](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/drag_and_drop.rs). +#[doc(alias = "drag and drop")] +#[derive(Clone, Default)] +pub struct DragAndDrop { + /// If set, something is currently being dragged + payload: Option>, +} + +impl DragAndDrop { + pub(crate) fn register(ctx: &Context) { + ctx.on_end_frame("debug_text", std::sync::Arc::new(Self::end_frame)); + } + + fn end_frame(ctx: &Context) { + let pointer_released = ctx.input(|i| i.pointer.any_released()); + + let mut is_dragging = false; + + ctx.data_mut(|data| { + let state = data.get_temp_mut_or_default::(Id::NULL); + + if pointer_released { + state.payload = None; + } + + is_dragging = state.payload.is_some(); + }); + + if is_dragging { + ctx.set_cursor_icon(CursorIcon::Grabbing); + } + } + + /// Set a drag-and-drop payload. + /// + /// This can be read by [`Self::payload`] until the pointer is released. + pub fn set_payload(ctx: &Context, payload: Payload) + where + Payload: Any + Send + Sync, + { + ctx.data_mut(|data| { + let state = data.get_temp_mut_or_default::(Id::NULL); + state.payload = Some(Arc::new(payload)); + }); + } + + /// Clears the payload, setting it to `None`. + pub fn clear_payload(ctx: &Context) { + ctx.data_mut(|data| { + let state = data.get_temp_mut_or_default::(Id::NULL); + state.payload = None; + }); + } + + /// Retrieve the payload, if any. + /// + /// Returns `None` if there is no payload, or if it is not of the requested type. + /// + /// Returns `Some` both during a drag and on the frame the pointer is released + /// (if there is a payload). + pub fn payload(ctx: &Context) -> Option> + where + Payload: Any + Send + Sync, + { + ctx.data(|data| { + let state = data.get_temp::(Id::NULL)?; + let payload = state.payload?; + payload.downcast().ok() + }) + } + + /// Retrieve and clear the payload, if any. + /// + /// Returns `None` if there is no payload, or if it is not of the requested type. + /// + /// Returns `Some` both during a drag and on the frame the pointer is released + /// (if there is a payload). + pub fn take_payload(ctx: &Context) -> Option> + where + Payload: Any + Send + Sync, + { + ctx.data_mut(|data| { + let state = data.get_temp_mut_or_default::(Id::NULL); + let payload = state.payload.take()?; + payload.downcast().ok() + }) + } + + /// Are we carrying a payload of the given type? + /// + /// Returns `true` both during a drag and on the frame the pointer is released + /// (if there is a payload). + pub fn has_payload_of_type(ctx: &Context) -> bool + where + Payload: Any + Send + Sync, + { + Self::payload::(ctx).is_some() + } + + /// Are we carrying a payload? + /// + /// Returns `true` both during a drag and on the frame the pointer is released + /// (if there is a payload). + pub fn has_any_payload(ctx: &Context) -> bool { + ctx.data(|data| { + let state = data.get_temp::(Id::NULL); + state.map_or(false, |state| state.payload.is_some()) + }) + } +} diff --git a/crates/egui/src/frame_state.rs b/crates/egui/src/frame_state.rs index 287b35c56e5..87074c2a725 100644 --- a/crates/egui/src/frame_state.rs +++ b/crates/egui/src/frame_state.rs @@ -38,11 +38,6 @@ pub(crate) struct FrameState { /// Initialized to `None` at the start of each frame. pub(crate) tooltip_state: Option, - /// Set to [`InputState::scroll_delta`] on the start of each frame. - /// - /// Cleared by the first [`ScrollArea`] that makes use of it. - pub(crate) scroll_delta: Vec2, // TODO(emilk): move to `InputState` ? - /// horizontal, vertical pub(crate) scroll_target: [Option<(Rangef, Option)>; 2], @@ -54,6 +49,9 @@ pub(crate) struct FrameState { /// Highlight these widgets the next frame. Write to this. pub(crate) highlight_next_frame: IdSet, + + #[cfg(debug_assertions)] + pub(crate) has_debug_viewed_this_frame: bool, } impl Default for FrameState { @@ -64,40 +62,49 @@ impl Default for FrameState { unused_rect: Rect::NAN, used_by_panels: Rect::NAN, tooltip_state: None, - scroll_delta: Vec2::ZERO, scroll_target: [None, None], #[cfg(feature = "accesskit")] accesskit_state: None, highlight_this_frame: Default::default(), highlight_next_frame: Default::default(), + + #[cfg(debug_assertions)] + has_debug_viewed_this_frame: false, } } } impl FrameState { - pub(crate) fn begin_frame(&mut self, input: &InputState) { + pub(crate) fn begin_frame(&mut self, screen_rect: Rect) { + crate::profile_function!(); let Self { used_ids, available_rect, unused_rect, used_by_panels, tooltip_state, - scroll_delta, scroll_target, #[cfg(feature = "accesskit")] accesskit_state, highlight_this_frame, highlight_next_frame, + + #[cfg(debug_assertions)] + has_debug_viewed_this_frame, } = self; used_ids.clear(); - *available_rect = input.screen_rect(); - *unused_rect = input.screen_rect(); + *available_rect = screen_rect; + *unused_rect = screen_rect; *used_by_panels = Rect::NOTHING; *tooltip_state = None; - *scroll_delta = input.scroll_delta; *scroll_target = [None, None]; + #[cfg(debug_assertions)] + { + *has_debug_viewed_this_frame = false; + } + #[cfg(feature = "accesskit")] { *accesskit_state = None; diff --git a/crates/egui/src/grid.rs b/crates/egui/src/grid.rs index acd5c3576c9..374902e3a4b 100644 --- a/crates/egui/src/grid.rs +++ b/crates/egui/src/grid.rs @@ -187,24 +187,27 @@ impl GridLayout { } pub(crate) fn advance(&mut self, cursor: &mut Rect, _frame_rect: Rect, widget_rect: Rect) { - let debug_expand_width = self.style.debug.show_expand_width; - let debug_expand_height = self.style.debug.show_expand_height; - if debug_expand_width || debug_expand_height { - let rect = widget_rect; - let too_wide = rect.width() > self.prev_col_width(self.col); - let too_high = rect.height() > self.prev_row_height(self.row); - - if (debug_expand_width && too_wide) || (debug_expand_height && too_high) { - let painter = self.ctx.debug_painter(); - painter.rect_stroke(rect, 0.0, (1.0, Color32::LIGHT_BLUE)); - - let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0)); - let paint_line_seg = |a, b| painter.line_segment([a, b], stroke); - - if debug_expand_width && too_wide { - paint_line_seg(rect.left_top(), rect.left_bottom()); - paint_line_seg(rect.left_center(), rect.right_center()); - paint_line_seg(rect.right_top(), rect.right_bottom()); + #[cfg(debug_assertions)] + { + let debug_expand_width = self.style.debug.show_expand_width; + let debug_expand_height = self.style.debug.show_expand_height; + if debug_expand_width || debug_expand_height { + let rect = widget_rect; + let too_wide = rect.width() > self.prev_col_width(self.col); + let too_high = rect.height() > self.prev_row_height(self.row); + + if (debug_expand_width && too_wide) || (debug_expand_height && too_high) { + let painter = self.ctx.debug_painter(); + painter.rect_stroke(rect, 0.0, (1.0, Color32::LIGHT_BLUE)); + + let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0)); + let paint_line_seg = |a, b| painter.line_segment([a, b], stroke); + + if debug_expand_width && too_wide { + paint_line_seg(rect.left_top(), rect.left_bottom()); + paint_line_seg(rect.left_center(), rect.right_center()); + paint_line_seg(rect.right_top(), rect.right_bottom()); + } } } } @@ -218,11 +221,17 @@ impl GridLayout { self.col += 1; } - fn paint_row(&mut self, cursor: &mut Rect, painter: &Painter) { + fn paint_row(&mut 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 }; - let Some(row_color) = color_picker(self.row, &self.style) else { return }; - let Some(height) = self.prev_state.row_height(self.row) else {return }; + let Some(color_picker) = self.color_picker.as_ref() else { + return; + }; + let Some(row_color) = color_picker(self.row, &self.style) else { + return; + }; + let Some(height) = self.prev_state.row_height(self.row) else { + return; + }; // Paint background for coming row: let size = Vec2::new(self.prev_state.full_width(self.spacing.x), height); let rect = Rect::from_min_size(cursor.min, size); @@ -310,6 +319,7 @@ impl Grid { } /// Setting this will allow for dynamic coloring of rows of the grid object + #[inline] pub fn with_row_color(mut self, color_picker: F) -> Self where F: Send + Sync + Fn(usize, &Style) -> Option + 'static, @@ -319,6 +329,7 @@ impl Grid { } /// Setting this will allow the last column to expand to take up the rest of the space of the parent [`Ui`]. + #[inline] pub fn num_columns(mut self, num_columns: usize) -> Self { self.num_columns = Some(num_columns); self @@ -330,19 +341,18 @@ impl Grid { /// Default is whatever is in [`crate::Visuals::striped`]. pub fn striped(self, striped: bool) -> Self { if striped { - self.with_row_color(move |row, style| { - if row % 2 == 1 { - return Some(style.visuals.faint_bg_color); - } - None - }) + self.with_row_color(striped_row_color) } else { - self + // Explicitly set the row color to nothing. + // Needed so that when the style.visuals.striped value is checked later on, + // it is clear that the user does not want stripes on this specific Grid. + self.with_row_color(|_row: usize, _style: &Style| None) } } /// Set minimum width of each column. /// Default: [`crate::style::Spacing::interact_size`]`.x`. + #[inline] pub fn min_col_width(mut self, min_col_width: f32) -> Self { self.min_col_width = Some(min_col_width); self @@ -350,12 +360,14 @@ impl Grid { /// Set minimum height of each row. /// Default: [`crate::style::Spacing::interact_size`]`.y`. + #[inline] pub fn min_row_height(mut self, min_row_height: f32) -> Self { self.min_row_height = Some(min_row_height); self } /// Set soft maximum width (wrapping width) of each column. + #[inline] pub fn max_col_width(mut self, max_col_width: f32) -> Self { self.max_cell_size.x = max_col_width; self @@ -363,6 +375,7 @@ impl Grid { /// Set spacing between columns/rows. /// Default: [`crate::style::Spacing::item_spacing`]. + #[inline] pub fn spacing(mut self, spacing: impl Into) -> Self { self.spacing = Some(spacing.into()); self @@ -370,6 +383,7 @@ impl Grid { /// Change which row number the grid starts on. /// This can be useful when you have a large [`Grid`] inside of [`ScrollArea::show_rows`]. + #[inline] pub fn start_row(mut self, start_row: usize) -> Self { self.start_row = start_row; self @@ -394,11 +408,14 @@ impl Grid { max_cell_size, spacing, start_row, - color_picker, + mut color_picker, } = self; let min_col_width = min_col_width.unwrap_or_else(|| ui.spacing().interact_size.x); let min_row_height = min_row_height.unwrap_or_else(|| ui.spacing().interact_size.y); let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing); + if color_picker.is_none() && ui.visuals().striped { + color_picker = Some(Box::new(striped_row_color)); + } let id = ui.make_persistent_id(id_source); let prev_state = State::load(ui.ctx(), id); @@ -424,9 +441,9 @@ impl Grid { // paint first incoming row if is_color { - let mut cursor = ui.cursor(); + let cursor = ui.cursor(); let painter = ui.painter(); - grid.paint_row(&mut cursor, painter); + grid.paint_row(&cursor, painter); } ui.set_grid(grid); @@ -438,3 +455,10 @@ impl Grid { }) } } + +fn striped_row_color(row: usize, style: &Style) -> Option { + if row % 2 == 1 { + return Some(style.visuals.faint_bg_color); + } + None +} diff --git a/crates/egui/src/gui_zoom.rs b/crates/egui/src/gui_zoom.rs index 1bee63880c7..c4b5deb5ed6 100644 --- a/crates/egui/src/gui_zoom.rs +++ b/crates/egui/src/gui_zoom.rs @@ -6,32 +6,37 @@ use crate::*; pub mod kb_shortcuts { use super::*; - pub const ZOOM_IN: KeyboardShortcut = - KeyboardShortcut::new(Modifiers::COMMAND, Key::PlusEquals); + /// Primary keyboard shortcut for zooming in (`Cmd` + `+`). + pub const ZOOM_IN: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::Plus); + + /// Secondary keyboard shortcut for zooming in (`Cmd` + `=`). + /// + /// On an English keyboard `+` is `Shift` + `=`, + /// but it is annoying to have to press shift. + /// So most browsers also allow `Cmd` + `=` for zooming in. + /// We do the same. + pub const ZOOM_IN_SECONDARY: KeyboardShortcut = + KeyboardShortcut::new(Modifiers::COMMAND, Key::Equals); + + /// Keyboard shortcut for zooming in (`Cmd` + `-`). pub const ZOOM_OUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::Minus); + + /// Keyboard shortcut for resetting zoom in (`Cmd` + `0`). pub const ZOOM_RESET: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::Num0); } -/// Let the user scale the GUI (change `Context::pixels_per_point`) by pressing +/// Let the user scale the GUI (change [`Context::zoom_factor`]) by pressing /// Cmd+Plus, Cmd+Minus or Cmd+0, just like in a browser. /// -/// When using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe), you want to call this as: -/// ```ignore -/// // On web, the browser controls the gui zoom. -/// if !frame.is_web() { -/// egui::gui_zoom::zoom_with_keyboard_shortcuts( -/// ctx, -/// frame.info().native_pixels_per_point, -/// ); -/// } -/// ``` -pub fn zoom_with_keyboard_shortcuts(ctx: &Context, native_pixels_per_point: Option) { +/// By default, [`crate::Context`] calls this function at the end of each frame, +/// controllable by [`crate::Options::zoom_with_keyboard`]. +pub(crate) fn zoom_with_keyboard(ctx: &Context) { if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_RESET)) { - if let Some(native_pixels_per_point) = native_pixels_per_point { - ctx.set_pixels_per_point(native_pixels_per_point); - } + ctx.set_zoom_factor(1.0); } else { - if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_IN)) { + if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_IN)) + || ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_IN_SECONDARY)) + { zoom_in(ctx); } if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_OUT)) { @@ -40,47 +45,34 @@ pub fn zoom_with_keyboard_shortcuts(ctx: &Context, native_pixels_per_point: Opti } } -const MIN_PIXELS_PER_POINT: f32 = 0.2; -const MAX_PIXELS_PER_POINT: f32 = 4.0; +const MIN_ZOOM_FACTOR: f32 = 0.2; +const MAX_ZOOM_FACTOR: f32 = 5.0; -/// Make everything larger. +/// Make everything larger by increasing [`Context::zoom_factor`]. pub fn zoom_in(ctx: &Context) { - let mut pixels_per_point = ctx.pixels_per_point(); - pixels_per_point += 0.1; - pixels_per_point = pixels_per_point.clamp(MIN_PIXELS_PER_POINT, MAX_PIXELS_PER_POINT); - pixels_per_point = (pixels_per_point * 10.).round() / 10.; - ctx.set_pixels_per_point(pixels_per_point); + let mut zoom_factor = ctx.zoom_factor(); + zoom_factor += 0.1; + zoom_factor = zoom_factor.clamp(MIN_ZOOM_FACTOR, MAX_ZOOM_FACTOR); + zoom_factor = (zoom_factor * 10.).round() / 10.; + ctx.set_zoom_factor(zoom_factor); } -/// Make everything smaller. +/// Make everything smaller by decreasing [`Context::zoom_factor`]. pub fn zoom_out(ctx: &Context) { - let mut pixels_per_point = ctx.pixels_per_point(); - pixels_per_point -= 0.1; - pixels_per_point = pixels_per_point.clamp(MIN_PIXELS_PER_POINT, MAX_PIXELS_PER_POINT); - pixels_per_point = (pixels_per_point * 10.).round() / 10.; - ctx.set_pixels_per_point(pixels_per_point); + let mut zoom_factor = ctx.zoom_factor(); + zoom_factor -= 0.1; + zoom_factor = zoom_factor.clamp(MIN_ZOOM_FACTOR, MAX_ZOOM_FACTOR); + zoom_factor = (zoom_factor * 10.).round() / 10.; + ctx.set_zoom_factor(zoom_factor); } /// Show buttons for zooming the ui. /// /// This is meant to be called from within a menu (See [`Ui::menu_button`]). -/// -/// When using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe), you want to call this as: -/// ```ignore -/// // On web, the browser controls the gui zoom. -/// if !frame.is_web() { -/// ui.menu_button("View", |ui| { -/// egui::gui_zoom::zoom_menu_buttons( -/// ui, -/// frame.info().native_pixels_per_point, -/// ); -/// }); -/// } -/// ``` -pub fn zoom_menu_buttons(ui: &mut Ui, native_pixels_per_point: Option) { +pub fn zoom_menu_buttons(ui: &mut Ui) { if ui .add_enabled( - ui.ctx().pixels_per_point() < MAX_PIXELS_PER_POINT, + ui.ctx().zoom_factor() < MAX_ZOOM_FACTOR, Button::new("Zoom In").shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_IN)), ) .clicked() @@ -91,7 +83,7 @@ pub fn zoom_menu_buttons(ui: &mut Ui, native_pixels_per_point: Option) { if ui .add_enabled( - ui.ctx().pixels_per_point() > MIN_PIXELS_PER_POINT, + ui.ctx().zoom_factor() > MIN_ZOOM_FACTOR, Button::new("Zoom Out") .shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_OUT)), ) @@ -101,17 +93,15 @@ pub fn zoom_menu_buttons(ui: &mut Ui, native_pixels_per_point: Option) { ui.close_menu(); } - if let Some(native_pixels_per_point) = native_pixels_per_point { - if ui - .add_enabled( - ui.ctx().pixels_per_point() != native_pixels_per_point, - Button::new("Reset Zoom") - .shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_RESET)), - ) - .clicked() - { - ui.ctx().set_pixels_per_point(native_pixels_per_point); - ui.close_menu(); - } + if ui + .add_enabled( + ui.ctx().zoom_factor() != 1.0, + Button::new("Reset Zoom") + .shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_RESET)), + ) + .clicked() + { + ui.ctx().set_zoom_factor(1.0); + ui.close_menu(); } } diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs new file mode 100644 index 00000000000..62a762aa52d --- /dev/null +++ b/crates/egui/src/hit_test.rs @@ -0,0 +1,431 @@ +use ahash::HashMap; + +use emath::TSTransform; + +use crate::*; + +/// Result of a hit-test against [`WidgetRects`]. +/// +/// Answers the question "what is under the mouse pointer?". +/// +/// Note that this doesn't care if the mouse button is pressed or not, +/// or if we're currently already dragging something. +#[derive(Clone, Debug, Default)] +pub struct WidgetHits { + /// All widgets that contains the pointer, back-to-front. + /// + /// 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. + pub contains_pointer: Vec, + + /// If the user would start a clicking now, this is what would be clicked. + /// + /// This is the top one under the pointer, or closest one of the top-most. + pub click: Option, + + /// If the user would start a dragging now, this is what would be dragged. + /// + /// This is the top one under the pointer, or closest one of the top-most. + pub drag: Option, +} + +/// Find the top or closest widgets to the given position, +/// none which is closer than `search_radius`. +pub fn hit_test( + widgets: &WidgetRects, + layer_order: &[LayerId], + layer_transforms: &HashMap, + pos: Pos2, + search_radius: f32, +) -> WidgetHits { + crate::profile_function!(); + + 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 + .iter() + .map(|(layer_id, t)| (*layer_id, t.inverse() * pos)) + .collect(); + + let mut closest_dist_sq = f32::INFINITY; + let mut closest_hit = None; + + // First pass: find the few widgets close to the given position, sorted back-to-front. + let mut close: Vec = layer_order + .iter() + .filter(|layer| layer.order.allow_interaction()) + .flat_map(|&layer_id| widgets.get_layer(layer_id)) + .filter(|&w| { + let pos_in_layer = pos_in_layers.get(&w.layer_id).copied().unwrap_or(pos); + let dist_sq = w.interact_rect.distance_sq_to_pos(pos_in_layer); + + // In tie, pick last = topmost. + if dist_sq <= closest_dist_sq { + closest_dist_sq = dist_sq; + closest_hit = Some(w); + } + + dist_sq <= search_radius_sq + }) + .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; + } + } + + 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); + } + + hits + } else { + // No close widgets. + Default::default() + } +} + +fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { + #![allow(clippy::collapsible_else_if)] + + // Only those widgets directly under the `pos`. + let hits: Vec = 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(); + + match (hit_click, hit_drag) { + (None, None) => { + // No direct hit on anything. Find the closest interactive widget. + + let closest = find_closest( + close + .iter() + .copied() + .filter(|w| w.sense.click || w.sense.drag), + pos, + ); + + if let Some(closest) = closest { + WidgetHits { + contains_pointer: hits, + click: closest.sense.click.then_some(closest), + drag: closest.sense.drag.then_some(closest), + } + } else { + // Found nothing + WidgetHits { + contains_pointer: hits, + click: None, + drag: None, + } + } + } + + (None, Some(hit_drag)) => { + // We have a perfect hit on a drag, but not on click. + + // We have a direct hit on something that implements drag. + // This could be a big background thing, like a `ScrollArea` background, + // or a moveable window. + // It could also be something small, like a slider, or panel resize handle. + + let closest_click = find_closest(close.iter().copied().filter(|w| w.sense.click), pos); + if let Some(closest_click) = closest_click { + if closest_click.sense.drag { + // We have something close that sense both clicks and drag. + // Should we use it over the direct drag-hit? + if hit_drag + .interact_rect + .contains_rect(closest_click.interact_rect) + { + // 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), + } + } else { + // The drag wiudth is separate from the click wiudth, + // so return only the drag widget + WidgetHits { + contains_pointer: hits, + click: None, + drag: Some(hit_drag), + } + } + } else { + // These is a close pure-click widget. + // However, we should be careful to only return two different widgets + // when it is absolutely not going to confuse the user. + if hit_drag + .interact_rect + .contains_rect(closest_click.interact_rect) + { + // 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), + } + } 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), + } + } + } + } else { + // No close clicks. + // Maybe there is a close drag widget, that is a smaller + // widget floating on top of a big background? + // If so, it would be nice to help the user click that. + let closest_drag = find_closest( + close + .iter() + .copied() + .filter(|w| w.sense.drag && w.id != hit_drag.id), + pos, + ); + + if let Some(closest_drag) = closest_drag { + if hit_drag + .interact_rect + .contains_rect(closest_drag.interact_rect) + { + // `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), + }; + } + } + + WidgetHits { + contains_pointer: hits, + click: None, + drag: Some(hit_drag), + } + } + } + + (Some(hit_click), None) => { + // We have a perfect hit on a click-widget, but not on a drag-widget. + // + // Note that we don't look for a close drag widget in this case, + // because I can't think of a case where that would be helpful. + // This is in contrast with the opposite case, + // 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. + // 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, + } + } + + (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_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), + } + } 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), + } + } + } 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), + } + } 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), + } + } + } + } + } +} + +fn find_closest(widgets: impl Iterator, pos: Pos2) -> Option { + let mut closest = None; + let mut closest_dist_sq = f32::INFINITY; + for widget in widgets { + let dist_sq = widget.interact_rect.distance_sq_to_pos(pos); + + // In case of a tie, take the last one = the one on top. + if dist_sq <= closest_dist_sq { + closest_dist_sq = dist_sq; + closest = Some(widget); + } + } + + closest +} + +#[cfg(test)] +mod tests { + use super::*; + + fn wr(id: Id, sense: Sense, rect: Rect) -> WidgetRect { + WidgetRect { + id, + layer_id: LayerId::background(), + rect, + interact_rect: rect, + sense, + enabled: true, + } + } + + #[test] + fn buttons_on_window() { + let widgets = vec![ + wr( + Id::new("bg-area"), + Sense::drag(), + Rect::from_min_size(pos2(0.0, 0.0), vec2(100.0, 100.0)), + ), + wr( + Id::new("click"), + Sense::click(), + Rect::from_min_size(pos2(10.0, 10.0), vec2(10.0, 10.0)), + ), + wr( + Id::new("click-and-drag"), + Sense::click_and_drag(), + Rect::from_min_size(pos2(100.0, 10.0), vec2(10.0, 10.0)), + ), + ]; + + // Perfect hit: + let hits = hit_test_on_close(&widgets, pos2(15.0, 15.0)); + assert_eq!(hits.click.unwrap().id, Id::new("click")); + assert_eq!(hits.drag.unwrap().id, Id::new("bg-area")); + + // Close hit: + let hits = hit_test_on_close(&widgets, pos2(5.0, 5.0)); + assert_eq!(hits.click.unwrap().id, Id::new("click")); + assert_eq!(hits.drag.unwrap().id, Id::new("bg-area")); + + // Perfect hit: + let hits = hit_test_on_close(&widgets, pos2(105.0, 15.0)); + assert_eq!(hits.click.unwrap().id, Id::new("click-and-drag")); + assert_eq!(hits.drag.unwrap().id, Id::new("click-and-drag")); + + // Close hit - should still ignore the drag-background so as not to confuse the userr: + let hits = hit_test_on_close(&widgets, pos2(105.0, 5.0)); + assert_eq!(hits.click.unwrap().id, Id::new("click-and-drag")); + assert_eq!(hits.drag.unwrap().id, Id::new("click-and-drag")); + } + + #[test] + fn thin_resize_handle_next_to_label() { + let widgets = vec![ + wr( + Id::new("bg-area"), + Sense::drag(), + Rect::from_min_size(pos2(0.0, 0.0), vec2(100.0, 100.0)), + ), + wr( + Id::new("bg-left-label"), + Sense::click_and_drag(), + Rect::from_min_size(pos2(0.0, 0.0), vec2(40.0, 100.0)), + ), + wr( + Id::new("thin-drag-handle"), + Sense::drag(), + Rect::from_min_size(pos2(30.0, 0.0), vec2(70.0, 100.0)), + ), + wr( + Id::new("fg-right-label"), + Sense::click_and_drag(), + Rect::from_min_size(pos2(60.0, 0.0), vec2(50.0, 100.0)), + ), + ]; + + for (i, w) in widgets.iter().enumerate() { + eprintln!("Widget {i}: {:?}", w.id); + } + + // In the middle of the bg-left-label: + let hits = hit_test_on_close(&widgets, pos2(25.0, 50.0)); + assert_eq!(hits.click.unwrap().id, Id::new("bg-left-label")); + assert_eq!(hits.drag.unwrap().id, Id::new("bg-left-label")); + + // On both the left click-and-drag and thin handle, but the thin handle is on top and should win: + let hits = hit_test_on_close(&widgets, pos2(35.0, 50.0)); + assert_eq!(hits.click, None); + assert_eq!(hits.drag.unwrap().id, Id::new("thin-drag-handle")); + + // Only on the thin-drag-handle: + let hits = hit_test_on_close(&widgets, pos2(50.0, 50.0)); + assert_eq!(hits.click, None); + assert_eq!(hits.drag.unwrap().id, Id::new("thin-drag-handle")); + + // On both the thin handle and right label. The label is on top and should win + let hits = hit_test_on_close(&widgets, pos2(65.0, 50.0)); + assert_eq!(hits.click.unwrap().id, Id::new("fg-right-label")); + assert_eq!(hits.drag.unwrap().id, Id::new("fg-right-label")); + } +} diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index 612314312fd..1cd96faec5f 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -1,5 +1,7 @@ // TODO(emilk): have separate types `PositionId` and `UniqueId`. ? +use std::num::NonZeroU64; + /// egui tracks widgets frame-to-frame using [`Id`]s. /// /// For instance, if you start dragging a slider one frame, egui stores @@ -25,9 +27,11 @@ /// /// Then there are widgets that need no identifiers at all, like labels, /// because they have no state nor are interacted with. +/// +/// This is niche-optimized to that `Option` is the same size as `Id`. #[derive(Clone, Copy, Hash, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct Id(u64); +pub struct Id(NonZeroU64); impl Id { /// A special [`Id`], in particular as a key to [`crate::Memory::data`] @@ -35,44 +39,47 @@ impl Id { /// /// The null [`Id`] is still a valid id to use in all circumstances, /// though obviously it will lead to a lot of collisions if you do use it! - pub fn null() -> Self { - Self(0) - } + pub const NULL: Self = Self(NonZeroU64::MAX); - pub(crate) fn background() -> Self { - Self(1) + #[inline] + const fn from_hash(hash: u64) -> Self { + if let Some(nonzero) = NonZeroU64::new(hash) { + Self(nonzero) + } else { + Self(NonZeroU64::MIN) // The hash was exactly zero (very bad luck) + } } /// Generate a new [`Id`] by hashing some source (e.g. a string or integer). - pub fn new(source: impl std::hash::Hash) -> Id { - use std::hash::{BuildHasher, Hasher}; - let mut hasher = epaint::ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher(); - source.hash(&mut hasher); - Id(hasher.finish()) + pub fn new(source: impl std::hash::Hash) -> Self { + Self::from_hash(epaint::ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(source)) } /// Generate a new [`Id`] by hashing the parent [`Id`] and the given argument. - pub fn with(self, child: impl std::hash::Hash) -> Id { + pub fn with(self, child: impl std::hash::Hash) -> Self { use std::hash::{BuildHasher, Hasher}; let mut hasher = epaint::ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher(); - hasher.write_u64(self.0); + hasher.write_u64(self.0.get()); child.hash(&mut hasher); - Id(hasher.finish()) + Self::from_hash(hasher.finish()) } /// Short and readable summary pub fn short_debug_format(&self) -> String { - format!("{:04X}", self.0 as u16) + format!("{:04X}", self.value() as u16) } + /// The inner value of the [`Id`]. + /// + /// This is a high-entropy hash, or [`Self::NULL`]. #[inline(always)] - pub(crate) fn value(&self) -> u64 { - self.0 + pub fn value(&self) -> u64 { + self.0.get() } #[cfg(feature = "accesskit")] pub(crate) fn accesskit_id(&self) -> accesskit::NodeId { - std::num::NonZeroU64::new(self.0).unwrap().into() + self.value().into() } } @@ -97,6 +104,12 @@ impl From for Id { } } +#[test] +fn id_size() { + assert_eq!(std::mem::size_of::(), 8); + assert_eq!(std::mem::size_of::>(), 8); +} + // ---------------------------------------------------------------------------- // Idea taken from the `nohash_hasher` crate. diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state.rs index ac6e6c5fa62..2a196c86528 100644 --- a/crates/egui/src/input_state.rs +++ b/crates/egui/src/input_state.rs @@ -4,15 +4,20 @@ use crate::data::input::*; use crate::{emath::*, util::History}; use std::collections::{BTreeMap, HashSet}; -pub use crate::data::input::Key; +pub use crate::Key; pub use touch_state::MultiTouchInfo; use touch_state::TouchState; /// If the pointer moves more than this, it won't become a click (but it is still a drag) const MAX_CLICK_DIST: f32 = 6.0; // TODO(emilk): move to settings -/// If the pointer is down for longer than this, it won't become a click (but it is still a drag) -const MAX_CLICK_DURATION: f64 = 0.6; // TODO(emilk): move to settings +/// If the pointer is down for longer than this it will no longer register as a click. +/// +/// If a touch is held for this many seconds while still, +/// then it will register as a "long-touch" which is equivalent to a secondary click. +/// +/// This is to support "press and hold for context menu" on touch screens. +const MAX_CLICK_DURATION: f64 = 0.8; // TODO(emilk): move to settings /// The new pointer press must come within this many seconds from previous pointer release const MAX_DOUBLE_CLICK_DELAY: f64 = 0.3; // TODO(emilk): move to settings @@ -22,6 +27,7 @@ const MAX_DOUBLE_CLICK_DELAY: f64 = 0.3; // TODO(emilk): move to settings /// You can check if `egui` is using the inputs using /// [`crate::Context::wants_pointer_input`] and [`crate::Context::wants_keyboard_input`]. #[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct InputState { /// The raw input we got this frame from the backend. pub raw: RawInput, @@ -33,7 +39,24 @@ pub struct InputState { /// (We keep a separate [`TouchState`] for each encountered touch device.) touch_states: BTreeMap, - /// How many points the user scrolled. + /// Used for smoothing the scroll delta. + unprocessed_scroll_delta: Vec2, + + /// The raw input of how many points the user scrolled. + /// + /// The delta dictates how the _content_ should move. + /// + /// A positive X-value indicates the content is being moved right, + /// as when swiping right on a touch-screen or track-pad with natural scrolling. + /// + /// A positive Y-value indicates the content is being moved down, + /// as when swiping down on a touch-screen or track-pad with natural scrolling. + /// + /// When using a notched scroll-wheel this will spike very large for one frame, + /// then drop to zero. For a smoother experience, use [`Self::smooth_scroll_delta`]. + pub raw_scroll_delta: Vec2, + + /// How many points the user scrolled, smoothed over a few frames. /// /// The delta dictates how the _content_ should move. /// @@ -42,7 +65,10 @@ pub struct InputState { /// /// A positive Y-value indicates the content is being moved down, /// as when swiping down on a touch-screen or track-pad with natural scrolling. - pub scroll_delta: Vec2, + /// + /// [`crate::ScrollArea`] will both read and write to this field, so that + /// at the end of the frame this will be zero if a scroll-area consumed the delta. + pub smooth_scroll_delta: Vec2, /// Zoom scale factor this frame (e.g. from ctrl-scroll or pinch gesture). /// @@ -125,7 +151,9 @@ impl Default for InputState { raw: Default::default(), pointer: Default::default(), touch_states: Default::default(), - scroll_delta: Vec2::ZERO, + unprocessed_scroll_delta: Vec2::ZERO, + raw_scroll_delta: Vec2::ZERO, + smooth_scroll_delta: Vec2::ZERO, zoom_factor_delta: 1.0, screen_rect: Rect::from_min_size(Default::default(), vec2(10_000.0, 10_000.0)), pixels_per_point: 1.0, @@ -147,12 +175,15 @@ impl InputState { pub fn begin_frame( mut self, mut new: RawInput, - requested_repaint_last_frame: bool, - ) -> InputState { + requested_immediate_repaint_prev_frame: bool, + pixels_per_point: f32, + ) -> Self { + crate::profile_function!(); + let time = new.time.unwrap_or(self.time + new.predicted_dt as f64); let unstable_dt = (time - self.time) as f32; - let stable_dt = if requested_repaint_last_frame { + let stable_dt = if requested_immediate_repaint_prev_frame { // we should have had a repaint straight away, // so this should be trustable. unstable_dt @@ -168,7 +199,7 @@ impl InputState { let pointer = self.pointer.begin_frame(time, &new); let mut keys_down = self.keys_down; - let mut scroll_delta = Vec2::ZERO; + let mut raw_scroll_delta = Vec2::ZERO; let mut zoom_factor_delta = 1.0; for event in &mut new.events { match event { @@ -186,7 +217,7 @@ impl InputState { } } Event::Scroll(delta) => { - scroll_delta += *delta; + raw_scroll_delta += *delta; } Event::Zoom(factor) => { zoom_factor_delta *= *factor; @@ -195,40 +226,57 @@ impl InputState { } } - let mut modifiers = new.modifiers; - - let focused_changed = self.focused != new.focused - || new - .events - .iter() - .any(|e| matches!(e, Event::WindowFocused(_))); - if focused_changed { - // It is very common for keys to become stuck when we alt-tab, or a save-dialog opens by Ctrl+S. - // Therefore we clear all the modifiers and down keys here to avoid that. - modifiers = Default::default(); - keys_down = Default::default(); + let mut unprocessed_scroll_delta = self.unprocessed_scroll_delta; + + let mut smooth_scroll_delta = Vec2::ZERO; + + { + // Mouse wheels often go very large steps. + // A single notch on a logitech mouse wheel connected to a Macbook returns 14.0 raw_scroll_delta. + // So we smooth it out over several frames for a nicer user experience when scrolling in egui. + unprocessed_scroll_delta += raw_scroll_delta; + let dt = stable_dt.at_most(0.1); + let t = crate::emath::exponential_smooth_factor(0.90, 0.1, dt); // reach _% in _ seconds. TODO: parameterize + + for d in 0..2 { + if unprocessed_scroll_delta[d].abs() < 1.0 { + smooth_scroll_delta[d] = unprocessed_scroll_delta[d]; + unprocessed_scroll_delta[d] = 0.0; + } else { + smooth_scroll_delta[d] = t * unprocessed_scroll_delta[d]; + unprocessed_scroll_delta[d] -= smooth_scroll_delta[d]; + } + } } - InputState { + Self { pointer, touch_states: self.touch_states, - scroll_delta, + unprocessed_scroll_delta, + raw_scroll_delta, + smooth_scroll_delta, zoom_factor_delta, screen_rect, - pixels_per_point: new.pixels_per_point.unwrap_or(self.pixels_per_point), + pixels_per_point, max_texture_side: new.max_texture_side.unwrap_or(self.max_texture_side), time, unstable_dt, predicted_dt: new.predicted_dt, stable_dt, focused: new.focused, - modifiers, + modifiers: new.modifiers, keys_down, events: new.events.clone(), // TODO(emilk): remove clone() and use raw.events raw: new, } } + /// Info about the active viewport + #[inline] + pub fn viewport(&self) -> &ViewportInfo { + self.raw.viewport() + } + #[inline(always)] pub fn screen_rect(&self) -> Rect { self.screen_rect @@ -273,14 +321,23 @@ impl InputState { ) } + /// The [`crate::Context`] will call this at the end of each frame to see if we need a repaint. pub fn wants_repaint(&self) -> bool { - self.pointer.wants_repaint() || self.scroll_delta != Vec2::ZERO || !self.events.is_empty() + self.pointer.wants_repaint() + || self.unprocessed_scroll_delta.abs().max_elem() > 0.2 + || !self.events.is_empty() } /// Count presses of a key. If non-zero, the presses are consumed, so that this will only return non-zero once. /// /// Includes key-repeat events. - pub fn count_and_consume_key(&mut self, modifiers: Modifiers, key: Key) -> usize { + /// + /// This uses [`Modifiers::matches_logically`] to match modifiers, + /// meaning extra Shift and Alt modifiers are ignored. + /// Therefore, you should match most specific shortcuts first, + /// i.e. check for `Cmd-Shift-S` ("Save as…") before `Cmd-S` ("Save"), + /// so that a user pressing `Cmd-Shift-S` won't trigger the wrong command! + pub fn count_and_consume_key(&mut self, modifiers: Modifiers, logical_key: Key) -> usize { let mut count = 0usize; self.events.retain(|event| { @@ -291,7 +348,7 @@ impl InputState { modifiers: ev_mods, pressed: true, .. - } if *ev_key == key && ev_mods.matches(modifiers) + } if *ev_key == logical_key && ev_mods.matches_logically(modifiers) ); count += is_match as usize; @@ -305,18 +362,31 @@ impl InputState { /// Check for a key press. If found, `true` is returned and the key pressed is consumed, so that this will only return `true` once. /// /// Includes key-repeat events. - pub fn consume_key(&mut self, modifiers: Modifiers, key: Key) -> bool { - self.count_and_consume_key(modifiers, key) > 0 + /// + /// This uses [`Modifiers::matches_logically`] to match modifiers, + /// meaning extra Shift and Alt modifiers are ignored. + /// Therefore, you should match most specific shortcuts first, + /// i.e. check for `Cmd-Shift-S` ("Save as…") before `Cmd-S` ("Save"), + /// so that a user pressing `Cmd-Shift-S` won't trigger the wrong command! + pub fn consume_key(&mut self, modifiers: Modifiers, logical_key: Key) -> bool { + self.count_and_consume_key(modifiers, logical_key) > 0 } /// Check if the given shortcut has been pressed. /// /// If so, `true` is returned and the key pressed is consumed, so that this will only return `true` once. /// - /// Includes key-repeat events. + /// This uses [`Modifiers::matches_logically`] to match modifiers, + /// meaning extra Shift and Alt modifiers are ignored. + /// Therefore, you should match most specific shortcuts first, + /// i.e. check for `Cmd-Shift-S` ("Save as…") before `Cmd-S` ("Save"), + /// so that a user pressing `Cmd-Shift-S` won't trigger the wrong command! pub fn consume_shortcut(&mut self, shortcut: &KeyboardShortcut) -> bool { - let KeyboardShortcut { modifiers, key } = *shortcut; - self.consume_key(modifiers, key) + let KeyboardShortcut { + modifiers, + logical_key, + } = *shortcut; + self.consume_key(modifiers, logical_key) } /// Was the given key pressed this frame? @@ -410,11 +480,7 @@ impl InputState { /// delivers a synthetic zoom factor based on ctrl-scroll events, as a fallback. pub fn multi_touch(&self) -> Option { // In case of multiple touch devices simply pick the touch_state of the first active device - if let Some(touch_state) = self.touch_states.values().find(|t| t.is_active()) { - touch_state.info() - } else { - None - } + self.touch_states.values().find_map(|t| t.info()) } /// True if there currently are any fingers touching egui. @@ -460,12 +526,30 @@ impl InputState { pub fn num_accesskit_action_requests(&self, id: crate::Id, action: accesskit::Action) -> usize { self.accesskit_action_requests(id, action).count() } + + /// Get all events that matches the given filter. + pub fn filtered_events(&self, filter: &EventFilter) -> Vec { + self.events + .iter() + .filter(|event| filter.matches(event)) + .cloned() + .collect() + } + + /// A long press is something we detect on touch screens + /// to trigger a secondary click (context menu). + /// + /// Returns `true` only on one frame. + pub(crate) fn is_long_touch(&self) -> bool { + self.any_touches() && self.pointer.is_long_press() + } } // ---------------------------------------------------------------------------- /// A pointer (mouse or touch) click. #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub(crate) struct Click { pub pos: Pos2, @@ -487,6 +571,7 @@ impl Click { } #[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub(crate) enum PointerEvent { Moved(Pos2), Pressed { @@ -501,20 +586,21 @@ pub(crate) enum PointerEvent { impl PointerEvent { pub fn is_press(&self) -> bool { - matches!(self, PointerEvent::Pressed { .. }) + matches!(self, Self::Pressed { .. }) } pub fn is_release(&self) -> bool { - matches!(self, PointerEvent::Released { .. }) + matches!(self, Self::Released { .. }) } pub fn is_click(&self) -> bool { - matches!(self, PointerEvent::Released { click: Some(_), .. }) + matches!(self, Self::Released { click: Some(_), .. }) } } /// Mouse or touch state. #[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct PointerState { /// Latest known time time: f64, @@ -537,6 +623,11 @@ pub struct PointerState { /// How much the pointer moved compared to last frame, in points. delta: Vec2, + /// How much the mouse moved since the last frame, in unspecified units. + /// Represents the actual movement of the mouse, without acceleration or clamped by screen edges. + /// May be unavailable on some integrations. + motion: Option, + /// Current velocity of pointer. velocity: Vec2, @@ -558,6 +649,11 @@ pub struct PointerState { /// for it to be registered as a click. pub(crate) has_moved_too_much_for_a_click: bool, + /// Did [`Self::is_decidedly_dragging`] go from `false` to `true` this frame? + /// + /// This could also be the trigger point for a long-touch. + pub(crate) started_decidedly_dragging: bool, + /// When did the pointer get click last? /// Used to check for double-clicks. last_click_time: f64, @@ -581,12 +677,14 @@ impl Default for PointerState { latest_pos: None, interact_pos: None, delta: Vec2::ZERO, + motion: None, velocity: Vec2::ZERO, pos_history: History::new(0..1000, 0.1), down: Default::default(), press_origin: None, 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, @@ -597,13 +695,18 @@ impl Default for PointerState { impl PointerState { #[must_use] - pub(crate) fn begin_frame(mut self, time: f64, new: &RawInput) -> PointerState { + pub(crate) fn begin_frame(mut self, time: f64, new: &RawInput) -> Self { + let was_decidedly_dragging = self.is_decidedly_dragging(); + self.time = time; self.pointer_events.clear(); let old_pos = self.latest_pos; self.interact_pos = self.latest_pos; + if self.motion.is_some() { + self.motion = Some(Vec2::ZERO); + } for event in &new.events { match event { @@ -649,6 +752,7 @@ impl PointerState { button, }); } else { + // Released let clicked = self.could_any_button_be_click(); let click = if clicked { @@ -689,6 +793,7 @@ impl PointerState { self.latest_pos = None; // NOTE: we do NOT clear `self.interact_pos` here. It will be cleared next frame. } + Event::MouseMoved(delta) => *self.motion.get_or_insert(Vec2::ZERO) += *delta, _ => {} } } @@ -718,6 +823,8 @@ impl PointerState { self.last_move_time = time; } + self.started_decidedly_dragging = self.is_decidedly_dragging() && !was_decidedly_dragging; + self } @@ -731,6 +838,14 @@ impl PointerState { self.delta } + /// How much the mouse moved since the last frame, in unspecified units. + /// Represents the actual movement of the mouse, without acceleration or clamped by screen edges. + /// May be unavailable on some integrations. + #[inline(always)] + pub fn motion(&self) -> Option { + self.motion + } + /// Current velocity of pointer. #[inline(always)] pub fn velocity(&self) -> Vec2 { @@ -754,7 +869,7 @@ impl PointerState { /// Latest reported pointer position. /// When tapping a touch screen, this will be `None`. #[inline(always)] - pub(crate) fn latest_pos(&self) -> Option { + pub fn latest_pos(&self) -> Option { self.latest_pos } @@ -798,11 +913,12 @@ impl PointerState { /// How long has it been (in seconds) since the pointer was last moved? #[inline(always)] - pub fn time_since_last_movement(&self) -> f64 { - self.time - self.last_move_time + pub fn time_since_last_movement(&self) -> f32 { + (self.time - self.last_move_time) as f32 } /// Was any pointer button pressed (`!down -> down`) this frame? + /// /// This can sometimes return `true` even if `any_down() == false` /// because a press can be shorted than one frame. pub fn any_pressed(&self) -> bool { @@ -858,11 +974,13 @@ impl PointerState { self.pointer_events.iter().any(|event| event.is_click()) } - /// Was the button given clicked this frame? + /// Was the given pointer button given clicked this frame? + /// + /// Returns true on double- and triple- clicks too. pub fn button_clicked(&self, button: PointerButton) -> bool { self.pointer_events .iter() - .any(|event| matches!(event, &PointerEvent::Pressed { button: b, .. } if button == b)) + .any(|event| matches!(event, &PointerEvent::Released { button: b, click: Some(_) } if button == b)) } /// Was the button given double clicked this frame? @@ -911,21 +1029,21 @@ impl PointerState { /// /// See also [`Self::is_decidedly_dragging`]. pub fn could_any_button_be_click(&self) -> bool { - if !self.any_down() { - return false; - } - - if self.has_moved_too_much_for_a_click { - return false; - } - - if let Some(press_start_time) = self.press_start_time { - if self.time - press_start_time > MAX_CLICK_DURATION { + if self.any_down() || self.any_released() { + if self.has_moved_too_much_for_a_click { return false; } - } - true + if let Some(press_start_time) = self.press_start_time { + if self.time - press_start_time > MAX_CLICK_DURATION { + return false; + } + } + + true + } else { + false + } } /// Just because the mouse is down doesn't mean we are dragging. @@ -944,6 +1062,19 @@ impl PointerState { && !self.any_click() } + /// A long press is something we detect on touch screens + /// to trigger a secondary click (context menu). + /// + /// Returns `true` only on one frame. + pub(crate) fn is_long_press(&self) -> bool { + self.started_decidedly_dragging + && !self.has_moved_too_much_for_a_click + && self.button_down(PointerButton::Primary) + && self.press_start_time.map_or(false, |press_start_time| { + self.time - press_start_time > MAX_CLICK_DURATION + }) + } + /// Is the primary button currently down? #[inline(always)] pub fn primary_down(&self) -> bool { @@ -969,7 +1100,11 @@ impl InputState { raw, pointer, touch_states, - scroll_delta, + + unprocessed_scroll_delta, + raw_scroll_delta, + smooth_scroll_delta, + zoom_factor_delta, screen_rect, pixels_per_point, @@ -1004,7 +1139,15 @@ impl InputState { }); } - ui.label(format!("scroll_delta: {scroll_delta:?} points")); + if cfg!(debug_assertions) { + ui.label(format!( + "unprocessed_scroll_delta: {unprocessed_scroll_delta:?} points" + )); + } + ui.label(format!("raw_scroll_delta: {raw_scroll_delta:?} points")); + ui.label(format!( + "smooth_scroll_delta: {smooth_scroll_delta:?} points" + )); ui.label(format!("zoom_factor_delta: {zoom_factor_delta:4.2}x")); ui.label(format!("screen_rect: {screen_rect:?} points")); ui.label(format!( @@ -1038,12 +1181,14 @@ impl PointerState { latest_pos, interact_pos, delta, + motion, velocity, pos_history: _, down, press_origin, press_start_time, has_moved_too_much_for_a_click, + started_decidedly_dragging, last_click_time, last_last_click_time, pointer_events, @@ -1053,6 +1198,7 @@ impl PointerState { ui.label(format!("latest_pos: {latest_pos:?}")); ui.label(format!("interact_pos: {interact_pos:?}")); ui.label(format!("delta: {delta:?}")); + ui.label(format!("motion: {motion:?}")); ui.label(format!( "velocity: [{:3.0} {:3.0}] points/sec", velocity.x, velocity.y @@ -1063,6 +1209,9 @@ impl PointerState { ui.label(format!( "has_moved_too_much_for_a_click: {has_moved_too_much_for_a_click}" )); + ui.label(format!( + "started_decidedly_dragging: {started_decidedly_dragging}" + )); ui.label(format!("last_click_time: {last_click_time:#?}")); ui.label(format!("last_last_click_time: {last_last_click_time:#?}")); ui.label(format!("last_move_time: {last_move_time:#?}")); diff --git a/crates/egui/src/input_state/touch_state.rs b/crates/egui/src/input_state/touch_state.rs index a948ab16fba..2a77a4d371b 100644 --- a/crates/egui/src/input_state/touch_state.rs +++ b/crates/egui/src/input_state/touch_state.rs @@ -64,6 +64,7 @@ pub struct MultiTouchInfo { /// The current state (for a specific touch device) of touch events and gestures. #[derive(Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub(crate) struct TouchState { /// Technical identifier of the touch device. This is used to identify relevant touch events /// for this [`TouchState`] instance. @@ -83,6 +84,7 @@ pub(crate) struct TouchState { } #[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] struct GestureState { start_time: f64, start_pointer_pos: Pos2, @@ -93,6 +95,7 @@ struct GestureState { /// Gesture data that can change over time #[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] struct DynGestureState { /// used for proportional zooming avg_distance: f32, @@ -110,6 +113,7 @@ struct DynGestureState { /// Describes an individual touch (finger or digitizer) on the touch surface. Instances exist as /// long as the finger/pen touches the surface. #[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] struct ActiveTouch { /// Current position of this touch, in device coordinates (not necessarily screen position) pos: Pos2, @@ -159,6 +163,7 @@ impl TouchState { _ => (), } } + // This needs to be called each frame, even if there are no new touch events. // Otherwise, we would send the same old delta information multiple times: self.update_gesture(time, pointer_pos); @@ -172,10 +177,6 @@ impl TouchState { } } - pub fn is_active(&self) -> bool { - self.gesture_state.is_some() - } - pub fn info(&self) -> Option { self.gesture_state.as_ref().map(|state| { // state.previous can be `None` when the number of simultaneous touches has just @@ -302,6 +303,7 @@ impl Debug for TouchState { } #[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] enum PinchType { Horizontal, Vertical, diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs new file mode 100644 index 00000000000..d5812d4d7c2 --- /dev/null +++ b/crates/egui/src/interaction.rs @@ -0,0 +1,268 @@ +//! How mouse and touch interzcts with widgets. + +use crate::*; + +use self::{hit_test::WidgetHits, id::IdSet, input_state::PointerEvent, memory::InteractionState}; + +/// Calculated at the start of each frame +/// based on: +/// * Widget rects from precious frame +/// * Mouse/touch input +/// * Current [`InteractionState`]. +#[derive(Clone, Default)] +pub struct InteractionSnapshot { + /// The widget that got clicked this frame. + pub clicked: Option, + + /// This widget was long-pressed on a touch screen, + /// so trigger a secondary click on it (context menu). + pub long_touched: Option, + + /// Drag started on this widget this frame. + /// + /// This will also be found in `dragged` this frame. + pub drag_started: Option, + + /// This widget is being dragged this frame. + /// + /// Set the same frame a drag starts, + /// but unset the frame a drag ends. + /// + /// NOTE: this may not have a corresponding [`WidgetRect`], + /// if this for instance is a drag-and-drop widget which + /// isn't painted whilst being dragged + pub dragged: Option, + + /// This widget was let go this frame, + /// after having been dragged. + /// + /// The widget will not be found in [`Self::dragged`] this frame. + pub drag_stopped: Option, + + /// A small set of widgets (usually 0-1) that the pointer is hovering over. + /// + /// Show these widgets as highlighted, if they are interactive. + /// + /// While dragging or clicking something, nothing else is hovered. + /// + /// Use [`Self::contains_pointer`] to find a drop-zone for drag-and-drop. + pub hovered: IdSet, + + /// All widgets that contain the pointer this frame, + /// regardless if the user is currently clicking or dragging. + /// + /// This is usually a larger set than [`Self::hovered`], + /// and can be used for e.g. drag-and-drop zones. + pub contains_pointer: IdSet, +} + +impl InteractionSnapshot { + pub fn ui(&self, ui: &mut crate::Ui) { + let Self { + clicked, + long_touched, + drag_started, + dragged, + drag_stopped, + hovered, + contains_pointer, + } = self; + + fn id_ui<'a>(ui: &mut crate::Ui, widgets: impl IntoIterator) { + for id in widgets { + ui.label(id.short_debug_format()); + } + } + + crate::Grid::new("interaction").show(ui, |ui| { + ui.label("clicked"); + id_ui(ui, clicked); + ui.end_row(); + + ui.label("long_touched"); + id_ui(ui, long_touched); + ui.end_row(); + + ui.label("drag_started"); + id_ui(ui, drag_started); + ui.end_row(); + + ui.label("dragged"); + id_ui(ui, dragged); + ui.end_row(); + + ui.label("drag_stopped"); + id_ui(ui, drag_stopped); + ui.end_row(); + + ui.label("hovered"); + id_ui(ui, hovered); + ui.end_row(); + + ui.label("contains_pointer"); + id_ui(ui, contains_pointer); + ui.end_row(); + }); + } +} + +pub(crate) fn interact( + prev_snapshot: &InteractionSnapshot, + widgets: &WidgetRects, + hits: &WidgetHits, + input: &InputState, + interaction: &mut InteractionState, +) -> InteractionSnapshot { + crate::profile_function!(); + + if let Some(id) = interaction.potential_click_id { + if !widgets.contains(id) { + // The widget we were interested in clicking is gone. + interaction.potential_click_id = None; + } + } + if let Some(id) = interaction.potential_drag_id { + if !widgets.contains(id) { + // The widget we were interested in dragging is gone. + // This is fine! This could be drag-and-drop, + // and the widget being dragged is now "in the air" and thus + // not registered in the new frame. + } + } + + let mut clicked = None; + let mut dragged = prev_snapshot.dragged; + let mut long_touched = None; + + if input.is_long_touch() { + // We implement "press-and-hold for context menu" on touch screens here + if let Some(widget) = interaction + .potential_click_id + .and_then(|id| widgets.get(id)) + { + dragged = None; + clicked = Some(widget.id); + long_touched = Some(widget.id); + interaction.potential_click_id = None; + interaction.potential_drag_id = None; + } + } + + // Note: in the current code a press-release in the same frame is NOT considered a drag. + for pointer_event in &input.pointer.pointer_events { + match pointer_event { + PointerEvent::Moved(_) => {} + + PointerEvent::Pressed { .. } => { + // Maybe new click? + if interaction.potential_click_id.is_none() { + interaction.potential_click_id = hits.click.map(|w| w.id); + } + + // Maybe new drag? + if interaction.potential_drag_id.is_none() { + interaction.potential_drag_id = hits.drag.map(|w| w.id); + } + } + + PointerEvent::Released { click, button: _ } => { + if click.is_some() && !input.pointer.is_decidedly_dragging() { + if let Some(widget) = interaction + .potential_click_id + .and_then(|id| widgets.get(id)) + { + clicked = Some(widget.id); + } + } + + interaction.potential_drag_id = None; + interaction.potential_click_id = None; + dragged = None; + } + } + } + + if dragged.is_none() { + // Check if we started dragging something new: + if let Some(widget) = interaction.potential_drag_id.and_then(|id| widgets.get(id)) { + if widget.enabled { + let is_dragged = if widget.sense.click && widget.sense.drag { + // This widget is sensitive to both clicks and drags. + // When the mouse first is pressed, it could be either, + // so we postpone the decision until we know. + input.pointer.is_decidedly_dragging() + } else { + // This widget is just sensitive to drags, so we can mark it as dragged right away: + widget.sense.drag + }; + + if is_dragged { + dragged = Some(widget.id); + } + } + } + } + + if !input.pointer.could_any_button_be_click() { + interaction.potential_click_id = None; + } + + if !input.pointer.any_down() || input.pointer.latest_pos().is_none() { + interaction.potential_click_id = None; + interaction.potential_drag_id = None; + } + + // ------------------------------------------------------------------------ + + let drag_changed = dragged != prev_snapshot.dragged; + let drag_stopped = drag_changed.then_some(prev_snapshot.dragged).flatten(); + let drag_started = drag_changed.then_some(dragged).flatten(); + + // if let Some(drag_started) = drag_started { + // eprintln!( + // "Started dragging {} {:?}", + // drag_started.id.short_debug_format(), + // drag_started.rect + // ); + // } + + let contains_pointer: IdSet = hits + .contains_pointer + .iter() + .chain(&hits.click) + .chain(&hits.drag) + .map(|w| w.id) + .collect(); + + let hovered = if clicked.is_some() || dragged.is_some() || long_touched.is_some() { + // If currently clicking or dragging, only that and nothing else is hovered. + clicked + .iter() + .chain(&dragged) + .chain(&long_touched) + .copied() + .collect() + } else if hits.click.is_some() || hits.drag.is_some() { + // We are hovering over an interactive widget or two. + hits.click.iter().chain(&hits.drag).map(|w| w.id).collect() + } else { + // Whatever is topmost is what we are hovering. + // TODO: consider handle hovering over multiple top-most widgets? + // TODO: allow hovering close widgets? + hits.contains_pointer + .last() + .map(|w| w.id) + .into_iter() + .collect() + }; + + InteractionSnapshot { + clicked, + long_touched, + drag_started, + dragged, + drag_stopped, + contains_pointer, + hovered, + } +} diff --git a/crates/egui/src/introspection.rs b/crates/egui/src/introspection.rs index 6f0cada7807..9c738153873 100644 --- a/crates/egui/src/introspection.rs +++ b/crates/egui/src/introspection.rs @@ -146,6 +146,8 @@ impl Widget for &mut epaint::TessellationOptions { debug_ignore_clip_rects, bezier_tolerance, epsilon: _, + parallel_tessellation, + validate_meshes, } = self; ui.checkbox(feathering, "Feathering (antialias)") @@ -176,19 +178,27 @@ impl Widget for &mut epaint::TessellationOptions { ui.checkbox(debug_paint_clip_rects, "Paint clip rectangles"); ui.checkbox(debug_paint_text_rects, "Paint text bounds"); }); + + ui.add_enabled(epaint::HAS_RAYON, crate::Checkbox::new(parallel_tessellation, "Parallelize tessellation") + ).on_hover_text("Only available if epaint was compiled with the rayon feature") + .on_disabled_hover_text("epaint was not compiled with the rayon feature"); + + ui.checkbox(validate_meshes, "Validate meshes").on_hover_text("Check that incoming meshes are valid, i.e. that all indices are in range, etc."); }) .response } } -impl Widget for &memory::Interaction { +impl Widget for &memory::InteractionState { fn ui(self, ui: &mut Ui) -> Response { + let memory::InteractionState { + potential_click_id, + potential_drag_id, + } = self; + ui.vertical(|ui| { - ui.label(format!("click_id: {:?}", self.click_id)); - ui.label(format!("drag_id: {:?}", self.drag_id)); - ui.label(format!("drag_is_window: {:?}", self.drag_is_window)); - ui.label(format!("click_interest: {:?}", self.click_interest)); - ui.label(format!("drag_interest: {:?}", self.drag_interest)); + ui.label(format!("potential_click_id: {potential_click_id:?}")); + ui.label(format!("potential_drag_id: {potential_drag_id:?}")); }) .response } diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index 83e7fdeb628..b9b1fb5a5c3 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -2,7 +2,7 @@ //! are sometimes painted behind or in front of other things. use crate::{Id, *}; -use epaint::{ClippedShape, Shape}; +use epaint::{emath::TSTransform, ClippedShape, Shape}; /// Different layer categories #[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] @@ -31,7 +31,7 @@ pub enum Order { impl Order { const COUNT: usize = 6; - const ALL: [Order; Self::COUNT] = [ + const ALL: [Self; Self::COUNT] = [ Self::Background, Self::PanelResizeLine, Self::Middle, @@ -90,7 +90,7 @@ impl LayerId { pub fn background() -> Self { Self { order: Order::Background, - id: Id::background(), + id: Id::new("background"), } } @@ -110,8 +110,9 @@ impl LayerId { } /// A unique identifier of a specific [`Shape`] in a [`PaintList`]. -#[derive(Clone, Copy, PartialEq, Eq)] -pub struct ShapeIdx(usize); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ShapeIdx(pub usize); /// A list of [`Shape`]s paired with a clip rectangle. #[derive(Clone, Default)] @@ -151,26 +152,55 @@ impl PaintList { self.0[idx.0] = ClippedShape { clip_rect, shape }; } - /// Translate each [`Shape`] and clip rectangle by this much, in-place - pub fn translate(&mut self, delta: Vec2) { + /// Set the given shape to be empty (a `Shape::Noop`). + #[inline(always)] + pub fn reset_shape(&mut self, idx: ShapeIdx) { + self.0[idx.0].shape = Shape::Noop; + } + + /// Transform each [`Shape`] and clip rectangle by this much, in-place + pub fn transform(&mut self, transform: TSTransform) { for ClippedShape { clip_rect, shape } in &mut self.0 { - *clip_rect = clip_rect.translate(delta); - shape.translate(delta); + *clip_rect = transform.mul_rect(*clip_rect); + shape.transform(transform); } } + + /// Read-only access to all held shapes. + pub fn all_entries(&self) -> impl ExactSizeIterator { + self.0.iter() + } } +/// This is where painted [`Shape`]s end up during a frame. #[derive(Clone, Default)] -pub(crate) struct GraphicLayers([IdMap; Order::COUNT]); +pub struct GraphicLayers([IdMap; Order::COUNT]); impl GraphicLayers { - pub fn list(&mut self, layer_id: LayerId) -> &mut PaintList { + /// Get or insert the [`PaintList`] for the given [`LayerId`]. + pub fn entry(&mut self, layer_id: LayerId) -> &mut PaintList { self.0[layer_id.order as usize] .entry(layer_id.id) .or_default() } - pub fn drain(&mut self, area_order: &[LayerId]) -> impl ExactSizeIterator { + /// Get the [`PaintList`] for the given [`LayerId`]. + pub fn get(&self, layer_id: LayerId) -> Option<&PaintList> { + self.0[layer_id.order as usize].get(&layer_id.id) + } + + /// Get the [`PaintList`] for the given [`LayerId`]. + pub fn get_mut(&mut self, layer_id: LayerId) -> Option<&mut PaintList> { + self.0[layer_id.order as usize].get_mut(&layer_id.id) + } + + pub fn drain( + &mut self, + area_order: &[LayerId], + transforms: &ahash::HashMap, + ) -> Vec { + crate::profile_function!(); + let mut all_shapes: Vec<_> = Default::default(); for &order in &Order::ALL { @@ -185,17 +215,32 @@ 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) { + for clipped_shape in &mut list.0 { + clipped_shape.clip_rect = *transform * clipped_shape.clip_rect; + clipped_shape.shape.transform(*transform); + } + } all_shapes.append(&mut list.0); } } } // Also draw areas that are missing in `area_order`: - for shapes in order_map.values_mut() { - all_shapes.append(&mut shapes.0); + for (id, list) in order_map { + let layer_id = LayerId::new(order, *id); + + if let Some(transform) = transforms.get(&layer_id) { + for clipped_shape in &mut list.0 { + clipped_shape.clip_rect = *transform * clipped_shape.clip_rect; + clipped_shape.shape.transform(*transform); + } + } + + all_shapes.append(&mut list.0); } } - all_shapes.into_iter() + all_shapes } } diff --git a/crates/egui/src/layout.rs b/crates/egui/src/layout.rs index 8da23700a23..a044dce9814 100644 --- a/crates/egui/src/layout.rs +++ b/crates/egui/src/layout.rs @@ -88,16 +88,16 @@ impl Direction { #[inline(always)] pub fn is_horizontal(self) -> bool { match self { - Direction::LeftToRight | Direction::RightToLeft => true, - Direction::TopDown | Direction::BottomUp => false, + Self::LeftToRight | Self::RightToLeft => true, + Self::TopDown | Self::BottomUp => false, } } #[inline(always)] pub fn is_vertical(self) -> bool { match self { - Direction::LeftToRight | Direction::RightToLeft => false, - Direction::TopDown | Direction::BottomUp => true, + Self::LeftToRight | Self::RightToLeft => false, + Self::TopDown | Self::BottomUp => true, } } } @@ -801,6 +801,7 @@ impl Layout { /// ## Debug stuff impl Layout { /// Shows where the next widget is going to be placed + #[cfg(debug_assertions)] pub(crate) fn paint_text_at_cursor( &self, painter: &crate::Painter, diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index aab9049b664..e64668ec807 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -12,6 +12,10 @@ //! Then you add a [`Window`] or a [`SidePanel`] to get a [`Ui`], which is what you'll be using to add all the buttons and labels that you need. //! //! +//! ## Feature flags +#![cfg_attr(feature = "document-features", doc = document_features::document_features!())] +//! +//! //! # Using egui //! //! To see what is possible to build with egui you can check out the online demo at . @@ -25,7 +29,7 @@ //! fn ui_counter(ui: &mut egui::Ui, counter: &mut i32) { //! // Put the buttons and label on the same row: //! ui.horizontal(|ui| { -//! if ui.button("-").clicked() { +//! if ui.button("−").clicked() { //! *counter -= 1; //! } //! ui.label(counter.to_string()); @@ -84,7 +88,7 @@ //! ui.separator(); //! //! # let my_image = egui::TextureId::default(); -//! ui.image(my_image, [640.0, 480.0]); +//! ui.image((my_image, egui::Vec2::new(640.0, 480.0))); //! //! ui.collapsing("Click to see what is hidden!", |ui| { //! ui.label("Not much, as it turns out"); @@ -92,22 +96,28 @@ //! # }); //! ``` //! -//! ## Conventions +//! ## Viewports +//! Some egui backends support multiple _viewports_, which is what egui calls the native OS windows it resides in. +//! See [`crate::viewport`] for more information. //! -//! Conventions unless otherwise specified: +//! ## Coordinate system +//! The left-top corner of the screen is `(0.0, 0.0)`, +//! with X increasing to the right and Y increasing downwards. //! -//! * angles are in radians -//! * `Vec2::X` is right and `Vec2::Y` is down. -//! * `Pos2::ZERO` is left top. -//! * Positions and sizes are measured in _points_. Each point may consist of many physical pixels. +//! `egui` uses logical _points_ as its coordinate system. +//! Those related to physical _pixels_ by the `pixels_per_point` scale factor. +//! For example, a high-dpi screeen can have `pixels_per_point = 2.0`, +//! meaning there are two physical screen pixels for each logical point. +//! +//! Angles are in radians, and are measured clockwise from the X-axis, which has angle=0. //! //! # Integrating with egui //! //! Most likely you are using an existing `egui` backend/integration such as [`eframe`](https://docs.rs/eframe), [`bevy_egui`](https://docs.rs/bevy_egui), //! or [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad), -//! but if you want to integrate `egui` into a new game engine, this is the section for you. +//! but if you want to integrate `egui` into a new game engine or graphics backend, this is the section for you. //! -//! To write your own integration for egui you need to do this: +//! You need to collect [`RawInput`] and handle [`FullOutput`]. The basic structure is this: //! //! ``` no_run //! # fn handle_platform_output(_: egui::PlatformOutput) {} @@ -128,15 +138,47 @@ //! }); //! }); //! handle_platform_output(full_output.platform_output); -//! let clipped_primitives = ctx.tessellate(full_output.shapes); // create triangles to paint +//! let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point); //! paint(full_output.textures_delta, clipped_primitives); //! } //! ``` //! +//! For a reference OpenGL renderer, see [the `egui_glow` painter](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs). +//! +//! +//! ### Debugging your renderer +//! +//! #### Things look jagged +//! +//! * Turn off backface culling. +//! +//! #### My text is blurry +//! +//! * Make sure you set the proper `pixels_per_point` in the input to egui. +//! * Make sure the texture sampler is not off by half a pixel. Try nearest-neighbor sampler to check. +//! +//! #### My windows are too transparent or too dark +//! +//! * egui uses premultiplied alpha, so make sure your blending function is `(ONE, ONE_MINUS_SRC_ALPHA)`. +//! * Make sure your texture sampler is clamped (`GL_CLAMP_TO_EDGE`). +//! * egui prefers linear color spaces for all blending so: +//! * Use an sRGBA-aware texture if available (e.g. `GL_SRGB8_ALPHA8`). +//! * Otherwise: remember to decode gamma in the fragment shader. +//! * Decode the gamma of the incoming vertex colors in your vertex shader. +//! * Turn on sRGBA/linear framebuffer if available (`GL_FRAMEBUFFER_SRGB`). +//! * Otherwise: gamma-encode the colors before you write them again. +//! //! //! # Understanding immediate mode //! -//! `egui` is an immediate mode GUI library. It is useful to fully grok what "immediate mode" implies. +//! `egui` is an immediate mode GUI library. +//! +//! Immediate mode has its roots in gaming, where everything on the screen is painted at the +//! display refresh rate, i.e. at 60+ frames per second. +//! In immediate mode GUIs, the entire interface is laid out and painted at the same high rate. +//! This makes immediate mode GUIs especially well suited for highly interactive applications. +//! +//! It is useful to fully grok what "immediate mode" implies. //! //! Here is an example to illustrate it: //! @@ -171,7 +213,7 @@ //! # }); //! ``` //! -//! Here egui will read `value` to display the slider, then look if the mouse is dragging the slider and if so change the `value`. +//! Here egui will read `value` (an `f32`) to display the slider, then look if the mouse is dragging the slider and if so change the `value`. //! Note that `egui` does not store the slider value for you - it only displays the current value, and changes it //! by how much the slider has been dragged in the previous few milliseconds. //! This means it is responsibility of the egui user to store the state (`value`) so that it persists between frames. @@ -225,6 +267,37 @@ //! } //! ``` //! +//! +//! ## Widget interaction +//! Each widget has a [`Sense`], which defines whether or not the widget +//! is sensitive to clickicking and/or drags. +//! +//! For instance, a [`Button`] only has a [`Sense::click`] (by default). +//! This means if you drag a button it will not respond with [`Response::dragged`]. +//! Instead, the drag will continue through the button to the first +//! widget behind it that is sensitive to dragging, which for instance could be +//! a [`ScrollArea`]. This lets you scroll by dragging a scroll area (important +//! on touch screens), just as long as you don't drag on a widget that is sensitive +//! to drags (e.g. a [`Slider`]). +//! +//! When widgets overlap it is the last added one +//! that is considered to be on top and which will get input priority. +//! +//! The widget interaction logic is run at the _start_ of each frame, +//! based on the output from the previous frame. +//! This means that when a new widget shows up you cannot click it in the same +//! frame (i.e. in the same fraction of a second), but unless the user +//! is spider-man, they wouldn't be fast enough to do so anyways. +//! +//! By running the interaction code early, egui can actually +//! tell you if a widget is being interacted with _before_ you add it, +//! as long as you know its [`Id`] before-hand (e.g. using [`Ui::next_auto_id`]), +//! by calling [`Context::read_response`]. +//! This can be useful in some circumstances in order to style a widget, +//! or to respond to interactions before adding the widget +//! (perhaps on top of other widgets). +//! +//! //! ## Auto-sizing panels and windows //! In egui, all panels and windows auto-shrink to fit the content. //! If the window or panel is also resizable, this can lead to a weird behavior @@ -292,26 +365,32 @@ //! # }); //! ``` //! -//! ## Feature flags -#![cfg_attr(feature = "document-features", doc = document_features::document_features!())] -//! +//! ## 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`]. #![allow(clippy::float_cmp)] #![allow(clippy::manual_range_contains)] -#![forbid(unsafe_code)] +#![cfg_attr(feature = "puffin", deny(unsafe_code))] +#![cfg_attr(not(feature = "puffin"), forbid(unsafe_code))] mod animation_manager; pub mod containers; mod context; mod data; +pub mod debug_text; +mod drag_and_drop; mod frame_state; pub(crate) mod grid; pub mod gui_zoom; +mod hit_test; mod id; mod input_state; +mod interaction; pub mod introspection; pub mod layers; mod layout; +pub mod load; mod memory; pub mod menu; pub mod os; @@ -320,14 +399,23 @@ pub(crate) mod placer; mod response; mod sense; pub mod style; +pub mod text_selection; mod ui; pub mod util; +pub mod viewport; +mod widget_rect; pub mod widget_text; pub mod widgets; +#[cfg(feature = "callstack")] +#[cfg(debug_assertions)] +mod callstack; + #[cfg(feature = "accesskit")] pub use accesskit; +pub use ahash; + pub use epaint; pub use epaint::ecolor; pub use epaint::emath; @@ -336,43 +424,50 @@ pub use epaint::emath; pub use ecolor::hex_color; pub use ecolor::{Color32, Rgba}; pub use emath::{ - lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rangef, Rect, Vec2, + lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rangef, Rect, Vec2, Vec2b, }; pub use epaint::{ mutex, text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak}, - textures::{TextureFilter, TextureOptions, TexturesDelta}, - ClippedPrimitive, ColorImage, FontImage, ImageData, Mesh, PaintCallback, PaintCallbackInfo, - Rounding, Shape, Stroke, TextureHandle, TextureId, + textures::{TextureFilter, TextureOptions, TextureWrapMode, TexturesDelta}, + ClippedPrimitive, ColorImage, FontImage, ImageData, Margin, Mesh, PaintCallback, + PaintCallbackInfo, Rounding, Shape, Stroke, TextureHandle, TextureId, }; pub mod text { - pub use crate::text_edit::CCursorRange; + pub use crate::text_selection::{CCursorRange, CursorRange}; pub use epaint::text::{ cursor::CCursor, FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob, - LayoutSection, TextFormat, TAB_SIZE, + LayoutSection, TextFormat, TextWrapping, TAB_SIZE, }; } pub use { containers::*, - context::{Context, RequestRepaintInfo}, + context::{Context, RepaintCause, RequestRepaintInfo}, data::{ input::*, - output::{self, CursorIcon, FullOutput, PlatformOutput, UserAttentionType, WidgetInfo}, + output::{ + self, CursorIcon, FullOutput, OpenUrl, PlatformOutput, UserAttentionType, WidgetInfo, + }, + Key, }, + drag_and_drop::DragAndDrop, grid::Grid, id::{Id, IdMap}, input_state::{InputState, MultiTouchInfo, PointerState}, layers::{LayerId, Order}, layout::*, + load::SizeHint, memory::{Memory, Options}, painter::Painter, response::{InnerResponse, Response}, sense::Sense, - style::{FontSelection, Margin, Style, TextStyle, Visuals}, + style::{FontSelection, Style, TextStyle, Visuals}, text::{Galley, TextFormat}, ui::Ui, + viewport::*, + widget_rect::{WidgetRect, WidgetRects}, widget_text::{RichText, WidgetText}, widgets::*, }; @@ -393,6 +488,35 @@ pub fn warn_if_debug_build(ui: &mut crate::Ui) { // ---------------------------------------------------------------------------- +/// Include an image in the binary. +/// +/// This is a wrapper over `include_bytes!`, and behaves in the same way. +/// +/// It produces an [`ImageSource`] which can be used directly in [`Ui::image`] or [`Image::new`]: +/// +/// ``` +/// # egui::__run_test_ui(|ui| { +/// ui.image(egui::include_image!("../assets/ferris.png")); +/// ui.add( +/// egui::Image::new(egui::include_image!("../assets/ferris.png")) +/// .max_width(200.0) +/// .rounding(10.0), +/// ); +/// +/// let image_source: egui::ImageSource = egui::include_image!("../assets/ferris.png"); +/// assert_eq!(image_source.uri(), Some("bytes://../assets/ferris.png")); +/// # }); +/// ``` +#[macro_export] +macro_rules! include_image { + ($path: literal) => { + $crate::ImageSource::Bytes { + uri: ::std::borrow::Cow::Borrowed(concat!("bytes://", $path)), + bytes: $crate::load::Bytes::Static(include_bytes!($path)), + } + }; +} + /// Create a [`Hyperlink`](crate::Hyperlink) to the current [`file!()`] (and line) on Github /// /// ``` @@ -425,32 +549,6 @@ macro_rules! github_link_file { // ---------------------------------------------------------------------------- -/// Show debug info on hover when [`Context::set_debug_on_hover`] has been turned on. -/// -/// ``` -/// # egui::__run_test_ui(|ui| { -/// // Turn on tracing of widgets -/// ui.ctx().set_debug_on_hover(true); -/// -/// /// Show [`std::file`], [`std::line`] and argument on hover -/// egui::trace!(ui, "MyWindow"); -/// -/// /// Show [`std::file`] and [`std::line`] on hover -/// egui::trace!(ui); -/// # }); -/// ``` -#[macro_export] -macro_rules! trace { - ($ui: expr) => {{ - $ui.trace_location(format!("{}:{}", file!(), line!())) - }}; - ($ui: expr, $label: expr) => {{ - $ui.trace_location(format!("{} - {}:{}", $label, file!(), line!())) - }}; -} - -// ---------------------------------------------------------------------------- - /// An assert that is only active when `egui` is compiled with the `extra_asserts` feature /// or with the `extra_debug_asserts` feature in debug builds. #[macro_export] @@ -467,6 +565,9 @@ macro_rules! egui_assert { // ---------------------------------------------------------------------------- +/// The minus character: +pub(crate) const MINUS_CHAR_STR: &str = "−"; + /// The default egui fonts supports around 1216 emojis in total. /// Here are some of the most useful: /// ∞⊗⎗⎘⎙⏏⏴⏵⏶⏷ @@ -542,6 +643,8 @@ pub enum WidgetType { CollapsingHeader, + ProgressIndicator, + /// If you cannot fit any of the above slots. /// /// If this is something you think should be added, file an issue. @@ -574,3 +677,33 @@ pub fn __run_test_ui(mut add_contents: impl FnMut(&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::*; diff --git a/crates/egui/src/load.rs b/crates/egui/src/load.rs new file mode 100644 index 00000000000..4d527de63be --- /dev/null +++ b/crates/egui/src/load.rs @@ -0,0 +1,543 @@ +//! # Image loading +//! +//! If you just want to display some images, [`egui_extras`](https://crates.io/crates/egui_extras/) +//! will get you up and running quickly with its reasonable default implementations of the traits described below. +//! +//! 1. Add [`egui_extras`](https://crates.io/crates/egui_extras/) as a dependency with the `all_loaders` feature. +//! 2. Add a call to [`egui_extras::install_image_loaders`](https://docs.rs/egui_extras/latest/egui_extras/fn.install_image_loaders.html) +//! in your app's setup code. +//! 3. Use [`Ui::image`][`crate::ui::Ui::image`] with some [`ImageSource`][`crate::ImageSource`]. +//! +//! ## Loading process +//! +//! There are three kinds of loaders: +//! - [`BytesLoader`]: load the raw bytes of an image +//! - [`ImageLoader`]: decode the bytes into an array of colors +//! - [`TextureLoader`]: ask the backend to put an image onto the GPU +//! +//! The different kinds of loaders represent different layers in the loading process: +//! +//! ```text,ignore +//! ui.image("file://image.png") +//! └► Context::try_load_texture +//! └► TextureLoader::load +//! └► Context::try_load_image +//! └► ImageLoader::load +//! └► Context::try_load_bytes +//! └► BytesLoader::load +//! ``` +//! +//! As each layer attempts to load the URI, it first asks the layer below it +//! for the data it needs to do its job. But this is not a strict requirement, +//! an implementation could instead generate the data it needs! +//! +//! Loader trait implementations may be registered on a context with: +//! - [`Context::add_bytes_loader`] +//! - [`Context::add_image_loader`] +//! - [`Context::add_texture_loader`] +//! +//! There may be multiple loaders of the same kind registered at the same time. +//! The `try_load` methods on [`Context`] will attempt to call each loader one by one, +//! until one of them returns something other than [`LoadError::NotSupported`]. +//! +//! The loaders are stored in the context. This means they may hold state across frames, +//! which they can (and _should_) use to cache the results of the operations they perform. +//! +//! For example, a [`BytesLoader`] that loads file URIs (`file://image.png`) +//! would cache each file read. A [`TextureLoader`] would cache each combination +//! of `(URI, TextureOptions)`, and so on. +//! +//! Each URI will be passed through the loaders as a plain `&str`. +//! The loaders are free to derive as much meaning from the URI as they wish to. +//! For example, a loader may determine that it doesn't support loading a specific URI +//! if the protocol does not match what it expects. + +mod bytes_loader; +mod texture_loader; + +use std::borrow::Cow; +use std::fmt::Debug; +use std::ops::Deref; +use std::{fmt::Display, sync::Arc}; + +use ahash::HashMap; + +use epaint::mutex::Mutex; +use epaint::util::FloatOrd; +use epaint::util::OrderedFloat; +use epaint::TextureHandle; +use epaint::{textures::TextureOptions, ColorImage, TextureId, Vec2}; + +use crate::Context; + +pub use self::bytes_loader::DefaultBytesLoader; +pub use self::texture_loader::DefaultTextureLoader; + +/// Represents a failed attempt at loading an image. +#[derive(Clone, Debug)] +pub enum LoadError { + /// Programmer error: There are no image loaders installed. + NoImageLoaders, + + /// A specific loader does not support this scheme, protocol or image format. + NotSupported, + + /// 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, + + /// Programmer error: no matching [`TextureLoader`]. + /// Because of the [`DefaultTextureLoader`], this error should never happen. + NoMatchingTextureLoader, + + /// Runtime error: Loading was attempted, but failed (e.g. "File not found"). + Loading(String), +} + +impl Display for LoadError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NoImageLoaders => f.write_str( + "No image loaders are installed. If you're trying to load some images \ + for the first time, follow the steps outlined in https://docs.rs/egui/latest/egui/load/index.html"), + + 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::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::Loading(message) => f.write_str(message), + } + } +} + +impl std::error::Error for LoadError {} + +pub type Result = std::result::Result; + +/// Given as a hint for image loading requests. +/// +/// Used mostly for rendering SVG:s to a good size. +/// +/// All variants will preserve the original aspect ratio. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum SizeHint { + /// Scale original size by some factor. + Scale(OrderedFloat), + + /// Scale to width. + Width(u32), + + /// Scale to height. + Height(u32), + + /// Scale to size. + Size(u32, u32), +} + +impl Default for SizeHint { + #[inline] + fn default() -> Self { + Self::Scale(1.0.ord()) + } +} + +impl From for SizeHint { + #[inline] + fn from(value: Vec2) -> Self { + Self::Size(value.x.round() as u32, value.y.round() as u32) + } +} + +/// Represents a byte buffer. +/// +/// This is essentially `Cow<'static, [u8]>` but with the `Owned` variant being an `Arc`. +#[derive(Clone)] +pub enum Bytes { + Static(&'static [u8]), + Shared(Arc<[u8]>), +} + +impl Debug for Bytes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Static(arg0) => f.debug_tuple("Static").field(&arg0.len()).finish(), + Self::Shared(arg0) => f.debug_tuple("Shared").field(&arg0.len()).finish(), + } + } +} + +impl From<&'static [u8]> for Bytes { + #[inline] + fn from(value: &'static [u8]) -> Self { + Self::Static(value) + } +} + +impl From<&'static [u8; N]> for Bytes { + #[inline] + fn from(value: &'static [u8; N]) -> Self { + Self::Static(value) + } +} + +impl From> for Bytes { + #[inline] + fn from(value: Arc<[u8]>) -> Self { + Self::Shared(value) + } +} + +impl From> for Bytes { + #[inline] + fn from(value: Vec) -> Self { + Self::Shared(value.into()) + } +} + +impl AsRef<[u8]> for Bytes { + #[inline] + fn as_ref(&self) -> &[u8] { + match self { + Self::Static(bytes) => bytes, + Self::Shared(bytes) => bytes, + } + } +} + +impl Deref for Bytes { + type Target = [u8]; + + #[inline] + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +/// Represents bytes which are currently being loaded. +/// +/// This is similar to [`std::task::Poll`], but the `Pending` variant +/// contains an optional `size`, which may be used during layout to +/// pre-allocate space the image. +#[derive(Clone)] +pub enum BytesPoll { + /// Bytes are being loaded. + Pending { + /// Set if known (e.g. from a HTTP header, or by parsing the image file header). + size: Option, + }, + + /// Bytes are loaded. + Ready { + /// Set if known (e.g. from a HTTP header, or by parsing the image file header). + size: Option, + + /// File contents, e.g. the contents of a `.png`. + bytes: Bytes, + + /// Mime type of the content, e.g. `image/png`. + /// + /// Set if known (e.g. from `Content-Type` HTTP header). + mime: Option, + }, +} + +/// Used to get a unique ID when implementing one of the loader traits: [`BytesLoader::id`], [`ImageLoader::id`], and [`TextureLoader::id`]. +/// +/// This just expands to `module_path!()` concatenated with the given type name. +#[macro_export] +macro_rules! generate_loader_id { + ($ty:ident) => { + concat!(module_path!(), "::", stringify!($ty)) + }; +} +pub use crate::generate_loader_id; + +pub type BytesLoadResult = Result; + +/// Represents a loader capable of loading raw unstructured bytes from somewhere, +/// e.g. from disk or network. +/// +/// It should also provide any subsequent loaders a hint for what the bytes may +/// represent using [`BytesPoll::Ready::mime`], if it can be inferred. +/// +/// Implementations are expected to cache at least each `URI`. +pub trait BytesLoader { + /// Unique ID of this loader. + /// + /// To reduce the chance of collisions, use [`generate_loader_id`] for this. + fn id(&self) -> &str; + + /// Try loading the bytes from the given uri. + /// + /// Implementations should call `ctx.request_repaint` to wake up the ui + /// once the data is ready. + /// + /// The implementation should cache any result, so that calling this + /// is immediate-mode safe. + /// + /// # Errors + /// This may fail with: + /// - [`LoadError::NotSupported`] if the loader does not support loading `uri`. + /// - [`LoadError::Loading`] if the loading process failed. + fn load(&self, ctx: &Context, uri: &str) -> BytesLoadResult; + + /// Forget the given `uri`. + /// + /// If `uri` is cached, it should be evicted from cache, + /// so that it may be fully reloaded. + fn forget(&self, uri: &str); + + /// Forget all URIs ever given to this loader. + /// + /// If the loader caches any URIs, the entire cache should be cleared, + /// so that all of them may be fully reloaded. + fn forget_all(&self); + + /// Implementations may use this to perform work at the end of a frame, + /// such as evicting unused entries from a cache. + fn end_frame(&self, frame_index: usize) { + let _ = frame_index; + } + + /// If the loader caches any data, this should return the size of that cache. + fn byte_size(&self) -> usize; +} + +/// Represents an image which is currently being loaded. +/// +/// This is similar to [`std::task::Poll`], but the `Pending` variant +/// contains an optional `size`, which may be used during layout to +/// pre-allocate space the image. +#[derive(Clone)] +pub enum ImagePoll { + /// Image is loading. + Pending { + /// Set if known (e.g. from a HTTP header, or by parsing the image file header). + size: Option, + }, + + /// Image is loaded. + Ready { image: Arc }, +} + +pub type ImageLoadResult = Result; + +/// An `ImageLoader` decodes raw bytes into a [`ColorImage`]. +/// +/// Implementations are expected to cache at least each `URI`. +pub trait ImageLoader { + /// Unique ID of this loader. + /// + /// To reduce the chance of collisions, include `module_path!()` as part of this ID. + /// + /// For example: `concat!(module_path!(), "::MyLoader")` + /// for `my_crate::my_loader::MyLoader`. + fn id(&self) -> &str; + + /// Try loading the image from the given uri. + /// + /// Implementations should call `ctx.request_repaint` to wake up the ui + /// once the image is ready. + /// + /// The implementation should cache any result, so that calling this + /// is immediate-mode safe. + /// + /// # Errors + /// This may fail with: + /// - [`LoadError::NotSupported`] if the loader does not support loading `uri`. + /// - [`LoadError::Loading`] if the loading process failed. + fn load(&self, ctx: &Context, uri: &str, size_hint: SizeHint) -> ImageLoadResult; + + /// Forget the given `uri`. + /// + /// If `uri` is cached, it should be evicted from cache, + /// so that it may be fully reloaded. + fn forget(&self, uri: &str); + + /// Forget all URIs ever given to this loader. + /// + /// If the loader caches any URIs, the entire cache should be cleared, + /// so that all of them may be fully reloaded. + fn forget_all(&self); + + /// Implementations may use this to perform work at the end of a frame, + /// such as evicting unused entries from a cache. + fn end_frame(&self, frame_index: usize) { + let _ = frame_index; + } + + /// If the loader caches any data, this should return the size of that cache. + fn byte_size(&self) -> usize; +} + +/// A texture with a known size. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SizedTexture { + pub id: TextureId, + pub size: Vec2, +} + +impl SizedTexture { + /// Create a [`SizedTexture`] from a texture `id` with a specific `size`. + pub fn new(id: impl Into, size: impl Into) -> Self { + Self { + id: id.into(), + size: size.into(), + } + } + + /// Fetch the [id][`SizedTexture::id`] and [size][`SizedTexture::size`] from a [`TextureHandle`]. + pub fn from_handle(handle: &TextureHandle) -> Self { + let size = handle.size(); + Self { + id: handle.id(), + size: Vec2::new(size[0] as f32, size[1] as f32), + } + } +} + +impl From<(TextureId, Vec2)> for SizedTexture { + #[inline] + fn from((id, size): (TextureId, Vec2)) -> Self { + Self { id, size } + } +} + +impl<'a> From<&'a TextureHandle> for SizedTexture { + #[inline] + fn from(handle: &'a TextureHandle) -> Self { + Self::from_handle(handle) + } +} + +/// Represents a texture is currently being loaded. +/// +/// This is similar to [`std::task::Poll`], but the `Pending` variant +/// contains an optional `size`, which may be used during layout to +/// pre-allocate space the image. +#[derive(Clone, Copy)] +pub enum TexturePoll { + /// Texture is loading. + Pending { + /// Set if known (e.g. from a HTTP header, or by parsing the image file header). + size: Option, + }, + + /// Texture is loaded. + Ready { texture: SizedTexture }, +} + +impl TexturePoll { + #[inline] + pub fn size(&self) -> Option { + match self { + Self::Pending { size } => *size, + Self::Ready { texture } => Some(texture.size), + } + } + + #[inline] + pub fn texture_id(&self) -> Option { + match self { + Self::Pending { .. } => None, + Self::Ready { texture } => Some(texture.id), + } + } +} + +pub type TextureLoadResult = Result; + +/// A `TextureLoader` uploads a [`ColorImage`] to the GPU, returning a [`SizedTexture`]. +/// +/// `egui` comes with an implementation that uses [`Context::load_texture`], +/// which just asks the egui backend to upload the image to the GPU. +/// +/// You can implement this trait if you do your own uploading of images to the GPU. +/// For instance, you can use this to refer to textures in a game engine that egui +/// doesn't otherwise know about. +/// +/// Implementations are expected to cache each combination of `(URI, TextureOptions)`. +pub trait TextureLoader { + /// Unique ID of this loader. + /// + /// To reduce the chance of collisions, include `module_path!()` as part of this ID. + /// + /// For example: `concat!(module_path!(), "::MyLoader")` + /// for `my_crate::my_loader::MyLoader`. + fn id(&self) -> &str; + + /// Try loading the texture from the given uri. + /// + /// Implementations should call `ctx.request_repaint` to wake up the ui + /// once the texture is ready. + /// + /// The implementation should cache any result, so that calling this + /// is immediate-mode safe. + /// + /// # Errors + /// This may fail with: + /// - [`LoadError::NotSupported`] if the loader does not support loading `uri`. + /// - [`LoadError::Loading`] if the loading process failed. + fn load( + &self, + ctx: &Context, + uri: &str, + texture_options: TextureOptions, + size_hint: SizeHint, + ) -> TextureLoadResult; + + /// Forget the given `uri`. + /// + /// If `uri` is cached, it should be evicted from cache, + /// so that it may be fully reloaded. + fn forget(&self, uri: &str); + + /// Forget all URIs ever given to this loader. + /// + /// If the loader caches any URIs, the entire cache should be cleared, + /// so that all of them may be fully reloaded. + fn forget_all(&self); + + /// Implementations may use this to perform work at the end of a frame, + /// such as evicting unused entries from a cache. + fn end_frame(&self, frame_index: usize) { + let _ = frame_index; + } + + /// If the loader caches any data, this should return the size of that cache. + fn byte_size(&self) -> usize; +} + +type BytesLoaderImpl = Arc; +type ImageLoaderImpl = Arc; +type TextureLoaderImpl = Arc; + +#[derive(Clone)] +/// The loaders of bytes, images, and textures. +pub struct Loaders { + pub include: Arc, + pub bytes: Mutex>, + pub image: Mutex>, + pub texture: Mutex>, +} + +impl Default for Loaders { + fn default() -> Self { + let include = Arc::new(DefaultBytesLoader::default()); + Self { + bytes: Mutex::new(vec![include.clone()]), + image: Mutex::new(Vec::new()), + // By default we only include `DefaultTextureLoader`. + texture: Mutex::new(vec![Arc::new(DefaultTextureLoader::default())]), + include, + } + } +} diff --git a/crates/egui/src/load/bytes_loader.rs b/crates/egui/src/load/bytes_loader.rs new file mode 100644 index 00000000000..3ab46794912 --- /dev/null +++ b/crates/egui/src/load/bytes_loader.rs @@ -0,0 +1,69 @@ +use super::*; + +/// Maps URI:s to [`Bytes`], e.g. found with `include_bytes!`. +/// +/// By convention, the URI:s should be prefixed with `bytes://`. +#[derive(Default)] +pub struct DefaultBytesLoader { + cache: Mutex, Bytes>>, +} + +impl DefaultBytesLoader { + pub fn insert(&self, uri: impl Into>, bytes: impl Into) { + self.cache + .lock() + .entry(uri.into()) + .or_insert_with_key(|_uri| { + let bytes: Bytes = bytes.into(); + + #[cfg(feature = "log")] + log::trace!("loaded {} bytes for uri {_uri:?}", bytes.len()); + + bytes + }); + } +} + +impl BytesLoader for DefaultBytesLoader { + fn id(&self) -> &str { + generate_loader_id!(DefaultBytesLoader) + } + + fn load(&self, _: &Context, uri: &str) -> BytesLoadResult { + // We accept uri:s that don't start with `bytes://` too… for now. + match self.cache.lock().get(uri).cloned() { + Some(bytes) => Ok(BytesPoll::Ready { + size: None, + bytes, + mime: None, + }), + None => { + if uri.starts_with("bytes://") { + Err(LoadError::Loading( + "Bytes not found. Did you forget to call Context::include_bytes?".into(), + )) + } else { + Err(LoadError::NotSupported) + } + } + } + } + + fn forget(&self, uri: &str) { + #[cfg(feature = "log")] + log::trace!("forget {uri:?}"); + + let _ = self.cache.lock().remove(uri); + } + + fn forget_all(&self) { + #[cfg(feature = "log")] + log::trace!("forget all"); + + self.cache.lock().clear(); + } + + fn byte_size(&self) -> usize { + self.cache.lock().values().map(|bytes| bytes.len()).sum() + } +} diff --git a/crates/egui/src/load/texture_loader.rs b/crates/egui/src/load/texture_loader.rs new file mode 100644 index 00000000000..89d616e4760 --- /dev/null +++ b/crates/egui/src/load/texture_loader.rs @@ -0,0 +1,60 @@ +use super::*; + +#[derive(Default)] +pub struct DefaultTextureLoader { + cache: Mutex>, +} + +impl TextureLoader for DefaultTextureLoader { + fn id(&self) -> &str { + crate::generate_loader_id!(DefaultTextureLoader) + } + + fn load( + &self, + ctx: &Context, + uri: &str, + texture_options: TextureOptions, + size_hint: SizeHint, + ) -> TextureLoadResult { + let mut cache = self.cache.lock(); + if let Some(handle) = cache.get(&(uri.into(), texture_options)) { + let texture = SizedTexture::from_handle(handle); + Ok(TexturePoll::Ready { texture }) + } else { + match ctx.try_load_image(uri, size_hint)? { + ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }), + ImagePoll::Ready { image } => { + let handle = ctx.load_texture(uri, image, texture_options); + let texture = SizedTexture::from_handle(&handle); + cache.insert((uri.into(), texture_options), handle); + Ok(TexturePoll::Ready { texture }) + } + } + } + } + + fn forget(&self, uri: &str) { + #[cfg(feature = "log")] + log::trace!("forget {uri:?}"); + + self.cache.lock().retain(|(u, _), _| u != uri); + } + + fn forget_all(&self) { + #[cfg(feature = "log")] + log::trace!("forget all"); + + self.cache.lock().clear(); + } + + fn end_frame(&self, _: usize) {} + + fn byte_size(&self) -> usize { + self.cache + .lock() + .values() + .map(|texture| texture.byte_size()) + .sum() + } +} diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index ede4fb70eb5..068347b5697 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -1,4 +1,12 @@ -use crate::{area, window, Id, IdMap, InputState, LayerId, Pos2, Rect, Style}; +#![warn(missing_docs)] // Let's keep this file well-documented.` to memory.rs + +use ahash::HashMap; +use epaint::emath::TSTransform; + +use crate::{ + area, vec2, EventFilter, Id, IdMap, LayerId, Order, Pos2, Rangef, RawInput, Rect, Style, Vec2, + ViewportId, ViewportIdMap, ViewportIdSet, +}; // ---------------------------------------------------------------------------- @@ -11,10 +19,11 @@ use crate::{area, window, Id, IdMap, InputState, LayerId, Pos2, Rect, Style}; /// For this you need to enable the `persistence`. /// /// If you want to store data for your widgets, you should look at [`Memory::data`] -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] #[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "persistence", serde(default))] pub struct Memory { + /// Global egui options. pub options: Options, /// This map stores some superficial state for all widgets with custom [`Id`]s. @@ -29,7 +38,7 @@ pub struct Memory { /// /// This will be saved between different program runs if you use the `persistence` feature. /// - /// To store a state common for all your widgets (a singleton), use [`Id::null`] as the key. + /// To store a state common for all your widgets (a singleton), use [`Id::NULL`] as the key. pub data: crate::util::IdTypeMap, // ------------------------------------------ @@ -61,38 +70,98 @@ pub struct Memory { pub caches: crate::util::cache::CacheStorage, // ------------------------------------------ - /// new scale that will be applied at the start of the next frame - #[cfg_attr(feature = "persistence", serde(skip))] - pub(crate) new_pixels_per_point: Option, - /// 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, + // Current active viewport #[cfg_attr(feature = "persistence", serde(skip))] - pub(crate) interaction: Interaction, + pub(crate) viewport_id: ViewportId, + /// Which popup-window is open (if any)? + /// Could be a combo box, color picker, menu etc. #[cfg_attr(feature = "persistence", serde(skip))] - pub(crate) window_interaction: Option, + popup: Option, #[cfg_attr(feature = "persistence", serde(skip))] - pub(crate) drag_value: crate::widgets::drag_value::MonoState, + everything_is_visible: bool, - pub(crate) areas: Areas, + /// Transforms per layer + pub layer_transforms: HashMap, + + // ------------------------------------------------- + // Per-viewport: + areas: ViewportIdMap, - /// Which popup-window is open (if any)? - /// Could be a combo box, color picker, menu etc. #[cfg_attr(feature = "persistence", serde(skip))] - popup: Option, + pub(crate) interactions: ViewportIdMap, #[cfg_attr(feature = "persistence", serde(skip))] - everything_is_visible: bool, + pub(crate) focus: ViewportIdMap, +} + +impl Default for Memory { + fn default() -> Self { + let mut slf = Self { + options: Default::default(), + data: Default::default(), + caches: Default::default(), + new_font_definitions: Default::default(), + interactions: Default::default(), + focus: Default::default(), + viewport_id: Default::default(), + areas: Default::default(), + layer_transforms: Default::default(), + popup: Default::default(), + everything_is_visible: Default::default(), + }; + slf.interactions.entry(slf.viewport_id).or_default(); + slf.areas.entry(slf.viewport_id).or_default(); + slf + } +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +enum FocusDirection { + /// Select the widget closest above the current focused widget. + Up, + + /// Select the widget to the right of the current focused widget. + Right, + + /// Select the widget below the current focused widget. + Down, + + /// Select the widget to the left of the the current focused widget. + Left, + + /// Select the previous widget that had focus. + Previous, + + /// Select the next widget that wants focus. + Next, + + /// Don't change focus. + #[default] + None, +} + +impl FocusDirection { + fn is_cardinal(&self) -> bool { + match self { + Self::Up | Self::Right | Self::Down | Self::Left => true, + + Self::Previous | Self::Next | Self::None => false, + } + } } // ---------------------------------------------------------------------------- /// Some global options that you can read and write. -#[derive(Clone, Debug)] +/// +/// See also [`crate::style::DebugOptions`]. +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct Options { @@ -100,16 +169,42 @@ pub struct Options { #[cfg_attr(feature = "serde", serde(skip))] pub(crate) style: std::sync::Arc