diff --git a/.cargo/config.toml b/.cargo/config.toml index be614b00236..b18b6ce735d 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -4,3 +4,6 @@ # we don't use `[build]` because of rust analyzer's build cache invalidation https://github.com/emilk/eframe_template/issues/93 [target.wasm32-unknown-unknown] rustflags = ["--cfg=web_sys_unstable_apis"] + +[alias] +xtask = "run --quiet --package xtask --" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..b1f5e1192e4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto eol=lf +Cargo.lock linguist-generated=false diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c3bc239276b..98a3b496a21 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,7 +6,7 @@ Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/ * 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. -* Remember to run `cargo fmt` and `cargo cranky`. +* Remember to run `cargo fmt` and `cargo clippy`. * 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. diff --git a/.github/workflows/deploy_web_demo.yml b/.github/workflows/deploy_web_demo.yml index ea2956db313..41203da390b 100644 --- a/.github/workflows/deploy_web_demo.yml +++ b/.github/workflows/deploy_web_demo.yml @@ -43,7 +43,7 @@ jobs: with: profile: minimal target: wasm32-unknown-unknown - toolchain: 1.72.0 + toolchain: 1.76.0 override: true - uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d641b17b284..8aaa18faa36 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -19,7 +19,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.72.0 + toolchain: 1.76.0 - name: Install packages (Linux) if: runner.os == 'Linux' @@ -40,11 +40,6 @@ jobs: - name: Lint vertical spacing run: ./scripts/lint.py - - name: Install cargo-cranky - uses: baptiste0928/cargo-install@v1 - with: - crate: cargo-cranky - - name: check --all-features run: cargo check --locked --all-features --all-targets @@ -78,11 +73,11 @@ jobs: - name: Test run: cargo test --all-features - - name: Cranky - run: cargo cranky --all-targets --all-features -- -D warnings + - name: clippy + run: cargo clippy --all-targets --all-features -- -D warnings - - name: Cranky release - run: cargo cranky --all-targets --all-features --release -- -D warnings + - name: clippy release + run: cargo clippy --all-targets --all-features --release -- -D warnings # --------------------------------------------------------------------------- @@ -93,7 +88,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.72.0 + toolchain: 1.76.0 targets: wasm32-unknown-unknown - run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev @@ -101,11 +96,6 @@ jobs: - name: Set up cargo cache uses: Swatinem/rust-cache@v2 - - name: Install cargo-cranky - uses: baptiste0928/cargo-install@v1 - with: - crate: cargo-cranky - - name: Check wasm32 egui_demo_app run: cargo check -p egui_demo_app --lib --target wasm32-unknown-unknown @@ -118,11 +108,11 @@ jobs: - name: wasm-bindgen uses: jetli/wasm-bindgen-action@v0.1.0 with: - version: "0.2.88" + version: "0.2.92" - run: ./scripts/wasm_bindgen_check.sh --skip-setup - - name: Cranky wasm32 + - name: clippy wasm32 run: ./scripts/clippy_wasm.sh # --------------------------------------------------------------------------- @@ -151,7 +141,7 @@ jobs: - uses: actions/checkout@v4 - uses: EmbarkStudios/cargo-deny-action@v1 with: - rust-version: "1.72.0" + rust-version: "1.76.0" log-level: error command: check arguments: --target ${{ matrix.target }} @@ -166,7 +156,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.72.0 + toolchain: 1.76.0 targets: aarch64-linux-android - name: Set up cargo cache @@ -184,7 +174,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.72.0 + toolchain: 1.76.0 - name: Set up cargo cache uses: Swatinem/rust-cache@v2 diff --git a/.gitignore b/.gitignore index c43b95594e4..7db0b9d06fb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /.*.json /.vscode /media/* +.idea/ diff --git a/.typos.toml b/.typos.toml index 6d856495178..de51a691cc0 100644 --- a/.typos.toml +++ b/.typos.toml @@ -3,7 +3,9 @@ # run: typos [default.extend-words] +ime = "ime" # Input Method Editor nknown = "nknown" # part of @55nknown username +ro = "ro" # read-only, also part of the username @Phen-Ro [files] extend-exclude = ["web_demo/egui_demo_app.js"] # auto-generated diff --git a/.vscode/settings.json b/.vscode/settings.json index 8ec1e4aa67f..681dc12fe15 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,7 +15,7 @@ // Tell Rust Analyzer to use its own target directory, so we don't need to wait for it to finish wen we want to `cargo run` "rust-analyzer.check.overrideCommand": [ "cargo", - "cranky", + "clippy", "--target-dir=target_ra", "--workspace", "--message-format=json", @@ -24,7 +24,7 @@ ], "rust-analyzer.cargo.buildScripts.overrideCommand": [ "cargo", - "cranky", + "clippy", "--quiet", "--target-dir=target_ra", "--workspace", diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c468fc815d..35cab321fcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,105 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.27.2 - 2024-04-02 +### 🐛 Fixed +* Fix tooltips for non-interactive widgets [#4291](https://github.com/emilk/egui/pull/4291) +* Fix problem clicking the edge of a `TextEdit` [#4272](https://github.com/emilk/egui/pull/4272) +* Fix: `Response::clicked_elsewhere` takes clip rect into account [#4274](https://github.com/emilk/egui/pull/4274) +* Fix incorrect `Response::interact_rect` for `Area/Window` [#4273](https://github.com/emilk/egui/pull/4273) + +### ⭐ Added +* Allow disabling animations on a `ScrollArea` [#4309](https://github.com/emilk/egui/pull/4309) (thanks [@lucasmerlin](https://github.com/lucasmerlin)!) + + +## 0.27.1 - 2024-03-29 +### 🐛 Fixed +* Fix visual glitch on the right side of highly rounded rectangles [#4244](https://github.com/emilk/egui/pull/4244) +* Prevent visual glitch when shadow blur width is very high [#4245](https://github.com/emilk/egui/pull/4245) +* Fix `InputState::any_touches` and add `InputState::has_touch_screen` [#4247](https://github.com/emilk/egui/pull/4247) +* Fix `Context::repaint_causes` returning no causes [#4248](https://github.com/emilk/egui/pull/4248) +* Fix touch-and-hold to open context menu [#4249](https://github.com/emilk/egui/pull/4249) +* Hide shortcut text on zoom buttons if `zoom_with_keyboard` is false [#4262](https://github.com/emilk/egui/pull/4262) + +### 🔧 Changed +* Don't apply a clip rect to the contents of an `Area` or `Window` [#4258](https://github.com/emilk/egui/pull/4258) + + +## 0.27.0 - 2024-03-26 - Nicer menus and new hit test logic +The hit test logic (what is the user clicking on?) has been completely rewritten, and should now be much more accurate and helpful. +The hit test and interaction logic is run at the start of the frame, using the widgets rects from the previous frame, but the latest mouse coordinates. +It enabled getting a `Response` for a widget _before_ creating it using `Context::read_response`. +This will in the future unlock more powerful widget styling options. +The new hit test also allows clicking slightly outside a button and still hit it, improving the support for touch screens. + +The menus have also been improved so that they both act and feel better, with no change in API. +Included in this is much nicer looking shadows, supporting an offset. + +Screenshot 2024-03-26 at 17 00 23 + + +### ⚠️ 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. +* `Memory::focus` has been renamed `Memory::focused` +* `Area::new` now takes an `Id` by argument [#4115](https://github.com/emilk/egui/pull/4115) +* Change the definition of `clicked_by` [#4192](https://github.com/emilk/egui/pull/4192) + +### ☰ Menu related improvements +* Add some distance between parent menu and submenu [#4230](https://github.com/emilk/egui/pull/4230) +* Add `Area::sense` and improve hit-testing of buttons in menus [#4234](https://github.com/emilk/egui/pull/4234) +* Improve logic for when submenus are kept open [#4166](https://github.com/emilk/egui/pull/4166) +* Better align menus with the button that opened them [#4233](https://github.com/emilk/egui/pull/4233) +* Hide hover UI when showing the context menu [#4138](https://github.com/emilk/egui/pull/4138) (thanks [@abey79](https://github.com/abey79)!) +* CSS-like `Shadow` with offset, spread, and blur [#4232](https://github.com/emilk/egui/pull/4232) +* On touch screens, press-and-hold equals a secondary click [#4195](https://github.com/emilk/egui/pull/4195) + +### ⭐ Added +* Add with_taskbar to viewport builder [#3958](https://github.com/emilk/egui/pull/3958) (thanks [@AnotherNathan](https://github.com/AnotherNathan)!) +* Add F21 to F35 key bindings [#4004](https://github.com/emilk/egui/pull/4004) (thanks [@oscargus](https://github.com/oscargus)!) +* Add `Options::debug_paint_interactive_widgets` [#4018](https://github.com/emilk/egui/pull/4018) +* Add `Ui::set_opacity` [#3965](https://github.com/emilk/egui/pull/3965) (thanks [@YgorSouza](https://github.com/YgorSouza)!) +* Add `Response::paint_debug_info()` to make it easy to visualize a widget's id and state [#4056](https://github.com/emilk/egui/pull/4056) (thanks [@abey79](https://github.com/abey79)!) +* Add layer transforms, interaction in layer [#3906](https://github.com/emilk/egui/pull/3906) (thanks [@Tweoss](https://github.com/Tweoss)!) +* Add `ColorImage::from_gray_iter` [#3536](https://github.com/emilk/egui/pull/3536) (thanks [@wangxiaochuTHU](https://github.com/wangxiaochuTHU)!) +* Add API for raw mouse motion [#4063](https://github.com/emilk/egui/pull/4063) (thanks [@GiantBlargg](https://github.com/GiantBlargg)!) +* Add accessibility to `ProgressBar` and `Spinner` [#4139](https://github.com/emilk/egui/pull/4139) (thanks [@DataTriny](https://github.com/DataTriny)!) +* Add x11 window type settings to viewport builder [#4175](https://github.com/emilk/egui/pull/4175) (thanks [@psethwick](https://github.com/psethwick)!) +* Add an API for customizing the return key in TextEdit [#4085](https://github.com/emilk/egui/pull/4085) (thanks [@lemon-sh](https://github.com/lemon-sh)!) +* Convenience `const fn` for `Margin`, `Rounding` and `Shadow` [#4080](https://github.com/emilk/egui/pull/4080) (thanks [@0Qwel](https://github.com/0Qwel)!) +* Serde feature: add serde derives to input related structs [#4100](https://github.com/emilk/egui/pull/4100) (thanks [@gweisert](https://github.com/gweisert)!) +* Give each menu `Area` an id distinct from the id of what was clicked [#4114](https://github.com/emilk/egui/pull/4114) +* `epaint`: Added `Shape::{scale,translate}` wrappers [#4090](https://github.com/emilk/egui/pull/4090) (thanks [@varphone](https://github.com/varphone)!) +* A `Window` can now be resizable in only one direction [#4155](https://github.com/emilk/egui/pull/4155) +* Add `EllipseShape` [#4122](https://github.com/emilk/egui/pull/4122) (thanks [@TheTacBanana](https://github.com/TheTacBanana)!) +* Adjustable Slider rail height [#4092](https://github.com/emilk/egui/pull/4092) (thanks [@rustbasic](https://github.com/rustbasic)!) +* Expose state override for `HeaderResponse` [#4200](https://github.com/emilk/egui/pull/4200) (thanks [@Zeenobit](https://github.com/Zeenobit)!) + +### 🔧 Changed +* `TextEdit`: Change `margin` property to `egui::Margin` type [#3993](https://github.com/emilk/egui/pull/3993) (thanks [@bu5hm4nn](https://github.com/bu5hm4nn)!) +* New widget interaction logic [#4026](https://github.com/emilk/egui/pull/4026) +* `ui.dnd_drop_zone()` now returns `InnerResponse`. [#4079](https://github.com/emilk/egui/pull/4079) (thanks [@sowbug](https://github.com/sowbug)!) +* Support interacting with the background of a `Ui` [#4074](https://github.com/emilk/egui/pull/4074) +* Quickly animate scroll when calling `ui.scroll_to_cursor` etc [#4119](https://github.com/emilk/egui/pull/4119) +* Don't clear modifier state on focus change [#4157](https://github.com/emilk/egui/pull/4157) (thanks [@ming08108](https://github.com/ming08108)!) +* Prevent `egui::Window` from becoming larger than viewport [#4199](https://github.com/emilk/egui/pull/4199) (thanks [@rustbasic](https://github.com/rustbasic)!) +* Don't show URLs when hovering hyperlinks [#4218](https://github.com/emilk/egui/pull/4218) + +### 🐛 Fixed +* Fix incorrect handling of item spacing in `Window` title bar [#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)!) +* Fix: images with background color now respects rounding [#4029](https://github.com/emilk/egui/pull/4029) (thanks [@vincent-sparks](https://github.com/vincent-sparks)!) +* Fixed the incorrect display of the `Window` frame with a wide border or large rounding [#4032](https://github.com/emilk/egui/pull/4032) (thanks [@varphone](https://github.com/varphone)!) +* TextEdit: fix crash when hitting SHIFT + TAB around non-ASCII text [#3984](https://github.com/emilk/egui/pull/3984) (thanks [@rustbasic](https://github.com/rustbasic)!) +* Fix two `ScrollArea` bugs: leaking scroll target and broken animation to target offset [#4174](https://github.com/emilk/egui/pull/4174) (thanks [@abey79](https://github.com/abey79)!) +* Fix bug in `Context::parent_viewport_id` [#4190](https://github.com/emilk/egui/pull/4190) (thanks [@rustbasic](https://github.com/rustbasic)!) +* Remove unnecessary allocation in `RepaintCause::new` [#4146](https://github.com/emilk/egui/pull/4146) (thanks [@valsteen](https://github.com/valsteen)!) + + ## 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)!) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b33189280e7..206829e7aa8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,7 +44,7 @@ Please keep pull requests small and focused. The smaller it is, the more likely 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 perpituity, so we have a high bar on what get merged! +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 @@ -123,7 +123,7 @@ 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 screeen can have `pixels_per_point = 2.0`, +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. diff --git a/Cargo.lock b/Cargo.lock index 5acd8a690c7..373236f1883 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -144,7 +144,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "052ad56e336bcc615a214bffbeca6c181ee9550acec193f0327e0b103b033a4d" dependencies = [ "android-properties", - "bitflags 2.4.0", + "bitflags 2.5.0", "cc", "cesu8", "jni", @@ -199,17 +199,16 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arboard" -version = "3.3.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1faa3c733d9a3dd6fbaf85da5d162a2e03b2e0033a90dceb0e2a90fdd1e5380a" +checksum = "9fb4009533e8ff8f1450a5bcbc30f4242a1d34442221f72314bea1f5dc9c7f89" dependencies = [ "clipboard-win", "log", - "objc", - "objc-foundation", - "objc_id", + "objc2 0.5.1", + "objc2-app-kit", + "objc2-foundation", "parking_lot", - "thiserror", "x11rb", ] @@ -511,9 +510,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" dependencies = [ "serde", ] @@ -548,7 +547,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dd7cf50912cddc06dc5ea7c08c5e81c1b2c842a70d19def1848d54c586fed92" dependencies = [ - "objc-sys 0.3.1", + "objc-sys 0.3.3", ] [[package]] @@ -571,6 +570,15 @@ dependencies = [ "objc2 0.4.1", ] +[[package]] +name = "block2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43ff7d91d3c1d568065b06c899777d1e48dcf76103a672a0adbc238a7f247f1e" +dependencies = [ + "objc2 0.5.1", +] + [[package]] name = "blocking" version = "1.4.0" @@ -641,7 +649,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b50b5a44d59a98c55a9eeb518f39bf7499ba19fd98ee7d22618687f3f10adbf" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "log", "polling 3.3.0", "rustix 0.38.21", @@ -788,43 +796,13 @@ checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "clipboard-win" -version = "5.1.0" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec832972fefb8cf9313b45a0d1945e29c9c251f1d4c6eafc5fe2124c02d2e81" +checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" dependencies = [ "error-code", ] -[[package]] -name = "cocoa" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation", - "core-foundation", - "core-graphics", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation", - "core-graphics-types", - "libc", - "objc", -] - [[package]] name = "codespan-reporting" version = "0.11.1" @@ -901,9 +879,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -911,9 +889,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "core-graphics" @@ -930,9 +908,9 @@ dependencies = [ [[package]] name = "core-graphics-types" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -1066,6 +1044,15 @@ dependencies = [ "env_logger", ] +[[package]] +name = "custom_keypad" +version = "0.1.0" +dependencies = [ + "eframe", + "egui_extras", + "env_logger", +] + [[package]] name = "custom_plot_manipulation" version = "0.1.0" @@ -1152,7 +1139,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.8.0", + "libloading 0.7.4", ] [[package]] @@ -1172,7 +1159,7 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "ecolor" -version = "0.26.2" +version = "0.27.2" dependencies = [ "bytemuck", "cint", @@ -1183,10 +1170,9 @@ dependencies = [ [[package]] name = "eframe" -version = "0.26.2" +version = "0.27.2" dependencies = [ "bytemuck", - "cocoa", "directories-next", "document-features", "egui", @@ -1199,7 +1185,9 @@ dependencies = [ "image", "js-sys", "log", - "objc", + "objc2 0.5.1", + "objc2-app-kit", + "objc2-foundation", "parking_lot", "percent-encoding", "pollster", @@ -1209,7 +1197,6 @@ dependencies = [ "ron", "serde", "static_assertions", - "thiserror", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -1221,12 +1208,13 @@ dependencies = [ [[package]] name = "egui" -version = "0.26.2" +version = "0.27.2" dependencies = [ "accesskit", "ahash", "backtrace", "document-features", + "emath", "epaint", "log", "nohash-hasher", @@ -1237,7 +1225,7 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.26.2" +version = "0.27.2" dependencies = [ "bytemuck", "document-features", @@ -1254,7 +1242,7 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.26.2" +version = "0.27.2" dependencies = [ "accesskit_winit", "arboard", @@ -1272,7 +1260,7 @@ dependencies = [ [[package]] name = "egui_demo_app" -version = "0.26.2" +version = "0.27.2" dependencies = [ "bytemuck", "chrono", @@ -1297,7 +1285,7 @@ dependencies = [ [[package]] name = "egui_demo_lib" -version = "0.26.2" +version = "0.27.2" dependencies = [ "chrono", "criterion", @@ -1312,7 +1300,7 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.26.2" +version = "0.27.2" dependencies = [ "chrono", "document-features", @@ -1330,7 +1318,7 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.26.2" +version = "0.27.2" dependencies = [ "bytemuck", "document-features", @@ -1350,7 +1338,7 @@ dependencies = [ [[package]] name = "egui_plot" -version = "0.26.2" +version = "0.27.2" dependencies = [ "document-features", "egui", @@ -1379,7 +1367,7 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "emath" -version = "0.26.2" +version = "0.27.2" dependencies = [ "bytemuck", "document-features", @@ -1454,7 +1442,7 @@ dependencies = [ [[package]] name = "epaint" -version = "0.26.2" +version = "0.27.2" dependencies = [ "ab_glyph", "ahash", @@ -1797,7 +1785,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "005459a22af86adc706522d78d360101118e2638ec21df3852fcc626e0dbb212" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "cfg_aliases", "cgl", "core-foundation", @@ -1873,7 +1861,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "gpu-alloc-types", ] @@ -1883,7 +1871,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", ] [[package]] @@ -1901,22 +1889,22 @@ dependencies = [ [[package]] name = "gpu-descriptor" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" +checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "gpu-descriptor-types", "hashbrown", ] [[package]] name = "gpu-descriptor-types" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", ] [[package]] @@ -1959,10 +1947,10 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "com", "libc", - "libloading 0.8.0", + "libloading 0.7.4", "thiserror", "widestring", "winapi", @@ -2195,9 +2183,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.67" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -2264,12 +2252,9 @@ dependencies = [ [[package]] name = "line-wrap" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" -dependencies = [ - "safemem", -] +checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" [[package]] name = "linked-hash-map" @@ -2361,11 +2346,11 @@ dependencies = [ [[package]] name = "metal" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" +checksum = "5637e166ea14be6063a3f8ba5ccb9a4159df7d8f6d61c02fc3d480b1f90dcfcb" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "block", "core-graphics-types", "foreign-types", @@ -2422,12 +2407,13 @@ dependencies = [ [[package]] name = "naga" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8878eb410fc90853da3908aebfe61d73d26d4437ef850b70050461f939509899" +checksum = "e536ae46fcab0876853bd4a632ede5df4b1c2527a58f6c5a4150fe86be858231" dependencies = [ + "arrayvec", "bit-set", - "bitflags 2.4.0", + "bitflags 2.5.0", "codespan-reporting", "hexf-parse", "indexmap", @@ -2446,7 +2432,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "jni-sys", "log", "ndk-sys", @@ -2536,7 +2522,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", - "objc_exception", ] [[package]] @@ -2558,9 +2543,9 @@ checksum = "df3b9834c1e95694a05a828b59f55fa2afec6288359cda67146126b3f90a55d7" [[package]] name = "objc-sys" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e1d07c6eab1ce8b6382b8e3c7246fe117ff3f8b34be065f5ebace6749fe845" +checksum = "da284c198fb9b7b0603f8635185e85fbd5b64ee154b1ed406d489077de2d6d60" [[package]] name = "objc2" @@ -2579,10 +2564,43 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d" dependencies = [ - "objc-sys 0.3.1", + "objc-sys 0.3.3", "objc2-encode 3.0.0", ] +[[package]] +name = "objc2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b25e1034d0e636cd84707ccdaa9f81243d399196b8a773946dcffec0401659" +dependencies = [ + "objc-sys 0.3.3", + "objc2-encode 4.0.1", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb79768a710a9a1798848179edb186d1af7e8a8679f369e4b8d201dd2a034047" +dependencies = [ + "block2 0.5.0", + "objc2 0.5.1", + "objc2-core-data", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e092bc42eaf30a08844e6a076938c60751225ec81431ab89f5d1ccd9f958d6c" +dependencies = [ + "block2 0.5.0", + "objc2 0.5.1", + "objc2-foundation", +] + [[package]] name = "objc2-encode" version = "2.0.0-pre.2" @@ -2599,12 +2617,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666" [[package]] -name = "objc_exception" -version = "0.1.2" +name = "objc2-encode" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +checksum = "88658da63e4cc2c8adb1262902cd6af51094df0488b760d6fd27194269c0950a" + +[[package]] +name = "objc2-foundation" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfaefe14254871ea16c7d88968c0ff14ba554712a20d76421eec52f0a7fb8904" dependencies = [ - "cc", + "block2 0.5.0", + "objc2 0.5.1", ] [[package]] @@ -2755,9 +2780,9 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "plist" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" +checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9" dependencies = [ "base64", "indexmap", @@ -3063,9 +3088,9 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "renderdoc-sys" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216080ab382b992234dda86873c18d4c48358f5cfcb70fd693d7f6f2131b628b" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "resvg" @@ -3115,17 +3140,17 @@ dependencies = [ [[package]] name = "ring" -version = "0.16.20" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if", + "getrandom", "libc", - "once_cell", "spin", "untrusted", - "web-sys", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -3135,7 +3160,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64", - "bitflags 2.4.0", + "bitflags 2.5.0", "serde", "serde_derive", ] @@ -3178,7 +3203,7 @@ version = "0.38.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys 0.4.11", @@ -3187,9 +3212,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.7" +version = "0.21.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4" dependencies = [ "log", "ring", @@ -3199,9 +3224,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.6" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ "ring", "untrusted", @@ -3213,12 +3238,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" -[[package]] -name = "safemem" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" - [[package]] name = "same-file" version = "1.0.6" @@ -3262,9 +3281,9 @@ dependencies = [ [[package]] name = "sct" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ "ring", "untrusted", @@ -3413,7 +3432,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60e3d9941fa3bacf7c2bf4b065304faa14164151254cd16ce1b1bc8fc381600f" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "calloop", "calloop-wayland-source", "cursor-icon", @@ -3464,9 +3483,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.5.2" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "spirv" @@ -3474,7 +3493,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", ] [[package]] @@ -3604,18 +3623,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.56" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.56" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", @@ -3855,9 +3874,9 @@ checksum = "446c96c6dd42604779487f0a981060717156648c1706aa1f464677f03c6cc059" [[package]] name = "untrusted" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" @@ -3974,9 +3993,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3984,9 +4003,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", @@ -3999,9 +4018,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.40" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -4011,9 +4030,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4021,9 +4040,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -4034,9 +4053,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.90" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wayland-backend" @@ -4058,7 +4077,7 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ca7d52347346f5473bf2f56705f360e8440873052e575e55890c4fa57843ed3" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "nix", "wayland-backend", "wayland-scanner", @@ -4070,7 +4089,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "cursor-icon", "wayland-backend", ] @@ -4092,7 +4111,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e253d7107ba913923dc253967f35e8561a3c65f914543e46843c88ddd729e21c" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -4104,7 +4123,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -4117,7 +4136,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -4149,9 +4168,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.67" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -4170,9 +4189,9 @@ dependencies = [ [[package]] name = "webbrowser" -version = "0.8.11" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2c79b77f525a2d670cb40619d7d9c673d09e0666f72c591ebd7861f84a87e57" +checksum = "60b6f804e41d0852e16d2eaee61c7e4f7d3e8ffdb7b8ed85886aeb0791fe9fcd" dependencies = [ "core-foundation", "home", @@ -4193,13 +4212,14 @@ checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] name = "wgpu" -version = "0.19.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfe9a310dcf2e6b85f00c46059aaeaf4184caa8e29a1ecd4b7a704c3482332d" +checksum = "32ff1bfee408e1028e2e3acbf6d32d98b08a5a059ccbf5f33305534453ba5d3e" dependencies = [ "arrayvec", "cfg-if", "cfg_aliases", + "document-features", "js-sys", "log", "naga", @@ -4218,15 +4238,16 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b15e451d4060ada0d99a64df44e4d590213496da7c4f245572d51071e8e30ed" +checksum = "ac6a86eaa5e763e59c73cf9e97d55fffd4dfda69fd8bda19589fcf851ddfef1f" dependencies = [ "arrayvec", "bit-vec", - "bitflags 2.4.0", + "bitflags 2.5.0", "cfg_aliases", "codespan-reporting", + "document-features", "indexmap", "log", "naga", @@ -4244,14 +4265,14 @@ dependencies = [ [[package]] name = "wgpu-hal" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f259ceb56727fb097da108d92f8a5cbdb5b74a77f9e396bd43626f67299d61" +checksum = "4d71c8ae05170583049b65ee562fd839fdc0b3e9ddb84f4e40c9d5f8ea0d4c8c" dependencies = [ "android_system_properties", "arrayvec", "ash", - "bitflags 2.4.0", + "bitflags 2.5.0", "block", "cfg_aliases", "core-graphics-types", @@ -4264,10 +4285,11 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading 0.8.0", + "libloading 0.7.4", "log", "metal", "naga", + "ndk-sys", "objc", "once_cell", "parking_lot", @@ -4285,11 +4307,11 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "895fcbeb772bfb049eb80b2d6e47f6c9af235284e9703c96fc0218a42ffd5af2" +checksum = "1353d9a46bff7f955a680577f34c69122628cc2076e1d6f3a9be6ef00ae793ef" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "js-sys", "web-sys", ] @@ -4401,6 +4423,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -4431,6 +4462,22 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -4443,6 +4490,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -4455,6 +4508,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -4467,6 +4526,18 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -4479,6 +4550,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -4491,6 +4568,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -4503,6 +4586,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -4515,6 +4604,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + [[package]] name = "winit" version = "0.29.10" @@ -4524,7 +4619,7 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.4.0", + "bitflags 2.5.0", "bytemuck", "calloop", "cfg_aliases", @@ -4630,7 +4725,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6924668544c48c0133152e7eec86d644a056ca3d09275eb8d5cdb9855f9d8699" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.5.0", "dlib", "log", "once_cell", @@ -4655,6 +4750,10 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "xtask" +version = "0.27.2" + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index 1572568b24c..aa0d4153fad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,13 +14,15 @@ members = [ "crates/epaint", "examples/*", + + "xtask", ] [workspace.package] edition = "2021" license = "MIT OR Apache-2.0" -rust-version = "1.72" -version = "0.26.2" +rust-version = "1.76" +version = "0.27.2" [profile.release] @@ -48,17 +50,17 @@ 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 } +emath = { version = "0.27.2", path = "crates/emath", default-features = false } +ecolor = { version = "0.27.2", path = "crates/ecolor", default-features = false } +epaint = { version = "0.27.2", path = "crates/epaint", default-features = false } +egui = { version = "0.27.2", path = "crates/egui", default-features = false } +egui_plot = { version = "0.27.2", path = "crates/egui_plot", default-features = false } +egui-winit = { version = "0.27.2", path = "crates/egui-winit", default-features = false } +egui_extras = { version = "0.27.2", path = "crates/egui_extras", default-features = false } +egui-wgpu = { version = "0.27.2", path = "crates/egui-wgpu", default-features = false } +egui_demo_lib = { version = "0.27.2", path = "crates/egui_demo_lib", default-features = false } +egui_glow = { version = "0.27.2", path = "crates/egui_glow", default-features = false } +eframe = { version = "0.27.2", path = "crates/eframe", default-features = false } #TODO(emilk): make more things workspace dependencies ahash = { version = "0.8.6", default-features = false, features = [ @@ -78,8 +80,174 @@ puffin_http = "0.16" raw-window-handle = "0.6.0" thiserror = "1.0.37" web-time = "0.2" # Timekeeping for native and web -wgpu = { version = "0.19.1", default-features = false, features = [ +wgpu = { version = "0.20.0", 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 } + + +[workspace.lints.rust] +unsafe_code = "deny" + +elided_lifetimes_in_paths = "warn" +future_incompatible = "warn" +nonstandard_style = "warn" +rust_2018_idioms = "warn" +rust_2021_prelude_collisions = "warn" +semicolon_in_expressions_from_macros = "warn" +trivial_numeric_casts = "warn" +unsafe_op_in_unsafe_fn = "warn" # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668 +unused_extern_crates = "warn" +unused_import_braces = "warn" +unused_lifetimes = "warn" + +trivial_casts = "allow" +unused_qualifications = "allow" + +[workspace.lints.rustdoc] +all = "warn" +missing_crate_level_docs = "warn" + +# See also clippy.toml +[workspace.lints.clippy] +as_ptr_cast_mut = "warn" +await_holding_lock = "warn" +bool_to_int_with_if = "warn" +char_lit_as_u8 = "warn" +checked_conversions = "warn" +clear_with_drain = "warn" +cloned_instead_of_copied = "warn" +dbg_macro = "warn" +debug_assert_with_mut_call = "warn" +derive_partial_eq_without_eq = "warn" +disallowed_macros = "warn" # See clippy.toml +disallowed_methods = "warn" # See clippy.toml +disallowed_names = "warn" # See clippy.toml +disallowed_script_idents = "warn" # See clippy.toml +disallowed_types = "warn" # See clippy.toml +doc_link_with_quotes = "warn" +doc_markdown = "warn" +empty_enum = "warn" +enum_glob_use = "warn" +equatable_if_let = "warn" +exit = "warn" +expl_impl_clone_on_copy = "warn" +explicit_deref_methods = "warn" +explicit_into_iter_loop = "warn" +explicit_iter_loop = "warn" +fallible_impl_from = "warn" +filter_map_next = "warn" +flat_map_option = "warn" +float_cmp_const = "warn" +fn_params_excessive_bools = "warn" +fn_to_numeric_cast_any = "warn" +from_iter_instead_of_collect = "warn" +get_unwrap = "warn" +if_let_mutex = "warn" +implicit_clone = "warn" +implied_bounds_in_impls = "warn" +imprecise_flops = "warn" +index_refutable_slice = "warn" +inefficient_to_string = "warn" +infinite_loop = "warn" +into_iter_without_iter = "warn" +invalid_upcast_comparisons = "warn" +iter_not_returning_iterator = "warn" +iter_on_empty_collections = "warn" +iter_on_single_items = "warn" +iter_without_into_iter = "warn" +large_digit_groups = "warn" +large_include_file = "warn" +large_stack_arrays = "warn" +large_stack_frames = "warn" +large_types_passed_by_value = "warn" +let_unit_value = "warn" +linkedlist = "warn" +lossy_float_literal = "warn" +macro_use_imports = "warn" +manual_assert = "warn" +manual_clamp = "warn" +manual_instant_elapsed = "warn" +manual_let_else = "warn" +manual_ok_or = "warn" +manual_string_new = "warn" +map_err_ignore = "warn" +map_flatten = "warn" +map_unwrap_or = "warn" +match_on_vec_items = "warn" +match_same_arms = "warn" +match_wild_err_arm = "warn" +match_wildcard_for_single_variants = "warn" +mem_forget = "warn" +mismatched_target_os = "warn" +mismatching_type_param_order = "warn" +missing_enforced_import_renames = "warn" +missing_errors_doc = "warn" +missing_safety_doc = "warn" +mut_mut = "warn" +mutex_integer = "warn" +needless_borrow = "warn" +needless_continue = "warn" +needless_for_each = "warn" +needless_pass_by_ref_mut = "warn" +needless_pass_by_value = "warn" +negative_feature_names = "warn" +nonstandard_macro_braces = "warn" +option_option = "warn" +path_buf_push_overwrite = "warn" +ptr_as_ptr = "warn" +ptr_cast_constness = "warn" +pub_without_shorthand = "warn" +rc_mutex = "warn" +readonly_write_lock = "warn" +redundant_type_annotations = "warn" +ref_option_ref = "warn" +ref_patterns = "warn" +rest_pat_in_fully_bound_structs = "warn" +same_functions_in_if_condition = "warn" +semicolon_if_nothing_returned = "warn" +single_match_else = "warn" +str_to_string = "warn" +string_add = "warn" +string_add_assign = "warn" +string_lit_as_bytes = "warn" +string_lit_chars_any = "warn" +string_to_string = "warn" +suspicious_command_arg_space = "warn" +suspicious_xor_used_as_pow = "warn" +todo = "warn" +trailing_empty_array = "warn" +trait_duplication_in_bounds = "warn" +tuple_array_conversions = "warn" +unchecked_duration_subtraction = "warn" +undocumented_unsafe_blocks = "warn" +unimplemented = "warn" +uninhabited_references = "warn" +uninlined_format_args = "warn" +unnecessary_box_returns = "warn" +unnecessary_safety_doc = "warn" +unnecessary_struct_initialization = "warn" +unnecessary_wraps = "warn" +unnested_or_patterns = "warn" +unused_peekable = "warn" +unused_rounding = "warn" +unused_self = "warn" +useless_transmute = "warn" +verbose_file_reads = "warn" +wildcard_dependencies = "warn" +zero_sized_map_values = "warn" + +# TODO(emilk): enable more of these lints: +iter_over_hash_type = "allow" +let_underscore_untyped = "allow" +missing_assert_message = "allow" +print_stderr = "allow" # TODO(emilk): use `log` crate instead +should_panic_without_expect = "allow" +too_many_lines = "allow" +unwrap_used = "allow" # TODO(emilk): We really wanna warn on this one + +manual_range_contains = "allow" # this one is just worse imho +self_named_module_files = "allow" # Disabled waiting on https://github.com/rust-lang/rust-clippy/issues/9602 +significant_drop_tightening = "allow" # Too many false positives +wildcard_imports = "allow" # we do this a lot in egui diff --git a/Cranky.toml b/Cranky.toml deleted file mode 100644 index db236bff421..00000000000 --- a/Cranky.toml +++ /dev/null @@ -1,176 +0,0 @@ -# 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_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", - "clippy::expl_impl_clone_on_copy", - "clippy::explicit_deref_methods", - "clippy::explicit_into_iter_loop", - "clippy::explicit_iter_loop", - "clippy::fallible_impl_from", - "clippy::filter_map_next", - "clippy::flat_map_option", - "clippy::float_cmp_const", - "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", - "clippy::index_refutable_slice", - "clippy::inefficient_to_string", - "clippy::invalid_upcast_comparisons", - "clippy::iter_not_returning_iterator", - "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", - "clippy::map_flatten", - "clippy::map_unwrap_or", - "clippy::match_on_vec_items", - "clippy::match_same_arms", - "clippy::match_wild_err_arm", - "clippy::match_wildcard_for_single_variants", - "clippy::mem_forget", - "clippy::mismatched_target_os", - "clippy::mismatching_type_param_order", - "clippy::missing_enforced_import_renames", - "clippy::missing_errors_doc", - "clippy::missing_safety_doc", - "clippy::mut_mut", - "clippy::mutex_integer", - "clippy::needless_borrow", - "clippy::needless_continue", - "clippy::needless_for_each", - "clippy::needless_pass_by_value", - "clippy::negative_feature_names", - "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", - "nonstandard_style", - "rust_2018_idioms", - "rust_2021_prelude_collisions", - "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 worse imho - "clippy::significant_drop_tightening", # A lot of false positives - - # 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", - "unused_qualifications", -] diff --git a/README.md b/README.md index 2f4d5d19a83..924d509bd80 100644 --- a/README.md +++ b/README.md @@ -255,6 +255,8 @@ You can also call the layout code twice (once to get the size, once to do the in For "atomic" widgets (e.g. a button) `egui` knows the size before showing it, so centering buttons, labels etc is possible in `egui` without any special workarounds. +See [this issue](https://github.com/emilk/egui/issues/4378) for more. + #### 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. diff --git a/bacon.toml b/bacon.toml deleted file mode 100644 index 63d72eeb055..00000000000 --- a/bacon.toml +++ /dev/null @@ -1,74 +0,0 @@ -# This is a configuration file for the bacon tool -# More info at https://github.com/Canop/bacon - -default_job = "cranky" - -[jobs] - -[jobs.cranky] -command = [ - "cargo", - "cranky", - "--all-targets", - "--all-features", - "--color=always", -] -need_stdout = false -watch = ["tests", "benches", "examples"] - -[jobs.wasm] -command = [ - "cargo", - "cranky", - "-p=egui_demo_app", - "--lib", - "--target=wasm32-unknown-unknown", - "--target-dir=target_wasm", - "--all-features", - "--color=always", -] -need_stdout = false -watch = ["tests", "benches", "examples"] - -[jobs.test] -command = ["cargo", "test", "--color=always"] -need_stdout = true -watch = ["tests"] - -[jobs.doc] -command = ["cargo", "doc", "--color=always", "--all-features", "--no-deps"] -need_stdout = false - -# if the doc compiles, then it opens in your browser and bacon switches -# to the previous job -[jobs.doc-open] -command = [ - "cargo", - "doc", - "--color=always", - "--all-features", - "--no-deps", - "--open", -] -need_stdout = false -on_success = "back" # so that we don't open the browser at each change - -# You can run your application and have the result displayed in bacon, -# *if* it makes sense for this crate. You can run an example the same -# way. Don't forget the `--color always` part or the errors won't be -# properly parsed. -[jobs.run] -command = ["cargo", "run", "--color=always"] -need_stdout = true - -# You may define here keybindings that would be specific to -# a project, for example a shortcut to launch a specific job. -# Shortcuts to internal functions (scrolling, toggling, etc.) -# should go in your personal prefs.toml file instead. -[keybindings] -i = "job:initial" -c = "job:cranky" -a = "job:wasm" -d = "job:doc-open" -t = "job:test" -r = "job:run" diff --git a/clippy.toml b/clippy.toml index 93d7874068e..f480ca93d7e 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,9 +1,9 @@ -# There is also a scripts/clippy_wasm/clippy.toml which forbids some mthods that are not available in wasm. +# There is also a scripts/clippy_wasm/clippy.toml which forbids some methods that are not available in wasm. # ----------------------------------------------------------------------------- # Section identical to scripts/clippy_wasm/clippy.toml: -msrv = "1.72" +msrv = "1.76" allow-unwrap-in-tests = true @@ -67,7 +67,7 @@ disallowed-types = [ # ----------------------------------------------------------------------------- -# Allow-list of words for markdown in dosctrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown +# Allow-list of words for markdown in docstrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown doc-valid-idents = [ # You must also update the same list in the root `clippy.toml`! "AccessKit", diff --git a/crates/ecolor/CHANGELOG.md b/crates/ecolor/CHANGELOG.md index 8f60af3df13..3316a408944 100644 --- a/crates/ecolor/CHANGELOG.md +++ b/crates/ecolor/CHANGELOG.md @@ -6,6 +6,18 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.27.2 - 2024-04-02 +* Nothing new + + +## 0.27.1 - 2024-03-29 +* Nothing new + + +## 0.27.0 - 2024-03-26 +* Nothing new + + ## 0.26.2 - 2024-02-14 * Nothing new diff --git a/crates/ecolor/Cargo.toml b/crates/ecolor/Cargo.toml index 4cdbea9124b..fe4c6f444b1 100644 --- a/crates/ecolor/Cargo.toml +++ b/crates/ecolor/Cargo.toml @@ -16,6 +16,9 @@ categories = ["mathematics", "encoding"] keywords = ["gui", "color", "conversion", "gamedev", "images"] include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +[lints] +workspace = true + [package.metadata.docs.rs] all-features = true @@ -25,11 +28,6 @@ all-features = true [features] default = [] -## Enable additional checks if debug assertions are enabled (debug builds). -extra_debug_asserts = [] -## Always enable additional checks. -extra_asserts = [] - [dependencies] #! ### Optional dependencies diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index 0b60176f5cd..807aa7a545b 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -199,7 +199,7 @@ impl Color32 { /// This is perceptually even, and faster that [`Self::linear_multiply`]. #[inline] pub fn gamma_multiply(self, factor: f32) -> Self { - crate::ecolor_assert!(0.0 <= factor && factor <= 1.0); + debug_assert!(0.0 <= factor && factor <= 1.0); let Self([r, g, b, a]) = self; Self([ (r as f32 * factor + 0.5) as u8, @@ -212,10 +212,10 @@ impl Color32 { /// Multiply with 0.5 to make color half as opaque in linear space. /// /// This is using linear space, which is not perceptually even. - /// You may want to use [`Self::gamma_multiply`] instead. + /// You likely want to use [`Self::gamma_multiply`] instead. #[inline] pub fn linear_multiply(self, factor: f32) -> Self { - crate::ecolor_assert!(0.0 <= factor && factor <= 1.0); + debug_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. Rgba::from(self).multiply(factor).into() diff --git a/crates/ecolor/src/hex_color_macro.rs b/crates/ecolor/src/hex_color_macro.rs index 16f8cc2b8d8..a0a0729fdd1 100644 --- a/crates/ecolor/src/hex_color_macro.rs +++ b/crates/ecolor/src/hex_color_macro.rs @@ -13,7 +13,7 @@ #[macro_export] macro_rules! hex_color { ($s:literal) => {{ - let array = color_hex::color_from_hex!($s); + let array = $crate::color_hex::color_from_hex!($s); if array.len() == 3 { $crate::Color32::from_rgb(array[0], array[1], array[2]) } else { diff --git a/crates/ecolor/src/lib.rs b/crates/ecolor/src/lib.rs index e3c077edd50..a9400d9de61 100644 --- a/crates/ecolor/src/lib.rs +++ b/crates/ecolor/src/lib.rs @@ -24,6 +24,9 @@ pub use hsva::*; #[cfg(feature = "color-hex")] mod hex_color_macro; +#[cfg(feature = "color-hex")] +#[doc(hidden)] +pub use color_hex; mod rgba; pub use rgba::*; @@ -132,22 +135,6 @@ pub fn gamma_from_linear(linear: f32) -> f32 { // ---------------------------------------------------------------------------- -/// An assert that is only active when `epaint` is compiled with the `extra_asserts` feature -/// or with the `extra_debug_asserts` feature in debug builds. -#[macro_export] -macro_rules! ecolor_assert { - ($($arg: tt)*) => { - if cfg!(any( - feature = "extra_asserts", - all(feature = "extra_debug_asserts", debug_assertions), - )) { - assert!($($arg)*); - } - } -} - -// ---------------------------------------------------------------------------- - /// Cheap and ugly. /// Made for graying out disabled `Ui`s. pub fn tint_color_towards(color: Color32, target: Color32) -> Color32 { diff --git a/crates/ecolor/src/rgba.rs b/crates/ecolor/src/rgba.rs index 2a61d311ee4..900286cda43 100644 --- a/crates/ecolor/src/rgba.rs +++ b/crates/ecolor/src/rgba.rs @@ -26,6 +26,7 @@ impl std::ops::IndexMut for Rgba { } } +/// Deterministically hash an `f32`, treating all NANs as equal, and ignoring the sign of zero. #[inline] pub(crate) fn f32_hash(state: &mut H, f: f32) { if f == 0.0 { @@ -97,22 +98,22 @@ impl Rgba { #[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); + debug_assert!(0.0 <= l && l <= 1.0); + debug_assert!(0.0 <= a && a <= 1.0); Self([l * a, l * a, l * a, a]) } /// Transparent black #[inline] pub fn from_black_alpha(a: f32) -> Self { - crate::ecolor_assert!(0.0 <= a && a <= 1.0); + debug_assert!(0.0 <= a && a <= 1.0); Self([0.0, 0.0, 0.0, a]) } /// Transparent white #[inline] pub fn from_white_alpha(a: f32) -> Self { - crate::ecolor_assert!(0.0 <= a && a <= 1.0, "a: {}", a); + debug_assert!(0.0 <= a && a <= 1.0, "a: {a}"); Self([a, a, a, a]) } diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index f0566955899..dfea5671a5d 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -7,6 +7,40 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.27.2 - 2024-04-02 +#### Desktop/Native +* Fix continuous repaint on Wayland when TextEdit is focused or IME output is set [#4269](https://github.com/emilk/egui/pull/4269) (thanks [@white-axe](https://github.com/white-axe)!) +* Remove a bunch of `unwrap()` [#4285](https://github.com/emilk/egui/pull/4285) + +#### Web +* Fix blurry rendering in some browsers [#4299](https://github.com/emilk/egui/pull/4299) +* Correctly identify if the browser tab has focus [#4280](https://github.com/emilk/egui/pull/4280) + + +## 0.27.1 - 2024-03-29 +* Web: repaint if the `#hash` in the URL changes [#4261](https://github.com/emilk/egui/pull/4261) +* Add web support for `zoom_factor` [#4260](https://github.com/emilk/egui/pull/4260) (thanks [@justusdieckmann](https://github.com/justusdieckmann)!) + + +## 0.27.0 - 2024-03-26 +* Update to document-features 0.2.8 [#4003](https://github.com/emilk/egui/pull/4003) +* Added `App::raw_input_hook` allows for the manipulation or filtering of raw input events [#4008](https://github.com/emilk/egui/pull/4008) (thanks [@varphone](https://github.com/varphone)!) + +#### Desktop/Native +* Add with_taskbar to viewport builder [#3958](https://github.com/emilk/egui/pull/3958) (thanks [@AnotherNathan](https://github.com/AnotherNathan)!) +* Add `winuser` feature to `winapi` to fix unresolved import [#4037](https://github.com/emilk/egui/pull/4037) (thanks [@varphone](https://github.com/varphone)!) +* Add `get_proc_address` in CreationContext [#4145](https://github.com/emilk/egui/pull/4145) (thanks [@Chaojimengnan](https://github.com/Chaojimengnan)!) +* Don't clear modifier state on focus change [#4157](https://github.com/emilk/egui/pull/4157) (thanks [@ming08108](https://github.com/ming08108)!) +* Add x11 window type settings to viewport builder [#4175](https://github.com/emilk/egui/pull/4175) (thanks [@psethwick](https://github.com/psethwick)!) + +#### Web +* Add `webgpu` feature by default to wgpu [#4124](https://github.com/emilk/egui/pull/4124) (thanks [@ctaggart](https://github.com/ctaggart)!) +* Update kb modifiers from web mouse events [#4156](https://github.com/emilk/egui/pull/4156) (thanks [@ming08108](https://github.com/ming08108)!) +* Fix crash on `request_animation_frame` when destroying web runner [#4169](https://github.com/emilk/egui/pull/4169) (thanks [@jprochazk](https://github.com/jprochazk)!) +* Fix bug parsing url query with escaped & or = [#4172](https://github.com/emilk/egui/pull/4172) +* `Location::query_map`: support repeated key [#4183](https://github.com/emilk/egui/pull/4183) + + ## 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)!) @@ -22,38 +56,38 @@ Changes since the last release can be found at { #[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`]. @@ -196,6 +200,24 @@ pub trait App { fn persist_egui_memory(&self) -> bool { true } + + /// A hook for manipulating or filtering raw input before it is processed by [`Self::update`]. + /// + /// This function provides a way to modify or filter input events before they are processed by egui. + /// + /// It can be used to prevent specific keyboard shortcuts or mouse events from being processed by egui. + /// + /// 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. + /// + /// # 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. @@ -210,7 +232,7 @@ pub enum HardwareAcceleration { /// Do NOT use graphics acceleration. /// - /// On some platforms (MacOS) this is ignored and treated the same as [`Self::Preferred`]. + /// On some platforms (macOS) this is ignored and treated the same as [`Self::Preferred`]. Off, } @@ -496,10 +518,10 @@ pub enum WebGlContextOption { /// Force use WebGL2. WebGl2, - /// Use WebGl2 first. + /// Use WebGL2 first. BestFirst, - /// Use WebGl1 first + /// Use WebGL1 first CompatibilityFirst, } @@ -592,6 +614,11 @@ pub struct Frame { #[cfg(feature = "glow")] pub(crate) gl: Option>, + /// Used to convert user custom [`glow::Texture`] to [`egui::TextureId`] + #[cfg(all(feature = "glow", not(target_arch = "wasm32")))] + pub(crate) glow_register_native_texture: + Option egui::TextureId>>, + /// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s. #[cfg(feature = "wgpu")] pub(crate) wgpu_render_state: Option, @@ -668,6 +695,15 @@ impl Frame { self.gl.as_ref() } + /// Register your own [`glow::Texture`], + /// and then you can use the returned [`egui::TextureId`] to render your texture with [`egui`]. + /// + /// This function will take the ownership of your [`glow::Texture`], so please do not delete your [`glow::Texture`] after registering. + #[cfg(all(feature = "glow", not(target_arch = "wasm32")))] + pub fn register_native_glow_texture(&mut self, native: glow::Texture) -> egui::TextureId { + self.glow_register_native_texture.as_mut().unwrap()(native) + } + /// The underlying WGPU render state. /// /// Only available when compiling with the `wgpu` feature and using [`Renderer::Wgpu`]. @@ -696,7 +732,7 @@ pub struct WebInfo { #[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, @@ -736,8 +772,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` /// diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 98576358a70..e1df19a8b0b 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -331,37 +331,112 @@ pub fn run_simple_native( // ---------------------------------------------------------------------------- /// The different problems that can occur when trying to run `eframe`. -#[derive(thiserror::Error, Debug)] +#[derive(Debug)] pub enum Error { /// An error from [`winit`]. #[cfg(not(target_arch = "wasm32"))] - #[error("winit error: {0}")] - Winit(#[from] winit::error::OsError), + Winit(winit::error::OsError), /// An error from [`winit::event_loop::EventLoop`]. #[cfg(not(target_arch = "wasm32"))] - #[error("winit EventLoopError: {0}")] - WinitEventLoop(#[from] winit::error::EventLoopError), + WinitEventLoop(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), + Glutin(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:?}")] NoGlutinConfigs(glutin::config::ConfigTemplate, Box), /// An error from [`glutin`] when using [`glow`]. #[cfg(feature = "glow")] - #[error("egui_glow: {0}")] - OpenGL(#[from] egui_glow::PainterError), + OpenGL(egui_glow::PainterError), /// An error from [`wgpu`]. #[cfg(feature = "wgpu")] - #[error("WGPU error: {0}")] - Wgpu(#[from] egui_wgpu::WgpuError), + Wgpu(egui_wgpu::WgpuError), +} + +impl std::error::Error for Error {} + +#[cfg(not(target_arch = "wasm32"))] +impl From for Error { + #[inline] + fn from(err: winit::error::OsError) -> Self { + Self::Winit(err) + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl From for Error { + #[inline] + fn from(err: winit::error::EventLoopError) -> Self { + Self::WinitEventLoop(err) + } +} + +#[cfg(all(feature = "glow", not(target_arch = "wasm32")))] +impl From for Error { + #[inline] + fn from(err: glutin::error::Error) -> Self { + Self::Glutin(err) + } +} + +#[cfg(feature = "glow")] +impl From for Error { + #[inline] + fn from(err: egui_glow::PainterError) -> Self { + Self::OpenGL(err) + } +} + +#[cfg(feature = "wgpu")] +impl From for Error { + #[inline] + fn from(err: egui_wgpu::WgpuError) -> Self { + Self::Wgpu(err) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + #[cfg(not(target_arch = "wasm32"))] + Self::Winit(err) => { + write!(f, "winit error: {err}") + } + + #[cfg(not(target_arch = "wasm32"))] + Self::WinitEventLoop(err) => { + write!(f, "winit EventLoopError: {err}") + } + + #[cfg(all(feature = "glow", not(target_arch = "wasm32")))] + Self::Glutin(err) => { + write!(f, "glutin error: {err}") + } + + #[cfg(all(feature = "glow", not(target_arch = "wasm32")))] + Self::NoGlutinConfigs(template, err) => { + write!( + f, + "Found no glutin configs matching the template: {template:?}. Error: {err}" + ) + } + + #[cfg(feature = "glow")] + Self::OpenGL(err) => { + write!(f, "egui_glow: {err}") + } + + #[cfg(feature = "wgpu")] + Self::Wgpu(err) => { + write!(f, "WGPU error: {err}") + } + } + } } /// Short for `Result`. diff --git a/crates/eframe/src/native/app_icon.rs b/crates/eframe/src/native/app_icon.rs index 77aef816d4f..840bf367b27 100644 --- a/crates/eframe/src/native/app_icon.rs +++ b/crates/eframe/src/native/app_icon.rs @@ -203,12 +203,9 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS use crate::icon_data::IconDataExt as _; crate::profile_function!(); - use cocoa::{ - appkit::{NSApp, NSApplication, NSImage, NSMenu, NSWindow}, - base::{id, nil}, - foundation::{NSData, NSString}, - }; - use objc::{msg_send, sel, sel_impl}; + use objc2::ClassType; + use objc2_app_kit::{NSApplication, NSImage}; + use objc2_foundation::{NSData, NSString}; let png_bytes = if let Some(icon_data) = icon_data { match icon_data.to_png_bytes() { @@ -222,38 +219,35 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS None }; - // SAFETY: Accessing raw data from icon in a read-only manner. Icon data is static! + // TODO(madsmtm): Move this into `objc2-app-kit` + extern "C" { + static NSApp: Option<&'static NSApplication>; + } + + // SAFETY: we don't do anything dangerous here unsafe { - let app = NSApp(); - if app.is_null() { + let Some(app) = NSApp else { log::debug!("NSApp is null"); return AppIconStatus::NotSetIgnored; - } + }; if let Some(png_bytes) = png_bytes { - let data = NSData::dataWithBytes_length_( - nil, - png_bytes.as_ptr().cast::(), - png_bytes.len() as u64, - ); + let data = NSData::from_vec(png_bytes); log::trace!("NSImage::initWithData…"); - let app_icon = NSImage::initWithData_(NSImage::alloc(nil), data); + let app_icon = NSImage::initWithData(NSImage::alloc(), &data); crate::profile_scope!("setApplicationIconImage_"); log::trace!("setApplicationIconImage…"); - app.setApplicationIconImage_(app_icon); + app.setApplicationIconImage(app_icon.as_deref()); } // Change the title in the top bar - for python processes this would be again "python" otherwise. - let main_menu = app.mainMenu(); - 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() { + if let Some(main_menu) = app.mainMenu() { + if let Some(item) = main_menu.itemAtIndex(0) { + if let Some(app_menu) = item.submenu() { crate::profile_scope!("setTitle_"); - app_menu.setTitle_(NSString::alloc(nil).init_str(title)); + app_menu.setTitle(&NSString::from_str(title)); } } } diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index ee54b521225..827b9ec249c 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -20,27 +20,41 @@ pub fn viewport_builder( let mut viewport_builder = native_options.viewport.clone(); + // On some Linux systems, a window size larger than the monitor causes crashes, + // and on Windows the window does not appear at all. + let clamp_size_to_monitor_size = viewport_builder.clamp_size_to_monitor_size.unwrap_or(true); + // 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(egui_zoom_factor, event_loop)); + if clamp_size_to_monitor_size { + 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); - viewport_builder = window_settings.initialize_viewport_builder(viewport_builder); + viewport_builder = window_settings.initialize_viewport_builder( + egui_zoom_factor, + event_loop, + viewport_builder, + ); window_settings.inner_size_points() } else { if let Some(pos) = viewport_builder.position { viewport_builder = viewport_builder.with_position(pos); } - 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); + if clamp_size_to_monitor_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); + } } viewport_builder.inner_size @@ -152,6 +166,9 @@ impl EpiIntegration { native_options: &crate::NativeOptions, storage: Option>, #[cfg(feature = "glow")] gl: Option>, + #[cfg(feature = "glow")] glow_register_native_texture: Option< + Box egui::TextureId>, + >, #[cfg(feature = "wgpu")] wgpu_render_state: Option, ) -> Self { let frame = epi::Frame { @@ -162,6 +179,8 @@ impl EpiIntegration { storage, #[cfg(feature = "glow")] gl, + #[cfg(feature = "glow")] + glow_register_native_texture, #[cfg(feature = "wgpu")] wgpu_render_state, raw_display_handle: window.display_handle().map(|h| h.as_raw()), @@ -274,6 +293,8 @@ impl EpiIntegration { let close_requested = raw_input.viewport().close_requested(); + app.raw_input_hook(&self.egui_ctx, &mut raw_input); + let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { if let Some(viewport_ui_cb) = viewport_ui_cb { // Child viewport diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index 79c0c97a071..51c668abdeb 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -99,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); } diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 0cb7ec331f9..0fbb48a09c7 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -5,10 +5,15 @@ //! 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 +// `clippy::arc_with_non_send_sync`: `glow::Context` was accidentally non-Sync in glow 0.13, +// but that will be fixed in future releases of glow. +// https://github.com/grovesNL/glow/commit/c4a5f7151b9b4bbb380faa06ec27415235d1bf7e +#![allow(clippy::arc_with_non_send_sync)] +#![allow(clippy::undocumented_unsafe_blocks)] -use std::{cell::RefCell, rc::Rc, sync::Arc, time::Instant}; +use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant}; +use egui_winit::ActionRequested; use glutin::{ config::GlConfig, context::NotCurrentGlContext, @@ -22,9 +27,9 @@ use winit::{ }; use egui::{ - epaint::ahash::HashMap, DeferredViewportUiCallback, ImmediateViewport, NumExt as _, - ViewportBuilder, ViewportClass, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, - ViewportInfo, ViewportOutput, + ahash::HashSet, epaint::ahash::HashMap, DeferredViewportUiCallback, ImmediateViewport, + ViewportBuilder, ViewportClass, ViewportId, ViewportIdMap, ViewportIdPair, ViewportInfo, + ViewportOutput, }; #[cfg(feature = "accesskit")] use egui_winit::accesskit_winit; @@ -103,8 +108,9 @@ struct Viewport { ids: ViewportIdPair, class: ViewportClass, builder: ViewportBuilder, + deferred_commands: Vec, info: ViewportInfo, - screenshot_requested: bool, + actions_requested: HashSet, /// The user-callback that shows the ui. /// None for immediate viewports. @@ -217,6 +223,7 @@ impl GlowWinitApp { let system_theme = winit_integration::system_theme(&glutin.window(ViewportId::ROOT), &self.native_options); + let painter = Rc::new(RefCell::new(painter)); let integration = EpiIntegration::new( egui_ctx, @@ -226,6 +233,10 @@ impl GlowWinitApp { &self.native_options, storage, Some(gl.clone()), + Some(Box::new({ + let painter = painter.clone(); + move |native| painter.borrow_mut().register_native_texture(native) + })), #[cfg(feature = "wgpu")] None, ); @@ -252,7 +263,7 @@ impl GlowWinitApp { #[cfg(feature = "accesskit")] { let event_loop_proxy = self.repaint_proxy.lock().clone(); - let viewport = glutin.viewports.get_mut(&ViewportId::ROOT).unwrap(); + let viewport = glutin.viewports.get_mut(&ViewportId::ROOT).unwrap(); // we always have a root if let Viewport { window: Some(window), egui_winit: Some(egui_winit), @@ -284,12 +295,14 @@ impl GlowWinitApp { // 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()), @@ -300,7 +313,6 @@ impl GlowWinitApp { }; let glutin = Rc::new(RefCell::new(glutin)); - let painter = Rc::new(RefCell::new(painter)); { // Create weak pointers so that we don't keep @@ -348,21 +360,6 @@ impl WinitApp for GlowWinitApp { .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(); @@ -542,13 +539,17 @@ impl GlowWinitRunning { 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(viewport) = glutin.viewports.get_mut(&viewport_id) else { + return EventResult::Wait; + }; let Some(window) = viewport.window.as_ref() else { return EventResult::Wait; }; - egui_winit::update_viewport_info(&mut viewport.info, &egui_ctx, window); + egui_winit::update_viewport_info(&mut viewport.info, &egui_ctx, window, false); - let egui_winit = viewport.egui_winit.as_mut().unwrap(); + let Some(egui_winit) = viewport.egui_winit.as_mut() else { + return EventResult::Wait; + }; let mut raw_input = egui_winit.take_egui_input(window); let viewport_ui_cb = viewport.viewport_ui_cb.clone(); @@ -581,8 +582,12 @@ impl GlowWinitRunning { .. } = &mut *glutin; let viewport = &viewports[&viewport_id]; - let window = viewport.window.as_ref().unwrap(); - let gl_surface = viewport.gl_surface.as_ref().unwrap(); + let Some(window) = viewport.window.as_ref() else { + return EventResult::Wait; + }; + let Some(gl_surface) = viewport.gl_surface.as_ref() else { + return EventResult::Wait; + }; let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); @@ -626,13 +631,18 @@ impl GlowWinitRunning { viewport_output, } = full_output; + glutin.remove_viewports_not_in(&viewport_output); + let GlutinWindowContext { viewports, current_gl_context, .. } = &mut *glutin; - let viewport = viewports.get_mut(&viewport_id).unwrap(); + let Some(viewport) = viewports.get_mut(&viewport_id) else { + return EventResult::Wait; + }; + viewport.info.events.clear(); // they should have been processed let window = viewport.window.clone().unwrap(); let gl_surface = viewport.gl_surface.as_ref().unwrap(); @@ -663,17 +673,38 @@ impl GlowWinitRunning { ); { - 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(), - }); + for action in viewport.actions_requested.drain() { + match action { + ActionRequested::Screenshot => { + 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(), + }); + } + ActionRequested::Cut => { + egui_winit.egui_input_mut().events.push(egui::Event::Cut); + } + ActionRequested::Copy => { + egui_winit.egui_input_mut().events.push(egui::Event::Copy); + } + ActionRequested::Paste => { + if let Some(contents) = egui_winit.clipboard_text() { + let contents = contents.replace("\r\n", "\n"); + if !contents.is_empty() { + egui_winit + .egui_input_mut() + .events + .push(egui::Event::Paste(contents)); + } + } + } + } } + integration.post_rendering(&window); } @@ -695,11 +726,11 @@ impl GlowWinitRunning { #[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); + save_screenshot_and_exit(&path, &painter, screen_size_in_pixels); } } - glutin.handle_viewport_output(event_loop, &integration.egui_ctx, viewport_output); + 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 @@ -830,6 +861,20 @@ fn change_gl_context( ) { crate::profile_function!(); + if !cfg!(target_os = "windows") { + // According to https://github.com/emilk/egui/issues/4289 + // we cannot do this early-out on Windows. + // TODO(emilk): optimize context switching on Windows too. + // See https://github.com/emilk/egui/issues/4173 + + if let Some(current_gl_context) = current_gl_context { + crate::profile_scope!("is_current"); + if gl_surface.is_current(current_gl_context) { + return; // Early-out to save a lot of time. + } + } + } + let not_current = { crate::profile_scope!("make_not_current"); current_gl_context @@ -838,6 +883,7 @@ fn change_gl_context( .make_not_current() .unwrap() }; + crate::profile_scope!("make_current"); *current_gl_context = Some(not_current.make_current(gl_surface).unwrap()); } @@ -863,7 +909,7 @@ impl GlutinWindowContext { crate::HardwareAcceleration::Off => Some(false), }; let swap_interval = if native_options.vsync { - glutin::surface::SwapInterval::Wait(std::num::NonZeroU32::new(1).unwrap()) + glutin::surface::SwapInterval::Wait(NonZeroU32::MIN) } else { glutin::surface::SwapInterval::DontWait }; @@ -974,8 +1020,7 @@ impl GlutinWindowContext { 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()); + egui_winit::update_viewport_info(&mut info, egui_ctx, window, true); } let mut viewports = ViewportIdMap::default(); @@ -985,8 +1030,9 @@ impl GlutinWindowContext { ids: ViewportIdPair::ROOT, class: ViewportClass::Root, builder: viewport_builder, + deferred_commands: vec![], info, - screenshot_requested: false, + actions_requested: Default::default(), viewport_ui_cb: None, gl_surface: None, window: window.map(Arc::new), @@ -1066,8 +1112,8 @@ impl GlutinWindowContext { &window, &viewport.builder, ); - viewport.info.minimized = window.is_minimized(); - viewport.info.maximized = Some(window.is_maximized()); + + egui_winit::update_viewport_info(&mut viewport.info, &self.egui_ctx, &window, true); viewport.window.insert(Arc::new(window)) }; @@ -1087,8 +1133,8 @@ impl GlutinWindowContext { // 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 width_px = NonZeroU32::new(width_px).unwrap_or(NonZeroU32::MIN); + let height_px = NonZeroU32::new(height_px).unwrap_or(NonZeroU32::MIN); let surface_attributes = { use rwh_05::HasRawWindowHandle as _; // glutin stuck on old version of raw-window-handle glutin::surface::SurfaceAttributesBuilder::::new() @@ -1167,20 +1213,12 @@ impl GlutinWindowContext { } 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(); + let width_px = NonZeroU32::new(physical_size.width).unwrap_or(NonZeroU32::MIN); + let height_px = NonZeroU32::new(physical_size.height).unwrap_or(NonZeroU32::MIN); 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(), - ); + change_gl_context(&mut self.current_gl_context, gl_surface); gl_surface.resize( self.current_gl_context .as_ref() @@ -1196,16 +1234,27 @@ impl GlutinWindowContext { self.gl_config.display().get_proc_address(addr) } + pub(crate) fn remove_viewports_not_in( + &mut self, + viewport_output: &ViewportIdMap, + ) { + // GC old viewports + self.viewports + .retain(|id, _| viewport_output.contains_key(id)); + self.viewport_from_window + .retain(|_, id| viewport_output.contains_key(id)); + self.window_from_viewport + .retain(|id, _| viewport_output.contains_key(id)); + } + fn handle_viewport_output( &mut self, event_loop: &EventLoopWindowTarget, egui_ctx: &egui::Context, - viewport_output: ViewportIdMap, + viewport_output: &ViewportIdMap, ) { crate::profile_function!(); - let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); - for ( viewport_id, ViewportOutput { @@ -1213,58 +1262,60 @@ impl GlutinWindowContext { class, builder, viewport_ui_cb, - commands, + mut commands, repaint_delay: _, // ignored - we listened to the repaint callback instead }, - ) in viewport_output + ) in viewport_output.clone() { 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 old_inner_size = window.inner_size(); + let is_viewport_focused = self.focused_viewport == Some(viewport_id); + viewport.deferred_commands.append(&mut commands); + egui_winit::process_viewport_commands( egui_ctx, &mut viewport.info, - commands, + std::mem::take(&mut viewport.deferred_commands), window, is_viewport_focused, - &mut viewport.screenshot_requested, + &mut viewport.actions_requested, ); + + // For Wayland : https://github.com/emilk/egui/issues/4196 + if cfg!(target_os = "linux") { + let new_inner_size = window.inner_size(); + if new_inner_size != old_inner_size { + self.resize(viewport_id, new_inner_size); + } + } } } // 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)); + self.remove_viewports_not_in(viewport_output); } } -fn initialize_or_update_viewport<'vp>( - egu_ctx: &egui::Context, - viewports: &'vp mut ViewportIdMap, +fn initialize_or_update_viewport( + viewports: &mut ViewportIdMap, ids: ViewportIdPair, class: ViewportClass, mut builder: ViewportBuilder, viewport_ui_cb: Option>, - focused_viewport: Option, -) -> &'vp mut Viewport { +) -> &mut Viewport { crate::profile_function!(); if builder.icon.is_none() { @@ -1282,8 +1333,9 @@ fn initialize_or_update_viewport<'vp>( ids, class, builder, + deferred_commands: vec![], info: Default::default(), - screenshot_requested: false, + actions_requested: Default::default(), viewport_ui_cb, window: None, egui_winit: None, @@ -1299,7 +1351,7 @@ fn initialize_or_update_viewport<'vp>( viewport.class = class; viewport.viewport_ui_cb = viewport_ui_cb; - let (delta_commands, recreate) = viewport.builder.patch(builder); + let (mut delta_commands, recreate) = viewport.builder.patch(builder); if recreate { log::debug!( @@ -1309,18 +1361,10 @@ fn initialize_or_update_viewport<'vp>( ); 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, - ); } + viewport.deferred_commands.append(&mut delta_commands); + entry.into_mut() } } @@ -1350,13 +1394,11 @@ fn render_immediate_viewport( 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) { @@ -1376,7 +1418,7 @@ fn render_immediate_viewport( 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); + egui_winit::update_viewport_info(&mut viewport.info, egui_ctx, window, false); let mut raw_input = egui_winit.take_egui_input(window); raw_input.viewports = glutin @@ -1430,18 +1472,7 @@ fn render_immediate_viewport( 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(), - ); - } + change_gl_context(current_gl_context, gl_surface); let current_gl_context = current_gl_context.as_ref().unwrap(); @@ -1475,11 +1506,11 @@ fn render_immediate_viewport( egui_winit.handle_platform_output(window, platform_output); - glutin.handle_viewport_output(event_loop, egui_ctx, viewport_output); + glutin.handle_viewport_output(event_loop, egui_ctx, &viewport_output); } #[cfg(feature = "__screenshot")] -fn save_screeshot_and_exit( +fn save_screenshot_and_exit( path: &str, painter: &egui_glow::Painter, screen_size_in_pixels: [u32; 2], diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 896d640de36..3ee249edf76 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -391,6 +391,8 @@ pub fn run_glow( mut native_options: epi::NativeOptions, app_creator: epi::AppCreator, ) -> Result<()> { + #![allow(clippy::needless_return_with_question_mark)] // False positive + use super::glow_integration::GlowWinitApp; #[cfg(not(target_os = "ios"))] @@ -414,6 +416,8 @@ pub fn run_wgpu( mut native_options: epi::NativeOptions, app_creator: epi::AppCreator, ) -> Result<()> { + #![allow(clippy::needless_return_with_question_mark)] // False positive + use super::wgpu_integration::WgpuWinitApp; #[cfg(not(target_os = "ios"))] diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index b3451be9cb0..f365d74ad93 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -5,8 +5,9 @@ //! 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 std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant}; +use egui_winit::ActionRequested; use parking_lot::Mutex; use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _}; use winit::{ @@ -15,9 +16,9 @@ use winit::{ }; use egui::{ - ahash::HashMap, DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportBuilder, - ViewportClass, ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, - ViewportOutput, + ahash::{HashMap, HashSet, HashSetExt}, + DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportBuilder, ViewportClass, + ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, ViewportOutput, }; #[cfg(feature = "accesskit")] use egui_winit::accesskit_winit; @@ -76,8 +77,9 @@ pub struct Viewport { ids: ViewportIdPair, class: ViewportClass, builder: ViewportBuilder, + deferred_commands: Vec, info: ViewportInfo, - screenshot_requested: bool, + actions_requested: HashSet, /// `None` for sync viewports. viewport_ui_cb: Option>, @@ -154,13 +156,11 @@ impl WgpuWinitApp { } = &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); } @@ -215,6 +215,8 @@ impl WgpuWinitApp { storage, #[cfg(feature = "glow")] None, + #[cfg(feature = "glow")] + None, wgpu_render_state.clone(), ); @@ -262,6 +264,8 @@ impl WgpuWinitApp { 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()), @@ -274,6 +278,9 @@ impl WgpuWinitApp { let mut viewport_from_window = HashMap::default(); viewport_from_window.insert(window.id(), ViewportId::ROOT); + let mut info = ViewportInfo::default(); + egui_winit::update_viewport_info(&mut info, &egui_ctx, &window, true); + let mut viewports = Viewports::default(); viewports.insert( ViewportId::ROOT, @@ -281,12 +288,9 @@ impl WgpuWinitApp { ids: ViewportIdPair::ROOT, class: ViewportClass::Root, builder, - info: ViewportInfo { - minimized: window.is_minimized(), - maximized: Some(window.is_maximized()), - ..Default::default() - }, - screenshot_requested: false, + deferred_commands: vec![], + info, + actions_requested: Default::default(), viewport_ui_cb: None, window: Some(window), egui_winit: Some(egui_winit), @@ -337,20 +341,6 @@ impl WinitApp for WgpuWinitApp { .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() @@ -433,13 +423,12 @@ impl WinitApp for WgpuWinitApp { self.init_run_state(egui_ctx, event_loop, storage, window, builder)? }; - EventResult::RepaintNow( - running.shared.borrow().viewports[&ViewportId::ROOT] - .window - .as_ref() - .unwrap() - .id(), - ) + let viewport = &running.shared.borrow().viewports[&ViewportId::ROOT]; + if let Some(window) = &viewport.window { + EventResult::RepaintNow(window.id()) + } else { + EventResult::Wait + } } winit::event::Event::Suspended => { @@ -600,7 +589,7 @@ impl WgpuWinitRunning { let Some(window) = window else { return EventResult::Wait; }; - egui_winit::update_viewport_info(info, &integration.egui_ctx, window); + egui_winit::update_viewport_info(info, &integration.egui_ctx, window, false); { crate::profile_scope!("set_window"); @@ -611,7 +600,9 @@ impl WgpuWinitRunning { } } - let egui_winit = egui_winit.as_mut().unwrap(); + let Some(egui_winit) = egui_winit.as_mut() else { + return EventResult::Wait; + }; let mut raw_input = egui_winit.take_egui_input(window); integration.pre_update(); @@ -633,7 +624,7 @@ impl WgpuWinitRunning { // ------------------------------------------------------------ - let mut shared = shared.borrow_mut(); + let mut shared_mut = shared.borrow_mut(); let SharedState { egui_ctx, @@ -641,7 +632,17 @@ impl WgpuWinitRunning { painter, viewport_from_window, focused_viewport, - } = &mut *shared; + } = &mut *shared_mut; + + let FullOutput { + platform_output, + textures_delta, + shapes, + pixels_per_point, + viewport_output, + } = full_output; + + remove_viewports_not_in(viewports, painter, viewport_from_window, &viewport_output); let Some(viewport) = viewports.get_mut(&viewport_id) else { return EventResult::Wait; @@ -658,19 +659,14 @@ impl WgpuWinitRunning { 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 screenshot_requested = viewport + .actions_requested + .take(&ActionRequested::Screenshot) + .is_some(); let (vsync_secs, screenshot) = painter.paint_and_update_textures( viewport_id, pixels_per_point, @@ -689,14 +685,41 @@ impl WgpuWinitRunning { }); } + for action in viewport.actions_requested.drain() { + match action { + ActionRequested::Screenshot => { + // already handled above + } + ActionRequested::Cut => { + egui_winit.egui_input_mut().events.push(egui::Event::Cut); + } + ActionRequested::Copy => { + egui_winit.egui_input_mut().events.push(egui::Event::Copy); + } + ActionRequested::Paste => { + if let Some(contents) = egui_winit.clipboard_text() { + let contents = contents.replace("\r\n", "\n"); + if !contents.is_empty() { + egui_winit + .egui_input_mut() + .events + .push(egui::Event::Paste(contents)); + } + } + } + } + } + integration.post_rendering(window); let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); handle_viewport_output( &integration.egui_ctx, - viewport_output, + &viewport_output, viewports, + painter, + viewport_from_window, *focused_viewport, ); @@ -771,7 +794,6 @@ impl WgpuWinitRunning { // 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), @@ -872,9 +894,7 @@ impl Viewport { painter.max_texture_side(), )); - self.info.minimized = window.is_minimized(); - self.info.maximized = Some(window.is_maximized()); - + egui_winit::update_viewport_info(&mut self.info, egui_ctx, &window, true); self.window = Some(window); } Err(err) => { @@ -929,15 +949,8 @@ fn render_immediate_viewport( .. } = &mut *shared.borrow_mut(); - let viewport = initialize_or_update_viewport( - egui_ctx, - viewports, - ids, - ViewportClass::Immediate, - builder, - None, - None, - ); + let viewport = + initialize_or_update_viewport(viewports, ids, ViewportClass::Immediate, builder, None); if viewport.window.is_none() { viewport.initialize_window(event_loop, egui_ctx, viewport_from_window, painter); } @@ -945,7 +958,7 @@ fn render_immediate_viewport( 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); + egui_winit::update_viewport_info(&mut viewport.info, egui_ctx, window, false); let mut input = egui_winit.take_egui_input(window); input.viewports = viewports @@ -974,13 +987,14 @@ fn render_immediate_viewport( // ------------------------------------------ - let mut shared = shared.borrow_mut(); + let mut shared_mut = shared.borrow_mut(); let SharedState { viewports, painter, + viewport_from_window, focused_viewport, .. - } = &mut *shared; + } = &mut *shared_mut; let Some(viewport) = viewports.get_mut(&ids.this) else { return; @@ -1012,14 +1026,37 @@ fn render_immediate_viewport( egui_winit.handle_platform_output(window, platform_output); - handle_viewport_output(&egui_ctx, viewport_output, viewports, *focused_viewport); + handle_viewport_output( + &egui_ctx, + &viewport_output, + viewports, + painter, + viewport_from_window, + *focused_viewport, + ); +} + +pub(crate) fn remove_viewports_not_in( + viewports: &mut ViewportIdMap, + painter: &mut egui_wgpu::winit::Painter, + viewport_from_window: &mut HashMap, + viewport_output: &ViewportIdMap, +) { + let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); + + // 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); } /// Add new viewports, and update existing ones: fn handle_viewport_output( egui_ctx: &egui::Context, - viewport_output: ViewportIdMap, + viewport_output: &ViewportIdMap, viewports: &mut ViewportIdMap, + painter: &mut egui_wgpu::winit::Painter, + viewport_from_window: &mut HashMap, focused_viewport: Option, ) { for ( @@ -1029,46 +1066,56 @@ fn handle_viewport_output( class, builder, viewport_ui_cb, - commands, + mut commands, repaint_delay: _, // ignored - we listened to the repaint callback instead }, - ) in viewport_output + ) in viewport_output.clone() { 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, - ); + let viewport = + initialize_or_update_viewport(viewports, ids, class, builder, viewport_ui_cb); if let Some(window) = viewport.window.as_ref() { + let old_inner_size = window.inner_size(); + let is_viewport_focused = focused_viewport == Some(viewport_id); + viewport.deferred_commands.append(&mut commands); + egui_winit::process_viewport_commands( egui_ctx, &mut viewport.info, - commands, + std::mem::take(&mut viewport.deferred_commands), window, is_viewport_focused, - &mut viewport.screenshot_requested, + &mut viewport.actions_requested, ); + + // For Wayland : https://github.com/emilk/egui/issues/4196 + if cfg!(target_os = "linux") { + let new_inner_size = window.inner_size(); + if new_inner_size != old_inner_size { + if let (Some(width), Some(height)) = ( + NonZeroU32::new(new_inner_size.width), + NonZeroU32::new(new_inner_size.height), + ) { + painter.on_window_resized(viewport_id, width, height); + } + } + } } } + + remove_viewports_not_in(viewports, painter, viewport_from_window, viewport_output); } -fn initialize_or_update_viewport<'vp>( - egui_ctx: &egui::Context, - viewports: &'vp mut Viewports, +fn initialize_or_update_viewport( + viewports: &mut Viewports, ids: ViewportIdPair, class: ViewportClass, mut builder: ViewportBuilder, viewport_ui_cb: Option>, - focused_viewport: Option, -) -> &'vp mut Viewport { +) -> &mut Viewport { crate::profile_function!(); if builder.icon.is_none() { @@ -1086,8 +1133,9 @@ fn initialize_or_update_viewport<'vp>( ids, class, builder, + deferred_commands: vec![], info: Default::default(), - screenshot_requested: false, + actions_requested: HashSet::new(), viewport_ui_cb, window: None, egui_winit: None, @@ -1102,7 +1150,7 @@ fn initialize_or_update_viewport<'vp>( viewport.ids.parent = ids.parent; viewport.viewport_ui_cb = viewport_ui_cb; - let (delta_commands, recreate) = viewport.builder.patch(builder); + let (mut delta_commands, recreate) = viewport.builder.patch(builder); if recreate { log::debug!( @@ -1112,18 +1160,10 @@ fn initialize_or_update_viewport<'vp>( ); 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, - ); } + viewport.deferred_commands.append(&mut delta_commands); + entry.into_mut() } } diff --git a/crates/eframe/src/native/winit_integration.rs b/crates/eframe/src/native/winit_integration.rs index 541253c0818..fbbd7910732 100644 --- a/crates/eframe/src/native/winit_integration.rs +++ b/crates/eframe/src/native/winit_integration.rs @@ -9,8 +9,6 @@ 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!(); @@ -64,10 +62,6 @@ 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; diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 6cb331dfba9..620fe86fd62 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -60,7 +60,9 @@ impl AppRunner { super::storage::load_memory(&egui_ctx); egui_ctx.options_mut(|o| { - // On web, the browser controls the zoom factor: + // On web by default egui follows the zoom factor of the browser, + // and lets the browser handle the zoom shortscuts. + // A user can still zoom egui separately by calling [`egui::Context::set_zoom_factor`]. o.zoom_with_keyboard = false; o.zoom_factor = 1.0; }); @@ -76,6 +78,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"))] @@ -162,8 +167,8 @@ impl AppRunner { self.last_save_time = now_sec(); } - pub fn canvas_id(&self) -> &str { - self.painter.canvas_id() + pub fn canvas(&self) -> &web_sys::HtmlCanvasElement { + self.painter.canvas() } pub fn destroy(mut self) { @@ -179,8 +184,8 @@ impl AppRunner { /// /// 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_id(), self.web_options.max_size_points); - let canvas_size = super::canvas_size_in_points(self.canvas_id()); + super::resize_canvas_to_screen_size(self.canvas(), self.web_options.max_size_points); + let canvas_size = super::canvas_size_in_points(self.canvas(), self.egui_ctx()); let raw_input = self.input.new_frame(canvas_size); let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { @@ -265,7 +270,7 @@ impl AppRunner { self.mutable_text_under_cursor = mutable_text_under_cursor; if self.ime != ime { - super::text_agent::move_text_cursor(ime, self.canvas_id()); + super::text_agent::move_text_cursor(ime, self.canvas()); self.ime = ime; } } diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index 2dc3af4e9ac..74853abe9fb 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -36,8 +36,10 @@ impl WebInput { raw_input } + /// On alt-tab and similar. pub fn on_web_page_focus_change(&mut self, focused: bool) { - self.raw.modifiers = egui::Modifiers::default(); + // log::debug!("on_web_page_focus_change: {focused}"); + self.raw.modifiers = egui::Modifiers::default(); // Avoid sticky modifier keys on alt-tab: self.raw.focused = focused; self.raw.events.push(egui::Event::WindowFocused(focused)); self.latest_touch_pos = None; @@ -99,44 +101,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()); @@ -157,4 +160,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 13bea256098..f7234d76fad 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -5,12 +5,12 @@ 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); drop(runner_lock); - request_animation_frame(runner_ref.clone())?; + runner_ref.request_animation_frame()?; } Ok(()) } @@ -45,37 +45,26 @@ fn paint_if_needed(runner: &mut AppRunner) { runner.auto_save_if_needed(); } -pub(crate) 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(()) -} - // ------------------------------------------------------------------------ pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsValue> { let document = web_sys::window().unwrap().document().unwrap(); - { - // Avoid sticky modifier keys on alt-tab: - for event_name in ["blur", "focus"] { - let closure = move |_event: web_sys::MouseEvent, runner: &mut AppRunner| { - let has_focus = event_name == "focus"; - - if !has_focus { - // We lost focus - good idea to save - runner.save(); - } + for event_name in ["blur", "focus"] { + let closure = move |_event: web_sys::MouseEvent, runner: &mut AppRunner| { + // log::debug!("{event_name:?}"); + let has_focus = event_name == "focus"; - runner.input.on_web_page_focus_change(has_focus); - runner.egui_ctx().request_repaint(); - // log::debug!("{event_name:?}"); - }; + if !has_focus { + // We lost focus - good idea to save + runner.save(); + } - runner_ref.add_event_listener(&document, event_name, closure)?; - } + runner.input.on_web_page_focus_change(has_focus); + runner.egui_ctx().request_repaint(); + }; + + runner_ref.add_event_listener(&document, event_name, closure)?; } runner_ref.add_event_listener( @@ -87,7 +76,7 @@ pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsVa return; } - let modifiers = modifiers_from_event(&event); + let modifiers = modifiers_from_kb_event(&event); runner.input.raw.modifiers = modifiers; let key = event.key(); @@ -96,7 +85,7 @@ pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsVa if let Some(key) = egui_key { runner.input.raw.events.push(egui::Event::Key { key, - physical_key: None, // TODO + physical_key: None, // TODO(fornwall) pressed: true, repeat: false, // egui will fill this in for us! modifiers, @@ -158,12 +147,12 @@ pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsVa &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 + physical_key: None, // TODO(fornwall) pressed: false, repeat: false, modifiers, @@ -236,13 +225,31 @@ pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsVa pub(crate) fn install_window_events(runner_ref: &WebRunner) -> Result<(), JsValue> { let window = web_sys::window().unwrap(); + for event_name in ["blur", "focus"] { + let closure = move |_event: web_sys::MouseEvent, runner: &mut AppRunner| { + // log::debug!("{event_name:?}"); + let has_focus = event_name == "focus"; + + if !has_focus { + // We lost focus - good idea to save + runner.save(); + } + + runner.input.on_web_page_focus_change(has_focus); + runner.egui_ctx().request_repaint(); + }; + + runner_ref.add_event_listener(&window, event_name, closure)?; + } + // Save-on-close runner_ref.add_event_listener(&window, "onbeforeunload", |_: web_sys::Event, runner| { runner.save(); })?; for event_name in &["load", "pagehide", "pageshow", "resize"] { - runner_ref.add_event_listener(&window, event_name, |_: web_sys::Event, runner| { + runner_ref.add_event_listener(&window, event_name, move |_: web_sys::Event, runner| { + // log::debug!("{event_name:?}"); runner.needs_repaint.repaint_asap(); })?; } @@ -250,6 +257,7 @@ pub(crate) fn install_window_events(runner_ref: &WebRunner) -> Result<(), JsValu runner_ref.add_event_listener(&window, "hashchange", |_: web_sys::Event, runner| { // `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here runner.frame.info.web_info.location.hash = location_hash(); + runner.needs_repaint.repaint_asap(); // tell the user about the new hash })?; Ok(()) @@ -275,7 +283,7 @@ pub(crate) fn install_color_scheme_change_event(runner_ref: &WebRunner) -> Resul } pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValue> { - let canvas = canvas_element(runner_ref.try_lock().unwrap().canvas_id()).unwrap(); + let canvas = runner_ref.try_lock().unwrap().canvas().clone(); { let prevent_default_events = [ @@ -301,8 +309,10 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu &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, runner.egui_ctx()); let modifiers = runner.input.raw.modifiers; runner.input.raw.events.push(egui::Event::PointerButton { pos, @@ -327,7 +337,9 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu &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.egui_ctx()); runner.input.raw.events.push(egui::Event::PointerMoved(pos)); runner.needs_repaint.repaint_asap(); event.stop_propagation(); @@ -336,8 +348,10 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu )?; 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, runner.egui_ctx()); let modifiers = runner.input.raw.modifiers; runner.input.raw.events.push(egui::Event::PointerButton { pos, @@ -375,7 +389,12 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu "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.egui_ctx(), + ); runner.input.latest_touch_pos_id = latest_touch_pos_id; runner.input.latest_touch_pos = Some(pos); let modifiers = runner.input.raw.modifiers; @@ -398,7 +417,12 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu "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.egui_ctx(), + ); 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)); @@ -461,7 +485,9 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu }); 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(), runner.egui_ctx()).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. @@ -474,7 +500,7 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu // 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)); diff --git a/crates/eframe/src/web/input.rs b/crates/eframe/src/web/input.rs index 96cad32e20f..361e8031d40 100644 --- a/crates/eframe/src/web/input.rs +++ b/crates/eframe/src/web/input.rs @@ -1,11 +1,15 @@ -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, + ctx: &egui::Context, +) -> egui::Pos2 { let rect = canvas.get_bounding_client_rect(); + let zoom_factor = ctx.zoom_factor(); egui::Pos2 { - x: event.client_x() as f32 - rect.left() as f32, - y: event.client_y() as f32 - rect.top() as f32, + x: (event.client_x() as f32 - rect.left() as f32) / zoom_factor, + y: (event.client_y() as f32 - rect.top() as f32) / zoom_factor, } } @@ -27,9 +31,10 @@ pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option, + egui_ctx: &egui::Context, ) -> egui::Pos2 { let touch_for_pos = if let Some(touch_id_for_pos) = touch_id_for_pos { // search for the touch we previously used for the position @@ -47,26 +52,31 @@ 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, egui_ctx) }) } -fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Pos2 { +fn pos_from_touch( + canvas_origin: egui::Pos2, + touch: &web_sys::Touch, + egui_ctx: &egui::Context, +) -> egui::Pos2 { + let zoom_factor = egui_ctx.zoom_factor(); egui::Pos2 { - x: touch.page_x() as f32 - canvas_origin.x, - y: touch.page_y() as f32 - canvas_origin.y, + x: (touch.page_x() as f32 - canvas_origin.x) / zoom_factor, + y: (touch.page_y() as f32 - canvas_origin.y) / zoom_factor, } } 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 { device_id: egui::TouchDeviceId(0), id: egui::TouchId::from(touch.identifier()), phase, - pos: pos_from_touch(canvas_origin, &touch), + pos: pos_from_touch(canvas_origin, &touch, runner.egui_ctx()), force: Some(touch.force()), }); } @@ -115,7 +125,23 @@ pub fn translate_key(key: &str) -> Option { egui::Key::from_name(key) } -pub fn modifiers_from_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers { +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(), + + // Ideally we should know if we are running or mac or not, + // but this works good enough for now. + mac_cmd: event.meta_key(), + + // 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_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 cb5d6937c4c..566c9b03c8d 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -100,82 +100,76 @@ fn theme_from_dark_mode(dark_mode: bool) -> Theme { } } -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() } -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) } -fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 { - let canvas = canvas_element(canvas_id).unwrap(); - let pixels_per_point = native_pixels_per_point(); +fn canvas_size_in_points(canvas: &web_sys::HtmlCanvasElement, ctx: &egui::Context) -> egui::Vec2 { + let pixels_per_point = ctx.pixels_per_point(); egui::vec2( canvas.width() as f32 / pixels_per_point, canvas.height() as f32 / pixels_per_point, ) } -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()?; + // In this function we use "pixel" to mean physical pixel, + // and "point" to mean "logical CSS pixel". + let pixels_per_point = native_pixels_per_point(); + // Prefer the client width and height so that if the parent // element is resized that the egui canvas resizes appropriately. - let width = parent.client_width(); - let height = parent.client_height(); - - let canvas_real_size = Vec2 { - x: width as f32, - y: height as f32, + let parent_size_points = Vec2 { + x: parent.client_width() as f32, + y: parent.client_height() as f32, }; - if width <= 0 || height <= 0 { - log::error!("egui canvas parent size is {}x{}. Try adding `html, body {{ height: 100%; width: 100% }}` to your CSS!", width, height); + if parent_size_points.x <= 0.0 || parent_size_points.y <= 0.0 { + log::error!("The parent element of the egui canvas is {}x{}. Try adding `html, body {{ height: 100%; width: 100% }}` to your CSS!", parent_size_points.x, parent_size_points.y); } - let pixels_per_point = native_pixels_per_point(); - - let max_size_pixels = pixels_per_point * max_size_points; + // We take great care here to ensure the rendered canvas aligns + // perfectly to the physical pixel grid, lest we get blurry text. + // At the time of writing, we get pixel perfection on Chromium and Firefox on Mac, + // but Desktop Safari will be blurry on most zoom levels. + // See https://github.com/emilk/egui/issues/4241 for more. - let canvas_size_pixels = pixels_per_point * canvas_real_size; - let canvas_size_pixels = canvas_size_pixels.min(max_size_pixels); - let canvas_size_points = canvas_size_pixels / pixels_per_point; + let canvas_size_pixels = pixels_per_point * parent_size_points.min(max_size_points); - // Make sure that the height and width are always even numbers. + // Make sure that the size is always an even number of pixels, // otherwise, the page renders blurry on some platforms. // See https://github.com/emilk/egui/issues/103 - fn round_to_even(v: f32) -> f32 { - (v / 2.0).round() * 2.0 - } + let canvas_size_pixels = (canvas_size_pixels / 2.0).round() * 2.0; + + let canvas_size_points = canvas_size_pixels / pixels_per_point; canvas .style() - .set_property( - "width", - &format!("{}px", round_to_even(canvas_size_points.x)), - ) + .set_property("width", &format!("{}px", canvas_size_points.x)) .ok()?; canvas .style() - .set_property( - "height", - &format!("{}px", round_to_even(canvas_size_points.y)), - ) + .set_property("height", &format!("{}px", canvas_size_points.y)) .ok()?; - canvas.set_width(round_to_even(canvas_size_pixels.x) as u32); - canvas.set_height(round_to_even(canvas_size_pixels.y) as u32); + canvas.set_width(canvas_size_pixels.x as u32); + canvas.set_height(canvas_size_pixels.y as u32); Some(()) } diff --git a/crates/eframe/src/web/storage.rs b/crates/eframe/src/web/storage.rs index 4a2a53326a1..170798dc662 100644 --- a/crates/eframe/src/web/storage.rs +++ b/crates/eframe/src/web/storage.rs @@ -31,7 +31,7 @@ pub(crate) fn load_memory(_: &egui::Context) {} #[cfg(feature = "persistence")] pub(crate) fn save_memory(ctx: &egui::Context) { - match ctx.memory(|mem| ron::to_string(mem)) { + match ctx.memory(ron::to_string) { Ok(ron) => { local_storage_set("egui_memory_ron", &ron); } diff --git a/crates/eframe/src/web/text_agent.rs b/crates/eframe/src/web/text_agent.rs index 0bf8b532b7c..5cfec81bf30 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"; @@ -68,7 +68,8 @@ pub fn install_text_agent(runner_ref: &WebRunner) -> Result<(), JsValue> { is_composing.set(true); input_clone.set_value(""); - runner.input.raw.events.push(egui::Event::CompositionStart); + let egui_event = egui::Event::Ime(egui::ImeEvent::Enabled); + runner.input.raw.events.push(egui_event); runner.needs_repaint.repaint_asap(); } })?; @@ -77,8 +78,9 @@ pub fn install_text_agent(runner_ref: &WebRunner) -> Result<(), JsValue> { &input, "compositionupdate", move |event: web_sys::CompositionEvent, runner: &mut AppRunner| { - if let Some(event) = event.data().map(egui::Event::CompositionUpdate) { - runner.input.raw.events.push(event); + if let Some(text) = event.data() { + let egui_event = egui::Event::Ime(egui::ImeEvent::Preedit(text)); + runner.input.raw.events.push(egui_event); runner.needs_repaint.repaint_asap(); } }, @@ -91,8 +93,9 @@ pub fn install_text_agent(runner_ref: &WebRunner) -> Result<(), JsValue> { is_composing.set(false); input_clone.set_value(""); - if let Some(event) = event.data().map(egui::Event::CompositionEnd) { - runner.input.raw.events.push(event); + if let Some(text) = event.data() { + let egui_event = egui::Event::Ime(egui::ImeEvent::Commit(text)); + runner.input.raw.events.push(egui_event); runner.needs_repaint.repaint_asap(); } } @@ -116,12 +119,12 @@ pub fn install_text_agent(runner_ref: &WebRunner) -> Result<(), JsValue> { } /// Focus or blur text agent to toggle mobile keyboard. -pub fn update_text_agent(runner: &mut AppRunner) -> Option<()> { +pub fn update_text_agent(runner: &AppRunner) -> Option<()> { use web_sys::HtmlInputElement; 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(); @@ -205,14 +208,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(ime: Option, canvas_id: &str) -> Option<()> { +pub fn move_text_cursor( + ime: Option, + canvas: &web_sys::HtmlCanvasElement, +) -> Option<()> { let style = text_agent().style(); // Note: moving agent on mobile devices will lead to unpredictable scroll. if is_mobile() == Some(false) { ime.as_ref().and_then(|ime| { let egui::Pos2 { x, y } = ime.cursor_rect.left_top(); - let canvas = canvas_element(canvas_id)?; 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_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 cd62758688f..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,7 +19,7 @@ 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)?; @@ -30,11 +29,7 @@ impl WebPainterGlow { 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 }) } } @@ -43,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 857a1f5404c..de5ba601111 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -41,7 +41,6 @@ impl HasDisplayHandle for EguiWebWindow { pub(crate) struct WebPainterWgpu { canvas: HtmlCanvasElement, - canvas_id: String, surface: wgpu::Surface<'static>, surface_configuration: wgpu::SurfaceConfiguration, render_state: Option, @@ -163,7 +162,7 @@ impl WebPainterWgpu { } } - let canvas = super::canvas_element_or_die(canvas_id); + 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}"))?; @@ -188,7 +187,6 @@ impl WebPainterWgpu { Ok(Self { canvas, - canvas_id: canvas_id.to_owned(), render_state: Some(render_state), surface, surface_configuration, @@ -200,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 { diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs index 602c566e9d7..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,6 +27,9 @@ 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 { @@ -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), } } @@ -71,7 +78,7 @@ impl WebRunner { events::install_color_scheme_change_event(self)?; } - events::request_animation_frame(self.clone())?; + self.request_animation_frame()?; } Ok(()) @@ -108,6 +115,11 @@ impl WebRunner { 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(); } @@ -179,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(()) + } } // ---------------------------------------------------------------------------- diff --git a/crates/egui-wgpu/CHANGELOG.md b/crates/egui-wgpu/CHANGELOG.md index 56cecde1cf2..48aa5cdb454 100644 --- a/crates/egui-wgpu/CHANGELOG.md +++ b/crates/egui-wgpu/CHANGELOG.md @@ -6,6 +6,18 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.27.2 - 2024-04-02 +* Nothing new + + +## 0.27.1 - 2024-03-29 +* Nothing new + + +## 0.27.0 - 2024-03-26 +* Improve panic message in egui-wgpu when failing to create buffers [#3986](https://github.com/emilk/egui/pull/3986) + + ## 0.26.2 - 2024-02-14 * Nothing new diff --git a/crates/egui-wgpu/Cargo.toml b/crates/egui-wgpu/Cargo.toml index 09e48e07643..817233b88ba 100644 --- a/crates/egui-wgpu/Cargo.toml +++ b/crates/egui-wgpu/Cargo.toml @@ -23,6 +23,9 @@ include = [ "Cargo.toml", ] +[lints] +workspace = true + [package.metadata.docs.rs] all-features = true diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index b028af605bd..49397c9af89 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -162,7 +162,7 @@ pub struct Renderer { texture_bind_group_layout: wgpu::BindGroupLayout, /// Map of egui texture IDs to textures and their associated bindgroups (texture view + - /// sampler). The texture may be None if the TextureId is just a handle to a user-provided + /// sampler). The texture may be None if the `TextureId` is just a handle to a user-provided /// sampler. textures: HashMap, wgpu::BindGroup)>, next_user_texture_id: u64, @@ -293,6 +293,7 @@ impl Renderer { // 2: uint color attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32], }], + compilation_options: wgpu::PipelineCompilationOptions::default() }, primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, @@ -334,6 +335,7 @@ impl Renderer { }), write_mask: wgpu::ColorWrites::ALL, })], + compilation_options: wgpu::PipelineCompilationOptions::default() }), multiview: None, } diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 07c917155aa..4a909bfc75f 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -1,3 +1,6 @@ +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::undocumented_unsafe_blocks)] + use std::{num::NonZeroU32, sync::Arc}; use egui::{ViewportId, ViewportIdMap, ViewportIdSet}; @@ -206,7 +209,7 @@ impl Painter { if let Some(window) = window { let size = window.inner_size(); - if self.surfaces.get(&viewport_id).is_none() { + if !self.surfaces.contains_key(&viewport_id) { let surface = self.instance.create_surface(window)?; self.add_surface(surface, viewport_id, size).await?; } @@ -232,7 +235,7 @@ impl Painter { if let Some(window) = window { let size = window.inner_size(); - if self.surfaces.get(&viewport_id).is_none() { + if !self.surfaces.contains_key(&viewport_id) { let surface = unsafe { self.instance .create_surface_unsafe(wgpu::SurfaceTargetUnsafe::from_window(&window)?)? diff --git a/crates/egui-winit/CHANGELOG.md b/crates/egui-winit/CHANGELOG.md index 95961377e68..20662752609 100644 --- a/crates/egui-winit/CHANGELOG.md +++ b/crates/egui-winit/CHANGELOG.md @@ -5,6 +5,19 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.27.2 - 2024-04-02 +* Fix continuous repaint on Wayland when TextEdit is focused or IME output is set [#4269](https://github.com/emilk/egui/pull/4269) (thanks [@white-axe](https://github.com/white-axe)!) + + +## 0.27.1 - 2024-03-29 +* Nothing new + + +## 0.27.0 - 2024-03-26 +* 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)!) +* Don't clear modifier state on focus change [#4157](https://github.com/emilk/egui/pull/4157) (thanks [@ming08108](https://github.com/ming08108)!) + + ## 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)!) diff --git a/crates/egui-winit/Cargo.toml b/crates/egui-winit/Cargo.toml index fdff0fcdcbf..fc2b8a53620 100644 --- a/crates/egui-winit/Cargo.toml +++ b/crates/egui-winit/Cargo.toml @@ -13,6 +13,9 @@ categories = ["gui", "game-development"] keywords = ["winit", "egui", "gui", "gamedev"] include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +[lints] +workspace = true + [package.metadata.docs.rs] all-features = true @@ -71,7 +74,7 @@ document-features = { workspace = true, optional = true } puffin = { workspace = true, optional = true } serde = { version = "1.0", optional = true, features = ["derive"] } -webbrowser = { version = "0.8.3", optional = true } +webbrowser = { version = "1.0.0", optional = true } [target.'cfg(any(target_os="linux", target_os="dragonfly", target_os="freebsd", target_os="netbsd", target_os="openbsd"))'.dependencies] smithay-clipboard = { version = "0.7.0", optional = true } diff --git a/crates/egui-winit/src/clipboard.rs b/crates/egui-winit/src/clipboard.rs index c86a9e19d5b..44e3840b64f 100644 --- a/crates/egui-winit/src/clipboard.rs +++ b/crates/egui-winit/src/clipboard.rs @@ -137,6 +137,8 @@ fn init_arboard() -> Option { fn init_smithay_clipboard( raw_display_handle: Option, ) -> Option { + #![allow(clippy::undocumented_unsafe_blocks)] + crate::profile_function!(); if let Some(RawDisplayHandle::Wayland(display)) = raw_display_handle { diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index ac29a0b2404..058a27f1661 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -14,7 +14,9 @@ pub use accesskit_winit; pub use egui; #[cfg(feature = "accesskit")] use egui::accesskit; -use egui::{Pos2, Rect, Vec2, ViewportBuilder, ViewportCommand, ViewportId, ViewportInfo}; +use egui::{ + ahash::HashSet, Pos2, Rect, Vec2, ViewportBuilder, ViewportCommand, ViewportId, ViewportInfo, +}; pub use winit; pub mod clipboard; @@ -54,7 +56,7 @@ pub struct EventResponse { /// (e.g. a mouse click on an egui window, or entering text into a text field). /// /// For instance, if you use egui for a game, you should only - /// pass on the events to your game when [`Self::consumed`] is `false. + /// pass on the events to your game when [`Self::consumed`] is `false`. /// /// Note that egui uses `tab` to move focus between elements, so this will always be `true` for tabs. pub consumed: bool, @@ -93,12 +95,13 @@ pub struct State { pointer_touch_id: Option, /// track ime state - input_method_editor_started: bool, + has_sent_ime_enabled: bool, #[cfg(feature = "accesskit")] accesskit: Option, allow_ime: bool, + ime_rect_px: Option, } impl State { @@ -133,12 +136,13 @@ impl State { simulate_touch_screen: false, pointer_touch_id: None, - input_method_editor_started: false, + has_sent_ime_enabled: false, #[cfg(feature = "accesskit")] accesskit: None, allow_ime: false, + ime_rect_px: None, }; slf.egui_input @@ -308,7 +312,7 @@ impl State { consumed: false, } } - // WindowEvent::TouchpadPressure {device_id, pressure, stage, .. } => {} // TODO + // WindowEvent::TouchpadPressure {device_id, pressure, stage, .. } => {} // TODO(emilk) WindowEvent::Touch(touch) => { self.on_touch(window, touch); let consumed = match touch.phase { @@ -338,23 +342,25 @@ impl State { // We use input_method_editor_started to manually insert CompositionStart // between Commits. match ime { - winit::event::Ime::Enabled | winit::event::Ime::Disabled => (), - winit::event::Ime::Commit(text) => { - self.input_method_editor_started = false; + winit::event::Ime::Enabled => {} + winit::event::Ime::Preedit(_, None) => { + self.ime_event_enable(); + } + winit::event::Ime::Preedit(text, Some(_cursor)) => { + self.ime_event_enable(); self.egui_input .events - .push(egui::Event::CompositionEnd(text.clone())); + .push(egui::Event::Ime(egui::ImeEvent::Preedit(text.clone()))); } - 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); - } + winit::event::Ime::Commit(text) => { self.egui_input .events - .push(egui::Event::CompositionUpdate(text.clone())); + .push(egui::Event::Ime(egui::ImeEvent::Commit(text.clone()))); + self.ime_event_disable(); + } + winit::event::Ime::Disabled => { + self.ime_event_disable(); } - winit::event::Ime::Preedit(_, None) => {} }; EventResponse { @@ -376,9 +382,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)); @@ -475,6 +478,22 @@ impl State { } } + pub fn ime_event_enable(&mut self) { + if !self.has_sent_ime_enabled { + self.egui_input + .events + .push(egui::Event::Ime(egui::ImeEvent::Enabled)); + self.has_sent_ime_enabled = true; + } + } + + pub fn ime_event_disable(&mut self) { + self.egui_input + .events + .push(egui::Event::Ime(egui::ImeEvent::Disabled)); + self.has_sent_ime_enabled = false; + } + pub fn on_mouse_motion(&mut self, delta: (f64, f64)) { self.egui_input.events.push(egui::Event::MouseMoved(Vec2 { x: delta.0 as f32, @@ -600,7 +619,8 @@ impl State { }); // If we're not yet translating a touch or we're translating this very // touch … - if self.pointer_touch_id.is_none() || self.pointer_touch_id.unwrap() == touch.id { + if self.pointer_touch_id.is_none() || self.pointer_touch_id.unwrap_or_default() == touch.id + { // … emit PointerButton resp. PointerMoved events to emulate mouse match touch.phase { winit::event::TouchPhase::Started => { @@ -728,15 +748,19 @@ impl State { physical_key ); - if let Some(logical_key) = logical_key { + // "Logical OR physical key" is a fallback mechanism for keyboard layouts without Latin characters: it lets them + // emit events as if the corresponding keys from the Latin layout were pressed. In this case, clipboard shortcuts + // are mapped to the physical keys that normally contain C, X, V, etc. + // See also: https://github.com/emilk/egui/issues/3653 + if let Some(active_key) = logical_key.or(physical_key) { if pressed { - if is_cut_command(self.egui_input.modifiers, logical_key) { + if is_cut_command(self.egui_input.modifiers, active_key) { self.egui_input.events.push(egui::Event::Cut); return; - } else if is_copy_command(self.egui_input.modifiers, logical_key) { + } else if is_copy_command(self.egui_input.modifiers, active_key) { self.egui_input.events.push(egui::Event::Copy); return; - } else if is_paste_command(self.egui_input.modifiers, logical_key) { + } else if is_paste_command(self.egui_input.modifiers, active_key) { if let Some(contents) = self.clipboard.get() { let contents = contents.replace("\r\n", "\n"); if !contents.is_empty() { @@ -748,7 +772,7 @@ impl State { } self.egui_input.events.push(egui::Event::Key { - key: logical_key, + key: active_key, physical_key, pressed, repeat: false, // egui will fill this in for us! @@ -820,19 +844,26 @@ impl State { } 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(), - }, - ); + let ime_rect_px = pixels_per_point * ime.rect; + if self.ime_rect_px != Some(ime_rect_px) + || self.egui_ctx.input(|i| !i.events.is_empty()) + { + self.ime_rect_px = Some(ime_rect_px); + crate::profile_scope!("set_ime_cursor_area"); + window.set_ime_cursor_area( + winit::dpi::PhysicalPosition { + x: ime_rect_px.min.x, + y: ime_rect_px.min.y, + }, + winit::dpi::PhysicalSize { + width: ime_rect_px.width(), + height: ime_rect_px.height(), + }, + ); + } + } else { + self.ime_rect_px = None; } #[cfg(feature = "accesskit")] @@ -868,70 +899,62 @@ impl State { } } +pub fn inner_rect_in_points(window: &Window, pixels_per_point: f32) -> Option { + let inner_pos_px = window.inner_position().ok()?; + let inner_pos_px = egui::pos2(inner_pos_px.x as f32, inner_pos_px.y as f32); + + let inner_size_px = window.inner_size(); + let inner_size_px = egui::vec2(inner_size_px.width as f32, inner_size_px.height as f32); + + let inner_rect_px = egui::Rect::from_min_size(inner_pos_px, inner_size_px); + + Some(inner_rect_px / pixels_per_point) +} + +pub fn outer_rect_in_points(window: &Window, pixels_per_point: f32) -> Option { + let outer_pos_px = window.outer_position().ok()?; + let outer_pos_px = egui::pos2(outer_pos_px.x as f32, outer_pos_px.y as f32); + + let outer_size_px = window.outer_size(); + let outer_size_px = egui::vec2(outer_size_px.width as f32, outer_size_px.height as f32); + + let outer_rect_px = egui::Rect::from_min_size(outer_pos_px, outer_size_px); + + Some(outer_rect_px / pixels_per_point) +} + /// Update the given viewport info with the current state of the window. /// /// Call before [`State::take_egui_input`]. +/// +/// If this is called right after window creation, `is_init` should be `true`, otherwise `false`. pub fn update_viewport_info( viewport_info: &mut ViewportInfo, egui_ctx: &egui::Context, window: &Window, + is_init: bool, ) { 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, + Some(true) => false, + Some(false) | None => 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() + let inner_rect = if has_a_position { + inner_rect_in_points(window, pixels_per_point) } 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() + let outer_rect = if has_a_position { + outer_rect_in_points(window, pixels_per_point) } 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() { @@ -942,21 +965,23 @@ pub fn update_viewport_info( } }; - 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.title = Some(window.title()); viewport_info.native_pixels_per_point = Some(window.scale_factor() as f32); + + viewport_info.monitor_size = monitor_size; + viewport_info.inner_rect = inner_rect; 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 + if is_init || !cfg!(target_os = "macos") { + // Asking for minimized/maximized state at runtime 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)); } + + viewport_info.fullscreen = Some(window.fullscreen().is_some()); + viewport_info.focused = Some(window.has_focus()); } fn open_url_in_browser(_url: &str) { @@ -1254,6 +1279,13 @@ fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option, window: &Window, is_viewport_focused: bool, - screenshot_requested: &mut bool, + actions_requested: &mut HashSet, ) { for command in commands { process_viewport_command( @@ -1270,7 +1302,7 @@ pub fn process_viewport_commands( command, info, is_viewport_focused, - screenshot_requested, + actions_requested, ); } } @@ -1281,13 +1313,13 @@ fn process_viewport_command( command: ViewportCommand, info: &mut ViewportInfo, is_viewport_focused: bool, - screenshot_requested: &mut bool, + actions_requested: &mut HashSet, ) { crate::profile_function!(); use winit::window::ResizeDirection; - log::debug!("Processing ViewportCommand::{command:?}"); + log::trace!("Processing ViewportCommand::{command:?}"); let pixels_per_point = pixels_per_point(egui_ctx, window); @@ -1301,7 +1333,7 @@ fn process_viewport_command( 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, + // TODO(emilk): 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 { @@ -1313,11 +1345,27 @@ fn process_viewport_command( 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"); + let requested_size = PhysicalSize::new(width_px, height_px); + if let Some(_returned_inner_size) = window.request_inner_size(requested_size) { + // On platforms where the size is entirely controlled by the user the + // applied size will be returned immediately, resize event in such case + // may not be generated. + // e.g. Linux + + // On platforms where resizing is disallowed by the windowing system, the current + // inner size is returned immediately, and the user one is ignored. + // e.g. Android, iOS, … + + // However, comparing the results is prone to numerical errors + // because the linux backend converts physical to logical and back again. + // So let's just assume it worked: + + info.inner_rect = inner_rect_in_points(window, pixels_per_point); + info.outer_rect = outer_rect_in_points(window, pixels_per_point); + } else { + // e.g. macOS, Windows + // The request went to the display system, + // and the actual size will be delivered later with the [`WindowEvent::Resized`]. } } ViewportCommand::BeginResize(direction) => { @@ -1462,7 +1510,16 @@ fn process_viewport_command( } } ViewportCommand::Screenshot => { - *screenshot_requested = true; + actions_requested.insert(ActionRequested::Screenshot); + } + ViewportCommand::RequestCut => { + actions_requested.insert(ActionRequested::Cut); + } + ViewportCommand::RequestCopy => { + actions_requested.insert(ActionRequested::Copy); + } + ViewportCommand::RequestPaste => { + actions_requested.insert(ActionRequested::Paste); } } } @@ -1470,6 +1527,9 @@ fn process_viewport_command( /// Build and intitlaize a window. /// /// Wrapper around `create_winit_window_builder` and `apply_viewport_builder_to_window`. +/// +/// # Errors +/// Possible causes of error include denied permission, incompatible system, and lack of memory. pub fn create_window( egui_ctx: &egui::Context, event_loop: &EventLoopWindowTarget, @@ -1497,7 +1557,7 @@ pub fn create_winit_window_builder( // 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`. + // we'll try to fix that after the window is created in the call to `apply_viewport_builder_to_window`. let native_pixels_per_point = event_loop .primary_monitor() .or_else(|| event_loop.available_monitors().next()) @@ -1543,7 +1603,11 @@ pub fn create_winit_window_builder( // wayland: app_id: _app_id, + // x11 + window_type: _window_type, + mouse_passthrough: _, // handled in `apply_viewport_builder_to_window` + clamp_size_to_monitor_size: _, // Handled in `viewport_builder` in `epi_integration.rs` } = viewport_builder; let mut window_builder = winit::window::WindowBuilder::new() @@ -1615,6 +1679,30 @@ pub fn create_winit_window_builder( 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 _; diff --git a/crates/egui-winit/src/window_settings.rs b/crates/egui-winit/src/window_settings.rs index c59a0f451ce..ec633d3df0a 100644 --- a/crates/egui-winit/src/window_settings.rs +++ b/crates/egui-winit/src/window_settings.rs @@ -50,8 +50,10 @@ impl WindowSettings { self.inner_size_points } - pub fn initialize_viewport_builder( + pub fn initialize_viewport_builder( &self, + egui_zoom_factor: f32, + event_loop: &winit::event_loop::EventLoopWindowTarget, mut viewport_builder: ViewportBuilder, ) -> ViewportBuilder { crate::profile_function!(); @@ -64,7 +66,15 @@ impl WindowSettings { self.outer_position_pixels }; if let Some(pos) = pos_px { - viewport_builder = viewport_builder.with_position(pos); + let monitor_scale_factor = if let Some(inner_size_points) = self.inner_size_points { + find_active_monitor(egui_zoom_factor, event_loop, inner_size_points, &pos) + .map_or(1.0, |monitor| monitor.scale_factor() as f32) + } else { + 1.0 + }; + + let scaled_pos = pos / (egui_zoom_factor * monitor_scale_factor); + viewport_builder = viewport_builder.with_position(scaled_pos); } if let Some(inner_size_points) = self.inner_size_points { @@ -127,12 +137,12 @@ impl WindowSettings { } } -fn clamp_pos_to_monitors( +fn find_active_monitor( egui_zoom_factor: f32, event_loop: &winit::event_loop::EventLoopWindowTarget, window_size_pts: egui::Vec2, - position_px: &mut egui::Pos2, -) { + position_px: &egui::Pos2, +) -> Option { crate::profile_function!(); let monitors = event_loop.available_monitors(); @@ -142,7 +152,7 @@ fn clamp_pos_to_monitors( .primary_monitor() .or_else(|| event_loop.available_monitors().next()) else { - return; // no monitors 🤷 + return None; // no monitors 🤷 }; for monitor in monitors { @@ -159,6 +169,23 @@ fn clamp_pos_to_monitors( } } + Some(active_monitor) +} + +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 Some(active_monitor) = + find_active_monitor(egui_zoom_factor, event_loop, window_size_pts, position_px) + else { + return; // no monitors 🤷 + }; + 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. diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index 347dbeda7cb..909beba133a 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -13,6 +13,9 @@ categories = ["gui", "game-development"] keywords = ["gui", "imgui", "immediate", "portable", "gamedev"] include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +[lints] +workspace = true + [package.metadata.docs.rs] all-features = true @@ -49,11 +52,6 @@ deadlock_detection = ["epaint/deadlock_detection"] ## If you plan on specifying your own fonts you may disable this feature. default_fonts = ["epaint/default_fonts"] -## Enable additional checks if debug assertions are enabled (debug builds). -extra_debug_asserts = ["epaint/extra_debug_asserts"] -## Always enable additional checks. -extra_asserts = ["epaint/extra_asserts"] - ## Turn on the `log` feature, that makes egui log some errors using the [`log`](https://docs.rs/log) crate. log = ["dep:log", "epaint/log"] @@ -81,6 +79,7 @@ unity = ["epaint/unity"] [dependencies] +emath = { workspace = true, default-features = false } epaint = { workspace = true, default-features = false } ahash.workspace = true diff --git a/crates/egui/src/callstack.rs b/crates/egui/src/callstack.rs index fac5ac5936e..aa6f44cb08d 100644 --- a/crates/egui/src/callstack.rs +++ b/crates/egui/src/callstack.rs @@ -77,7 +77,7 @@ pub fn capture() -> String { // Remove stuff that isn't user calls: let skip_prefixes = [ - // "backtrace::", // not needed, since we cut at at egui::callstack::capture + // "backtrace::", // not needed, since we cut at egui::callstack::capture "egui::", "", diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 3c6b711be93..857569da3ac 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -61,6 +61,7 @@ impl State { #[derive(Clone, Copy, Debug)] pub struct Area { pub(crate) id: Id, + sense: Option, movable: bool, interactable: bool, enabled: bool, @@ -78,6 +79,7 @@ impl Area { pub fn new(id: Id) -> Self { Self { id, + sense: None, movable: true, interactable: true, constrain: false, @@ -114,7 +116,7 @@ impl Area { self } - /// moveable by dragging the area? + /// Moveable by dragging the area? #[inline] pub fn movable(mut self, movable: bool) -> Self { self.movable = movable; @@ -139,6 +141,15 @@ impl Area { self } + /// Explicitly set a sense. + /// + /// If not set, this will default to `Sense::drag()` if movable, `Sense::click()` if interactable, and `Sense::hover()` otherwise. + #[inline] + pub fn sense(mut self, sense: Sense) -> Self { + self.sense = Some(sense); + self + } + /// `order(Order::Foreground)` for an Area that should always be on top #[inline] pub fn order(mut self, order: Order) -> Self { @@ -255,6 +266,7 @@ impl Area { pub(crate) fn begin(self, ctx: &Context) -> Prepared { let Self { id, + sense, movable, order, interactable, @@ -299,13 +311,15 @@ impl Area { // interact right away to prevent frame-delay let mut move_response = { let interact_id = layer_id.id.with("move"); - let sense = if movable { - Sense::drag() - } else if interactable { - Sense::click() // allow clicks to bring to front - } else { - Sense::hover() - }; + let sense = sense.unwrap_or_else(|| { + if movable { + Sense::drag() + } else if interactable { + Sense::click() // allow clicks to bring to front + } else { + Sense::hover() + } + }); let move_response = ctx.create_widget(WidgetRect { id: interact_id, @@ -341,7 +355,8 @@ impl Area { 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()); + move_response.rect = state.rect(); + move_response.interact_rect = state.rect(); Prepared { layer_id, @@ -370,7 +385,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())); if let Some(area_rect) = area_rect { - let clip_rect = ctx.available_rect(); + let clip_rect = Rect::EVERYTHING; let painter = Painter::new(ctx.clone(), layer_id, clip_rect); // shrinkage: looks kinda a bad on its own @@ -423,12 +438,7 @@ impl Prepared { .at_least(self.state.left_top_pos() + Vec2::splat(32.0)), ); - let shadow_radius = ctx.style().visuals.window_shadow.extrusion; // 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(), constrain_rect.max) - .expand(clip_rect_margin) - .intersect(constrain_rect); + let clip_rect = constrain_rect; // Don't paint outside our bounds let mut ui = Ui::new( ctx.clone(), diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index cb48ca1ede9..7c541a1a2f7 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -230,7 +230,7 @@ impl CollapsingState { } } - /// Paint this [CollapsingState](CollapsingState)'s toggle button. Takes an [IconPainter](IconPainter) as the icon. + /// Paint this [`CollapsingState`]'s toggle button. Takes an [`IconPainter`] as the icon. /// ``` /// # egui::__run_test_ui(|ui| { /// fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) { @@ -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, diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index bd7f8eb7029..40bd17d3c4d 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`]. @@ -52,6 +52,7 @@ use epaint::*; /// Note that you cannot change the margins after calling `begin`. #[doc(alias = "border")] #[derive(Clone, Copy, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[must_use = "You should call .show()"] pub struct Frame { /// Margin within the painted frame. @@ -195,11 +196,15 @@ impl Frame { self } + /// Opacity multiplier in gamma space. + /// + /// For instance, multiplying with `0.5` + /// will make the frame half transparent. #[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); - self.shadow.color = self.shadow.color.linear_multiply(opacity); + self.fill = self.fill.gamma_multiply(opacity); + self.stroke.color = self.stroke.color.gamma_multiply(opacity); + self.shadow.color = self.shadow.color.gamma_multiply(opacity); self } } @@ -239,7 +244,7 @@ impl Frame { let where_to_put_background = ui.painter().add(Shape::Noop); let outer_rect_bounds = ui.available_rect_before_wrap(); - let mut inner_rect = (self.inner_margin + self.outer_margin).shrink_rect(outer_rect_bounds); + let mut inner_rect = outer_rect_bounds - self.outer_margin - self.inner_margin; // Make sure we don't shrink to the negative: inner_rect.max.x = inner_rect.max.x.max(inner_rect.min.x); @@ -290,19 +295,18 @@ impl Frame { if shadow == Default::default() { frame_shape } else { - let shadow = shadow.tessellate(outer_rect, rounding); - let shadow = Shape::Mesh(shadow); - Shape::Vec(vec![shadow, frame_shape]) + let shadow = shadow.as_shape(outer_rect, rounding); + Shape::Vec(vec![Shape::from(shadow), frame_shape]) } } } impl Prepared { fn content_with_margin(&self) -> Rect { - (self.frame.inner_margin + self.frame.outer_margin).expand_rect(self.content_ui.min_rect()) + self.content_ui.min_rect() + self.frame.inner_margin + self.frame.outer_margin } - /// Allocate the the space that was used by [`Self::content_ui`]. + /// Allocate 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. /// @@ -315,10 +319,7 @@ impl Prepared { /// /// 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()); + let paint_rect = self.content_ui.min_rect() + self.frame.inner_margin; if ui.is_rect_visible(paint_rect) { let shape = self.frame.paint(paint_rect); diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index 11e69b8289c..324e17bf91f 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -150,7 +150,7 @@ impl SidePanel { self } - /// The initial wrapping width of the [`SidePanel`]. + /// The initial wrapping width of the [`SidePanel`], including margins. #[inline] pub fn default_width(mut self, default_width: f32) -> Self { self.default_width = default_width; @@ -161,21 +161,21 @@ impl SidePanel { self } - /// Minimum width of the panel. + /// Minimum width of the panel, including margins. #[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. + /// Maximum width of the panel, including margins. #[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. + /// The allowable width range for the panel, including margins. #[inline] pub fn width_range(mut self, width_range: impl Into) -> Self { let width_range = width_range.into(); @@ -184,7 +184,7 @@ impl SidePanel { self } - /// Enforce this exact width. + /// Enforce this exact width, including margins. #[inline] pub fn exact_width(mut self, width: f32) -> Self { self.default_width = width; @@ -228,8 +228,8 @@ impl SidePanel { let available_rect = ui.available_rect_before_wrap(); let mut panel_rect = available_rect; + let mut width = default_width; { - let mut width = default_width; if let Some(state) = PanelState::load(ui.ctx(), id) { width = state.rect.width(); } @@ -249,9 +249,8 @@ impl SidePanel { 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()); + width = (pointer.x - side.side_x(panel_rect)).abs(); + width = clamp_to_range(width, width_range).at_most(available_rect.width()); side.set_rect_width(&mut panel_rect, width); } } @@ -263,7 +262,7 @@ impl SidePanel { let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style())); let inner_response = frame.show(&mut panel_ui, |ui| { ui.set_min_height(ui.max_rect().height()); // Make sure the frame fills the full height - ui.set_min_width(width_range.min); + ui.set_min_width((width_range.min - frame.inner_margin.sum().x).at_least(0.0)); add_contents(ui) }); @@ -296,7 +295,14 @@ impl SidePanel { } if resize_hover || is_resizing { - ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal); + let cursor_icon = if width <= width_range.min { + CursorIcon::ResizeEast + } else if width < width_range.max { + CursorIcon::ResizeHorizontal + } else { + CursorIcon::ResizeWest + }; + ui.ctx().set_cursor_icon(cursor_icon); } PanelState { rect }.store(ui.ctx(), id); @@ -603,7 +609,7 @@ impl TopBottomPanel { self } - /// The initial height of the [`TopBottomPanel`]. + /// The initial height of the [`TopBottomPanel`], including margins. /// Defaults to [`style::Spacing::interact_size`].y. #[inline] pub fn default_height(mut self, default_height: f32) -> Self { @@ -615,21 +621,21 @@ impl TopBottomPanel { self } - /// Minimum height of the panel. + /// Minimum height of the panel, including margins. #[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. + /// Maximum height of the panel, including margins. #[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. + /// The allowable height range for the panel, including margins. #[inline] pub fn height_range(mut self, height_range: impl Into) -> Self { let height_range = height_range.into(); @@ -640,7 +646,7 @@ impl TopBottomPanel { self } - /// Enforce this exact height. + /// Enforce this exact height, including margins. #[inline] pub fn exact_height(mut self, height: f32) -> Self { self.default_height = Some(height); @@ -684,12 +690,13 @@ impl TopBottomPanel { let available_rect = ui.available_rect_before_wrap(); let mut panel_rect = available_rect; + + let mut height = if let Some(state) = PanelState::load(ui.ctx(), id) { + state.rect.height() + } else { + default_height.unwrap_or_else(|| ui.style().spacing.interact_size.y) + }; { - let mut height = if let Some(state) = PanelState::load(ui.ctx(), id) { - state.rect.height() - } else { - default_height.unwrap_or_else(|| ui.style().spacing.interact_size.y) - }; height = clamp_to_range(height, height_range).at_most(available_rect.height()); side.set_rect_height(&mut panel_rect, height); ui.ctx() @@ -707,8 +714,8 @@ impl TopBottomPanel { if is_resizing { if let Some(pointer) = resize_response.interact_pointer_pos() { - let height = (pointer.y - side.side_y(panel_rect)).abs(); - let height = + height = (pointer.y - side.side_y(panel_rect)).abs(); + height = clamp_to_range(height, height_range).at_most(available_rect.height()); side.set_rect_height(&mut panel_rect, height); } @@ -721,7 +728,7 @@ impl TopBottomPanel { let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style())); let inner_response = frame.show(&mut panel_ui, |ui| { ui.set_min_width(ui.max_rect().width()); // Make the frame fill full width - ui.set_min_height(height_range.min); + ui.set_min_height((height_range.min - frame.inner_margin.sum().y).at_least(0.0)); add_contents(ui) }); @@ -755,7 +762,14 @@ impl TopBottomPanel { } if resize_hover || is_resizing { - ui.ctx().set_cursor_icon(CursorIcon::ResizeVertical); + let cursor_icon = if height <= height_range.min { + CursorIcon::ResizeSouth + } else if height < height_range.max { + CursorIcon::ResizeVertical + } else { + CursorIcon::ResizeNorth + }; + ui.ctx().set_cursor_icon(cursor_icon); } PanelState { rect }.store(ui.ctx(), id); diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 99165149f1d..9643a8eee31 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -333,13 +333,13 @@ pub fn popup_below_widget( /// # }); /// ``` pub fn popup_above_or_below_widget( - ui: &Ui, + parent_ui: &Ui, popup_id: Id, widget_response: &Response, above_or_below: AboveOrBelow, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { - if ui.memory(|mem| mem.is_popup_open(popup_id)) { + if parent_ui.memory(|mem| mem.is_popup_open(popup_id)) { let (pos, pivot) = match above_or_below { AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM), AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP), @@ -350,8 +350,8 @@ pub fn popup_above_or_below_widget( .constrain(true) .fixed_pos(pos) .pivot(pivot) - .show(ui.ctx(), |ui| { - let frame = Frame::popup(ui.style()); + .show(parent_ui.ctx(), |ui| { + let frame = Frame::popup(parent_ui.style()); let frame_margin = frame.total_margin(); frame .show(ui, |ui| { @@ -365,8 +365,8 @@ pub fn popup_above_or_below_widget( }) .inner; - if ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() { - ui.memory_mut(|mem| mem.close_popup()); + if parent_ui.input(|i| i.key_pressed(Key::Escape)) || widget_response.clicked_elsewhere() { + parent_ui.memory_mut(|mem| mem.close_popup()); } Some(inner) } else { diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 27128f67126..700c2040253 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -176,6 +176,9 @@ pub struct ScrollArea { /// end position until user manually changes position. It will become true /// again once scroll handle makes contact with end. stick_to_end: Vec2b, + + /// If false, `scroll_to_*` functions will not be animated + animated: bool, } impl ScrollArea { @@ -219,6 +222,7 @@ impl ScrollArea { scrolling_enabled: true, drag_to_scroll: true, stick_to_end: Vec2b::FALSE, + animated: true, } } @@ -337,6 +341,16 @@ impl ScrollArea { } /// Turn on/off scrolling on the horizontal/vertical axes. + /// + /// You can pass in `false`, `true`, `[false, true]` etc. + #[inline] + pub fn scroll(mut self, scroll_enabled: impl Into) -> Self { + self.scroll_enabled = scroll_enabled.into(); + self + } + + /// Turn on/off scrolling on the horizontal/vertical axes. + #[deprecated = "Renamed to `scroll`"] #[inline] pub fn scroll2(mut self, scroll_enabled: impl Into) -> Self { self.scroll_enabled = scroll_enabled.into(); @@ -383,6 +397,15 @@ impl ScrollArea { self } + /// Should the scroll area animate `scroll_to_*` functions? + /// + /// Default: `true`. + #[inline] + pub fn animated(mut self, animated: bool) -> Self { + self.animated = animated; + self + } + /// Is any scrolling enabled? pub(crate) fn is_any_scroll_enabled(&self) -> bool { self.scroll_enabled[0] || self.scroll_enabled[1] @@ -449,6 +472,7 @@ struct Prepared { scrolling_enabled: bool, stick_to_end: Vec2b, + animated: bool, } impl ScrollArea { @@ -465,9 +489,11 @@ impl ScrollArea { scrolling_enabled, drag_to_scroll, stick_to_end, + animated, } = self; let ctx = ui.ctx().clone(); + let scrolling_enabled = scrolling_enabled && ui.is_enabled(); let id_source = id_source.unwrap_or_else(|| Id::new("scroll_area")); let id = ui.make_persistent_id(id_source); @@ -553,6 +579,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]) @@ -577,48 +604,50 @@ impl ScrollArea { } } else { for d in 0..2 { - let dt = ui.input(|i| i.stable_dt).at_most(0.1); + // Kinetic scrolling + let stop_speed = 20.0; // Pixels per second. + let friction_coeff = 1000.0; // Pixels per second squared. - if let Some(scroll_target) = state.offset_target[d] { + let friction = friction_coeff * dt; + if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed { 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 { - // 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; - } - } } else { - // Kinetic scrolling - let stop_speed = 20.0; // Pixels per second. - let friction_coeff = 1000.0; // Pixels per second squared. + 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(); + } + } + } + } - 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 { + // 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; } } } @@ -637,6 +666,7 @@ impl ScrollArea { viewport, scrolling_enabled, stick_to_end, + animated, } } @@ -748,16 +778,19 @@ impl Prepared { viewport: _, scrolling_enabled, stick_to_end, + animated, } = self; let content_size = content_ui.min_size(); for d in 0..2 { + // 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] { - // 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((target_range, align)) = scroll_target { let min = content_ui.min_rect().min[d]; let clip_rect = content_ui.clip_rect(); @@ -789,7 +822,9 @@ impl Prepared { if delta != 0.0 { let target_offset = state.offset[d] + delta; - if let Some(animation) = &mut state.offset_target[d] { + if !animated { + state.offset[d] = target_offset; + } else 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; diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 698033cf9c4..0f336743bd8 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -104,6 +104,13 @@ impl<'open> Window<'open> { self } + /// `order(Order::Foreground)` for a Window that should always be on top + #[inline] + pub fn order(mut self, order: Order) -> Self { + self.area = self.area.order(order); + self + } + /// 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] @@ -328,9 +335,19 @@ impl<'open> Window<'open> { } /// Enable/disable horizontal/vertical scrolling. `false` by default. + /// + /// You can pass in `false`, `true`, `[false, true]` etc. + #[inline] + pub fn scroll(mut self, scroll: impl Into) -> Self { + self.scroll = self.scroll.scroll(scroll); + self + } + + /// Enable/disable horizontal/vertical scrolling. `false` by default. + #[deprecated = "Renamed to `scroll`"] #[inline] pub fn scroll2(mut self, scroll: impl Into) -> Self { - self.scroll = self.scroll.scroll2(scroll); + self.scroll = self.scroll.scroll(scroll); self } @@ -431,6 +448,16 @@ impl<'open> Window<'open> { (0.0, 0.0) }; + { + // 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 = @@ -687,6 +714,7 @@ impl ResizeInteraction { let top = self.top.any(); let bottom = self.bottom.any(); + // TODO(emilk): use one-sided cursors for when we reached the min/max size. if (left && top) || (right && bottom) { ctx.set_cursor_icon(CursorIcon::ResizeNwSe); } else if (right && top) || (left && bottom) { diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 8e09414a6be..2d58be7e126 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2,7 +2,6 @@ use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration}; -use ahash::HashMap; use epaint::{ emath::TSTransform, mutex::*, stats::*, text::Fonts, util::OrderedFloat, TessellationOptions, *, }; @@ -253,15 +252,21 @@ struct ViewportState { } /// What called [`Context::request_repaint`]? -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct RepaintCause { /// What file had the call that requested the repaint? - pub file: String, + pub file: &'static str, - /// What line number of the the call that requested the repaint? + /// What line number of the call that requested the repaint? pub line: u32, } +impl std::fmt::Debug for RepaintCause { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.file, self.line) + } +} + impl RepaintCause { /// Capture the file and line number of the call site. #[allow(clippy::new_without_default)] @@ -269,7 +274,7 @@ impl RepaintCause { pub fn new() -> Self { let caller = Location::caller(); Self { - file: caller.file().to_owned(), + file: caller.file(), line: caller.line(), } } @@ -421,18 +426,17 @@ impl ContextImpl { // but the `screen_rect` is the most important part. } } - let pixels_per_point = self.memory.options.zoom_factor - * new_raw_input - .viewport() - .native_pixels_per_point - .unwrap_or(1.0); + 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; let all_viewport_ids: ViewportIdSet = self.all_viewport_ids(); let viewport = self.viewports.entry(self.viewport_id()).or_default(); - self.memory - .begin_frame(&viewport.input, &new_raw_input, &all_viewport_ids); + self.memory.begin_frame(&new_raw_input, &all_viewport_ids); viewport.input = std::mem::take(&mut viewport.input).begin_frame( new_raw_input, @@ -440,17 +444,12 @@ impl ContextImpl { pixels_per_point, ); - viewport.frame_state.begin_frame(&viewport.input); + let screen_rect = viewport.input.screen_rect; + + viewport.frame_state.begin_frame(screen_rect); { - let area_order: HashMap = self - .memory - .areas() - .order() - .iter() - .enumerate() - .map(|(i, id)| (*id, i)) - .collect(); + let area_order = self.memory.areas().order_map(); let mut layers: Vec = viewport.widgets_prev_frame.layer_ids().collect(); @@ -488,7 +487,6 @@ impl ContextImpl { } // Ensure we register the background area so panels and background ui can catch clicks: - let screen_rect = viewport.input.screen_rect(); self.memory.areas_mut().set_state( LayerId::background(), containers::area::State { @@ -600,11 +598,11 @@ impl ContextImpl { /// /// For the root viewport this will return [`ViewportId::ROOT`]. pub(crate) fn parent_viewport_id(&self) -> ViewportId { - self.viewport_stack - .last() - .copied() - .unwrap_or_default() - .parent + let viewport_id = self.viewport_id(); + *self + .viewport_parents + .get(&viewport_id) + .unwrap_or(&ViewportId::ROOT) } fn all_viewport_ids(&self) -> ViewportIdSet { @@ -926,7 +924,7 @@ impl Context { self.write(move |ctx| writer(&mut ctx.memory.options.tessellation_options)) } - /// If the given [`Id`] has been used previously the same frame at at different position, + /// If the given [`Id`] has been used previously the same frame at different position, /// then an error will be printed on screen. /// /// This function is already called for all widgets that do any interaction, @@ -1018,12 +1016,7 @@ impl Context { /// /// If the widget already exists, its state (sense, Rect, etc) will be updated. #[allow(clippy::too_many_arguments)] - pub(crate) fn create_widget(&self, mut w: WidgetRect) -> Response { - if !w.enabled { - w.sense.click = false; - w.sense.drag = false; - } - + pub(crate) fn create_widget(&self, w: WidgetRect) -> Response { // Remember this widget self.write(|ctx| { let viewport = ctx.viewport(); @@ -1109,9 +1102,9 @@ impl Context { 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_stopped: false, @@ -1120,8 +1113,6 @@ impl Context { changed: false, }; - let clicked_elsewhere = res.clicked_elsewhere(); - self.write(|ctx| { let viewport = ctx.viewports.entry(ctx.viewport_id()).or_default(); @@ -1130,17 +1121,25 @@ impl Context { let input = &viewport.input; let memory = &mut ctx.memory; - if sense.click + 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 - res.clicked[PointerButton::Primary as usize] = true; + res.fake_primary_click = true; } #[cfg(feature = "accesskit")] - if sense.click && input.has_accesskit_action_request(id, accesskit::Action::Default) { - res.clicked[PointerButton::Primary as usize] = true; + if enabled + && sense.click + && input.has_accesskit_action_request(id, accesskit::Action::Default) + { + res.fake_primary_click = true; + } + + if enabled && sense.click && Some(id) == viewport.interact_widgets.long_touched { + res.long_touched = true; } let interaction = memory.interaction(); @@ -1156,25 +1155,29 @@ impl Context { } let clicked = Some(id) == viewport.interact_widgets.clicked; + let mut any_press = false; for pointer_event in &input.pointer.pointer_events { - if let PointerEvent::Released { click, button } = pointer_event { - if sense.click && clicked { - if let Some(click) = click { - res.clicked[*button as usize] = true; - res.double_clicked[*button as usize] = click.is_double(); - res.triple_clicked[*button as usize] = click.is_triple(); - } + match pointer_event { + PointerEvent::Moved(_) => {} + PointerEvent::Pressed { .. } => { + any_press = true; } + PointerEvent::Released { click, .. } => { + if enabled && sense.click && clicked && click.is_some() { + res.clicked = true; + } - res.is_pointer_button_down_on = false; - res.dragged = false; + 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 || clicked || res.drag_stopped; + 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)) = ( @@ -1185,22 +1188,36 @@ impl Context { } } - if input.pointer.any_down() && !res.is_pointer_button_down_on { + 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) { + let pointer_pressed_elsewhere = any_press && !res.hovered; + if pointer_pressed_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(); + res + } + + /// This is called by [`Response::widget_info`], but can also be called directly. + /// + /// With some debug flags it will store the widget info in [`WidgetRects`] for later display. + #[inline] + pub fn register_widget_info(&self, id: Id, make_info: impl Fn() -> crate::WidgetInfo) { + #[cfg(debug_assertions)] + self.write(|ctx| { + if ctx.memory.options.style.debug.show_interactive_widgets { + ctx.viewport().widgets_this_frame.set_info(id, make_info()); } }); - res + #[cfg(not(debug_assertions))] + { + _ = (self, id, make_info); + } } /// Get a full-screen painter for a new or existing layer @@ -1473,7 +1490,7 @@ impl Context { self.read(|ctx| { ctx.viewports .get(&ctx.viewport_id()) - .map(|v| v.repaint.causes.clone()) + .map(|v| v.repaint.prev_causes.clone()) }) .unwrap_or_default() } @@ -1569,7 +1586,7 @@ impl Context { /// The [`Style`] used by all new windows, panels etc. /// - /// You can also change this using [`Self::style_mut]` + /// 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>) { @@ -1731,7 +1748,7 @@ impl Context { let name = name.into(); let image = image.into(); let max_texture_side = self.input(|i| i.max_texture_side); - crate::egui_assert!( + debug_assert!( image.width() <= max_texture_side && image.height() <= max_texture_side, "Texture {:?} has size {}x{}, but the maximum texture side is {}", name, @@ -1808,6 +1825,7 @@ impl Context { self.write(|ctx| ctx.end_frame()) } + /// Called at the end of the frame. #[cfg(debug_assertions)] fn debug_painting(&self) { let paint_widget = |widget: &WidgetRect, text: &str, color: Color32| { @@ -1820,7 +1838,7 @@ impl Context { 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()) + self.write(|ctx| ctx.viewport().widgets_this_frame.get(id).copied()) { paint_widget(&widget, text, color); } @@ -1840,8 +1858,8 @@ impl Context { } else if rect.sense.drag { (Color32::from_rgb(0, 0, 0x88), "drag") } else { - continue; - // (Color32::from_rgb(0, 0, 0x88), "hover") + // unreachable since we only show interactive + (Color32::from_rgb(0, 0, 0x88), "hover") }; painter.debug_rect(rect.interact_rect, color, text); } @@ -1853,17 +1871,40 @@ impl Context { let interact_widgets = self.write(|ctx| ctx.viewport().interact_widgets.clone()); let InteractionSnapshot { clicked, + long_touched: _, drag_started: _, dragged, drag_stopped: _, - contains_pointer, hovered, + contains_pointer, } = interact_widgets; - if false { - for widget in contains_pointer { - paint_widget_id(widget, "contains_pointer", Color32::BLUE); + if true { + for &id in &contains_pointer { + paint_widget_id(id, "contains_pointer", Color32::BLUE); + } + + let widget_rects = self.write(|w| w.viewport().widgets_this_frame.clone()); + + let mut contains_pointer: Vec = contains_pointer.iter().copied().collect(); + contains_pointer.sort_by_key(|&id| { + widget_rects + .order(id) + .map(|(layer_id, order_in_layer)| (layer_id.order, order_in_layer)) + }); + + let mut debug_text = "Widgets in order:\n".to_owned(); + for id in contains_pointer { + let mut widget_text = format!("{id:?}"); + if let Some(rect) = widget_rects.get(id) { + widget_text += &format!(" {:?} {:?}", rect.rect, rect.sense); + } + if let Some(info) = widget_rects.info(id) { + widget_text += &format!(" {info:?}"); + } + debug_text += &format!("{widget_text}\n"); } + self.debug_text(debug_text); } if true { for widget in hovered { @@ -1887,7 +1928,7 @@ impl Context { drag, } = hits; - if false { + if true { for widget in &contains_pointer { paint_widget(widget, "contains_pointer", Color32::BLUE); } @@ -1962,7 +2003,10 @@ impl ContextImpl { }) .collect() }; - let focus_id = self.memory.focus().map_or(root_id, |id| id.accesskit_id()); + 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)), @@ -2227,7 +2271,7 @@ impl Context { /// 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. @@ -2354,7 +2398,7 @@ impl Context { /// See also [`Response::contains_pointer`]. pub fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool { let rect = - if let Some(transform) = self.memory(|m| m.layer_transforms.get(&layer_id).cloned()) { + if let Some(transform) = self.memory(|m| m.layer_transforms.get(&layer_id).copied()) { transform * rect } else { rect @@ -2487,7 +2531,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() @@ -2774,7 +2818,7 @@ impl Context { /// The `Context` lock is held while the given closure is called! /// /// Returns `None` if acesskit is off. - // TODO: consider making both RO and RW versions + // TODO(emilk): consider making both read-only and read-write versions #[cfg(feature = "accesskit")] pub fn accesskit_node_builder( &self, diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index f61b9312f89..2260db1f433 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -361,10 +361,12 @@ pub enum Event { /// A key was pressed or released. Key { - /// The logical key, heeding the users keymap. + /// Most of the time, it's the logical key, heeding the active keymap -- for instance, if the user has Dvorak + /// keyboard layout, it will be taken into account. /// - /// For instance, if the user is using Dvorak keyboard layout, - /// this will take that into account. + /// If it's impossible to determine the logical key on desktop platforms (say, in case of non-Latin letters), + /// `key` falls back to the value of the corresponding physical key. This is necessary for proper work of + /// standard shortcuts that only respond to Latin-based bindings (such as `Ctrl` + `V`). key: Key, /// The physical key, corresponding to the actual position on the keyboard. @@ -445,14 +447,8 @@ pub enum Event { /// * `zoom > 1`: pinch spread Zoom(f32), - /// IME composition start. - CompositionStart, - - /// A new IME candidate is being suggested. - CompositionUpdate(String), - - /// IME composition ended with this final result. - CompositionEnd(String), + /// IME Event + Ime(ImeEvent), /// On touch screens, report this *in addition to* /// [`Self::PointerMoved`], [`Self::PointerButton`], [`Self::PointerGone`] @@ -507,6 +503,25 @@ pub enum Event { }, } +/// IME event. +/// +/// See +#[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum ImeEvent { + /// Notifies when the IME was enabled. + Enabled, + + /// A new IME candidate is being suggested. + Preedit(String), + + /// IME composition ended with this final result. + Commit(String), + + /// Notifies when the IME was disabled. + Disabled, +} + /// Mouse button (or similar for touch input) #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] diff --git a/crates/egui/src/data/key.rs b/crates/egui/src/data/key.rs index 92e6b69dba2..66fe6b9336f 100644 --- a/crates/egui/src/data/key.rs +++ b/crates/egui/src/data/key.rs @@ -35,25 +35,25 @@ pub enum Key { /// `,` Comma, - /// '\\' + /// `\` Backslash, - /// '/' + /// `/` Slash, - /// '|', a vertical bar + /// `|`, a vertical bar Pipe, /// `?` Questionmark, - // '[' + // `[` OpenBracket, - // ']' + // `]` CloseBracket, - /// '`', also known as "backquote" or "grave" + /// \`, also known as "backquote" or "grave" Backtick, /// `-` diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index d1db5fc2a41..97794f4d13e 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -504,7 +504,10 @@ impl std::fmt::Debug for WidgetInfo { let mut s = f.debug_struct("WidgetInfo"); s.field("typ", typ); - s.field("enabled", enabled); + + if !enabled { + s.field("enabled", enabled); + } if let Some(label) = label { s.field("label", label); diff --git a/crates/egui/src/frame_state.rs b/crates/egui/src/frame_state.rs index d94f1222aa6..5f966d117a0 100644 --- a/crates/egui/src/frame_state.rs +++ b/crates/egui/src/frame_state.rs @@ -21,12 +21,12 @@ pub(crate) struct FrameState { /// All [`Id`]s that were used this frame. pub(crate) used_ids: IdMap, - /// Starts off as the screen_rect, shrinks as panels are added. + /// Starts off as the `screen_rect`, shrinks as panels are added. /// The [`CentralPanel`] does not change this. /// This is the area available to Window's. pub(crate) available_rect: Rect, - /// Starts off as the screen_rect, shrinks as panels are added. + /// Starts off as the `screen_rect`, shrinks as panels are added. /// The [`CentralPanel`] retracts from this. pub(crate) unused_rect: Rect, @@ -75,7 +75,7 @@ impl Default for FrameState { } 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, @@ -94,8 +94,8 @@ impl FrameState { } = 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_target = [None, None]; @@ -117,7 +117,7 @@ impl FrameState { /// This is the "background" area, what egui doesn't cover with panels (but may cover with windows). /// This is also the area to which windows are constrained. pub(crate) fn available_rect(&self) -> Rect { - crate::egui_assert!( + debug_assert!( self.available_rect.is_finite(), "Called `available_rect()` before `Context::run()`" ); @@ -126,7 +126,7 @@ impl FrameState { /// Shrink `available_rect`. pub(crate) fn allocate_left_panel(&mut self, panel_rect: Rect) { - crate::egui_assert!( + debug_assert!( panel_rect.min.distance(self.available_rect.min) < 0.1, "Mismatching left panel. You must not create a panel from within another panel." ); @@ -137,7 +137,7 @@ impl FrameState { /// Shrink `available_rect`. pub(crate) fn allocate_right_panel(&mut self, panel_rect: Rect) { - crate::egui_assert!( + debug_assert!( panel_rect.max.distance(self.available_rect.max) < 0.1, "Mismatching right panel. You must not create a panel from within another panel." ); @@ -148,7 +148,7 @@ impl FrameState { /// Shrink `available_rect`. pub(crate) fn allocate_top_panel(&mut self, panel_rect: Rect) { - crate::egui_assert!( + debug_assert!( panel_rect.min.distance(self.available_rect.min) < 0.1, "Mismatching top panel. You must not create a panel from within another panel." ); @@ -159,7 +159,7 @@ impl FrameState { /// Shrink `available_rect`. pub(crate) fn allocate_bottom_panel(&mut self, panel_rect: Rect) { - crate::egui_assert!( + debug_assert!( panel_rect.max.distance(self.available_rect.max) < 0.1, "Mismatching bottom panel. You must not create a panel from within another panel." ); diff --git a/crates/egui/src/grid.rs b/crates/egui/src/grid.rs index 374902e3a4b..b888f756b5e 100644 --- a/crates/egui/src/grid.rs +++ b/crates/egui/src/grid.rs @@ -85,7 +85,7 @@ impl GridLayout { // TODO(emilk): respect current layout let initial_available = ui.placer().max_rect().intersect(ui.cursor()); - crate::egui_assert!( + debug_assert!( initial_available.min.x.is_finite(), "Grid not yet available for right-to-left layouts" ); diff --git a/crates/egui/src/gui_zoom.rs b/crates/egui/src/gui_zoom.rs index c4b5deb5ed6..bde60f4321b 100644 --- a/crates/egui/src/gui_zoom.rs +++ b/crates/egui/src/gui_zoom.rs @@ -70,10 +70,20 @@ pub fn zoom_out(ctx: &Context) { /// /// This is meant to be called from within a menu (See [`Ui::menu_button`]). pub fn zoom_menu_buttons(ui: &mut Ui) { + fn button(ctx: &Context, text: &str, shortcut: &KeyboardShortcut) -> Button<'static> { + let btn = Button::new(text); + let zoom_with_keyboard = ctx.options(|o| o.zoom_with_keyboard); + if zoom_with_keyboard { + btn.shortcut_text(ctx.format_shortcut(shortcut)) + } else { + btn + } + } + if ui .add_enabled( ui.ctx().zoom_factor() < MAX_ZOOM_FACTOR, - Button::new("Zoom In").shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_IN)), + button(ui.ctx(), "Zoom In", &kb_shortcuts::ZOOM_IN), ) .clicked() { @@ -84,8 +94,7 @@ pub fn zoom_menu_buttons(ui: &mut Ui) { if ui .add_enabled( ui.ctx().zoom_factor() > MIN_ZOOM_FACTOR, - Button::new("Zoom Out") - .shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_OUT)), + button(ui.ctx(), "Zoom Out", &kb_shortcuts::ZOOM_OUT), ) .clicked() { @@ -96,8 +105,7 @@ pub fn zoom_menu_buttons(ui: &mut Ui) { if ui .add_enabled( ui.ctx().zoom_factor() != 1.0, - Button::new("Reset Zoom") - .shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_RESET)), + button(ui.ctx(), "Reset Zoom", &kb_shortcuts::ZOOM_RESET), ) .clicked() { diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 2b778e9d719..62a762aa52d 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -78,6 +78,16 @@ pub fn hit_test( 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); diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index 1cd96faec5f..0465c32d76c 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -85,7 +85,7 @@ impl Id { impl std::fmt::Debug for Id { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:016X}", self.0) + write!(f, "{:04X}", self.value() as u16) } } diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state.rs index ed29282e950..22851a470b6 100644 --- a/crates/egui/src/input_state.rs +++ b/crates/egui/src/input_state.rs @@ -11,8 +11,13 @@ 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 @@ -30,7 +35,7 @@ pub struct InputState { /// State of the mouse or simple touch gestures which can be mapped to mouse operations. pub pointer: PointerState, - /// State of touches, except those covered by PointerState (like clicks and drags). + /// State of touches, except those covered by `PointerState` (like clicks and drags). /// (We keep a separate [`TouchState`] for each encountered touch device.) touch_states: BTreeMap, @@ -231,7 +236,7 @@ impl InputState { // 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 + let t = crate::emath::exponential_smooth_factor(0.90, 0.1, dt); // reach _% in _ seconds. TODO(emilk): parameterize for d in 0..2 { if unprocessed_scroll_delta[d].abs() < 1.0 { @@ -244,20 +249,6 @@ 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(); - } - Self { pointer, touch_states: self.touch_states, @@ -273,7 +264,7 @@ impl InputState { 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, @@ -335,6 +326,10 @@ impl InputState { self.pointer.wants_repaint() || self.unprocessed_scroll_delta.abs().max_elem() > 0.2 || !self.events.is_empty() + + // We need to wake up and check for press-and-hold for the context menu. + // TODO(emilk): wake up after `MAX_CLICK_DURATION` instead of every frame. + || (self.any_touches() && !self.pointer.is_decidedly_dragging()) } /// Count presses of a key. If non-zero, the presses are consumed, so that this will only return non-zero once. @@ -489,15 +484,16 @@ 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. pub fn any_touches(&self) -> bool { + self.touch_states.values().any(|t| t.any_touches()) + } + + /// True if we have ever received a touch event. + pub fn has_touch_screen(&self) -> bool { !self.touch_states.is_empty() } @@ -548,6 +544,14 @@ impl InputState { .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() + } } // ---------------------------------------------------------------------------- @@ -655,6 +659,8 @@ pub struct PointerState { 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? @@ -755,6 +761,7 @@ impl PointerState { button, }); } else { + // Released let clicked = self.could_any_button_be_click(); let click = if clicked { @@ -793,6 +800,10 @@ impl PointerState { } Event::PointerGone => { self.latest_pos = None; + self.pointer_events.push(PointerEvent::Released { + click: None, + button: PointerButton::Primary, + }); // 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, @@ -976,11 +987,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? @@ -1029,21 +1042,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. @@ -1062,6 +1075,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 { diff --git a/crates/egui/src/input_state/touch_state.rs b/crates/egui/src/input_state/touch_state.rs index 2ee7a91de75..e9425dba24c 100644 --- a/crates/egui/src/input_state/touch_state.rs +++ b/crates/egui/src/input_state/touch_state.rs @@ -72,7 +72,7 @@ pub(crate) struct TouchState { /// Active touches, if any. /// - /// TouchId is the unique identifier of the touch. It is valid as long as the finger/pen touches the surface. The + /// `TouchId` is the unique identifier of the touch. It is valid as long as the finger/pen touches the surface. The /// next touch will receive a new unique ID. /// /// Refer to [`ActiveTouch`]. @@ -163,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); @@ -176,8 +177,9 @@ impl TouchState { } } - pub fn is_active(&self) -> bool { - self.gesture_state.is_some() + /// Are there currently any fingers touching the surface? + pub fn any_touches(&self) -> bool { + !self.active_touches.is_empty() } pub fn info(&self) -> Option { diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index 2f1925c7528..e25e1c4aa13 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -14,6 +14,10 @@ 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. @@ -56,6 +60,7 @@ impl InteractionSnapshot { pub fn ui(&self, ui: &mut crate::Ui) { let Self { clicked, + long_touched, drag_started, dragged, drag_stopped, @@ -74,6 +79,10 @@ impl InteractionSnapshot { 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(); @@ -123,6 +132,21 @@ pub(crate) fn interact( 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 { @@ -142,7 +166,7 @@ pub(crate) fn interact( } PointerEvent::Released { click, button: _ } => { - if click.is_some() { + if click.is_some() && !input.pointer.is_decidedly_dragging() { if let Some(widget) = interaction .potential_click_id .and_then(|id| widgets.get(id)) @@ -161,22 +185,33 @@ pub(crate) fn interact( 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)) { - 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 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; @@ -199,29 +234,56 @@ pub(crate) fn interact( .map(|w| w.id) .collect(); - let hovered = if clicked.is_some() || dragged.is_some() { - // If currently clicking or dragging, nothing else is hovered. - clicked.iter().chain(&dragged).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() + 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 { + // We may be hovering a an interactive widget or two. + // We must also consider the case where non-interactive widgets + // are _on top_ of an interactive widget. + // For instance: a label in a draggable window. + // In that case we want to hover _both_ widgets, + // otherwise we won't see tooltips for the label. + // + // Because of how `Ui` work, we will often allocate the `Ui` rect + // _after_ adding the children in it (once we know the size it will occopy) + // so we will also have a lot of such `Ui` widgets rects covering almost any widget. + // + // So: we want to hover _all_ widgets above the interactive widget (if any), + // but none below it (an interactive widget stops the hover search). + // + // To know when to stop we need to first know the order of the widgets, + // which luckily we have in the `WidgetRects`. + + let order = |id| widgets.order(id).map(|(_layer, order)| order); // we ignore the layer, since all widgets at this point is in the same layer + + let click_order = hits.click.and_then(|w| order(w.id)).unwrap_or(0); + let drag_order = hits.drag.and_then(|w| order(w.id)).unwrap_or(0); + let top_interactive_order = click_order.max(drag_order); + + let mut hovered: IdSet = hits.click.iter().chain(&hits.drag).map(|w| w.id).collect(); + + for w in &hits.contains_pointer { + if top_interactive_order <= order(w.id).unwrap_or(0) { + hovered.insert(w.id); + } + } + + hovered }; InteractionSnapshot { clicked, + long_touched, drag_started, dragged, drag_stopped, - contains_pointer, hovered, + contains_pointer, } } diff --git a/crates/egui/src/introspection.rs b/crates/egui/src/introspection.rs index 240ff6974a5..f71e82194be 100644 --- a/crates/egui/src/introspection.rs +++ b/crates/egui/src/introspection.rs @@ -150,23 +150,33 @@ impl Widget for &mut epaint::TessellationOptions { validate_meshes, } = self; - ui.checkbox(feathering, "Feathering (antialias)") - .on_hover_text("Apply feathering to smooth out the edges of shapes. Turn off for small performance gain."); - let feathering_slider = crate::Slider::new(feathering_size_in_pixels, 0.0..=10.0) - .smallest_positive(0.1) - .logarithmic(true) - .text("Feathering size in pixels"); - ui.add_enabled(*feathering, feathering_slider); + ui.horizontal(|ui| { + ui.checkbox(feathering, "Feathering (antialias)") + .on_hover_text("Apply feathering to smooth out the edges of shapes. Turn off for small performance gain."); + + if *feathering { + ui.add(crate::DragValue::new(feathering_size_in_pixels).clamp_range(0.0..=10.0).speed(0.1).suffix(" px")); + } + }); ui.checkbox(prerasterized_discs, "Speed up filled circles with pre-rasterization"); - ui.add( - crate::widgets::Slider::new(bezier_tolerance, 0.0001..=10.0) - .logarithmic(true) - .show_value(true) - .text("Spline Tolerance"), - ); - ui.collapsing("debug", |ui| { + ui.horizontal(|ui| { + ui.label("Spline tolerance"); + let speed = 0.01 * *bezier_tolerance; + ui.add( + crate::DragValue::new(bezier_tolerance).clamp_range(0.0001..=10.0) + .speed(speed) + ); + }); + + 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."); + + ui.collapsing("Debug", |ui| { ui.checkbox( coarse_tessellation_culling, "Do coarse culling in the tessellator", @@ -178,12 +188,6 @@ 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 } @@ -194,7 +198,6 @@ impl Widget for &memory::InteractionState { let memory::InteractionState { potential_click_id, potential_drag_id, - focus: _, } = self; ui.vertical(|ui| { diff --git a/crates/egui/src/layout.rs b/crates/egui/src/layout.rs index a044dce9814..15ed2fef079 100644 --- a/crates/egui/src/layout.rs +++ b/crates/egui/src/layout.rs @@ -1,4 +1,4 @@ -use crate::{egui_assert, emath::*, Align}; +use crate::{emath::*, Align}; use std::f32::INFINITY; // ---------------------------------------------------------------------------- @@ -13,7 +13,7 @@ pub(crate) struct Region { /// Always finite. /// /// The bounding box of all child widgets, but not necessarily a tight bounding box - /// since [`Ui`](crate::Ui) can start with a non-zero min_rect size. + /// since [`Ui`](crate::Ui) can start with a non-zero `min_rect` size. pub min_rect: Rect, /// The maximum size of this [`Ui`](crate::Ui). This is a *soft max* @@ -38,7 +38,7 @@ pub(crate) struct Region { /// So one can think of `cursor` as a constraint on the available region. /// /// If something has already been added, this will point to `style.spacing.item_spacing` beyond the latest child. - /// The cursor can thus be `style.spacing.item_spacing` pixels outside of the min_rect. + /// The cursor can thus be `style.spacing.item_spacing` pixels outside of the `min_rect`. pub(crate) cursor: Rect, } @@ -66,9 +66,9 @@ impl Region { } pub fn sanity_check(&self) { - egui_assert!(!self.min_rect.any_nan()); - egui_assert!(!self.max_rect.any_nan()); - egui_assert!(!self.cursor.any_nan()); + debug_assert!(!self.min_rect.any_nan()); + debug_assert!(!self.max_rect.any_nan()); + debug_assert!(!self.cursor.any_nan()); } } @@ -389,8 +389,8 @@ impl Layout { /// ## Doing layout impl Layout { pub fn align_size_within_rect(&self, size: Vec2, outer: Rect) -> Rect { - egui_assert!(size.x >= 0.0 && size.y >= 0.0); - egui_assert!(!outer.is_negative()); + debug_assert!(size.x >= 0.0 && size.y >= 0.0); + debug_assert!(!outer.is_negative()); self.align2().align_size_within_rect(size, outer) } @@ -416,8 +416,8 @@ impl Layout { } pub(crate) fn region_from_max_rect(&self, max_rect: Rect) -> Region { - egui_assert!(!max_rect.any_nan()); - egui_assert!(max_rect.is_finite()); + debug_assert!(!max_rect.any_nan()); + debug_assert!(max_rect.is_finite()); let mut region = Region { min_rect: Rect::NOTHING, // temporary max_rect, @@ -450,9 +450,9 @@ impl Layout { /// Given the cursor in the region, how much space is available /// for the next widget? fn available_from_cursor_max_rect(&self, cursor: Rect, max_rect: Rect) -> Rect { - egui_assert!(!cursor.any_nan()); - egui_assert!(!max_rect.any_nan()); - egui_assert!(max_rect.is_finite()); + debug_assert!(!cursor.any_nan()); + debug_assert!(!max_rect.any_nan()); + debug_assert!(max_rect.is_finite()); // NOTE: in normal top-down layout the cursor has moved below the current max_rect, // but the available shouldn't be negative. @@ -506,7 +506,7 @@ impl Layout { avail.max.y = y; } - egui_assert!(!avail.any_nan()); + debug_assert!(!avail.any_nan()); avail } @@ -517,7 +517,7 @@ impl Layout { /// Use `justify_and_align` to get the inner `widget_rect`. pub(crate) fn next_frame(&self, region: &Region, child_size: Vec2, spacing: Vec2) -> Rect { region.sanity_check(); - egui_assert!(child_size.x >= 0.0 && child_size.y >= 0.0); + debug_assert!(child_size.x >= 0.0 && child_size.y >= 0.0); if self.main_wrap { let available_size = self.available_rect_before_wrap(region).size(); @@ -597,7 +597,7 @@ impl Layout { fn next_frame_ignore_wrap(&self, region: &Region, child_size: Vec2) -> Rect { region.sanity_check(); - egui_assert!(child_size.x >= 0.0 && child_size.y >= 0.0); + debug_assert!(child_size.x >= 0.0 && child_size.y >= 0.0); let available_rect = self.available_rect_before_wrap(region); @@ -630,16 +630,16 @@ impl Layout { frame_rect = frame_rect.translate(Vec2::Y * (region.cursor.top() - frame_rect.top())); } - egui_assert!(!frame_rect.any_nan()); - egui_assert!(!frame_rect.is_negative()); + debug_assert!(!frame_rect.any_nan()); + debug_assert!(!frame_rect.is_negative()); frame_rect } /// Apply justify (fill width/height) and/or alignment after calling `next_space`. pub(crate) fn justify_and_align(&self, frame: Rect, mut child_size: Vec2) -> Rect { - egui_assert!(child_size.x >= 0.0 && child_size.y >= 0.0); - egui_assert!(!frame.is_negative()); + debug_assert!(child_size.x >= 0.0 && child_size.y >= 0.0); + debug_assert!(!frame.is_negative()); if self.horizontal_justify() { child_size.x = child_size.x.at_least(frame.width()); // fill full width @@ -657,10 +657,10 @@ impl Layout { ) -> Rect { let frame = self.next_frame_ignore_wrap(region, size); let rect = self.align_size_within_rect(size, frame); - egui_assert!(!rect.any_nan()); - egui_assert!(!rect.is_negative()); - egui_assert!((rect.width() - size.x).abs() < 1.0 || size.x == f32::INFINITY); - egui_assert!((rect.height() - size.y).abs() < 1.0 || size.y == f32::INFINITY); + debug_assert!(!rect.any_nan()); + debug_assert!(!rect.is_negative()); + debug_assert!((rect.width() - size.x).abs() < 1.0 || size.x == f32::INFINITY); + debug_assert!((rect.height() - size.y).abs() < 1.0 || size.y == f32::INFINITY); rect } @@ -703,7 +703,7 @@ impl Layout { widget_rect: Rect, item_spacing: Vec2, ) { - egui_assert!(!cursor.any_nan()); + debug_assert!(!cursor.any_nan()); if self.main_wrap { if cursor.intersects(frame_rect.shrink(1.0)) { // make row/column larger if necessary diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 69328045387..b367158a58b 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -371,8 +371,6 @@ #![allow(clippy::float_cmp)] #![allow(clippy::manual_range_contains)] -#![cfg_attr(feature = "puffin", deny(unsafe_code))] -#![cfg_attr(not(feature = "puffin"), forbid(unsafe_code))] mod animation_manager; pub mod containers; @@ -430,8 +428,8 @@ pub use epaint::{ mutex, text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak}, textures::{TextureFilter, TextureOptions, TextureWrapMode, TexturesDelta}, - ClippedPrimitive, ColorImage, FontImage, ImageData, Mesh, PaintCallback, PaintCallbackInfo, - Rounding, Shape, Stroke, TextureHandle, TextureId, + ClippedPrimitive, ColorImage, FontImage, ImageData, Margin, Mesh, PaintCallback, + PaintCallbackInfo, Rounding, Shadow, Shape, Stroke, TextureHandle, TextureId, }; pub mod text { @@ -463,7 +461,7 @@ pub use { painter::Painter, response::{InnerResponse, Response}, sense::Sense, - style::{FontSelection, Margin, Style, TextStyle, Visuals}, + style::{FontSelection, Style, TextStyle, Visuals}, text::{Galley, TextFormat}, ui::Ui, viewport::*, @@ -517,7 +515,7 @@ macro_rules! include_image { }; } -/// Create a [`Hyperlink`](crate::Hyperlink) to the current [`file!()`] (and line) on Github +/// Create a [`Hyperlink`] to the current [`file!()`] (and line) on Github /// /// ``` /// # egui::__run_test_ui(|ui| { @@ -532,7 +530,7 @@ macro_rules! github_link_file_line { }}; } -/// Create a [`Hyperlink`](crate::Hyperlink) to the current [`file!()`] on github. +/// Create a [`Hyperlink`] to the current [`file!()`] on github. /// /// ``` /// # egui::__run_test_ui(|ui| { @@ -549,22 +547,6 @@ macro_rules! github_link_file { // ---------------------------------------------------------------------------- -/// 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] -macro_rules! egui_assert { - ($($arg: tt)*) => { - if cfg!(any( - feature = "extra_asserts", - all(feature = "extra_debug_asserts", debug_assertions), - )) { - assert!($($arg)*); - } - } -} - -// ---------------------------------------------------------------------------- - /// The minus character: pub(crate) const MINUS_CHAR_STR: &str = "−"; diff --git a/crates/egui/src/load.rs b/crates/egui/src/load.rs index 4d527de63be..71d9d086f56 100644 --- a/crates/egui/src/load.rs +++ b/crates/egui/src/load.rs @@ -55,23 +55,21 @@ 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 std::{ + borrow::Cow, + fmt::{Debug, Display}, + ops::Deref, + 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 emath::{Float, OrderedFloat}; +use epaint::{mutex::Mutex, textures::TextureOptions, ColorImage, TextureHandle, TextureId, Vec2}; use crate::Context; -pub use self::bytes_loader::DefaultBytesLoader; -pub use self::texture_loader::DefaultTextureLoader; +pub use self::{bytes_loader::DefaultBytesLoader, texture_loader::DefaultTextureLoader}; /// Represents a failed attempt at loading an image. #[derive(Clone, Debug)] diff --git a/crates/egui/src/load/bytes_loader.rs b/crates/egui/src/load/bytes_loader.rs index 3ab46794912..d03b2ad418a 100644 --- a/crates/egui/src/load/bytes_loader.rs +++ b/crates/egui/src/load/bytes_loader.rs @@ -53,7 +53,7 @@ impl BytesLoader for DefaultBytesLoader { #[cfg(feature = "log")] log::trace!("forget {uri:?}"); - let _ = self.cache.lock().remove(uri); + self.cache.lock().remove(uri); } fn forget_all(&self) { diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index fb18258eb12..4bee395c530 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -4,7 +4,7 @@ use ahash::HashMap; use epaint::emath::TSTransform; use crate::{ - area, vec2, EventFilter, Id, IdMap, LayerId, Order, Pos2, Rangef, Rect, Style, Vec2, + area, vec2, EventFilter, Id, IdMap, LayerId, Order, Pos2, Rangef, RawInput, Rect, Style, Vec2, ViewportId, ViewportIdMap, ViewportIdSet, }; @@ -95,6 +95,9 @@ pub struct Memory { #[cfg_attr(feature = "persistence", serde(skip))] pub(crate) interactions: ViewportIdMap, + + #[cfg_attr(feature = "persistence", serde(skip))] + pub(crate) focus: ViewportIdMap, } impl Default for Memory { @@ -105,6 +108,7 @@ impl Default for Memory { 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(), @@ -128,7 +132,7 @@ enum FocusDirection { /// Select the widget below the current focused widget. Down, - /// Select the widget to the left of the the current focused widget. + /// Select the widget to the left of the current focused widget. Left, /// Select the previous widget that had focus. @@ -181,6 +185,12 @@ pub struct Options { /// presses Cmd+Plus, Cmd+Minus or Cmd+0, just like in a browser. /// /// This is `true` by default. + /// + /// On the web-backend of `eframe` this is set to false by default, + /// so that the zoom shortcuts are handled exclusively by the browser, + /// which will change the `native_pixels_per_point` (`devicePixelRatio`). + /// You can still zoom egui independently by calling [`crate::Context::set_zoom_factor`], + /// which will be applied on top of the browsers global zoom. #[cfg_attr(feature = "serde", serde(skip))] pub zoom_with_keyboard: bool, @@ -239,7 +249,7 @@ impl Options { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { style, // covered above - zoom_factor: _, // TODO + zoom_factor: _, // TODO(emilk) zoom_with_keyboard, tessellation_options, repaint_on_widget_change, @@ -274,13 +284,15 @@ impl Options { }); CollapsingHeader::new("✒ Painting") - .default_open(true) + .default_open(false) .show(ui, |ui| { tessellation_options.ui(ui); - ui.vertical_centered(|ui| crate::reset_button(ui, tessellation_options)); + ui.vertical_centered(|ui| { + crate::reset_button(ui, tessellation_options, "Reset paint settings"); + }); }); - ui.vertical_centered(|ui| crate::reset_button(ui, self)); + ui.vertical_centered(|ui| crate::reset_button(ui, self, "Reset all")); } } @@ -308,8 +320,6 @@ pub(crate) struct InteractionState { /// as that can only happen after the mouse has moved a bit /// (at least if the widget is interesated in both clicks and drags). pub potential_drag_id: Option, - - pub focus: Focus, } /// Keeps tracks of what widget has keyboard focus @@ -362,24 +372,6 @@ impl InteractionState { pub fn is_using_pointer(&self) -> bool { self.potential_click_id.is_some() || self.potential_drag_id.is_some() } - - fn begin_frame( - &mut self, - prev_input: &crate::input_state::InputState, - new_input: &crate::data::input::RawInput, - ) { - if !prev_input.pointer.could_any_button_be_click() { - self.potential_click_id = None; - } - - if !prev_input.pointer.any_down() || prev_input.pointer.latest_pos().is_none() { - // pointer button was not down last frame - self.potential_click_id = None; - self.potential_drag_id = None; - } - - self.focus.begin_frame(new_input); - } } impl Focus { @@ -537,9 +529,7 @@ impl Focus { } } - let Some(current_focused) = self.focused_widget else { - return None; - }; + let current_focused = self.focused_widget?; // In what direction we are looking for the next widget. let search_direction = match self.focus_direction { @@ -562,9 +552,7 @@ impl Focus { } }); - let Some(current_rect) = self.focus_widgets_cache.get(¤t_focused.id) else { - return None; - }; + let current_rect = self.focus_widgets_cache.get(¤t_focused.id)?; let mut best_score = std::f32::INFINITY; let mut best_id = None; @@ -603,30 +591,29 @@ impl Focus { } impl Memory { - pub(crate) fn begin_frame( - &mut self, - prev_input: &crate::input_state::InputState, - new_input: &crate::data::input::RawInput, - viewports: &ViewportIdSet, - ) { + pub(crate) fn begin_frame(&mut self, new_raw_input: &RawInput, viewports: &ViewportIdSet) { crate::profile_function!(); + self.viewport_id = new_raw_input.viewport_id; + // Cleanup self.interactions.retain(|id, _| viewports.contains(id)); self.areas.retain(|id, _| viewports.contains(id)); - self.viewport_id = new_input.viewport_id; - self.interactions + self.areas.entry(self.viewport_id).or_default(); + + // self.interactions is handled elsewhere + + self.focus .entry(self.viewport_id) .or_default() - .begin_frame(prev_input, new_input); - self.areas.entry(self.viewport_id).or_default(); + .begin_frame(new_raw_input); } pub(crate) fn end_frame(&mut self, used_ids: &IdMap) { self.caches.update(); self.areas_mut().end_frame(); - self.interaction_mut().focus.end_frame(used_ids); + self.focus_mut().end_frame(used_ids); } pub(crate) fn set_viewport_id(&mut self, viewport_id: ViewportId) { @@ -656,7 +643,7 @@ impl Memory { } pub(crate) fn had_focus_last_frame(&self, id: Id) -> bool { - self.interaction().focus.id_previous_frame == Some(id) + self.focus().and_then(|f| f.id_previous_frame) == Some(id) } /// True if the given widget had keyboard focus last frame, but not this one. @@ -677,12 +664,12 @@ impl Memory { /// from the window and back. #[inline(always)] pub fn has_focus(&self, id: Id) -> bool { - self.interaction().focus.focused() == Some(id) + self.focused() == Some(id) } /// Which widget has keyboard focus? - pub fn focus(&self) -> Option { - self.interaction().focus.focused() + pub fn focused(&self) -> Option { + self.focus().and_then(|f| f.focused()) } /// Set an event filter for a widget. @@ -693,7 +680,7 @@ impl Memory { /// You must first give focus to the widget before calling this. pub fn set_focus_lock_filter(&mut self, id: Id, event_filter: EventFilter) { if self.had_focus_last_frame(id) && self.has_focus(id) { - if let Some(focused) = &mut self.interaction_mut().focus.focused_widget { + if let Some(focused) = &mut self.focus_mut().focused_widget { if focused.id == id { focused.filter = event_filter; } @@ -705,16 +692,16 @@ impl Memory { /// See also [`crate::Response::request_focus`]. #[inline(always)] pub fn request_focus(&mut self, id: Id) { - self.interaction_mut().focus.focused_widget = Some(FocusWidget::new(id)); + self.focus_mut().focused_widget = Some(FocusWidget::new(id)); } /// Surrender keyboard focus for a specific widget. /// See also [`crate::Response::surrender_focus`]. #[inline(always)] pub fn surrender_focus(&mut self, id: Id) { - let interaction = self.interaction_mut(); - if interaction.focus.focused() == Some(id) { - interaction.focus.focused_widget = None; + let focus = self.focus_mut(); + if focus.focused() == Some(id) { + focus.focused_widget = None; } } @@ -727,13 +714,13 @@ impl Memory { /// and rendered correctly in a single frame. #[inline(always)] pub fn interested_in_focus(&mut self, id: Id) { - self.interaction_mut().focus.interested_in_focus(id); + self.focus_mut().interested_in_focus(id); } /// Stop editing of active [`TextEdit`](crate::TextEdit) (if any). #[inline(always)] pub fn stop_text_input(&mut self) { - self.interaction_mut().focus.focused_widget = None; + self.focus_mut().focused_widget = None; } /// Is any widget being dragged? @@ -813,11 +800,19 @@ impl Memory { pub(crate) fn interaction_mut(&mut self) -> &mut InteractionState { self.interactions.entry(self.viewport_id).or_default() } + + pub(crate) fn focus(&self) -> Option<&Focus> { + self.focus.get(&self.viewport_id) + } + + pub(crate) fn focus_mut(&mut self) -> &mut Focus { + self.focus.entry(self.viewport_id).or_default() + } } /// ## Popups /// Popups are things like combo-boxes, color pickers, menus etc. -/// Only one can be be open at a time. +/// Only one can be open at a time. impl Memory { /// Is the given popup open? pub fn is_popup_open(&self, popup_id: Id) -> bool { @@ -873,7 +868,7 @@ impl Memory { // ---------------------------------------------------------------------------- /// Keeps track of [`Area`](crate::containers::area::Area)s, which are free-floating [`Ui`](crate::Ui)s. -/// These [`Area`](crate::containers::area::Area)s can be in any [`Order`](crate::Order). +/// These [`Area`](crate::containers::area::Area)s can be in any [`Order`]. #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] @@ -908,6 +903,15 @@ impl Areas { &self.order } + /// For each layer, which order is it in [`Self::order`]? + pub(crate) fn order_map(&self) -> HashMap { + self.order + .iter() + .enumerate() + .map(|(i, id)| (*id, i)) + .collect() + } + pub(crate) fn set_state(&mut self, layer_id: LayerId, state: area::State) { self.visible_current_frame.insert(layer_id); self.areas.insert(layer_id.id, state); diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 67689ca9d4e..84fd1b2e8e3 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -43,11 +43,11 @@ impl BarState { /// Should be called from [`Context`] on a [`Response`] pub fn bar_menu( &mut self, - response: &Response, + button: &Response, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option> { - MenuRoot::stationary_click_interaction(response, &mut self.open_menu); - self.open_menu.show(response, add_contents) + MenuRoot::stationary_click_interaction(button, &mut self.open_menu); + self.open_menu.show(button, add_contents) } pub(crate) fn has_root(&self) -> bool { @@ -149,22 +149,25 @@ pub(crate) fn menu_ui<'c, R>( .order(Order::Foreground) .fixed_pos(pos) .constrain_to(ctx.screen_rect()) - .interactable(true); + .interactable(true) + .sense(Sense::hover()); - area.show(ctx, |ui| { + let area_response = area.show(ctx, |ui| { set_menu_style(ui.style_mut()); - let frame = Frame::menu(ui.style()).show(ui, |ui| { - ui.set_max_width(ui.spacing().menu_width); - ui.set_menu_state(Some(menu_state_arc.clone())); - ui.with_layout(Layout::top_down_justified(Align::LEFT), add_contents) - .inner - }); + Frame::menu(ui.style()) + .show(ui, |ui| { + ui.set_max_width(ui.spacing().menu_width); + ui.set_menu_state(Some(menu_state_arc.clone())); + ui.with_layout(Layout::top_down_justified(Align::LEFT), add_contents) + .inner + }) + .inner + }); - menu_state_arc.write().rect = frame.response.rect; + menu_state_arc.write().rect = area_response.response.rect; - frame.inner - }) + area_response } /// Build a top level menu with a button. @@ -249,11 +252,11 @@ impl MenuRootManager { /// Should be called from [`Context`] on a [`Response`] pub fn show( &mut self, - response: &Response, + button: &Response, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option> { if let Some(root) = self.inner.as_mut() { - let (menu_response, inner_response) = root.show(response, add_contents); + let (menu_response, inner_response) = root.show(button, add_contents); if MenuResponse::Close == menu_response { self.inner = None; } @@ -299,12 +302,12 @@ impl MenuRoot { pub fn show( &mut self, - response: &Response, + button: &Response, add_contents: impl FnOnce(&mut Ui) -> R, ) -> (MenuResponse, Option>) { - if self.id == response.id { + if self.id == button.id { let inner_response = - MenuState::show(&response.ctx, &self.menu_state, self.id, add_contents); + MenuState::show(&button.ctx, &self.menu_state, self.id, add_contents); let menu_state = self.menu_state.read(); if menu_state.response.is_close() { @@ -317,26 +320,31 @@ impl MenuRoot { /// Interaction with a stationary menu, i.e. fixed in another Ui. /// /// Responds to primary clicks. - fn stationary_interaction(response: &Response, root: &mut MenuRootManager) -> MenuResponse { - let id = response.id; + fn stationary_interaction(button: &Response, root: &mut MenuRootManager) -> MenuResponse { + let id = button.id; - if (response.clicked() && root.is_menu_open(id)) - || response.ctx.input(|i| i.key_pressed(Key::Escape)) + if (button.clicked() && root.is_menu_open(id)) + || button.ctx.input(|i| i.key_pressed(Key::Escape)) { // menu open and button clicked or esc pressed return MenuResponse::Close; - } else if (response.clicked() && !root.is_menu_open(id)) - || (response.hovered() && root.is_some()) + } else if (button.clicked() && !root.is_menu_open(id)) + || (button.hovered() && root.is_some()) { // menu not open and button clicked // or button hovered while other menu is open - let mut pos = response.rect.left_bottom(); + let mut pos = button.rect.left_bottom(); + + let menu_frame = Frame::menu(&button.ctx.style()); + pos.x -= menu_frame.total_margin().left; // Make fist button in menu align with the parent button + pos.y += button.ctx.style().spacing.menu_spacing; + if let Some(root) = root.inner.as_mut() { let menu_rect = root.menu_state.read().rect; - let screen_rect = response.ctx.input(|i| i.screen_rect); + let screen_rect = button.ctx.input(|i| i.screen_rect); if pos.y + menu_rect.height() > screen_rect.max.y { - pos.y = screen_rect.max.y - menu_rect.height() - response.rect.height(); + pos.y = screen_rect.max.y - menu_rect.height() - button.rect.height(); } if pos.x + menu_rect.width() > screen_rect.max.x { @@ -345,11 +353,11 @@ impl MenuRoot { } return MenuResponse::Create(pos, id); - } else if response + } else if button .ctx .input(|i| i.pointer.any_pressed() && i.pointer.primary_down()) { - if let Some(pos) = response.ctx.input(|i| i.pointer.interact_pos()) { + if let Some(pos) = button.ctx.input(|i| i.pointer.interact_pos()) { if let Some(root) = root.inner.as_mut() { if root.id == id { // pressed somewhere while this menu is open @@ -367,6 +375,9 @@ impl MenuRoot { /// Interaction with a context menu (secondary click). fn context_interaction(response: &Response, root: &mut Option) -> MenuResponse { let response = response.interact(Sense::click()); + let hovered = response.hovered(); + let secondary_clicked = response.secondary_clicked(); + response.ctx.input(|input| { let pointer = &input.pointer; if let Some(pos) = pointer.interact_pos() { @@ -377,9 +388,9 @@ impl MenuRoot { destroy = !in_old_menu && pointer.any_pressed() && root.id == response.id; } if !in_old_menu { - if response.hovered() && response.secondary_clicked() { + if hovered && secondary_clicked { return MenuResponse::Create(pos, response.id); - } else if (response.hovered() && pointer.primary_down()) || destroy { + } else if destroy || hovered && pointer.primary_down() { return MenuResponse::Close; } } @@ -405,8 +416,8 @@ impl MenuRoot { } // Responds to primary clicks. - pub fn stationary_click_interaction(response: &Response, root: &mut MenuRootManager) { - let menu_response = Self::stationary_interaction(response, root); + pub fn stationary_click_interaction(button: &Response, root: &mut MenuRootManager) { + let menu_response = Self::stationary_interaction(button, root); Self::handle_menu_response(root, menu_response); } } @@ -546,7 +557,8 @@ pub(crate) struct MenuState { /// The opened sub-menu and its [`Id`] sub_menu: Option<(Id, Arc>)>, - /// Bounding box of this menu (without the sub-menu) + /// Bounding box of this menu (without the sub-menu), + /// including the frame and everything. pub rect: Rect, /// Used to check if any menu in the tree wants to close @@ -613,30 +625,46 @@ impl MenuState { let pointer = ui.input(|i| i.pointer.clone()); let open = self.is_open(sub_id); if self.moving_towards_current_submenu(&pointer) { + // We don't close the submenu if the pointer is on its way to hover it. // ensure to repaint once even when pointer is not moving ui.ctx().request_repaint(); } else if !open && button.hovered() { - let pos = button.rect.right_top(); + // TODO(emilk): open menu to the left if there isn't enough space to the right + let mut pos = button.rect.right_top(); + pos.x = self.rect.right() + ui.spacing().menu_spacing; + pos.y -= Frame::menu(ui.style()).total_margin().top; // align the first button in the submenu with the parent button + self.open_submenu(sub_id, pos); + } else if open + && ui.interact_bg(Sense::hover()).contains_pointer() + && !button.hovered() + && !self.hovering_current_submenu(&pointer) + { + // We are hovering something else in the menu, so close the submenu. + self.close_submenu(); } } - /// Check if `dir` points from `pos` towards left side of `rect`. - fn points_at_left_of_rect(pos: Pos2, dir: Vec2, rect: Rect) -> bool { - let vel_a = dir.angle(); - let top_a = (rect.left_top() - pos).angle(); - let bottom_a = (rect.left_bottom() - pos).angle(); - bottom_a - vel_a >= 0.0 && top_a - vel_a <= 0.0 - } - /// Check if pointer is moving towards current submenu. fn moving_towards_current_submenu(&self, pointer: &PointerState) -> bool { if pointer.is_still() { return false; } + if let Some(sub_menu) = self.current_submenu() { if let Some(pos) = pointer.hover_pos() { - return Self::points_at_left_of_rect(pos, pointer.velocity(), sub_menu.read().rect); + let rect = sub_menu.read().rect; + return rect.intersects_ray(pos, pointer.velocity().normalized()); + } + } + false + } + + /// Check if pointer is hovering current submenu. + fn hovering_current_submenu(&self, pointer: &PointerState) -> bool { + if let Some(sub_menu) = self.current_submenu() { + if let Some(pos) = pointer.hover_pos() { + return sub_menu.read().area_contains(pos); } } false @@ -673,4 +701,8 @@ impl MenuState { self.sub_menu = Some((id, Arc::new(RwLock::new(Self::new(pos))))); } } + + fn close_submenu(&mut self) { + self.sub_menu = None; + } } diff --git a/crates/egui/src/os.rs b/crates/egui/src/os.rs index 93db910deb4..766ecf55ae6 100644 --- a/crates/egui/src/os.rs +++ b/crates/egui/src/os.rs @@ -5,19 +5,19 @@ pub enum OperatingSystem { /// Unknown OS - could be wasm Unknown, - /// Android OS. + /// Android OS Android, - /// Apple iPhone OS. + /// Apple iPhone OS IOS, - /// Linux or Unix other than Android. + /// Linux or Unix other than Android Nix, - /// MacOS. + /// macOS Mac, - /// Windows. + /// Windows Windows, } diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index 788319dc936..0935587cc7b 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -7,7 +7,7 @@ use crate::{ }; use epaint::{ text::{Fonts, Galley, LayoutJob}, - CircleShape, ClippedShape, RectShape, Rounding, Shape, Stroke, + CircleShape, ClippedShape, PathStroke, RectShape, Rounding, Shape, Stroke, }; /// Helper to paint shapes and text to a specific region on a specific layer. @@ -280,7 +280,7 @@ impl Painter { /// # Paint different primitives impl Painter { /// Paints a line from the first point to the second. - pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into) -> ShapeIdx { + pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into) -> ShapeIdx { self.add(Shape::LineSegment { points, stroke: stroke.into(), @@ -288,13 +288,13 @@ impl Painter { } /// Paints a horizontal line. - pub fn hline(&self, x: impl Into, y: f32, stroke: impl Into) -> ShapeIdx { - self.add(Shape::hline(x, y, stroke)) + pub fn hline(&self, x: impl Into, y: f32, stroke: impl Into) -> ShapeIdx { + self.add(Shape::hline(x, y, stroke.into())) } /// Paints a vertical line. - pub fn vline(&self, x: f32, y: impl Into, stroke: impl Into) -> ShapeIdx { - self.add(Shape::vline(x, y, stroke)) + pub fn vline(&self, x: f32, y: impl Into, stroke: impl Into) -> ShapeIdx { + self.add(Shape::vline(x, y, stroke.into())) } pub fn circle( @@ -513,7 +513,7 @@ impl Painter { } fn tint_shape_towards(shape: &mut Shape, target: Color32) { - epaint::shape_transform::adjust_colors(shape, &|color| { + epaint::shape_transform::adjust_colors(shape, move |color| { if *color != Color32::PLACEHOLDER { *color = crate::ecolor::tint_color_towards(*color, target); } @@ -521,7 +521,7 @@ fn tint_shape_towards(shape: &mut Shape, target: Color32) { } fn multiply_opacity(shape: &mut Shape, opacity: f32) { - epaint::shape_transform::adjust_colors(shape, &|color| { + epaint::shape_transform::adjust_colors(shape, move |color| { if *color != Color32::PLACEHOLDER { *color = color.gamma_multiply(opacity); } diff --git a/crates/egui/src/placer.rs b/crates/egui/src/placer.rs index 81c137f4e83..0be6b413b22 100644 --- a/crates/egui/src/placer.rs +++ b/crates/egui/src/placer.rs @@ -106,7 +106,7 @@ impl Placer { /// This is what you then pass to `advance_after_rects`. /// Use `justify_and_align` to get the inner `widget_rect`. pub(crate) fn next_space(&self, child_size: Vec2, item_spacing: Vec2) -> Rect { - egui_assert!(child_size.is_finite() && child_size.x >= 0.0 && child_size.y >= 0.0); + debug_assert!(child_size.is_finite() && child_size.x >= 0.0 && child_size.y >= 0.0); self.region.sanity_check(); if let Some(grid) = &self.grid { grid.next_cell(self.region.cursor, child_size) @@ -127,8 +127,8 @@ impl Placer { /// Apply justify or alignment after calling `next_space`. pub(crate) fn justify_and_align(&self, rect: Rect, child_size: Vec2) -> Rect { - crate::egui_assert!(!rect.any_nan()); - crate::egui_assert!(!child_size.any_nan()); + debug_assert!(!rect.any_nan()); + debug_assert!(!child_size.any_nan()); if let Some(grid) = &self.grid { grid.justify_and_align(rect, child_size) @@ -140,7 +140,7 @@ impl Placer { /// Advance the cursor by this many points. /// [`Self::min_rect`] will expand to contain the cursor. pub(crate) fn advance_cursor(&mut self, amount: f32) { - crate::egui_assert!( + debug_assert!( self.grid.is_none(), "You cannot advance the cursor when in a grid layout" ); @@ -158,8 +158,8 @@ impl Placer { widget_rect: Rect, item_spacing: Vec2, ) { - egui_assert!(!frame_rect.any_nan()); - egui_assert!(!widget_rect.any_nan()); + debug_assert!(!frame_rect.any_nan()); + debug_assert!(!widget_rect.any_nan()); self.region.sanity_check(); if let Some(grid) = &mut self.grid { @@ -248,6 +248,9 @@ impl Placer { /// Set the minimum width of the ui. /// This can't shrink the ui, only make it larger. pub(crate) fn set_min_width(&mut self, width: f32) { + if width <= 0.0 { + return; + } let rect = self.next_widget_space_ignore_wrap_justify(vec2(width, 0.0)); self.region.expand_to_include_x(rect.min.x); self.region.expand_to_include_x(rect.max.x); @@ -256,6 +259,9 @@ impl Placer { /// Set the minimum height of the ui. /// This can't shrink the ui, only make it larger. pub(crate) fn set_min_height(&mut self, height: f32) { + if height <= 0.0 { + return; + } let rect = self.next_widget_space_ignore_wrap_justify(vec2(0.0, height)); self.region.expand_to_include_y(rect.min.y); self.region.expand_to_include_y(rect.max.y); diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index a34ed8fa782..d980bd78eb6 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -3,7 +3,6 @@ use std::{any::Any, sync::Arc}; use crate::{ emath::{Align, Pos2, Rect, Vec2}, menu, Context, CursorIcon, Id, LayerId, PointerButton, Sense, Ui, WidgetRect, WidgetText, - NUM_POINTER_BUTTONS, }; // ---------------------------------------------------------------------------- @@ -15,7 +14,10 @@ use crate::{ /// /// Whenever something gets added to a [`Ui`], a [`Response`] object is returned. /// [`ui.add`] returns a [`Response`], as does [`ui.button`], and all similar shortcuts. -// TODO(emilk): we should be using bit sets instead of so many bools +/// +/// ⚠️ The `Response` contains a clone of [`Context`], and many methods lock the `Context`. +/// It can therefor be a deadlock to use `Context` from within a context-locking closures, +/// such as [`Context::input`]. #[derive(Clone, Debug)] pub struct Response { // CONTEXT: @@ -36,12 +38,16 @@ pub struct Response { /// /// This is sometimes smaller than [`Self::rect`] because of clipping /// (e.g. when inside a scroll area). - /// - /// The interact rect may also be slightly larger than the widget rect, - /// because egui adds half if the item spacing to make the interact rect easier to hit. pub interact_rect: Rect, /// The senses (click and/or drag) that the widget was interested in (if any). + /// + /// Note: if [`Self::enabled`] is `false`, then + /// the widget _effectively_ doesn't sense anything, + /// but can still have the same `Sense`. + /// This is because the sense informs the styling of the widget, + /// but we don't want to change the style when a widget is disabled + /// (that is handled by the `Painter` directly). pub sense: Sense, /// Was the widget enabled? @@ -50,7 +56,7 @@ pub struct Response { pub enabled: bool, // OUT: - /// The pointer is hovering above this widget. + /// The pointer is above this widget with no other blocking it. #[doc(hidden)] pub contains_pointer: bool, @@ -62,18 +68,27 @@ pub struct Response { #[doc(hidden)] pub highlighted: bool, - /// The pointer clicked this thing this frame. + /// This widget was clicked this frame. + /// + /// Which pointer and how many times we don't know, + /// and ask [`crate::InputState`] about at runtime. + /// + /// This is only set to true if the widget was clicked + /// by an actual mouse. #[doc(hidden)] - pub clicked: [bool; NUM_POINTER_BUTTONS], + pub clicked: bool, - // TODO(emilk): `released` for sliders - /// The thing was double-clicked. + /// This widget should act as if clicked due + /// to something else than a click. + /// + /// This is set to true if the widget has keyboard focus and + /// the user hit the Space or Enter key. #[doc(hidden)] - pub double_clicked: [bool; NUM_POINTER_BUTTONS], + pub fake_primary_click: bool, - /// The thing was triple-clicked. + /// This widget was long-pressed on a touch screen to simulate a secondary click. #[doc(hidden)] - pub triple_clicked: [bool; NUM_POINTER_BUTTONS], + pub long_touched: bool, /// The widget started being dragged this frame. #[doc(hidden)] @@ -111,58 +126,81 @@ impl Response { /// A click is registered when the mouse or touch is released within /// a certain amount of time and distance from when and where it was pressed. /// + /// This will also return true if the widget was clicked via accessibility integration, + /// or if the widget had keyboard focus and the use pressed Space/Enter. + /// /// Note that the widget must be sensing clicks with [`Sense::click`]. /// [`crate::Button`] senses clicks; [`crate::Label`] does not (unless you call [`crate::Label::sense`]). /// /// You can use [`Self::interact`] to sense more things *after* adding a widget. #[inline(always)] pub fn clicked(&self) -> bool { - self.clicked[PointerButton::Primary as usize] + self.fake_primary_click || self.clicked_by(PointerButton::Primary) } - /// Returns true if this widget was clicked this frame by the given button. + /// Returns true if this widget was clicked this frame by the given mouse button. + /// + /// This will NOT return true if the widget was "clicked" via + /// some accessibility integration, or if the widget had keyboard focus and the + /// user pressed Space/Enter. For that, use [`Self::clicked`] instead. + /// + /// This will likewise ignore the press-and-hold action on touch screens. + /// Use [`Self::secondary_clicked`] instead to also detect that. #[inline] pub fn clicked_by(&self, button: PointerButton) -> bool { - self.clicked[button as usize] + self.clicked && self.ctx.input(|i| i.pointer.button_clicked(button)) } /// Returns true if this widget was clicked this frame by the secondary mouse button (e.g. the right mouse button). + /// + /// This also returns true if the widget was pressed-and-held on a touch screen. #[inline] pub fn secondary_clicked(&self) -> bool { - self.clicked[PointerButton::Secondary as usize] + self.long_touched || self.clicked_by(PointerButton::Secondary) + } + + /// Was this long-pressed on a touch screen? + /// + /// Usually you want to check [`Self::secondary_clicked`] instead. + #[inline] + pub fn long_touched(&self) -> bool { + self.long_touched } /// Returns true if this widget was clicked this frame by the middle mouse button. #[inline] pub fn middle_clicked(&self) -> bool { - self.clicked[PointerButton::Middle as usize] + self.clicked_by(PointerButton::Middle) } /// Returns true if this widget was double-clicked this frame by the primary button. #[inline] pub fn double_clicked(&self) -> bool { - self.double_clicked[PointerButton::Primary as usize] + self.double_clicked_by(PointerButton::Primary) } /// Returns true if this widget was triple-clicked this frame by the primary button. #[inline] pub fn triple_clicked(&self) -> bool { - self.triple_clicked[PointerButton::Primary as usize] + self.triple_clicked_by(PointerButton::Primary) } /// Returns true if this widget was double-clicked this frame by the given button. #[inline] pub fn double_clicked_by(&self, button: PointerButton) -> bool { - self.double_clicked[button as usize] + self.clicked && self.ctx.input(|i| i.pointer.button_double_clicked(button)) } /// Returns true if this widget was triple-clicked this frame by the given button. #[inline] pub fn triple_clicked_by(&self, button: PointerButton) -> bool { - self.triple_clicked[button as usize] + self.clicked && self.ctx.input(|i| i.pointer.button_triple_clicked(button)) } - /// `true` if there was a click *outside* this widget this frame. + /// `true` if there was a click *outside* the rect of this widget. + /// + /// Clicks on widgets contained in this one counts as clicks inside this widget, + /// so that clicking a button in an area will not be considered as clicking "elsewhere" from the area. pub fn clicked_elsewhere(&self) -> bool { // We do not use self.clicked(), because we want to catch all clicks within our frame, // even if we aren't clickable (or even enabled). @@ -171,14 +209,10 @@ impl Response { let pointer = &i.pointer; if pointer.any_click() { - // We detect clicks/hover on a "interact_rect" that is slightly larger than - // self.rect. See Context::interact. - // This means we can be hovered and clicked even though `!self.rect.contains(pos)` is true, - // hence the extra complexity here. - if self.contains_pointer() { + if self.contains_pointer || self.hovered { false } else if let Some(pos) = pointer.interact_pos() { - !self.rect.contains(pos) + !self.interact_rect.contains(pos) } else { false // clicked without a pointer, weird } @@ -205,14 +239,13 @@ impl Response { self.hovered } - /// Returns true if the pointer is contained by the response rect. + /// Returns true if the pointer is contained by the response rect, and no other widget is covering it. /// /// In contrast to [`Self::hovered`], this can be `true` even if some other widget is being dragged. /// This means it is useful for styling things like drag-and-drop targets. /// `contains_pointer` can also be `true` for disabled widgets. /// - /// This is slightly different from [`Ui::rect_contains_pointer`] and [`Context::rect_contains_pointer`], - /// The rectangle used here is slightly larger, by half of the current item spacing. + /// This is slightly different from [`Ui::rect_contains_pointer`] and [`Context::rect_contains_pointer`], in that /// [`Self::contains_pointer`] also checks that no other widget is covering this response rectangle. #[inline(always)] pub fn contains_pointer(&self) -> bool { @@ -328,13 +361,13 @@ impl Response { /// The widget was being dragged, but now it has been released. #[inline] - #[deprecated = "Renamed 'dragged_stopped'"] + #[deprecated = "Renamed 'drag_stopped'"] pub fn drag_released(&self) -> bool { self.drag_stopped } /// The widget was being dragged by the button, but now it has been released. - #[deprecated = "Renamed 'dragged_stopped_by'"] + #[deprecated = "Renamed 'drag_stopped_by'"] pub fn drag_released_by(&self, button: PointerButton) -> bool { self.drag_stopped_by(button) } @@ -432,7 +465,7 @@ impl Response { let mut pos = self.ctx.input(|i| i.pointer.hover_pos())?; if let Some(transform) = self .ctx - .memory(|m| m.layer_transforms.get(&self.layer_id).cloned()) + .memory(|m| m.layer_transforms.get(&self.layer_id).copied()) { pos = transform * pos; } @@ -712,6 +745,7 @@ impl Response { /// Call after interacting and potential calls to [`Self::mark_changed`]. pub fn widget_info(&self, make_info: impl Fn() -> crate::WidgetInfo) { use crate::output::OutputEvent; + let event = if self.clicked() { Some(OutputEvent::Clicked(make_info())) } else if self.double_clicked() { @@ -725,6 +759,7 @@ impl Response { } else { None }; + if let Some(event) = event { self.output_event(event); } else { @@ -732,6 +767,8 @@ impl Response { self.ctx.accesskit_node_builder(self.id, |builder| { self.fill_accesskit_node_from_widget_info(builder, make_info()); }); + + self.ctx.register_widget_info(self.id, make_info); } } @@ -740,6 +777,10 @@ impl Response { self.ctx.accesskit_node_builder(self.id, |builder| { self.fill_accesskit_node_from_widget_info(builder, event.widget_info().clone()); }); + + self.ctx + .register_widget_info(self.id, || event.widget_info().clone()); + self.ctx.output_mut(|o| o.events.push(event)); } @@ -895,7 +936,7 @@ impl Response { /// You may not call [`Self::interact`] on the resulting `Response`. pub fn union(&self, other: Self) -> Self { assert!(self.ctx == other.ctx); - crate::egui_assert!( + debug_assert!( self.layer_id == other.layer_id, "It makes no sense to combine Responses from two different layers" ); @@ -910,27 +951,9 @@ impl Response { contains_pointer: self.contains_pointer || other.contains_pointer, hovered: self.hovered || other.hovered, highlighted: self.highlighted || other.highlighted, - clicked: [ - self.clicked[0] || other.clicked[0], - self.clicked[1] || other.clicked[1], - self.clicked[2] || other.clicked[2], - self.clicked[3] || other.clicked[3], - self.clicked[4] || other.clicked[4], - ], - double_clicked: [ - self.double_clicked[0] || other.double_clicked[0], - self.double_clicked[1] || other.double_clicked[1], - self.double_clicked[2] || other.double_clicked[2], - self.double_clicked[3] || other.double_clicked[3], - self.double_clicked[4] || other.double_clicked[4], - ], - triple_clicked: [ - self.triple_clicked[0] || other.triple_clicked[0], - self.triple_clicked[1] || other.triple_clicked[1], - self.triple_clicked[2] || other.triple_clicked[2], - self.triple_clicked[3] || other.triple_clicked[3], - self.triple_clicked[4] || other.triple_clicked[4], - ], + clicked: self.clicked || other.clicked, + fake_primary_click: self.fake_primary_click || other.fake_primary_click, + long_touched: self.long_touched || other.long_touched, drag_started: self.drag_started || other.drag_started, dragged: self.dragged || other.dragged, drag_stopped: self.drag_stopped || other.drag_stopped, diff --git a/crates/egui/src/sense.rs b/crates/egui/src/sense.rs index ca216896aee..1b6394b2cc3 100644 --- a/crates/egui/src/sense.rs +++ b/crates/egui/src/sense.rs @@ -1,5 +1,5 @@ /// What sort of interaction is a widget sensitive to? -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Eq, PartialEq)] // #[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct Sense { /// Buttons, sliders, windows, … @@ -15,6 +15,28 @@ pub struct Sense { pub focusable: bool, } +impl std::fmt::Debug for Sense { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self { + click, + drag, + focusable, + } = self; + + write!(f, "Sense {{")?; + if *click { + write!(f, " click")?; + } + if *drag { + write!(f, " drag")?; + } + if *focusable { + write!(f, " focusable")?; + } + write!(f, " }}") + } +} + impl Sense { /// Senses no clicks or drags. Only senses mouse hover. #[doc(alias = "none")] diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index a5b19e14d9d..d2d7ce99188 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -7,7 +7,8 @@ use std::collections::BTreeMap; use epaint::{Rounding, Shadow, Stroke}; use crate::{ - ecolor::*, emath::*, ComboBox, CursorIcon, FontFamily, FontId, Response, RichText, WidgetText, + ecolor::*, emath::*, ComboBox, CursorIcon, FontFamily, FontId, Grid, Margin, Response, + RichText, WidgetText, }; // ---------------------------------------------------------------------------- @@ -151,7 +152,7 @@ pub struct Style { /// /// The most convenient way to look something up in this is to use [`TextStyle::resolve`]. /// - /// If you would like to overwrite app text_styles + /// If you would like to overwrite app `text_styles` /// /// ``` /// # let mut ctx = egui::Context::default(); @@ -213,6 +214,9 @@ pub struct Style { /// This only affects a few egui widgets. pub explanation_tooltips: bool, + /// Show the URL of hyperlinks in a tooltip when hovered. + pub url_in_tooltip: bool, + /// If true and scrolling is enabled for only one direction, allow horizontal scrolling without pressing shift pub always_scroll_the_only_direction: bool, } @@ -281,7 +285,10 @@ pub struct Spacing { /// Default width of a [`Slider`]. pub slider_width: f32, - /// Default (minimum) width of a [`ComboBox`](crate::ComboBox). + /// Default rail height of a [`Slider`]. + pub slider_rail_height: f32, + + /// Default (minimum) width of a [`ComboBox`]. pub combo_width: f32, /// Default width of a [`TextEdit`]. @@ -305,6 +312,9 @@ pub struct Spacing { /// The default width of a menu. pub menu_width: f32, + /// Horizontal distance between a menu and a submenu. + pub menu_spacing: f32, + /// End indented regions with a horizontal line pub indent_ends_with_horizontal_line: bool, @@ -604,216 +614,6 @@ impl ScrollStyle { // ---------------------------------------------------------------------------- -#[derive(Clone, Copy, Debug, Default, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct Margin { - pub left: f32, - pub right: f32, - pub top: f32, - pub bottom: f32, -} - -impl Margin { - pub const ZERO: Self = Self { - left: 0.0, - right: 0.0, - top: 0.0, - bottom: 0.0, - }; - - #[inline] - pub const fn same(margin: f32) -> Self { - Self { - left: margin, - right: margin, - top: margin, - bottom: margin, - } - } - - /// Margins with the same size on opposing sides - #[inline] - pub const fn symmetric(x: f32, y: f32) -> Self { - Self { - left: x, - right: x, - top: y, - bottom: y, - } - } - - /// Total margins on both sides - #[inline] - pub fn sum(&self) -> Vec2 { - vec2(self.left + self.right, self.top + self.bottom) - } - - #[inline] - pub const fn left_top(&self) -> Vec2 { - vec2(self.left, self.top) - } - - #[inline] - pub const fn right_bottom(&self) -> Vec2 { - vec2(self.right, self.bottom) - } - - #[inline] - pub fn is_same(&self) -> bool { - self.left == self.right && self.left == self.top && self.left == self.bottom - } - - #[inline] - pub fn expand_rect(&self, rect: Rect) -> Rect { - Rect::from_min_max(rect.min - self.left_top(), rect.max + self.right_bottom()) - } - - #[inline] - pub fn shrink_rect(&self, rect: Rect) -> Rect { - Rect::from_min_max(rect.min + self.left_top(), rect.max - self.right_bottom()) - } -} - -impl From for Margin { - #[inline] - fn from(v: f32) -> Self { - Self::same(v) - } -} - -impl From for Margin { - #[inline] - fn from(v: Vec2) -> Self { - Self::symmetric(v.x, v.y) - } -} - -impl std::ops::Add for Margin { - type Output = Self; - - #[inline] - fn add(self, other: Self) -> Self { - Self { - left: self.left + other.left, - right: self.right + other.right, - top: self.top + other.top, - bottom: self.bottom + other.bottom, - } - } -} - -impl std::ops::Add for Margin { - type Output = Self; - - #[inline] - fn add(self, v: f32) -> Self { - Self { - left: self.left + v, - right: self.right + v, - top: self.top + v, - bottom: self.bottom + v, - } - } -} - -impl std::ops::AddAssign for Margin { - #[inline] - fn add_assign(&mut self, v: f32) { - self.left += v; - self.right += v; - self.top += v; - self.bottom += v; - } -} - -impl std::ops::Div for Margin { - type Output = Self; - - #[inline] - fn div(self, v: f32) -> Self { - Self { - left: self.left / v, - right: self.right / v, - top: self.top / v, - bottom: self.bottom / v, - } - } -} - -impl std::ops::DivAssign for Margin { - #[inline] - fn div_assign(&mut self, v: f32) { - self.left /= v; - self.right /= v; - self.top /= v; - self.bottom /= v; - } -} - -impl std::ops::Mul for Margin { - type Output = Self; - - #[inline] - fn mul(self, v: f32) -> Self { - Self { - left: self.left * v, - right: self.right * v, - top: self.top * v, - bottom: self.bottom * v, - } - } -} - -impl std::ops::MulAssign for Margin { - #[inline] - fn mul_assign(&mut self, v: f32) { - self.left *= v; - self.right *= v; - self.top *= v; - self.bottom *= v; - } -} - -impl std::ops::Sub for Margin { - type Output = Self; - - #[inline] - fn sub(self, other: Self) -> Self { - Self { - left: self.left - other.left, - right: self.right - other.right, - top: self.top - other.top, - bottom: self.bottom - other.bottom, - } - } -} - -impl std::ops::Sub for Margin { - type Output = Self; - - #[inline] - fn sub(self, v: f32) -> Self { - Self { - left: self.left - v, - right: self.right - v, - top: self.top - v, - bottom: self.bottom - v, - } - } -} - -impl std::ops::SubAssign for Margin { - #[inline] - fn sub_assign(&mut self, v: f32) { - self.left -= v; - self.right -= v; - self.top -= v; - self.bottom -= v; - } -} - -// ---------------------------------------------------------------------------- - /// How and when interaction happens. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -847,6 +647,39 @@ pub struct Interaction { pub multi_widget_text_select: bool, } +/// Look and feel of the text cursor. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +pub struct TextCursorStyle { + /// The color and width of the text cursor + pub stroke: Stroke, + + /// Show where the text cursor would be if you clicked? + pub preview: bool, + + /// Should the cursor blink? + pub blink: bool, + + /// When blinking, this is how long the cursor is visible. + pub on_duration: f32, + + /// When blinking, this is how long the cursor is invisible. + pub off_duration: f32, +} + +impl Default for TextCursorStyle { + fn default() -> Self { + Self { + stroke: Stroke::new(2.0, Color32::from_rgb(192, 222, 255)), // Dark mode + preview: false, + blink: true, + on_duration: 0.5, + off_duration: 0.5, + } + } +} + /// Controls the visual style (colors etc) of egui. /// /// You can change the visuals of a [`Ui`] with [`Ui::visuals_mut`] @@ -922,11 +755,8 @@ pub struct Visuals { pub resize_corner_size: f32, - /// The color and width of the text cursor - pub text_cursor: Stroke, - - /// show where the text cursor would be if you clicked - pub text_cursor_preview: bool, + /// How the text cursor acts. + pub text_cursor: TextCursorStyle, /// Allow child widgets to be just on the border and still have a stroke with some thickness pub clip_rect_margin: f32, @@ -1209,6 +1039,7 @@ impl Default for Style { #[cfg(debug_assertions)] debug: Default::default(), explanation_tooltips: false, + url_in_tooltip: false, always_scroll_the_only_direction: false, } } @@ -1224,6 +1055,7 @@ impl Default for Spacing { indent: 18.0, // match checkbox/radio-button with `button_padding.x + icon_width + icon_spacing` interact_size: vec2(40.0, 18.0), slider_width: 100.0, + slider_rail_height: 8.0, combo_width: 100.0, text_edit_width: 280.0, icon_width: 14.0, @@ -1231,6 +1063,7 @@ impl Default for Spacing { icon_spacing: 4.0, tooltip_width: 600.0, menu_width: 150.0, + menu_spacing: 2.0, combo_height: 200.0, scroll: Default::default(), indent_ends_with_horizontal_line: false, @@ -1268,7 +1101,12 @@ impl Visuals { error_fg_color: Color32::from_rgb(255, 0, 0), // red window_rounding: Rounding::same(6.0), - window_shadow: Shadow::big_dark(), + window_shadow: Shadow { + offset: vec2(10.0, 20.0), + blur: 15.0, + spread: 0.0, + color: Color32::from_black_alpha(96), + }, window_fill: Color32::from_gray(27), window_stroke: Stroke::new(1.0, Color32::from_gray(60)), window_highlight_topmost: true, @@ -1277,10 +1115,17 @@ impl Visuals { panel_fill: Color32::from_gray(27), - popup_shadow: Shadow::small_dark(), + popup_shadow: Shadow { + offset: vec2(6.0, 10.0), + blur: 8.0, + spread: 0.0, + color: Color32::from_black_alpha(96), + }, + resize_corner_size: 12.0, - text_cursor: Stroke::new(2.0, Color32::from_rgb(192, 222, 255)), - text_cursor_preview: false, + + text_cursor: Default::default(), + clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion button_frame: true, collapsing_header_frame: false, @@ -1312,14 +1157,29 @@ impl Visuals { warn_fg_color: Color32::from_rgb(255, 100, 0), // slightly orange red. it's difficult to find a warning color that pops on bright background. error_fg_color: Color32::from_rgb(255, 0, 0), // red - window_shadow: Shadow::big_light(), + window_shadow: Shadow { + offset: vec2(10.0, 20.0), + blur: 15.0, + spread: 0.0, + color: Color32::from_black_alpha(25), + }, window_fill: Color32::from_gray(248), window_stroke: Stroke::new(1.0, Color32::from_gray(190)), panel_fill: Color32::from_gray(248), - popup_shadow: Shadow::small_light(), - text_cursor: Stroke::new(2.0, Color32::from_rgb(0, 83, 125)), + popup_shadow: Shadow { + offset: vec2(6.0, 10.0), + blur: 8.0, + spread: 0.0, + color: Color32::from_black_alpha(25), + }, + + text_cursor: TextCursorStyle { + stroke: Stroke::new(2.0, Color32::from_rgb(0, 83, 125)), + ..Default::default() + }, + ..Self::dark() } } @@ -1470,25 +1330,28 @@ impl Style { #[cfg(debug_assertions)] debug, explanation_tooltips, + url_in_tooltip, always_scroll_the_only_direction, } = self; visuals.light_dark_radio_buttons(ui); crate::Grid::new("_options").show(ui, |ui| { - ui.label("Override font id:"); - ui.horizontal(|ui| { - ui.radio_value(override_font_id, None, "None"); - if ui.radio(override_font_id.is_some(), "override").clicked() { - *override_font_id = Some(FontId::default()); - } + ui.label("Override font id"); + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.radio_value(override_font_id, None, "None"); + if ui.radio(override_font_id.is_some(), "override").clicked() { + *override_font_id = Some(FontId::default()); + } + }); if let Some(override_font_id) = override_font_id { crate::introspection::font_id_ui(ui, override_font_id); } }); ui.end_row(); - ui.label("Override text style:"); + ui.label("Override text style"); crate::ComboBox::from_id_source("Override text style") .selected_text(match override_text_style { None => "None".to_owned(), @@ -1505,7 +1368,7 @@ impl Style { }); ui.end_row(); - ui.label("Text style of DragValue:"); + ui.label("Text style of DragValue"); crate::ComboBox::from_id_source("drag_value_text_style") .selected_text(drag_value_text_style.to_string()) .show_ui(ui, |ui| { @@ -1518,10 +1381,11 @@ impl Style { }); ui.end_row(); - ui.label("Animation duration:"); + ui.label("Animation duration"); ui.add( - Slider::new(animation_time, 0.0..=1.0) - .clamp_to_range(true) + DragValue::new(animation_time) + .clamp_range(0.0..=1.0) + .speed(0.02) .suffix(" s"), ); ui.end_row(); @@ -1540,12 +1404,14 @@ impl Style { "Show explanatory text when hovering DragValue:s and other egui widgets", ); + ui.checkbox(url_in_tooltip, "Show url when hovering links"); + ui.checkbox(always_scroll_the_only_direction, "Always scroll the only enabled direction") .on_hover_text( "If scrolling is enabled for only one direction, allow horizontal scrolling without pressing shift", ); - ui.vertical_centered(|ui| reset_button(ui, self)); + ui.vertical_centered(|ui| reset_button(ui, self, "Reset style")); } } @@ -1558,7 +1424,7 @@ fn text_styles_ui(ui: &mut Ui, text_styles: &mut BTreeMap) -> ui.end_row(); } }); - crate::reset_button_with(ui, text_styles, default_text_styles()); + crate::reset_button_with(ui, text_styles, "Reset text styles", default_text_styles()); }) .response } @@ -1573,6 +1439,7 @@ impl Spacing { indent, interact_size, slider_width, + slider_rail_height, combo_width, text_edit_width, icon_width, @@ -1580,68 +1447,91 @@ impl Spacing { icon_spacing, tooltip_width, menu_width, + menu_spacing, indent_ends_with_horizontal_line, combo_height, scroll, } = self; - ui.add(slider_vec2(item_spacing, 0.0..=20.0, "Item spacing")); + Grid::new("spacing") + .num_columns(2) + .spacing([12.0, 8.0]) + .striped(true) + .show(ui, |ui| { + ui.label("Item spacing"); + ui.add(two_drag_values(item_spacing, 0.0..=20.0)); + ui.end_row(); - margin_ui(ui, "Window margin:", window_margin); - margin_ui(ui, "Menu margin:", menu_margin); + ui.label("Window margin"); + ui.add(window_margin); + ui.end_row(); - ui.add(slider_vec2(button_padding, 0.0..=20.0, "Button padding")); - ui.add(slider_vec2(interact_size, 4.0..=60.0, "Interact size")) - .on_hover_text("Minimum size of an interactive widget"); - ui.horizontal(|ui| { - ui.add(DragValue::new(indent).clamp_range(0.0..=100.0)); - ui.label("Indent"); - }); - ui.horizontal(|ui| { - ui.add(DragValue::new(slider_width).clamp_range(0.0..=1000.0)); - ui.label("Slider width"); - }); - ui.horizontal(|ui| { - ui.add(DragValue::new(combo_width).clamp_range(0.0..=1000.0)); - ui.label("ComboBox width"); - }); - ui.horizontal(|ui| { - ui.add(DragValue::new(text_edit_width).clamp_range(0.0..=1000.0)); - ui.label("TextEdit width"); - }); + ui.label("Menu margin"); + ui.add(menu_margin); + ui.end_row(); - ui.collapsing("Scroll Area", |ui| { - scroll.ui(ui); - }); + ui.label("Button padding"); + ui.add(two_drag_values(button_padding, 0.0..=20.0)); + ui.end_row(); - ui.horizontal(|ui| { - ui.label("Checkboxes etc:"); - ui.add( - DragValue::new(icon_width) - .prefix("outer icon width:") - .clamp_range(0.0..=60.0), - ); - ui.add( - DragValue::new(icon_width_inner) - .prefix("inner icon width:") - .clamp_range(0.0..=60.0), - ); - ui.add( - DragValue::new(icon_spacing) - .prefix("spacing:") - .clamp_range(0.0..=10.0), - ); - }); + ui.label("Interact size") + .on_hover_text("Minimum size of an interactive widget"); + ui.add(two_drag_values(interact_size, 4.0..=60.0)); + ui.end_row(); - ui.horizontal(|ui| { - ui.add(DragValue::new(tooltip_width).clamp_range(0.0..=1000.0)); - ui.label("Tooltip wrap width"); - }); + ui.label("Indent"); + ui.add(DragValue::new(indent).clamp_range(0.0..=100.0)); + ui.end_row(); - ui.horizontal(|ui| { - ui.add(DragValue::new(menu_width).clamp_range(0.0..=1000.0)); - ui.label("Default width of a menu"); - }); + ui.label("Slider width"); + ui.add(DragValue::new(slider_width).clamp_range(0.0..=1000.0)); + ui.end_row(); + + ui.label("Slider rail height"); + ui.add(DragValue::new(slider_rail_height).clamp_range(0.0..=50.0)); + ui.end_row(); + + ui.label("ComboBox width"); + ui.add(DragValue::new(combo_width).clamp_range(0.0..=1000.0)); + ui.end_row(); + + ui.label("TextEdit width"); + ui.add(DragValue::new(text_edit_width).clamp_range(0.0..=1000.0)); + ui.end_row(); + + ui.label("Tooltip wrap width"); + ui.add(DragValue::new(tooltip_width).clamp_range(0.0..=1000.0)); + ui.end_row(); + + ui.label("Default menu width"); + ui.add(DragValue::new(menu_width).clamp_range(0.0..=1000.0)); + ui.end_row(); + + ui.label("Menu spacing") + .on_hover_text("Horizontal spacing between menus"); + ui.add(DragValue::new(menu_spacing).clamp_range(0.0..=10.0)); + ui.end_row(); + + ui.label("Checkboxes etc"); + ui.vertical(|ui| { + ui.add( + DragValue::new(icon_width) + .prefix("outer icon width:") + .clamp_range(0.0..=60.0), + ); + ui.add( + DragValue::new(icon_width_inner) + .prefix("inner icon width:") + .clamp_range(0.0..=60.0), + ); + ui.add( + DragValue::new(icon_spacing) + .prefix("spacing:") + .clamp_range(0.0..=10.0), + ); + }); + ui.end_row(); + }); ui.checkbox( indent_ends_with_horizontal_line, @@ -1653,57 +1543,12 @@ impl Spacing { ui.add(DragValue::new(combo_height).clamp_range(0.0..=1000.0)); }); - ui.vertical_centered(|ui| reset_button(ui, self)); - } -} - -fn margin_ui(ui: &mut Ui, text: &str, margin: &mut Margin) { - let margin_range = 0.0..=20.0; - - ui.horizontal(|ui| { - ui.label(text); - - let mut same = margin.is_same(); - ui.checkbox(&mut same, "Same"); - - if same { - let mut value = margin.left; - ui.add(DragValue::new(&mut value).clamp_range(margin_range.clone())); - *margin = Margin::same(value); - } else { - if margin.is_same() { - // HACK: prevent collapse: - margin.right = margin.left + 1.0; - margin.bottom = margin.left + 2.0; - margin.top = margin.left + 3.0; - } + ui.collapsing("Scroll Area", |ui| { + scroll.ui(ui); + }); - ui.add( - DragValue::new(&mut margin.left) - .clamp_range(margin_range.clone()) - .prefix("L: "), - ) - .on_hover_text("Left margin"); - ui.add( - DragValue::new(&mut margin.right) - .clamp_range(margin_range.clone()) - .prefix("R: "), - ) - .on_hover_text("Right margin"); - ui.add( - DragValue::new(&mut margin.top) - .clamp_range(margin_range.clone()) - .prefix("T: "), - ) - .on_hover_text("Top margin"); - ui.add( - DragValue::new(&mut margin.bottom) - .clamp_range(margin_range) - .prefix("B: "), - ) - .on_hover_text("Bottom margin"); - } - }); + ui.vertical_centered(|ui| reset_button(ui, self, "Reset spacing")); + } } impl Interaction { @@ -1717,21 +1562,42 @@ impl Interaction { selectable_labels, multi_widget_text_select, } = self; - ui.add(Slider::new(interact_radius, 0.0..=20.0).text("interact_radius")) - .on_hover_text("Interact with the closest widget within this radius."); - ui.add(Slider::new(resize_grab_radius_side, 0.0..=20.0).text("resize_grab_radius_side")); - ui.add( - Slider::new(resize_grab_radius_corner, 0.0..=20.0).text("resize_grab_radius_corner"), - ); + + ui.spacing_mut().item_spacing = vec2(12.0, 8.0); + + Grid::new("interaction") + .num_columns(2) + .striped(true) + .show(ui, |ui| { + ui.label("interact_radius") + .on_hover_text("Interact with the closest widget within this radius."); + ui.add(DragValue::new(interact_radius).clamp_range(0.0..=20.0)); + ui.end_row(); + + ui.label("resize_grab_radius_side").on_hover_text("Radius of the interactive area of the side of a window during drag-to-resize"); + ui.add(DragValue::new(resize_grab_radius_side).clamp_range(0.0..=20.0)); + ui.end_row(); + + ui.label("resize_grab_radius_corner").on_hover_text("Radius of the interactive area of the corner of a window during drag-to-resize."); + ui.add(DragValue::new(resize_grab_radius_corner).clamp_range(0.0..=20.0)); + ui.end_row(); + + ui.label("Tooltip delay").on_hover_text( + "Delay in seconds before showing tooltips after the mouse stops moving", + ); + ui.add( + DragValue::new(tooltip_delay) + .clamp_range(0.0..=1.0) + .speed(0.05) + .suffix(" s"), + ); + ui.end_row(); + }); + ui.checkbox( show_tooltips_only_when_still, "Only show tooltips if mouse is still", ); - ui.add( - Slider::new(tooltip_delay, 0.0..=1.0) - .suffix(" s") - .text("tooltip_delay"), - ); ui.horizontal(|ui| { ui.checkbox(selectable_labels, "Selectable text in labels"); @@ -1740,7 +1606,7 @@ impl Interaction { } }); - ui.vertical_centered(|ui| reset_button(ui, self)); + ui.vertical_centered(|ui| reset_button(ui, self, "Reset interaction settings")); } } @@ -1785,8 +1651,16 @@ impl Selection { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { bg_fill, stroke } = self; ui.label("Selectable labels"); - ui_color(ui, bg_fill, "background fill"); - stroke_ui(ui, stroke, "stroke"); + + Grid::new("selectiom").num_columns(2).show(ui, |ui| { + ui.label("Background fill"); + ui.color_edit_button_srgba(bg_fill); + ui.end_row(); + + ui.label("Stroke"); + ui.add(stroke); + ui.end_row(); + }); } } @@ -1800,17 +1674,39 @@ impl WidgetVisuals { fg_stroke, expansion, } = self; - ui_color(ui, weak_bg_fill, "optional background fill") - .on_hover_text("For buttons, combo-boxes, etc"); - ui_color(ui, mandatory_bg_fill, "mandatory background fill") - .on_hover_text("For checkboxes, sliders, etc"); - stroke_ui(ui, bg_stroke, "background stroke"); - rounding_ui(ui, rounding); + Grid::new("widget") + .num_columns(2) + .spacing([12.0, 8.0]) + .striped(true) + .show(ui, |ui| { + ui.label("Optional background fill") + .on_hover_text("For buttons, combo-boxes, etc"); + ui.color_edit_button_srgba(weak_bg_fill); + ui.end_row(); - stroke_ui(ui, fg_stroke, "foreground stroke (text)"); - ui.add(Slider::new(expansion, -5.0..=5.0).text("expansion")) - .on_hover_text("make shapes this much larger"); + ui.label("Mandatory background fill") + .on_hover_text("For checkboxes, sliders, etc"); + ui.color_edit_button_srgba(mandatory_bg_fill); + ui.end_row(); + + ui.label("Background stroke"); + ui.add(bg_stroke); + ui.end_row(); + + ui.label("Rounding"); + ui.add(rounding); + ui.end_row(); + + ui.label("Foreground stroke (text)"); + ui.add(fg_stroke); + ui.end_row(); + + ui.label("Expansion") + .on_hover_text("make shapes this much larger"); + ui.add(DragValue::new(expansion).speed(0.1)); + ui.end_row(); + }); } } @@ -1873,8 +1769,9 @@ impl Visuals { popup_shadow, resize_corner_size, + text_cursor, - text_cursor_preview, + clip_rect_margin, button_frame, collapsing_header_frame, @@ -1902,80 +1799,167 @@ impl Visuals { .on_hover_text("Background of plots and paintings"); }); + ui.collapsing("Text color", |ui| { + ui_text_color(ui, &mut widgets.noninteractive.fg_stroke.color, "Label"); + ui_text_color( + ui, + &mut widgets.inactive.fg_stroke.color, + "Unhovered button", + ); + ui_text_color(ui, &mut widgets.hovered.fg_stroke.color, "Hovered button"); + ui_text_color(ui, &mut widgets.active.fg_stroke.color, "Clicked button"); + + ui_text_color(ui, warn_fg_color, RichText::new("Warnings")); + ui_text_color(ui, error_fg_color, RichText::new("Errors")); + + ui_text_color(ui, hyperlink_color, "hyperlink_color"); + + ui_color(ui, code_bg_color, RichText::new("Code background").code()).on_hover_ui( + |ui| { + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("For monospaced inlined text "); + ui.code("like this"); + ui.label("."); + }); + }, + ); + }); + + ui.collapsing("Text cursor", |ui| { + text_cursor.ui(ui); + }); + ui.collapsing("Window", |ui| { - ui_color(ui, window_fill, "Fill"); - stroke_ui(ui, window_stroke, "Outline"); - rounding_ui(ui, window_rounding); - shadow_ui(ui, window_shadow, "Shadow"); + Grid::new("window") + .num_columns(2) + .spacing([12.0, 8.0]) + .striped(true) + .show(ui, |ui| { + ui.label("Fill"); + ui.color_edit_button_srgba(window_fill); + ui.end_row(); + + ui.label("Stroke"); + ui.add(window_stroke); + ui.end_row(); + + ui.label("Rounding"); + ui.add(window_rounding); + ui.end_row(); + + ui.label("Shadow"); + ui.add(window_shadow); + ui.end_row(); + }); + ui.checkbox(window_highlight_topmost, "Highlight topmost Window"); }); ui.collapsing("Menus and popups", |ui| { - rounding_ui(ui, menu_rounding); - shadow_ui(ui, popup_shadow, "Shadow"); + Grid::new("menus_and_popups") + .num_columns(2) + .spacing([12.0, 8.0]) + .striped(true) + .show(ui, |ui| { + ui.label("Rounding"); + ui.add(menu_rounding); + ui.end_row(); + + ui.label("Shadow"); + ui.add(popup_shadow); + ui.end_row(); + }); }); ui.collapsing("Widgets", |ui| widgets.ui(ui)); ui.collapsing("Selection", |ui| selection.ui(ui)); - ui.horizontal(|ui| { - ui_color( - ui, - &mut widgets.noninteractive.fg_stroke.color, - "Text color", - ); - ui_color(ui, warn_fg_color, RichText::new("Warnings")); - ui_color(ui, error_fg_color, RichText::new("Errors")); - }); - - ui_color(ui, code_bg_color, RichText::new("Code background").code()).on_hover_ui(|ui| { - ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 0.0; - ui.label("For monospaced inlined text "); - ui.code("like this"); - ui.label("."); - }); - }); + ui.collapsing("Misc", |ui| { + ui.add(Slider::new(resize_corner_size, 0.0..=20.0).text("resize_corner_size")); + ui.add(Slider::new(clip_rect_margin, 0.0..=20.0).text("clip_rect_margin")); - ui_color(ui, hyperlink_color, "hyperlink_color"); - stroke_ui(ui, text_cursor, "Text Cursor"); + ui.checkbox(button_frame, "Button has a frame"); + ui.checkbox(collapsing_header_frame, "Collapsing header has a frame"); + ui.checkbox( + indent_has_left_vline, + "Paint a vertical line to the left of indented regions", + ); - ui.add(Slider::new(resize_corner_size, 0.0..=20.0).text("resize_corner_size")); - ui.checkbox(text_cursor_preview, "Preview text cursor on hover"); - ui.add(Slider::new(clip_rect_margin, 0.0..=20.0).text("clip_rect_margin")); + ui.checkbox(striped, "Default stripes on grids and tables"); - ui.checkbox(button_frame, "Button has a frame"); - ui.checkbox(collapsing_header_frame, "Collapsing header has a frame"); - ui.checkbox( - indent_has_left_vline, - "Paint a vertical line to the left of indented regions", - ); + ui.checkbox(slider_trailing_fill, "Add trailing color to sliders"); - ui.checkbox(striped, "By default, add stripes to grids and tables?"); + handle_shape.ui(ui); - ui.checkbox(slider_trailing_fill, "Add trailing color to sliders"); + ComboBox::from_label("Interact cursor") + .selected_text( + interact_cursor.map_or_else(|| "-".to_owned(), |cursor| format!("{cursor:?}")), + ) + .show_ui(ui, |ui| { + ui.selectable_value(interact_cursor, None, "-"); - handle_shape.ui(ui); + for cursor in CursorIcon::ALL { + ui.selectable_value(interact_cursor, Some(cursor), format!("{cursor:?}")) + .on_hover_cursor(cursor); + } + }) + .response + .on_hover_text("Use this cursor when hovering buttons etc"); - ComboBox::from_label("Interact Cursor") - .selected_text(format!("{interact_cursor:?}")) - .show_ui(ui, |ui| { - ui.selectable_value(interact_cursor, None, "None"); + ui.checkbox(image_loading_spinners, "Image loading spinners") + .on_hover_text("Show a spinner when an Image is loading"); - for icon in CursorIcon::ALL { - ui.selectable_value(interact_cursor, Some(icon), format!("{icon:?}")); - } + ui.horizontal(|ui| { + ui.label("Color picker type"); + numeric_color_space.toggle_button_ui(ui); }); + }); - ui.checkbox(image_loading_spinners, "Image loading spinners") - .on_hover_text("Show a spinner when an Image is loading"); + ui.vertical_centered(|ui| reset_button(ui, self, "Reset visuals")); + } +} + +impl TextCursorStyle { + fn ui(&mut self, ui: &mut Ui) { + let Self { + stroke, + preview, + blink, + on_duration, + off_duration, + } = self; ui.horizontal(|ui| { - ui.label("Color picker type:"); - numeric_color_space.toggle_button_ui(ui); + ui.label("Stroke"); + ui.add(stroke); }); - ui.vertical_centered(|ui| reset_button(ui, self)); + ui.checkbox(preview, "Preview text cursor on hover"); + + ui.checkbox(blink, "Blink"); + + if *blink { + Grid::new("cursor_blink").show(ui, |ui| { + ui.label("On time"); + ui.add( + DragValue::new(on_duration) + .speed(0.1) + .clamp_range(0.0..=2.0) + .suffix(" s"), + ); + ui.end_row(); + + ui.label("Off time"); + ui.add( + DragValue::new(off_duration) + .speed(0.1) + .clamp_range(0.0..=2.0) + .suffix(" s"), + ); + ui.end_row(); + }); + } } } @@ -2020,16 +2004,12 @@ impl DebugOptions { ui.checkbox(show_widget_hits, "Show widgets under mouse pointer"); - ui.vertical_centered(|ui| reset_button(ui, self)); + ui.vertical_centered(|ui| reset_button(ui, self, "Reset debug options")); } } -// TODO(emilk): improve and standardize `slider_vec2` -fn slider_vec2<'a>( - value: &'a mut Vec2, - range: std::ops::RangeInclusive, - text: &'a str, -) -> impl Widget + 'a { +// TODO(emilk): improve and standardize +fn two_drag_values(value: &mut Vec2, range: std::ops::RangeInclusive) -> impl Widget + '_ { move |ui: &mut crate::Ui| { ui.horizontal(|ui| { ui.add( @@ -2042,50 +2022,31 @@ fn slider_vec2<'a>( .clamp_range(range.clone()) .prefix("y: "), ); - ui.label(text); }) .response } } -fn ui_color(ui: &mut Ui, srgba: &mut Color32, label: impl Into) -> Response { +fn ui_color(ui: &mut Ui, color: &mut Color32, label: impl Into) -> Response { ui.horizontal(|ui| { - ui.color_edit_button_srgba(srgba); + ui.color_edit_button_srgba(color); ui.label(label); }) .response } -fn rounding_ui(ui: &mut Ui, rounding: &mut Rounding) { - const MAX: f32 = 20.0; - let mut same = rounding.is_same(); - ui.group(|ui| { - ui.horizontal(|ui| { - ui.label("Rounding: "); - ui.radio_value(&mut same, true, "Same"); - ui.radio_value(&mut same, false, "Separate"); - }); - - if same { - let mut cr = rounding.nw; - ui.add(Slider::new(&mut cr, 0.0..=MAX)); - *rounding = Rounding::same(cr); - } else { - ui.add(Slider::new(&mut rounding.nw, 0.0..=MAX).text("North-West")); - ui.add(Slider::new(&mut rounding.ne, 0.0..=MAX).text("North-East")); - ui.add(Slider::new(&mut rounding.sw, 0.0..=MAX).text("South-West")); - ui.add(Slider::new(&mut rounding.se, 0.0..=MAX).text("South-East")); - if rounding.is_same() { - rounding.se *= 1.00001; - } - } - }); +fn ui_text_color(ui: &mut Ui, color: &mut Color32, label: impl Into) -> Response { + ui.horizontal(|ui| { + ui.color_edit_button_srgba(color); + ui.label(label.into().color(*color)); + }) + .response } impl HandleShape { pub fn ui(&mut self, ui: &mut Ui) { - ui.label("Widget handle shape"); ui.horizontal(|ui| { + ui.label("Slider handle"); ui.radio_value(self, Self::Circle, "Circle"); if ui .radio(matches!(self, Self::Rect { .. }), "Rectangle") @@ -2141,3 +2102,213 @@ impl std::fmt::Display for NumericColorSpace { } } } + +impl Widget for &mut Margin { + fn ui(self, ui: &mut Ui) -> Response { + let mut same = self.is_same(); + + let response = if same { + ui.horizontal(|ui| { + ui.checkbox(&mut same, "same"); + + let mut value = self.left; + ui.add(DragValue::new(&mut value)); + *self = Margin::same(value); + }) + .response + } else { + ui.vertical(|ui| { + ui.checkbox(&mut same, "same"); + + crate::Grid::new("margin").num_columns(2).show(ui, |ui| { + ui.label("Left"); + ui.add(DragValue::new(&mut self.left)); + ui.end_row(); + + ui.label("Right"); + ui.add(DragValue::new(&mut self.right)); + ui.end_row(); + + ui.label("Top"); + ui.add(DragValue::new(&mut self.top)); + ui.end_row(); + + ui.label("Bottom"); + ui.add(DragValue::new(&mut self.bottom)); + ui.end_row(); + }); + }) + .response + }; + + // Apply the checkbox: + if same { + *self = Margin::same((self.left + self.right + self.top + self.bottom) / 4.0); + } else if self.is_same() { + self.right *= 1.00001; // prevent collapsing into sameness + } + + response + } +} + +impl Widget for &mut Rounding { + fn ui(self, ui: &mut Ui) -> Response { + let mut same = self.is_same(); + + let response = if same { + ui.horizontal(|ui| { + ui.checkbox(&mut same, "same"); + + let mut cr = self.nw; + ui.add(DragValue::new(&mut cr).clamp_range(0.0..=f32::INFINITY)); + *self = Rounding::same(cr); + }) + .response + } else { + ui.vertical(|ui| { + ui.checkbox(&mut same, "same"); + + crate::Grid::new("rounding").num_columns(2).show(ui, |ui| { + ui.label("NW"); + ui.add(DragValue::new(&mut self.nw).clamp_range(0.0..=f32::INFINITY)); + ui.end_row(); + + ui.label("NE"); + ui.add(DragValue::new(&mut self.ne).clamp_range(0.0..=f32::INFINITY)); + ui.end_row(); + + ui.label("SW"); + ui.add(DragValue::new(&mut self.sw).clamp_range(0.0..=f32::INFINITY)); + ui.end_row(); + + ui.label("SE"); + ui.add(DragValue::new(&mut self.se).clamp_range(0.0..=f32::INFINITY)); + ui.end_row(); + }); + }) + .response + }; + + // Apply the checkbox: + if same { + *self = Rounding::same((self.nw + self.ne + self.sw + self.se) / 4.0); + } else if self.is_same() { + self.se *= 1.00001; // prevent collapsing into sameness + } + + response + } +} + +impl Widget for &mut Shadow { + fn ui(self, ui: &mut Ui) -> Response { + let epaint::Shadow { + offset, + blur, + spread, + color, + } = self; + + ui.vertical(|ui| { + crate::Grid::new("shadow_ui").show(ui, |ui| { + ui.add( + DragValue::new(&mut offset.x) + .speed(1.0) + .clamp_range(-100.0..=100.0) + .prefix("x: "), + ); + ui.add( + DragValue::new(&mut offset.y) + .speed(1.0) + .clamp_range(-100.0..=100.0) + .prefix("y: "), + ); + ui.end_row(); + + ui.add( + DragValue::new(blur) + .speed(1.0) + .clamp_range(0.0..=100.0) + .prefix("blur: "), + ); + + ui.add( + DragValue::new(spread) + .speed(1.0) + .clamp_range(0.0..=100.0) + .prefix("spread: "), + ); + }); + ui.color_edit_button_srgba(color); + }) + .response + } +} + +impl Widget for &mut Stroke { + fn ui(self, ui: &mut Ui) -> Response { + let Stroke { width, color } = self; + + ui.horizontal(|ui| { + ui.add( + DragValue::new(width) + .speed(0.1) + .clamp_range(0.0..=f32::INFINITY), + ) + .on_hover_text("Width"); + ui.color_edit_button_srgba(color); + + // stroke preview: + let (_id, stroke_rect) = ui.allocate_space(ui.spacing().interact_size); + let left = stroke_rect.left_center(); + let right = stroke_rect.right_center(); + ui.painter().line_segment([left, right], (*width, *color)); + }) + .response + } +} + +impl Widget for &mut crate::Frame { + fn ui(self, ui: &mut Ui) -> Response { + let crate::Frame { + inner_margin, + outer_margin, + rounding, + shadow, + fill, + stroke, + } = self; + + crate::Grid::new("frame") + .num_columns(2) + .spacing([12.0, 8.0]) + .striped(true) + .show(ui, |ui| { + ui.label("Inner margin"); + ui.add(inner_margin); + ui.end_row(); + + ui.label("Outer margin"); + ui.add(outer_margin); + ui.end_row(); + + ui.label("Rounding"); + ui.add(rounding); + ui.end_row(); + + ui.label("Shadow"); + ui.add(shadow); + ui.end_row(); + + ui.label("Fill"); + ui.color_edit_button_srgba(fill); + ui.end_row(); + + ui.label("Stroke"); + ui.add(stroke); + ui.end_row(); + }) + .response + } +} diff --git a/crates/egui/src/text_selection/cursor_range.rs b/crates/egui/src/text_selection/cursor_range.rs index cebcf3fb56d..cf17a2e9ba1 100644 --- a/crates/egui/src/text_selection/cursor_range.rs +++ b/crates/egui/src/text_selection/cursor_range.rs @@ -63,7 +63,7 @@ impl CursorRange { self.primary.ccursor == self.secondary.ccursor } - /// Is `self` a super-set of the the other range? + /// Is `self` a super-set of the other range? pub fn contains(&self, other: &Self) -> bool { let [self_min, self_max] = self.sorted_cursors(); let [other_min, other_max] = other.sorted_cursors(); diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index af9e3039eeb..ec5a4729eee 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -8,7 +8,7 @@ use super::{ }; /// Turn on to help debug this -const DEBUG: bool = false; // TODO: don't merge this while `true` +const DEBUG: bool = false; // Don't merge `true`! fn paint_selection( ui: &Ui, @@ -169,7 +169,7 @@ impl LabelSelectionState { if ctx.input(|i| i.pointer.any_pressed() && !i.modifiers.shift) { // Maybe a new selection is about to begin, but the old one is over: - // state.selection = None; // TODO: this makes sense, but doesn't work as expected. + // state.selection = None; // TODO(emilk): this makes sense, but doesn't work as expected. } state.selection_bbox_last_frame = state.selection_bbox_this_frame; @@ -562,7 +562,7 @@ impl LabelSelectionState { old.widget_id != new_primary.widget_id || old.ccursor != new_primary.ccursor }); if primary_changed && new_primary.widget_id == widget_id { - let is_fully_visible = ui.clip_rect().contains_rect(response.rect); // TODO: remove this HACK workaround for https://github.com/emilk/egui/issues/1531 + let is_fully_visible = ui.clip_rect().contains_rect(response.rect); // TODO(emilk): remove this HACK workaround for https://github.com/emilk/egui/issues/1531 if selection_changed && !is_fully_visible { // Scroll to keep primary cursor in view: let row_height = estimate_row_height(galley); diff --git a/crates/egui/src/text_selection/text_cursor_state.rs b/crates/egui/src/text_selection/text_cursor_state.rs index 7aca96f8b19..bd596e7e846 100644 --- a/crates/egui/src/text_selection/text_cursor_state.rs +++ b/crates/egui/src/text_selection/text_cursor_state.rs @@ -46,7 +46,7 @@ impl TextCursorState { self.cursor_range.is_none() && self.ccursor_range.is_none() } - /// The the currently selected range of characters. + /// The currently selected range of characters. pub fn char_range(&self) -> Option { self.ccursor_range.or_else(|| { self.cursor_range diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index ca85e59ef66..d31f1756edd 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -51,8 +51,10 @@ pub fn paint_text_selection( } /// Paint one end of the selection, e.g. the primary cursor. -pub fn paint_cursor(painter: &Painter, visuals: &Visuals, cursor_rect: Rect) { - let stroke = visuals.text_cursor; +/// +/// This will never blink. +pub fn paint_cursor_end(painter: &Painter, visuals: &Visuals, cursor_rect: Rect) { + let stroke = visuals.text_cursor.stroke; let top = cursor_rect.center_top(); let bottom = cursor_rect.center_bottom(); @@ -73,3 +75,33 @@ pub fn paint_cursor(painter: &Painter, visuals: &Visuals, cursor_rect: Rect) { ); } } + +/// Paint one end of the selection, e.g. the primary cursor, with blinking (if enabled). +pub fn paint_text_cursor( + ui: &Ui, + painter: &Painter, + primary_cursor_rect: Rect, + time_since_last_edit: f64, +) { + if ui.visuals().text_cursor.blink { + let on_duration = ui.visuals().text_cursor.on_duration; + let off_duration = ui.visuals().text_cursor.off_duration; + let total_duration = on_duration + off_duration; + + let time_in_cycle = (time_since_last_edit % (total_duration as f64)) as f32; + + let wake_in = if time_in_cycle < on_duration { + // Cursor is visible + paint_cursor_end(painter, ui.visuals(), primary_cursor_rect); + on_duration - time_in_cycle + } else { + // Cursor is not visible + total_duration - time_in_cycle + }; + + ui.ctx() + .request_repaint_after(std::time::Duration::from_secs_f32(wake_in)); + } else { + paint_cursor_end(painter, ui.visuals(), primary_cursor_rect); + } +} diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 4e28ece40f3..00b7beef6e4 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -111,7 +111,7 @@ impl Ui { layout: Layout, id_source: impl Hash, ) -> Self { - crate::egui_assert!(!max_rect.any_nan()); + debug_assert!(!max_rect.any_nan()); let next_auto_id_source = Id::new(self.next_auto_id_source).with("child").value(); self.next_auto_id_source = self.next_auto_id_source.wrapping_add(1); let child_ui = Ui { @@ -530,12 +530,14 @@ impl Ui { /// Set the minimum width of the ui. /// This can't shrink the ui, only make it larger. pub fn set_min_width(&mut self, width: f32) { + debug_assert!(0.0 <= width); self.placer.set_min_width(width); } /// Set the minimum height of the ui. /// This can't shrink the ui, only make it larger. pub fn set_min_height(&mut self, height: f32) { + debug_assert!(0.0 <= height); self.placer.set_min_height(height); } @@ -714,8 +716,13 @@ impl Ui { .rect_contains_pointer(self.layer_id(), self.clip_rect().intersect(rect)) } - /// Is the pointer (mouse/touch) above this [`Ui`]? + /// Is the pointer (mouse/touch) above the current [`Ui`]? + /// /// Equivalent to `ui.rect_contains_pointer(ui.min_rect())` + /// + /// Note that this tests against the _current_ [`Ui::min_rect`]. + /// If you want to test against the final `min_rect`, + /// use [`Self::interact_bg`] instead. pub fn ui_contains_pointer(&self) -> bool { self.rect_contains_pointer(self.min_rect()) } @@ -840,7 +847,7 @@ impl Ui { fn allocate_space_impl(&mut self, desired_size: Vec2) -> Rect { let item_spacing = self.spacing().item_spacing; let frame_rect = self.placer.next_space(desired_size, item_spacing); - egui_assert!(!frame_rect.any_nan()); + debug_assert!(!frame_rect.any_nan()); let widget_rect = self.placer.justify_and_align(frame_rect, desired_size); self.placer @@ -863,7 +870,7 @@ impl Ui { /// Allocate a rect without interacting with it. pub fn advance_cursor_after_rect(&mut self, rect: Rect) -> Id { - egui_assert!(!rect.any_nan()); + debug_assert!(!rect.any_nan()); let item_spacing = self.spacing().item_spacing; self.placer.advance_after_rects(rect, rect, item_spacing); @@ -932,7 +939,7 @@ impl Ui { layout: Layout, add_contents: Box R + 'c>, ) -> InnerResponse { - crate::egui_assert!(desired_size.x >= 0.0 && desired_size.y >= 0.0); + debug_assert!(desired_size.x >= 0.0 && desired_size.y >= 0.0); let item_spacing = self.spacing().item_spacing; let frame_rect = self.placer.next_space(desired_size, item_spacing); let child_rect = self.placer.justify_and_align(frame_rect, desired_size); @@ -957,7 +964,7 @@ impl Ui { max_rect: Rect, add_contents: impl FnOnce(&mut Self) -> R, ) -> InnerResponse { - egui_assert!(max_rect.is_finite()); + debug_assert!(max_rect.is_finite()); let mut child_ui = self.child_ui(max_rect, *self.layout()); let ret = add_contents(&mut child_ui); let final_child_rect = child_ui.min_rect(); @@ -996,7 +1003,7 @@ impl Ui { pub fn allocate_painter(&mut self, desired_size: Vec2, sense: Sense) -> (Response, Painter) { let response = self.allocate_response(desired_size, sense); let clip_rect = self.clip_rect().intersect(response.rect); // Make sure we don't paint out of bounds - let painter = Painter::new(self.ctx().clone(), self.layer_id(), clip_rect); + let painter = self.painter().with_clip_rect(clip_rect); (response, painter) } @@ -1801,6 +1808,9 @@ impl Ui { } /// A [`CollapsingHeader`] that starts out collapsed. + /// + /// The name must be unique within the current parent, + /// or you need to use [`CollapsingHeader::id_source`]. pub fn collapsing( &mut self, heading: impl Into, @@ -1878,7 +1888,7 @@ impl Ui { /// adjusted up and down to lie in the center of the horizontal layout. /// The initial height is `style.spacing.interact_size.y`. /// Centering is almost always what you want if you are - /// planning to to mix widgets or use different types of text. + /// planning to mix widgets or use different types of text. /// /// If you don't want the contents to be centered, use [`Self::horizontal_top`] instead. /// @@ -1939,7 +1949,7 @@ impl Ui { /// adjusted up and down to lie in the center of the horizontal layout. /// The initial height is `style.spacing.interact_size.y`. /// Centering is almost always what you want if you are - /// planning to to mix widgets or use different types of text. + /// planning to mix widgets or use different types of text. /// /// The returned [`Response`] will only have checked for mouse hover /// but can be used for tooltips (`on_hover_text`). diff --git a/crates/egui/src/util/undoer.rs b/crates/egui/src/util/undoer.rs index de6d2716171..cd11b6d1630 100644 --- a/crates/egui/src/util/undoer.rs +++ b/crates/egui/src/util/undoer.rs @@ -47,7 +47,7 @@ impl Default for Settings { /// /// Rule 1) will make sure an undo point is not created until you _stop_ dragging that slider. /// Rule 2) will make sure that you will get some undo points even if you are constantly changing the state. -#[derive(Clone, Default)] +#[derive(Clone)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Undoer { settings: Settings, @@ -59,8 +59,8 @@ pub struct Undoer { /// Stores redos immediately after a sequence of undos. /// Gets cleared every time the state changes. - /// Does not need to be a deque, because there can only be up to undos.len() redos, - /// which is already limited to settings.max_undos. + /// Does not need to be a deque, because there can only be up to `undos.len()` redos, + /// which is already limited to `settings.max_undos`. redos: Vec, #[cfg_attr(feature = "serde", serde(skip))] @@ -77,6 +77,21 @@ impl std::fmt::Debug for Undoer { } } +impl Default for Undoer +where + State: Clone + PartialEq, +{ + #[inline] + fn default() -> Self { + Self { + settings: Settings::default(), + undos: VecDeque::new(), + redos: Vec::new(), + flux: None, + } + } +} + /// Represents how the current state is changing #[derive(Clone)] struct Flux { @@ -89,6 +104,14 @@ impl Undoer where State: Clone + PartialEq, { + /// Create a new [`Undoer`] with the given [`Settings`]. + pub fn with_settings(settings: Settings) -> Self { + Self { + settings, + ..Default::default() + } + } + /// Do we have an undo point different from the given state? pub fn has_undo(&self, current_state: &State) -> bool { match self.undos.len() { diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index 4e88eab4b39..9dd71b4747b 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -275,6 +275,11 @@ pub struct ViewportBuilder { pub min_inner_size: Option, pub max_inner_size: Option, + /// Whether clamp the window's size to monitor's size. The default is `true` on linux, otherwise it is `false`. + /// + /// Note: On some Linux systems, a window size larger than the monitor causes crashes + pub clamp_size_to_monitor_size: Option, + pub fullscreen: Option, pub maximized: Option, pub resizable: Option, @@ -301,6 +306,9 @@ pub struct ViewportBuilder { pub window_level: Option, pub mouse_passthrough: Option, + + // X11 + pub window_type: Option, } impl ViewportBuilder { @@ -381,7 +389,7 @@ impl ViewportBuilder { /// The application icon, e.g. in the Windows task bar or the alt-tab menu. /// /// 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`. + /// If you prefer the OS default, set this to `IconData::default()`. #[inline] pub fn with_icon(mut self, icon: impl Into>) -> Self { self.icon = Some(icon.into()); @@ -490,6 +498,15 @@ impl ViewportBuilder { self } + /// Sets whether clamp the window's size to monitor's size. The default is `true` on linux, otherwise it is `false`. + /// + /// Note: On some Linux systems, a window size larger than the monitor causes crashes + #[inline] + pub fn with_clamp_size_to_monitor_size(mut self, value: bool) -> Self { + self.clamp_size_to_monitor_size = Some(value); + self + } + /// Does not work on X11. #[inline] pub fn with_close_button(mut self, value: bool) -> Self { @@ -583,6 +600,15 @@ impl ViewportBuilder { self } + /// ### On X11 + /// This sets the window type. + /// Maps directly to [`_NET_WM_WINDOW_TYPE`](https://specifications.freedesktop.org/wm-spec/wm-spec-1.5.html). + #[inline] + pub fn with_window_type(mut self, value: X11WindowType) -> Self { + self.window_type = Some(value); + self + } + /// Update this `ViewportBuilder` with a delta, /// returning a list of commands and a bool indicating if the window needs to be recreated. #[must_use] @@ -594,6 +620,7 @@ impl ViewportBuilder { inner_size: new_inner_size, min_inner_size: new_min_inner_size, max_inner_size: new_max_inner_size, + clamp_size_to_monitor_size: new_clamp_size_to_monitor_size, fullscreen: new_fullscreen, maximized: new_maximized, resizable: new_resizable, @@ -613,6 +640,7 @@ impl ViewportBuilder { window_level: new_window_level, mouse_passthrough: new_mouse_passthrough, taskbar: new_taskbar, + window_type: new_window_type, } = new_vp_builder; let mut commands = Vec::new(); @@ -727,6 +755,13 @@ impl ViewportBuilder { let mut recreate_window = false; + if new_clamp_size_to_monitor_size.is_some() + && self.clamp_size_to_monitor_size != new_clamp_size_to_monitor_size + { + self.clamp_size_to_monitor_size = new_clamp_size_to_monitor_size; + recreate_window = true; + } + if new_active.is_some() && self.active != new_active { self.active = new_active; recreate_window = true; @@ -786,6 +821,11 @@ impl ViewportBuilder { recreate_window = true; } + if new_window_type.is_some() && self.window_type != new_window_type { + self.window_type = new_window_type; + recreate_window = true; + } + (commands, recreate_window) } } @@ -799,6 +839,61 @@ pub enum WindowLevel { AlwaysOnTop, } +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum X11WindowType { + /// This is a normal, top-level window. + #[default] + Normal, + + /// A desktop feature. This can include a single window containing desktop icons with the same dimensions as the + /// screen, allowing the desktop environment to have full control of the desktop, without the need for proxying + /// root window clicks. + Desktop, + + /// A dock or panel feature. Typically a Window Manager would keep such windows on top of all other windows. + Dock, + + /// Toolbar windows. "Torn off" from the main application. + Toolbar, + + /// Pinnable menu windows. "Torn off" from the main application. + Menu, + + /// A small persistent utility window, such as a palette or toolbox. + Utility, + + /// The window is a splash screen displayed as an application is starting up. + Splash, + + /// This is a dialog window. + Dialog, + + /// A dropdown menu that usually appears when the user clicks on an item in a menu bar. + /// This property is typically used on override-redirect windows. + DropdownMenu, + + /// A popup menu that usually appears when the user right clicks on an object. + /// This property is typically used on override-redirect windows. + PopupMenu, + + /// A tooltip window. Usually used to show additional information when hovering over an object with the cursor. + /// This property is typically used on override-redirect windows. + Tooltip, + + /// The window is a notification. + /// This property is typically used on override-redirect windows. + Notification, + + /// This should be used on the windows that are popped up by combo boxes. + /// This property is typically used on override-redirect windows. + Combo, + + /// This indicates the window is being dragged. + /// This property is typically used on override-redirect windows. + Dnd, +} + #[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum IMEPurpose { @@ -920,7 +1015,7 @@ pub enum ViewportCommand { /// Set window to be always-on-top, always-on-bottom, or neither. WindowLevel(WindowLevel), - /// The the window icon. + /// The window icon. Icon(Option>), /// Set the IME cursor editing area. @@ -965,6 +1060,21 @@ pub enum ViewportCommand { /// /// The results are returned in `crate::Event::Screenshot`. Screenshot, + + /// Request cut of the current selection + /// + /// This is equivalent to the system keyboard shortcut for cut (e.g. CTRL + X). + RequestCut, + + /// Request a copy of the current selection. + /// + /// This is equivalent to the system keyboard shortcut for copy (e.g. CTRL + C). + RequestCopy, + + /// Request a paste from the clipboard to the current focused `TextEdit` if any. + /// + /// This is equivalent to the system keyboard shortcut for paste (e.g. CTRL + V). + RequestPaste, } impl ViewportCommand { diff --git a/crates/egui/src/widget_rect.rs b/crates/egui/src/widget_rect.rs index acf3dd95df5..76d03270f26 100644 --- a/crates/egui/src/widget_rect.rs +++ b/crates/egui/src/widget_rect.rs @@ -29,6 +29,13 @@ pub struct WidgetRect { pub interact_rect: Rect, /// How the widget responds to interaction. + /// + /// Note: if [`Self::enabled`] is `false`, then + /// the widget _effectively_ doesn't sense anything, + /// but can still have the same `Sense`. + /// This is because the sense informs the styling of the widget, + /// but we don't want to change the style when a widget is disabled + /// (that is handled by the `Painter` directly). pub sense: Sense, /// Is the widget enabled? @@ -39,13 +46,25 @@ pub struct WidgetRect { /// /// All [`Ui`]s have a [`WidgetRects`], but whether or not their rects are correct /// depends on if [`Ui::interact_bg`] was ever called. -#[derive(Default, Clone, PartialEq, Eq)] +#[derive(Default, Clone)] pub struct WidgetRects { /// All widgets, in painting order. by_layer: HashMap>, /// All widgets, by id, and their order in their respective layer by_id: IdMap<(usize, WidgetRect)>, + + /// Info about some widgets. + /// + /// Only filled in if the widget is interacted with, + /// or if this is a debug build. + infos: IdMap, +} + +impl PartialEq for WidgetRects { + fn eq(&self, other: &Self) -> bool { + self.by_layer == other.by_layer + } } impl WidgetRects { @@ -65,6 +84,11 @@ impl WidgetRects { self.by_id.get(&id).map(|(_, w)| w) } + /// In which layer, and in which order in that layer? + pub fn order(&self, id: Id) -> Option<(LayerId, usize)> { + self.by_id.get(&id).map(|(idx, w)| (w.layer_id, *idx)) + } + #[inline] pub fn contains(&self, id: Id) -> bool { self.by_id.contains_key(&id) @@ -78,18 +102,28 @@ impl WidgetRects { /// Clear the contents while retaining allocated memory. pub fn clear(&mut self) { - let Self { by_layer, by_id } = self; + let Self { + by_layer, + by_id, + infos, + } = self; for rects in by_layer.values_mut() { rects.clear(); } by_id.clear(); + + infos.clear(); } /// Insert the given widget rect in the given layer. pub fn insert(&mut self, layer_id: LayerId, widget_rect: WidgetRect) { - let Self { by_layer, by_id } = self; + let Self { + by_layer, + by_id, + infos: _, + } = self; let layer_widgets = by_layer.entry(layer_id).or_default(); @@ -105,7 +139,7 @@ impl WidgetRects { // e.g. calling `response.interact(…)` to add more interaction. let (idx_in_layer, existing) = entry.get_mut(); - egui_assert!( + debug_assert!( existing.layer_id == widget_rect.layer_id, "Widget changed layer_id during the frame" ); @@ -122,4 +156,12 @@ impl WidgetRects { } } } + + pub fn set_info(&mut self, id: Id, info: WidgetInfo) { + self.infos.insert(id, info); + } + + pub fn info(&self, id: Id) -> Option<&WidgetInfo> { + self.infos.get(&id) + } } diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 75da2a9c716..76a62125045 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -336,379 +336,3 @@ impl Widget for Button<'_> { response } } - -// ---------------------------------------------------------------------------- - -// TODO(emilk): allow checkbox without a text label -/// Boolean on/off control with text label. -/// -/// Usually you'd use [`Ui::checkbox`] instead. -/// -/// ``` -/// # egui::__run_test_ui(|ui| { -/// # let mut my_bool = true; -/// // These are equivalent: -/// ui.checkbox(&mut my_bool, "Checked"); -/// ui.add(egui::Checkbox::new(&mut my_bool, "Checked")); -/// # }); -/// ``` -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] -pub struct Checkbox<'a> { - checked: &'a mut bool, - text: WidgetText, - indeterminate: bool, -} - -impl<'a> Checkbox<'a> { - pub fn new(checked: &'a mut bool, text: impl Into) -> Self { - Checkbox { - checked, - text: text.into(), - indeterminate: false, - } - } - - pub fn without_text(checked: &'a mut bool) -> Self { - Self::new(checked, WidgetText::default()) - } - - /// Display an indeterminate state (neither checked nor unchecked) - /// - /// This only affects the checkbox's appearance. It will still toggle its boolean value when - /// clicked. - #[inline] - pub fn indeterminate(mut self, indeterminate: bool) -> Self { - self.indeterminate = indeterminate; - self - } -} - -impl<'a> Widget for Checkbox<'a> { - fn ui(self, ui: &mut Ui) -> Response { - let Checkbox { - checked, - text, - indeterminate, - } = self; - - let spacing = &ui.spacing(); - let icon_width = spacing.icon_width; - let icon_spacing = spacing.icon_spacing; - - let (galley, mut desired_size) = if text.is_empty() { - (None, vec2(icon_width, 0.0)) - } else { - let total_extra = vec2(icon_width + icon_spacing, 0.0); - - let wrap_width = ui.available_width() - total_extra.x; - let galley = text.into_galley(ui, None, wrap_width, TextStyle::Button); - - let mut desired_size = total_extra + galley.size(); - desired_size = desired_size.at_least(spacing.interact_size); - - (Some(galley), desired_size) - }; - - desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y)); - desired_size.y = desired_size.y.max(icon_width); - let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click()); - - if response.clicked() { - *checked = !*checked; - response.mark_changed(); - } - response.widget_info(|| { - if indeterminate { - WidgetInfo::labeled( - WidgetType::Checkbox, - galley.as_ref().map_or("", |x| x.text()), - ) - } else { - WidgetInfo::selected( - WidgetType::Checkbox, - *checked, - galley.as_ref().map_or("", |x| x.text()), - ) - } - }); - - if ui.is_rect_visible(rect) { - // let visuals = ui.style().interact_selectable(&response, *checked); // too colorful - let visuals = ui.style().interact(&response); - let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); - ui.painter().add(epaint::RectShape::new( - big_icon_rect.expand(visuals.expansion), - visuals.rounding, - visuals.bg_fill, - visuals.bg_stroke, - )); - - if indeterminate { - // Horizontal line: - ui.painter().add(Shape::hline( - small_icon_rect.x_range(), - small_icon_rect.center().y, - visuals.fg_stroke, - )); - } else if *checked { - // Check mark: - ui.painter().add(Shape::line( - vec![ - pos2(small_icon_rect.left(), small_icon_rect.center().y), - pos2(small_icon_rect.center().x, small_icon_rect.bottom()), - pos2(small_icon_rect.right(), small_icon_rect.top()), - ], - visuals.fg_stroke, - )); - } - if let Some(galley) = galley { - let text_pos = pos2( - rect.min.x + icon_width + icon_spacing, - rect.center().y - 0.5 * galley.size().y, - ); - ui.painter().galley(text_pos, galley, visuals.text_color()); - } - } - - response - } -} - -// ---------------------------------------------------------------------------- - -/// One out of several alternatives, either selected or not. -/// -/// Usually you'd use [`Ui::radio_value`] or [`Ui::radio`] instead. -/// -/// ``` -/// # egui::__run_test_ui(|ui| { -/// #[derive(PartialEq)] -/// enum Enum { First, Second, Third } -/// let mut my_enum = Enum::First; -/// -/// ui.radio_value(&mut my_enum, Enum::First, "First"); -/// -/// // is equivalent to: -/// -/// if ui.add(egui::RadioButton::new(my_enum == Enum::First, "First")).clicked() { -/// my_enum = Enum::First -/// } -/// # }); -/// ``` -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] -pub struct RadioButton { - checked: bool, - text: WidgetText, -} - -impl RadioButton { - pub fn new(checked: bool, text: impl Into) -> Self { - Self { - checked, - text: text.into(), - } - } -} - -impl Widget for RadioButton { - fn ui(self, ui: &mut Ui) -> Response { - let Self { checked, text } = self; - - let spacing = &ui.spacing(); - let icon_width = spacing.icon_width; - let icon_spacing = spacing.icon_spacing; - - let (galley, mut desired_size) = if text.is_empty() { - (None, vec2(icon_width, 0.0)) - } else { - let total_extra = vec2(icon_width + icon_spacing, 0.0); - - let wrap_width = ui.available_width() - total_extra.x; - let text = text.into_galley(ui, None, wrap_width, TextStyle::Button); - - let mut desired_size = total_extra + text.size(); - desired_size = desired_size.at_least(spacing.interact_size); - - (Some(text), desired_size) - }; - - desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y)); - desired_size.y = desired_size.y.max(icon_width); - let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click()); - - response.widget_info(|| { - WidgetInfo::selected( - WidgetType::RadioButton, - checked, - galley.as_ref().map_or("", |x| x.text()), - ) - }); - - if ui.is_rect_visible(rect) { - // let visuals = ui.style().interact_selectable(&response, checked); // too colorful - let visuals = ui.style().interact(&response); - - let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); - - let painter = ui.painter(); - - painter.add(epaint::CircleShape { - center: big_icon_rect.center(), - radius: big_icon_rect.width() / 2.0 + visuals.expansion, - fill: visuals.bg_fill, - stroke: visuals.bg_stroke, - }); - - if checked { - painter.add(epaint::CircleShape { - center: small_icon_rect.center(), - radius: small_icon_rect.width() / 3.0, - fill: visuals.fg_stroke.color, // Intentional to use stroke and not fill - // fill: ui.visuals().selection.stroke.color, // too much color - stroke: Default::default(), - }); - } - - if let Some(galley) = galley { - let text_pos = pos2( - rect.min.x + icon_width + icon_spacing, - rect.center().y - 0.5 * galley.size().y, - ); - ui.painter().galley(text_pos, galley, visuals.text_color()); - } - } - - response - } -} - -// ---------------------------------------------------------------------------- - -/// A clickable image within a frame. -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] -#[derive(Clone, Debug)] -pub struct ImageButton<'a> { - image: Image<'a>, - sense: Sense, - frame: bool, - selected: bool, -} - -impl<'a> ImageButton<'a> { - pub fn new(image: impl Into>) -> Self { - Self { - image: image.into(), - sense: Sense::click(), - frame: true, - selected: false, - } - } - - /// Select UV range. Default is (0,0) in top-left, (1,1) bottom right. - #[inline] - pub fn uv(mut self, uv: impl Into) -> Self { - self.image = self.image.uv(uv); - self - } - - /// Multiply image color with this. Default is WHITE (no tint). - #[inline] - pub fn tint(mut self, tint: impl Into) -> Self { - self.image = self.image.tint(tint); - self - } - - /// If `true`, mark this button as "selected". - #[inline] - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - - /// Turn off the frame - #[inline] - pub fn frame(mut self, frame: bool) -> Self { - self.frame = frame; - self - } - - /// By default, buttons senses clicks. - /// Change this to a drag-button with `Sense::drag()`. - #[inline] - pub fn sense(mut self, sense: Sense) -> Self { - self.sense = sense; - self - } - - /// Set rounding for the `ImageButton`. - /// If the underlying image already has rounding, this - /// will override that value. - #[inline] - pub fn rounding(mut self, rounding: impl Into) -> Self { - self.image = self.image.rounding(rounding.into()); - self - } -} - -impl<'a> Widget for ImageButton<'a> { - fn ui(self, ui: &mut Ui) -> Response { - let padding = if self.frame { - // so we can see that it is a button: - Vec2::splat(ui.spacing().button_padding.x) - } else { - Vec2::ZERO - }; - - let available_size_for_image = ui.available_size() - 2.0 * padding; - let tlr = self.image.load_for_size(ui.ctx(), available_size_for_image); - let original_image_size = tlr.as_ref().ok().and_then(|t| t.size()); - let image_size = self - .image - .calc_size(available_size_for_image, original_image_size); - - let padded_size = image_size + 2.0 * padding; - let (rect, response) = ui.allocate_exact_size(padded_size, self.sense); - response.widget_info(|| WidgetInfo::new(WidgetType::ImageButton)); - - if ui.is_rect_visible(rect) { - let (expansion, rounding, fill, stroke) = if self.selected { - let selection = ui.visuals().selection; - ( - Vec2::ZERO, - self.image.image_options().rounding, - selection.bg_fill, - selection.stroke, - ) - } else if self.frame { - let visuals = ui.style().interact(&response); - let expansion = Vec2::splat(visuals.expansion); - ( - expansion, - self.image.image_options().rounding, - visuals.weak_bg_fill, - visuals.bg_stroke, - ) - } else { - Default::default() - }; - - // Draw frame background (for transparent images): - ui.painter() - .rect_filled(rect.expand2(expansion), rounding, fill); - - let image_rect = ui - .layout() - .align_size_within_rect(image_size, rect.shrink2(padding)); - // let image_rect = image_rect.expand2(expansion); // can make it blurry, so let's not - let image_options = self.image.image_options().clone(); - - widgets::image::paint_texture_load_result(ui, &tlr, image_rect, None, &image_options); - - // Draw frame outline: - ui.painter() - .rect_stroke(rect.expand2(expansion), rounding, stroke); - } - - widgets::image::texture_load_result_response(self.image.source(), &tlr, response) - } -} diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs new file mode 100644 index 00000000000..e416a6e0681 --- /dev/null +++ b/crates/egui/src/widgets/checkbox.rs @@ -0,0 +1,136 @@ +use crate::*; + +// TODO(emilk): allow checkbox without a text label +/// Boolean on/off control with text label. +/// +/// Usually you'd use [`Ui::checkbox`] instead. +/// +/// ``` +/// # egui::__run_test_ui(|ui| { +/// # let mut my_bool = true; +/// // These are equivalent: +/// ui.checkbox(&mut my_bool, "Checked"); +/// ui.add(egui::Checkbox::new(&mut my_bool, "Checked")); +/// # }); +/// ``` +#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +pub struct Checkbox<'a> { + checked: &'a mut bool, + text: WidgetText, + indeterminate: bool, +} + +impl<'a> Checkbox<'a> { + pub fn new(checked: &'a mut bool, text: impl Into) -> Self { + Checkbox { + checked, + text: text.into(), + indeterminate: false, + } + } + + pub fn without_text(checked: &'a mut bool) -> Self { + Self::new(checked, WidgetText::default()) + } + + /// Display an indeterminate state (neither checked nor unchecked) + /// + /// This only affects the checkbox's appearance. It will still toggle its boolean value when + /// clicked. + #[inline] + pub fn indeterminate(mut self, indeterminate: bool) -> Self { + self.indeterminate = indeterminate; + self + } +} + +impl<'a> Widget for Checkbox<'a> { + fn ui(self, ui: &mut Ui) -> Response { + let Checkbox { + checked, + text, + indeterminate, + } = self; + + let spacing = &ui.spacing(); + let icon_width = spacing.icon_width; + let icon_spacing = spacing.icon_spacing; + + let (galley, mut desired_size) = if text.is_empty() { + (None, vec2(icon_width, 0.0)) + } else { + let total_extra = vec2(icon_width + icon_spacing, 0.0); + + let wrap_width = ui.available_width() - total_extra.x; + let galley = text.into_galley(ui, None, wrap_width, TextStyle::Button); + + let mut desired_size = total_extra + galley.size(); + desired_size = desired_size.at_least(spacing.interact_size); + + (Some(galley), desired_size) + }; + + desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y)); + desired_size.y = desired_size.y.max(icon_width); + let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click()); + + if response.clicked() { + *checked = !*checked; + response.mark_changed(); + } + response.widget_info(|| { + if indeterminate { + WidgetInfo::labeled( + WidgetType::Checkbox, + galley.as_ref().map_or("", |x| x.text()), + ) + } else { + WidgetInfo::selected( + WidgetType::Checkbox, + *checked, + galley.as_ref().map_or("", |x| x.text()), + ) + } + }); + + if ui.is_rect_visible(rect) { + // let visuals = ui.style().interact_selectable(&response, *checked); // too colorful + let visuals = ui.style().interact(&response); + let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); + ui.painter().add(epaint::RectShape::new( + big_icon_rect.expand(visuals.expansion), + visuals.rounding, + visuals.bg_fill, + visuals.bg_stroke, + )); + + if indeterminate { + // Horizontal line: + ui.painter().add(Shape::hline( + small_icon_rect.x_range(), + small_icon_rect.center().y, + visuals.fg_stroke, + )); + } else if *checked { + // Check mark: + ui.painter().add(Shape::line( + vec![ + pos2(small_icon_rect.left(), small_icon_rect.center().y), + pos2(small_icon_rect.center().x, small_icon_rect.bottom()), + pos2(small_icon_rect.right(), small_icon_rect.top()), + ], + visuals.fg_stroke, + )); + } + if let Some(galley) = galley { + let text_pos = pos2( + rect.min.x + icon_width + icon_spacing, + rect.center().y - 0.5 * galley.size().y, + ); + ui.painter().galley(text_pos, galley, visuals.text_color()); + } + } + + response + } +} diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs index 8426adee968..6935de4536d 100644 --- a/crates/egui/src/widgets/color_picker.rs +++ b/crates/egui/src/widgets/color_picker.rs @@ -93,7 +93,7 @@ fn color_button(ui: &mut Ui, color: Color32, open: bool) -> Response { show_color_at(ui.painter(), color, rect); - let rounding = visuals.rounding.at_most(2.0); + let rounding = visuals.rounding.at_most(2.0); // Can't do more rounding because the background grid doesn't do any rounding ui.painter() .rect_stroke(rect, rounding, (2.0, visuals.bg_fill)); // fill is intentional, because default style has no border } diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index f0875ba3055..8a7f19028b6 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -525,8 +525,16 @@ impl<'a> Widget for DragValue<'a> { .sense(Sense::click_and_drag()) .min_size(ui.spacing().interact_size); // TODO(emilk): find some more generic solution to `min_size` + let cursor_icon = if value <= *clamp_range.start() { + CursorIcon::ResizeEast + } else if value < *clamp_range.end() { + CursorIcon::ResizeHorizontal + } else { + CursorIcon::ResizeWest + }; + let response = ui.add(button); - let mut response = response.on_hover_cursor(CursorIcon::ResizeHorizontal); + let mut response = response.on_hover_cursor(cursor_icon); if ui.style().explanation_tooltips { response = response.on_hover_text(format!( @@ -552,7 +560,7 @@ impl<'a> Widget for DragValue<'a> { ))); state.store(ui.ctx(), response.id); } else if response.dragged() { - ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal); + ui.ctx().set_cursor_icon(cursor_icon); let mdelta = response.drag_delta(); let delta_points = mdelta.x - mdelta.y; // Increase to the right and up diff --git a/crates/egui/src/widgets/hyperlink.rs b/crates/egui/src/widgets/hyperlink.rs index f4a508e64f7..ad3b76eeddd 100644 --- a/crates/egui/src/widgets/hyperlink.rs +++ b/crates/egui/src/widgets/hyperlink.rs @@ -136,6 +136,11 @@ impl Widget for Hyperlink { new_tab: true, }); } - response.on_hover_text(url) + + if ui.style().url_in_tooltip { + response.on_hover_text(url) + } else { + response + } } } diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index 1d1ec581d8a..a5aecdce931 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -1,12 +1,12 @@ use std::borrow::Cow; -use crate::load::TextureLoadResult; +use emath::{Float as _, Rot2}; +use epaint::RectShape; + use crate::{ - load::{Bytes, SizeHint, SizedTexture, TexturePoll}, + load::{Bytes, SizeHint, SizedTexture, TextureLoadResult, TexturePoll}, *, }; -use emath::Rot2; -use epaint::{util::FloatOrd, RectShape}; /// A widget which displays an image. /// @@ -452,7 +452,7 @@ impl ImageSize { } } -// TODO: unit-tests +// TODO(jprochazk): unit-tests fn scale_to_fit(image_size: Vec2, available_size: Vec2, maintain_aspect_ratio: bool) -> Vec2 { if maintain_aspect_ratio { let ratio_x = available_size.x / image_size.x; @@ -746,7 +746,7 @@ pub fn paint_texture_at( Some((rot, origin)) => { // TODO(emilk): implement this using `PathShape` (add texture support to it). // This will also give us anti-aliasing of rotated images. - egui_assert!( + debug_assert!( options.rounding == Rounding::ZERO, "Image had both rounding and rotation. Please pick only one" ); @@ -762,6 +762,7 @@ pub fn paint_texture_at( rounding: options.rounding, fill: options.tint, stroke: Stroke::NONE, + blur_width: 0.0, fill_texture_id: texture.id, uv: options.uv, }); diff --git a/crates/egui/src/widgets/image_button.rs b/crates/egui/src/widgets/image_button.rs new file mode 100644 index 00000000000..65ef3072807 --- /dev/null +++ b/crates/egui/src/widgets/image_button.rs @@ -0,0 +1,130 @@ +use crate::*; + +/// A clickable image within a frame. +#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +#[derive(Clone, Debug)] +pub struct ImageButton<'a> { + image: Image<'a>, + sense: Sense, + frame: bool, + selected: bool, +} + +impl<'a> ImageButton<'a> { + pub fn new(image: impl Into>) -> Self { + Self { + image: image.into(), + sense: Sense::click(), + frame: true, + selected: false, + } + } + + /// Select UV range. Default is (0,0) in top-left, (1,1) bottom right. + #[inline] + pub fn uv(mut self, uv: impl Into) -> Self { + self.image = self.image.uv(uv); + self + } + + /// Multiply image color with this. Default is WHITE (no tint). + #[inline] + pub fn tint(mut self, tint: impl Into) -> Self { + self.image = self.image.tint(tint); + self + } + + /// If `true`, mark this button as "selected". + #[inline] + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } + + /// Turn off the frame + #[inline] + pub fn frame(mut self, frame: bool) -> Self { + self.frame = frame; + self + } + + /// By default, buttons senses clicks. + /// Change this to a drag-button with `Sense::drag()`. + #[inline] + pub fn sense(mut self, sense: Sense) -> Self { + self.sense = sense; + self + } + + /// Set rounding for the `ImageButton`. + /// If the underlying image already has rounding, this + /// will override that value. + #[inline] + pub fn rounding(mut self, rounding: impl Into) -> Self { + self.image = self.image.rounding(rounding.into()); + self + } +} + +impl<'a> Widget for ImageButton<'a> { + fn ui(self, ui: &mut Ui) -> Response { + let padding = if self.frame { + // so we can see that it is a button: + Vec2::splat(ui.spacing().button_padding.x) + } else { + Vec2::ZERO + }; + + let available_size_for_image = ui.available_size() - 2.0 * padding; + let tlr = self.image.load_for_size(ui.ctx(), available_size_for_image); + let original_image_size = tlr.as_ref().ok().and_then(|t| t.size()); + let image_size = self + .image + .calc_size(available_size_for_image, original_image_size); + + let padded_size = image_size + 2.0 * padding; + let (rect, response) = ui.allocate_exact_size(padded_size, self.sense); + response.widget_info(|| WidgetInfo::new(WidgetType::ImageButton)); + + if ui.is_rect_visible(rect) { + let (expansion, rounding, fill, stroke) = if self.selected { + let selection = ui.visuals().selection; + ( + Vec2::ZERO, + self.image.image_options().rounding, + selection.bg_fill, + selection.stroke, + ) + } else if self.frame { + let visuals = ui.style().interact(&response); + let expansion = Vec2::splat(visuals.expansion); + ( + expansion, + self.image.image_options().rounding, + visuals.weak_bg_fill, + visuals.bg_stroke, + ) + } else { + Default::default() + }; + + // Draw frame background (for transparent images): + ui.painter() + .rect_filled(rect.expand2(expansion), rounding, fill); + + let image_rect = ui + .layout() + .align_size_within_rect(image_size, rect.shrink2(padding)); + // let image_rect = image_rect.expand2(expansion); // can make it blurry, so let's not + let image_options = self.image.image_options().clone(); + + widgets::image::paint_texture_load_result(ui, &tlr, image_rect, None, &image_options); + + // Draw frame outline: + ui.painter() + .rect_stroke(rect.expand2(expansion), rounding, stroke); + } + + widgets::image::texture_load_result_response(self.image.source(), &tlr, response) + } +} diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index f5bb7ac425a..f09a17baeb2 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -128,7 +128,7 @@ impl Label { // dragging select text, or scroll the enclosing [`ScrollArea`] (if any)? // Since currently copying selected text in not supported on `eframe` web, // we prioritize touch-scrolling: - let allow_drag_to_select = ui.input(|i| !i.any_touches()); + let allow_drag_to_select = ui.input(|i| !i.has_touch_screen()); let mut select_sense = if allow_drag_to_select { Sense::click_and_drag() @@ -170,7 +170,7 @@ impl Label { let cursor = ui.cursor(); let first_row_indentation = available_width - ui.available_size_before_wrap().x; - egui_assert!(first_row_indentation.is_finite()); + debug_assert!(first_row_indentation.is_finite()); layout_job.wrap.max_width = available_width; layout_job.first_row_min_height = cursor.height(); diff --git a/crates/egui/src/widgets/mod.rs b/crates/egui/src/widgets/mod.rs index d589e386ac7..0ab9273c7a5 100644 --- a/crates/egui/src/widgets/mod.rs +++ b/crates/egui/src/widgets/mod.rs @@ -7,29 +7,37 @@ use crate::*; mod button; +mod checkbox; pub mod color_picker; pub(crate) mod drag_value; mod hyperlink; mod image; +mod image_button; mod label; mod progress_bar; +mod radio_button; mod selected_label; mod separator; mod slider; mod spinner; pub mod text_edit; -pub use button::*; -pub use drag_value::DragValue; -pub use hyperlink::*; -pub use image::{paint_texture_at, Image, ImageFit, ImageOptions, ImageSize, ImageSource}; -pub use label::*; -pub use progress_bar::ProgressBar; -pub use selected_label::SelectableLabel; -pub use separator::Separator; -pub use slider::*; -pub use spinner::*; -pub use text_edit::{TextBuffer, TextEdit}; +pub use self::{ + button::Button, + checkbox::Checkbox, + drag_value::DragValue, + hyperlink::{Hyperlink, Link}, + image::{paint_texture_at, Image, ImageFit, ImageOptions, ImageSize, ImageSource}, + image_button::ImageButton, + label::Label, + progress_bar::ProgressBar, + radio_button::RadioButton, + selected_label::SelectableLabel, + separator::Separator, + slider::{Slider, SliderOrientation}, + spinner::Spinner, + text_edit::{TextBuffer, TextEdit}, +}; // ---------------------------------------------------------------------------- @@ -92,15 +100,19 @@ pub trait WidgetWithState { /// Show a button to reset a value to its default. /// The button is only enabled if the value does not already have its original value. -pub fn reset_button(ui: &mut Ui, value: &mut T) { - reset_button_with(ui, value, T::default()); +/// +/// The `text` could be something like "Reset foo". +pub fn reset_button(ui: &mut Ui, value: &mut T, text: &str) { + reset_button_with(ui, value, text, T::default()); } /// Show a button to reset a value to its default. /// The button is only enabled if the value does not already have its original value. -pub fn reset_button_with(ui: &mut Ui, value: &mut T, reset_value: T) { +/// +/// The `text` could be something like "Reset foo". +pub fn reset_button_with(ui: &mut Ui, value: &mut T, text: &str, reset_value: T) { if ui - .add_enabled(*value != reset_value, Button::new("Reset")) + .add_enabled(*value != reset_value, Button::new(text)) .clicked() { *value = reset_value; @@ -109,33 +121,11 @@ pub fn reset_button_with(ui: &mut Ui, value: &mut T, reset_value: // ---------------------------------------------------------------------------- +#[deprecated = "Use `ui.add(&mut stroke)` instead"] pub fn stroke_ui(ui: &mut crate::Ui, stroke: &mut epaint::Stroke, text: &str) { - let epaint::Stroke { width, color } = stroke; - ui.horizontal(|ui| { - ui.add(DragValue::new(width).speed(0.1).clamp_range(0.0..=5.0)) - .on_hover_text("Width"); - ui.color_edit_button_srgba(color); - ui.label(text); - - // stroke preview: - let (_id, stroke_rect) = ui.allocate_space(ui.spacing().interact_size); - let left = stroke_rect.left_center(); - let right = stroke_rect.right_center(); - ui.painter().line_segment([left, right], (*width, *color)); - }); -} - -pub(crate) fn shadow_ui(ui: &mut Ui, shadow: &mut epaint::Shadow, text: &str) { - let epaint::Shadow { extrusion, color } = shadow; ui.horizontal(|ui| { ui.label(text); - ui.add( - DragValue::new(extrusion) - .speed(1.0) - .clamp_range(0.0..=100.0), - ) - .on_hover_text("Extrusion"); - ui.color_edit_button_srgba(color); + ui.add(stroke); }); } diff --git a/crates/egui/src/widgets/radio_button.rs b/crates/egui/src/widgets/radio_button.rs new file mode 100644 index 00000000000..ccbe785f6ab --- /dev/null +++ b/crates/egui/src/widgets/radio_button.rs @@ -0,0 +1,107 @@ +use crate::*; + +/// One out of several alternatives, either selected or not. +/// +/// Usually you'd use [`Ui::radio_value`] or [`Ui::radio`] instead. +/// +/// ``` +/// # egui::__run_test_ui(|ui| { +/// #[derive(PartialEq)] +/// enum Enum { First, Second, Third } +/// let mut my_enum = Enum::First; +/// +/// ui.radio_value(&mut my_enum, Enum::First, "First"); +/// +/// // is equivalent to: +/// +/// if ui.add(egui::RadioButton::new(my_enum == Enum::First, "First")).clicked() { +/// my_enum = Enum::First +/// } +/// # }); +/// ``` +#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] +pub struct RadioButton { + checked: bool, + text: WidgetText, +} + +impl RadioButton { + pub fn new(checked: bool, text: impl Into) -> Self { + Self { + checked, + text: text.into(), + } + } +} + +impl Widget for RadioButton { + fn ui(self, ui: &mut Ui) -> Response { + let Self { checked, text } = self; + + let spacing = &ui.spacing(); + let icon_width = spacing.icon_width; + let icon_spacing = spacing.icon_spacing; + + let (galley, mut desired_size) = if text.is_empty() { + (None, vec2(icon_width, 0.0)) + } else { + let total_extra = vec2(icon_width + icon_spacing, 0.0); + + let wrap_width = ui.available_width() - total_extra.x; + let text = text.into_galley(ui, None, wrap_width, TextStyle::Button); + + let mut desired_size = total_extra + text.size(); + desired_size = desired_size.at_least(spacing.interact_size); + + (Some(text), desired_size) + }; + + desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y)); + desired_size.y = desired_size.y.max(icon_width); + let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click()); + + response.widget_info(|| { + WidgetInfo::selected( + WidgetType::RadioButton, + checked, + galley.as_ref().map_or("", |x| x.text()), + ) + }); + + if ui.is_rect_visible(rect) { + // let visuals = ui.style().interact_selectable(&response, checked); // too colorful + let visuals = ui.style().interact(&response); + + let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect); + + let painter = ui.painter(); + + painter.add(epaint::CircleShape { + center: big_icon_rect.center(), + radius: big_icon_rect.width() / 2.0 + visuals.expansion, + fill: visuals.bg_fill, + stroke: visuals.bg_stroke, + }); + + if checked { + painter.add(epaint::CircleShape { + center: small_icon_rect.center(), + radius: small_icon_rect.width() / 3.0, + fill: visuals.fg_stroke.color, // Intentional to use stroke and not fill + // fill: ui.visuals().selection.stroke.color, // too much color + stroke: Default::default(), + }); + } + + if let Some(galley) = galley { + let text_pos = pos2( + rect.min.x + icon_width + icon_spacing, + rect.center().y - 0.5 * galley.size().y, + ); + ui.painter().galley(text_pos, galley, visuals.text_color()); + } + } + + response + } +} diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index 431dd6e55c2..644694cbe81 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -680,17 +680,16 @@ impl<'a> Slider<'a> { if ui.is_rect_visible(response.rect) { let value = self.get_value(); - let rail_radius = ui.painter().round_to_pixel(self.rail_radius_limit(rect)); - let rail_rect = self.rail_rect(rect, rail_radius); - let visuals = ui.style().interact(response); let widget_visuals = &ui.visuals().widgets; + let spacing = &ui.style().spacing; + + let rail_radius = (spacing.slider_rail_height / 2.0).at_least(0.0); + let rail_rect = self.rail_rect(rect, rail_radius); + let rounding = widget_visuals.inactive.rounding; - ui.painter().rect_filled( - rail_rect, - widget_visuals.inactive.rounding, - widget_visuals.inactive.bg_fill, - ); + ui.painter() + .rect_filled(rail_rect, rounding, widget_visuals.inactive.bg_fill); let position_1d = self.position_from_value(value, position_range); let center = self.marker_center(position_1d, &rail_rect); @@ -706,13 +705,17 @@ impl<'a> Slider<'a> { // The trailing rect has to be drawn differently depending on the orientation. match self.orientation { - SliderOrientation::Vertical => trailing_rail_rect.min.y = center.y, - SliderOrientation::Horizontal => trailing_rail_rect.max.x = center.x, + SliderOrientation::Horizontal => { + trailing_rail_rect.max.x = center.x + rounding.nw; + } + SliderOrientation::Vertical => { + trailing_rail_rect.min.y = center.y - rounding.se; + } }; ui.painter().rect_filled( trailing_rail_rect, - widget_visuals.inactive.rounding, + rounding, ui.visuals().selection.bg_fill, ); } @@ -738,14 +741,8 @@ impl<'a> Slider<'a> { }; let v = v + Vec2::splat(visuals.expansion); let rect = Rect::from_center_size(center, 2.0 * v); - ui.painter().add(epaint::RectShape { - fill: visuals.bg_fill, - stroke: visuals.fg_stroke, - rect, - rounding: visuals.rounding, - fill_texture_id: Default::default(), - uv: Rect::ZERO, - }); + ui.painter() + .rect(rect, visuals.rounding, visuals.bg_fill, visuals.fg_stroke); } } } @@ -800,13 +797,6 @@ impl<'a> Slider<'a> { limit / 2.5 } - fn rail_radius_limit(&self, rect: &Rect) -> f32 { - match self.orientation { - SliderOrientation::Horizontal => (rect.height() / 4.0).at_least(2.0), - SliderOrientation::Vertical => (rect.width() / 4.0).at_least(2.0), - } - } - fn value_ui(&mut self, ui: &mut Ui, position_range: Rangef) -> Response { // If [`DragValue`] is controlled from the keyboard and `step` is defined, set speed to `step` let change = ui.input(|input| { @@ -993,7 +983,7 @@ fn value_from_normalized(normalized: f64, range: RangeInclusive, spec: &Sli } } } else { - crate::egui_assert!( + debug_assert!( min.is_finite() && max.is_finite(), "You should use a logarithmic range" ); @@ -1042,7 +1032,7 @@ fn normalized_from_value(value: f64, range: RangeInclusive, spec: &SliderSp } } } else { - crate::egui_assert!( + debug_assert!( min.is_finite() && max.is_finite(), "You should use a logarithmic range" ); @@ -1090,6 +1080,6 @@ fn logarithmic_zero_cutoff(min: f64, max: f64) -> f64 { }; let cutoff = min_magnitude / (min_magnitude + max_magnitude); - crate::egui_assert!(0.0 <= cutoff && cutoff <= 1.0); + debug_assert!(0.0 <= cutoff && cutoff <= 1.0); cutoff } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 12077b93292..440916f31b2 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -6,9 +6,7 @@ use crate::{ os::OperatingSystem, output::OutputEvent, text_selection::{ - text_cursor_state::cursor_rect, - visuals::{paint_cursor, paint_text_selection}, - CCursorRange, CursorRange, + text_cursor_state::cursor_rect, visuals::paint_text_selection, CCursorRange, CursorRange, }, *, }; @@ -80,6 +78,7 @@ pub struct TextEdit<'t> { align: Align2, clip_text: bool, char_limit: usize, + return_key: KeyboardShortcut, } impl<'t> WidgetWithState for TextEdit<'t> { @@ -107,7 +106,7 @@ impl<'t> TextEdit<'t> { } } - /// A [`TextEdit`] for multiple lines. Pressing enter key will create a new line. + /// A [`TextEdit`] for multiple lines. Pressing enter key will create a new line by default (can be changed with [`return_key`](TextEdit::return_key)). pub fn multiline(text: &'t mut dyn TextBuffer) -> Self { Self { text, @@ -136,6 +135,7 @@ impl<'t> TextEdit<'t> { align: Align2::LEFT_TOP, clip_text: false, char_limit: usize::MAX, + return_key: KeyboardShortcut::new(Modifiers::NONE, Key::Enter), } } @@ -348,6 +348,16 @@ impl<'t> TextEdit<'t> { self.min_size = min_size; self } + + /// Set the return key combination. + /// + /// This combination will cause a newline on multiline, + /// whereas on singleline it will cause the widget to lose focus. + #[inline] + pub fn return_key(mut self, return_key: KeyboardShortcut) -> Self { + self.return_key = return_key; + self + } } // ---------------------------------------------------------------------------- @@ -377,29 +387,20 @@ impl<'t> TextEdit<'t> { pub fn show(self, ui: &mut Ui) -> TextEditOutput { let is_mutable = self.text.is_mutable(); let frame = self.frame; - let interactive = self.interactive; let where_to_put_background = ui.painter().add(Shape::Noop); let margin = self.margin; - let available = ui.available_rect_before_wrap(); - let max_rect = margin.shrink_rect(available); - let mut content_ui = ui.child_ui(max_rect, *ui.layout()); + let mut output = self.show_content(ui); - let mut output = self.show_content(&mut content_ui); - - let id = output.response.id; - let frame_rect = margin.expand_rect(output.response.rect); - ui.allocate_space(frame_rect.size()); - if interactive { - output.response |= ui.interact(frame_rect, id, Sense::click()); - } - if output.response.clicked() && !output.response.lost_focus() { - ui.memory_mut(|mem| mem.request_focus(output.response.id)); - } + // TODO(emilk): return full outer_rect in `TextEditOutput`. + // Can't do it now because this fix is ging into a patch release. + let outer_rect = output.response.rect; + let inner_rect = outer_rect - margin; + output.response.rect = inner_rect; if frame { let visuals = ui.style().interact(&output.response); - let frame_rect = frame_rect.expand(visuals.expansion); + let frame_rect = outer_rect.expand(visuals.expansion); let shape = if is_mutable { if output.response.has_focus() { epaint::RectShape::new( @@ -453,6 +454,7 @@ impl<'t> TextEdit<'t> { align, clip_text, char_limit, + return_key, } = self; let text_color = text_color @@ -465,7 +467,7 @@ impl<'t> TextEdit<'t> { let font_id = font_selection.resolve(ui.style()); let row_height = ui.fonts(|f| f.row_height(&font_id)); const MIN_WIDTH: f32 = 24.0; // Never make a [`TextEdit`] more narrow than this. - let available_width = ui.available_width().at_least(MIN_WIDTH); + let available_width = (ui.available_width() - margin.sum().x).at_least(MIN_WIDTH); let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width); let wrap_width = if ui.layout().horizontal_justify() { available_width @@ -494,11 +496,10 @@ impl<'t> TextEdit<'t> { galley.size().x.max(wrap_width) }; let desired_height = (desired_height_rows.at_least(1) as f32) * row_height; - let at_least = min_size - margin.sum(); - let desired_size = - vec2(desired_width, galley.size().y.max(desired_height)).at_least(at_least); - - let (auto_id, rect) = ui.allocate_space(desired_size); + let desired_inner_size = vec2(desired_width, galley.size().y.max(desired_height)); + let desired_outer_size = (desired_inner_size + margin.sum()).at_least(min_size); + let (auto_id, outer_rect) = ui.allocate_space(desired_outer_size); + let rect = outer_rect - margin; // inner rect (excluding frame/margin). let id = id.unwrap_or_else(|| { if let Some(id_source) = id_source { @@ -514,7 +515,7 @@ impl<'t> TextEdit<'t> { // Since currently copying selected text in not supported on `eframe` web, // we prioritize touch-scrolling: let allow_drag_to_select = - ui.input(|i| !i.any_touches()) || ui.memory(|mem| mem.has_focus(id)); + ui.input(|i| !i.has_touch_screen()) || ui.memory(|mem| mem.has_focus(id)); let sense = if interactive { if allow_drag_to_select { @@ -525,7 +526,7 @@ impl<'t> TextEdit<'t> { } else { Sense::hover() }; - let mut response = ui.interact(rect, id, sense); + let mut response = ui.interact(outer_rect, id, sense); let text_clip_rect = rect; let painter = ui.painter_at(text_clip_rect.expand(1.0)); // expand to avoid clipping cursor @@ -539,16 +540,16 @@ impl<'t> TextEdit<'t> { let singleline_offset = vec2(state.singleline_offset, 0.0); let cursor_at_pointer = - galley.cursor_from_pos(pointer_pos - response.rect.min + singleline_offset); + galley.cursor_from_pos(pointer_pos - rect.min + singleline_offset); - if ui.visuals().text_cursor_preview + if ui.visuals().text_cursor.preview && response.hovered() && ui.input(|i| i.pointer.is_moving()) { - // preview: + // text cursor preview: let cursor_rect = - cursor_rect(response.rect.min, &galley, &cursor_at_pointer, row_height); - paint_cursor(&painter, ui.visuals(), cursor_rect); + cursor_rect(rect.min, &galley, &cursor_at_pointer, row_height); + text_selection::visuals::paint_cursor_end(&painter, ui.visuals(), cursor_rect); } let is_being_dragged = ui.ctx().is_being_dragged(response.id); @@ -594,6 +595,7 @@ impl<'t> TextEdit<'t> { default_cursor_range, char_limit, event_filter, + return_key, ); if changed { @@ -603,10 +605,10 @@ impl<'t> TextEdit<'t> { } let mut galley_pos = align - .align_size_within_rect(galley.size(), response.rect) - .intersect(response.rect) // limit pos to the response rect area + .align_size_within_rect(galley.size(), rect) + .intersect(rect) // limit pos to the response rect area .min; - let align_offset = response.rect.left() - galley_pos.x; + let align_offset = rect.left() - galley_pos.x; // Visual clipping for singleline text editor with text larger than width if clip_text && align_offset == 0.0 { @@ -616,18 +618,18 @@ impl<'t> TextEdit<'t> { }; let mut offset_x = state.singleline_offset; - let visible_range = offset_x..=offset_x + desired_size.x; + let visible_range = offset_x..=offset_x + desired_inner_size.x; if !visible_range.contains(&cursor_pos) { if cursor_pos < *visible_range.start() { offset_x = cursor_pos; } else { - offset_x = cursor_pos - desired_size.x; + offset_x = cursor_pos - desired_inner_size.x; } } offset_x = offset_x - .at_most(galley.size().x - desired_size.x) + .at_most(galley.size().x - desired_inner_size.x) .at_least(0.0); state.singleline_offset = offset_x; @@ -650,11 +652,11 @@ impl<'t> TextEdit<'t> { if text.as_str().is_empty() && !hint_text.is_empty() { let hint_text_color = ui.visuals().weak_text_color(); let galley = if multiline { - hint_text.into_galley(ui, Some(true), desired_size.x, font_id) + hint_text.into_galley(ui, Some(true), desired_inner_size.x, font_id) } else { hint_text.into_galley(ui, Some(false), f32::INFINITY, font_id) }; - painter.galley(response.rect.min, galley, hint_text_color); + painter.galley(rect.min, galley, hint_text_color); } if ui.memory(|mem| mem.has_focus(id)) { @@ -673,24 +675,42 @@ impl<'t> TextEdit<'t> { let primary_cursor_rect = cursor_rect(galley_pos, &galley, &cursor_range.primary, row_height); - let is_fully_visible = ui.clip_rect().contains_rect(rect); // TODO: remove this HACK workaround for https://github.com/emilk/egui/issues/1531 + let is_fully_visible = ui.clip_rect().contains_rect(rect); // TODO(emilk): remove this HACK workaround for https://github.com/emilk/egui/issues/1531 if (response.changed || selection_changed) && !is_fully_visible { // Scroll to keep primary cursor in view: ui.scroll_to_rect(primary_cursor_rect, None); } - if text.is_mutable() { - paint_cursor(&painter, ui.visuals(), primary_cursor_rect); + if text.is_mutable() && interactive { + let now = ui.ctx().input(|i| i.time); + if response.changed || selection_changed { + state.last_edit_time = now; + } - if interactive { - // For IME, so only set it when text is editable and visible! - ui.ctx().output_mut(|o| { - o.ime = Some(crate::output::IMEOutput { - rect, - cursor_rect: primary_cursor_rect, - }); - }); + // Only show (and blink) cursor if the egui viewport has focus. + // This is for two reasons: + // * Don't give the impression that the user can type into a window without focus + // * Don't repaint the ui because of a blinking cursor in an app that is not in focus + if ui.ctx().input(|i| i.focused) { + text_selection::visuals::paint_text_cursor( + ui, + &painter, + primary_cursor_rect, + now - state.last_edit_time, + ); } + + // Set IME output (in screen coords) when text is editable and visible + let transform = ui + .memory(|m| m.layer_transforms.get(&ui.layer_id()).copied()) + .unwrap_or_default(); + + ui.ctx().output_mut(|o| { + o.ime = Some(crate::output::IMEOutput { + rect: transform * rect, + cursor_rect: transform * primary_cursor_rect, + }); + }); } } } @@ -785,6 +805,7 @@ fn events( default_cursor_range: CursorRange, char_limit: usize, event_filter: EventFilter, + return_key: KeyboardShortcut, ) -> (bool, CursorRange) { let os = ui.ctx().os(); @@ -867,10 +888,13 @@ fn events( Some(CCursorRange::one(ccursor)) } Event::Key { - key: Key::Enter, + key, pressed: true, + modifiers, .. - } => { + } if *key == return_key.logical_key + && modifiers.matches_logically(return_key.modifiers) => + { if multiline { let mut ccursor = text.delete_selected(&cursor_range); text.insert_text_at(&mut ccursor, "\n", char_limit); @@ -924,46 +948,53 @@ fn events( key, pressed: true, .. - } => check_for_mutating_key_press(os, &mut cursor_range, text, galley, modifiers, *key), + } => check_for_mutating_key_press(os, &cursor_range, text, galley, modifiers, *key), - Event::CompositionStart => { - state.has_ime = true; - None - } - - Event::CompositionUpdate(text_mark) => { - // empty prediction can be produced when user press backspace - // or escape during ime. We should clear current text. - if text_mark != "\n" && text_mark != "\r" && state.has_ime { - let mut ccursor = text.delete_selected(&cursor_range); - let start_cursor = ccursor; - if !text_mark.is_empty() { - text.insert_text_at(&mut ccursor, text_mark, char_limit); - } + Event::Ime(ime_event) => match ime_event { + ImeEvent::Enabled => { + state.ime_enabled = true; state.ime_cursor_range = cursor_range; - Some(CCursorRange::two(start_cursor, ccursor)) - } else { None } - } - - Event::CompositionEnd(prediction) => { - // CompositionEnd only characters may be typed into TextEdit without trigger CompositionStart first, - // so do not check `state.has_ime = true` in the following statement. - if prediction != "\n" && prediction != "\r" { - state.has_ime = false; - let mut ccursor; - if !prediction.is_empty() && cursor_range == state.ime_cursor_range { - ccursor = text.delete_selected(&cursor_range); - text.insert_text_at(&mut ccursor, prediction, char_limit); + ImeEvent::Preedit(text_mark) => { + if text_mark == "\n" || text_mark == "\r" { + None + } else { + // Empty prediction can be produced when user press backspace + // or escape during IME, so we clear current text. + let mut ccursor = text.delete_selected(&cursor_range); + let start_cursor = ccursor; + if !text_mark.is_empty() { + text.insert_text_at(&mut ccursor, text_mark, char_limit); + } + state.ime_cursor_range = cursor_range; + Some(CCursorRange::two(start_cursor, ccursor)) + } + } + ImeEvent::Commit(prediction) => { + if prediction == "\n" || prediction == "\r" { + None } else { - ccursor = cursor_range.primary.ccursor; + state.ime_enabled = false; + + if !prediction.is_empty() + && cursor_range.secondary.ccursor.index + == state.ime_cursor_range.secondary.ccursor.index + { + let mut ccursor = text.delete_selected(&cursor_range); + text.insert_text_at(&mut ccursor, prediction, char_limit); + Some(CCursorRange::one(ccursor)) + } else { + let ccursor = cursor_range.primary.ccursor; + Some(CCursorRange::one(ccursor)) + } } - Some(CCursorRange::one(ccursor)) - } else { + } + ImeEvent::Disabled => { + state.ime_enabled = false; None } - } + }, _ => None, }; @@ -997,7 +1028,7 @@ fn events( /// Returns `Some(new_cursor)` if we did mutate `text`. fn check_for_mutating_key_press( os: OperatingSystem, - cursor_range: &mut CursorRange, + cursor_range: &CursorRange, text: &mut dyn TextBuffer, galley: &Galley, modifiers: &Modifiers, diff --git a/crates/egui/src/widgets/text_edit/state.rs b/crates/egui/src/widgets/text_edit/state.rs index 8901fd56f27..c95d676910a 100644 --- a/crates/egui/src/widgets/text_edit/state.rs +++ b/crates/egui/src/widgets/text_edit/state.rs @@ -42,7 +42,7 @@ pub struct TextEditState { // If IME candidate window is shown on this text edit. #[cfg_attr(feature = "serde", serde(skip))] - pub(crate) has_ime: bool, + pub(crate) ime_enabled: bool, // cursor range for IME candidate. #[cfg_attr(feature = "serde", serde(skip))] @@ -51,6 +51,11 @@ pub struct TextEditState { // Visual offset when editing singleline text bigger than the width. #[cfg_attr(feature = "serde", serde(skip))] pub(crate) singleline_offset: f32, + + /// When did the user last press a key? + /// Used to pause the cursor animation when typing. + #[cfg_attr(feature = "serde", serde(skip))] + pub(crate) last_edit_time: f64, } impl TextEditState { @@ -62,7 +67,7 @@ impl TextEditState { ctx.data_mut(|d| d.insert_persisted(id, self)); } - /// The the currently selected range of characters. + /// The currently selected range of characters. #[deprecated = "Use `self.cursor.char_range` instead"] pub fn ccursor_range(&self) -> Option { self.cursor.char_range() diff --git a/crates/egui/src/widgets/text_edit/text_buffer.rs b/crates/egui/src/widgets/text_edit/text_buffer.rs index f28878a1dd3..ea3992c57e4 100644 --- a/crates/egui/src/widgets/text_edit/text_buffer.rs +++ b/crates/egui/src/widgets/text_edit/text_buffer.rs @@ -89,10 +89,12 @@ pub trait TextBuffer { fn decrease_indentation(&mut self, ccursor: &mut CCursor) { let line_start = find_line_start(self.as_str(), *ccursor); - let remove_len = if self.as_str()[line_start.index..].starts_with('\t') { + let remove_len = if self.as_str().chars().nth(line_start.index) == Some('\t') { Some(1) - } else if self.as_str()[line_start.index..] + } else if self + .as_str() .chars() + .skip(line_start.index) .take(TAB_SIZE) .all(|c| c == ' ') { @@ -218,7 +220,7 @@ impl TextBuffer for String { } fn replace_with(&mut self, text: &str) { - *self = text.to_owned(); + text.clone_into(self); } fn take(&mut self) -> String { diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 7939e74cd25..5878848ceae 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -8,6 +8,9 @@ rust-version.workspace = true publish = false default-run = "egui_demo_app" +[lints] +workspace = true + [package.metadata.docs.rs] all-features = true @@ -41,12 +44,7 @@ chrono = { version = "0.4", default-features = false, features = [ eframe = { workspace = true, default-features = false, features = [ "web_screen_reader", ] } -egui = { workspace = true, features = [ - "callstack", - "default", - "extra_debug_asserts", - "log", -] } +egui = { workspace = true, features = ["callstack", "default", "log"] } egui_demo_lib = { workspace = true, features = ["default", "chrono"] } egui_extras = { workspace = true, features = ["default", "image"] } log.workspace = true @@ -79,6 +77,6 @@ rfd = { version = "0.13", optional = true } # web: [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "=0.2.90" +wasm-bindgen = "=0.2.92" wasm-bindgen-futures = "0.4" web-sys = "0.3" diff --git a/crates/egui_demo_app/src/apps/custom3d_glow.rs b/crates/egui_demo_app/src/apps/custom3d_glow.rs index 7f488e7671e..ad6bfc3bc60 100644 --- a/crates/egui_demo_app/src/apps/custom3d_glow.rs +++ b/crates/egui_demo_app/src/apps/custom3d_glow.rs @@ -1,3 +1,5 @@ +#![allow(clippy::undocumented_unsafe_blocks)] + use std::sync::Arc; use eframe::egui_glow; diff --git a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs index 1676a0ba70c..95b3ee1a0af 100644 --- a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs +++ b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs @@ -49,11 +49,13 @@ impl Custom3d { module: &shader, entry_point: "vs_main", buffers: &[], + compilation_options: wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: "fs_main", targets: &[Some(wgpu_render_state.target_format.into())], + compilation_options: wgpu::PipelineCompilationOptions::default(), }), primitive: wgpu::PrimitiveState::default(), depth_stencil: None, diff --git a/crates/egui_demo_app/src/apps/fractal_clock.rs b/crates/egui_demo_app/src/apps/fractal_clock.rs index 6afe6f0459e..1a6f4f55ebb 100644 --- a/crates/egui_demo_app/src/apps/fractal_clock.rs +++ b/crates/egui_demo_app/src/apps/fractal_clock.rs @@ -79,7 +79,7 @@ impl FractalClock { ui.add(Slider::new(&mut self.luminance_factor, 0.0..=1.0).text("luminance factor")); ui.add(Slider::new(&mut self.width_factor, 0.0..=1.0).text("width factor")); - egui::reset_button(ui, self); + egui::reset_button(ui, self, "Reset"); ui.hyperlink_to( "Inspired by a screensaver by Rob Mayoff", diff --git a/crates/egui_demo_app/src/apps/http_app.rs b/crates/egui_demo_app/src/apps/http_app.rs index d0da5c0a395..d6b57284267 100644 --- a/crates/egui_demo_app/src/apps/http_app.rs +++ b/crates/egui_demo_app/src/apps/http_app.rs @@ -116,7 +116,7 @@ impl eframe::App for HttpApp { } } -fn ui_url(ui: &mut egui::Ui, frame: &mut eframe::Frame, url: &mut String) -> bool { +fn ui_url(ui: &mut egui::Ui, frame: &eframe::Frame, url: &mut String) -> bool { let mut trigger_fetch = false; ui.horizontal(|ui| { diff --git a/crates/egui_demo_app/src/backend_panel.rs b/crates/egui_demo_app/src/backend_panel.rs index 2e2f0f0a71c..2176d57a8b6 100644 --- a/crates/egui_demo_app/src/backend_panel.rs +++ b/crates/egui_demo_app/src/backend_panel.rs @@ -296,14 +296,29 @@ fn integration_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame) { } } - if ui - .button("📱 Phone Size") - .on_hover_text("Resize the window to be small like a phone.") - .clicked() - { - // let size = egui::vec2(375.0, 812.0); // iPhone 12 mini - let size = egui::vec2(375.0, 667.0); // iPhone SE 2nd gen + let mut size = None; + egui::ComboBox::from_id_source("viewport-size-combo") + .selected_text("Resize to...") + .show_ui(ui, |ui| { + ui.selectable_value( + &mut size, + Some(egui::vec2(375.0, 667.0)), + "📱 iPhone SE 2nd Gen", + ); + ui.selectable_value(&mut size, Some(egui::vec2(393.0, 852.0)), "📱 iPhone 15"); + ui.selectable_value( + &mut size, + Some(egui::vec2(1280.0, 720.0)), + "🖥 Desktop 720p", + ); + ui.selectable_value( + &mut size, + Some(egui::vec2(1920.0, 1080.0)), + "🖥 Desktop 1080p", + ); + }); + if let Some(size) = size { ui.ctx() .send_viewport_cmd(egui::ViewportCommand::InnerSize(size)); ui.ctx() diff --git a/crates/egui_demo_app/src/main.rs b/crates/egui_demo_app/src/main.rs index 45b0ee067a4..9d660ed046c 100644 --- a/crates/egui_demo_app/src/main.rs +++ b/crates/egui_demo_app/src/main.rs @@ -1,6 +1,7 @@ //! Demo app for egui #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example #![allow(clippy::never_loop)] // False positive // When compiling natively: diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index e87814cb401..42adf2b857a 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -79,15 +79,22 @@ impl eframe::App for ColorTestApp { #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] enum Anchor { Demo, + EasyMarkEditor, + #[cfg(feature = "http")] Http, + #[cfg(feature = "image_viewer")] ImageViewer, + Clock, + #[cfg(any(feature = "glow", feature = "wgpu"))] Custom3d, - Colors, + + /// Rendering test + Rendering, } impl Anchor { @@ -101,7 +108,7 @@ impl Anchor { Self::Clock, #[cfg(any(feature = "glow", feature = "wgpu"))] Self::Custom3d, - Self::Colors, + Self::Rendering, ] } } @@ -147,7 +154,7 @@ pub struct State { #[cfg(feature = "image_viewer")] image_viewer: crate::apps::ImageViewer, clock: FractalClockApp, - color_test: ColorTestApp, + rendering_test: ColorTestApp, selected_anchor: Anchor, backend_panel: super::backend_panel::BackendPanel, @@ -229,9 +236,9 @@ impl WrapApp { } vec.push(( - "🎨 Color test", - Anchor::Colors, - &mut self.state.color_test as &mut dyn eframe::App, + "🎨 Rendering test", + Anchor::Rendering, + &mut self.state.rendering_test as &mut dyn eframe::App, )); vec.into_iter() @@ -245,7 +252,13 @@ impl eframe::App for WrapApp { } fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] { - visuals.panel_fill.to_normalized_gamma_f32() + // Give the area behind the floating windows a different color, because it looks better: + let color = egui::lerp( + egui::Rgba::from(visuals.panel_fill)..=egui::Rgba::from(visuals.extreme_bg_color), + 0.5, + ); + let color = egui::Color32::from(color); + color.to_normalized_gamma_f32() } fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { @@ -451,7 +464,7 @@ impl WrapApp { // Collect dropped files: ctx.input(|i| { if !i.raw.dropped_files.is_empty() { - self.dropped_files = i.raw.dropped_files.clone(); + self.dropped_files.clone_from(&i.raw.dropped_files); } }); diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index a08a88fe8d0..d1daa5b8cfb 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -19,6 +19,9 @@ include = [ "data/icon.png", ] +[lints] +workspace = true + [package.metadata.docs.rs] all-features = true @@ -38,7 +41,7 @@ syntect = ["egui_extras/syntect"] [dependencies] -egui = { workspace = true, default-features = false } +egui = { workspace = true, default-features = false, features = ["color-hex"] } egui_extras = { workspace = true, features = ["default"] } egui_plot = { workspace = true, features = ["default"] } diff --git a/crates/egui_demo_lib/src/demo/code_example.rs b/crates/egui_demo_lib/src/demo/code_example.rs index 6786e55a14c..2b09b557be3 100644 --- a/crates/egui_demo_lib/src/demo/code_example.rs +++ b/crates/egui_demo_lib/src/demo/code_example.rs @@ -15,78 +15,64 @@ impl Default for CodeExample { impl CodeExample { fn samples_in_grid(&mut self, ui: &mut egui::Ui) { - show_code(ui, r#"ui.heading("Code samples");"#); - ui.heading("Code samples"); + // Note: we keep the code narrow so that the example fits on a mobile screen. + + let Self { name, age } = self; // for brevity later on + + show_code(ui, r#"ui.heading("Example");"#); + ui.heading("Example"); ui.end_row(); show_code( ui, r#" - // Putting things on the same line using ui.horizontal: ui.horizontal(|ui| { - ui.label("Your name: "); - ui.text_edit_singleline(&mut self.name); + ui.label("Name"); + ui.text_edit_singleline(name); });"#, ); // Putting things on the same line using ui.horizontal: ui.horizontal(|ui| { - ui.label("Your name: "); - ui.text_edit_singleline(&mut self.name); + ui.label("Name"); + ui.text_edit_singleline(name); }); ui.end_row(); show_code( ui, - r#"ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));"#, + r#" + ui.add( + egui::DragValue::new(age) + .clamp_range(0..=120) + .suffix(" years"), + );"#, + ); + ui.add( + egui::DragValue::new(age) + .clamp_range(0..=120) + .suffix(" years"), ); - ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age")); ui.end_row(); show_code( ui, r#" if ui.button("Increment").clicked() { - self.age += 1; + *age += 1; }"#, ); if ui.button("Increment").clicked() { - self.age += 1; + *age += 1; } ui.end_row(); - show_code( - ui, - r#"ui.label(format!("Hello '{}', age {}", self.name, self.age));"#, - ); - ui.label(format!("Hello '{}', age {}", self.name, self.age)); + show_code(ui, r#"ui.label(format!("{name} is {age}"));"#); + ui.label(format!("{name} is {age}")); ui.end_row(); } -} - -impl super::Demo for CodeExample { - fn name(&self) -> &'static str { - "🖮 Code Example" - } - - fn show(&mut self, ctx: &egui::Context, open: &mut bool) { - use super::View; - egui::Window::new(self.name()) - .open(open) - .default_size([800.0, 400.0]) - .vscroll(false) - .hscroll(true) - .resizable([true, false]) - .show(ctx, |ui| self.ui(ui)); - } -} - -impl super::View for CodeExample { - fn ui(&mut self, ui: &mut egui::Ui) { - ui.vertical_centered(|ui| { - ui.add(crate::egui_github_link_file!()); - }); - crate::rust_view_ui( + fn code(&mut self, ui: &mut egui::Ui) { + show_code( ui, r" pub struct CodeExample { @@ -96,8 +82,8 @@ pub struct CodeExample { impl CodeExample { fn ui(&mut self, ui: &mut egui::Ui) { -" - .trim(), + // Saves us from writing `&mut self.name` etc + let Self { name, age } = self;", ); ui.horizontal(|ui| { @@ -109,14 +95,38 @@ impl CodeExample { egui::Grid::new("code_samples") .striped(true) .num_columns(2) - .min_col_width(16.0) - .spacing([16.0, 8.0]) .show(ui, |ui| { self.samples_in_grid(ui); }); }); crate::rust_view_ui(ui, " }\n}"); + } +} + +impl super::Demo for CodeExample { + fn name(&self) -> &'static str { + "🖮 Code Example" + } + + fn show(&mut self, ctx: &egui::Context, open: &mut bool) { + use super::View; + egui::Window::new(self.name()) + .open(open) + .min_width(375.0) + .default_size([390.0, 500.0]) + .scroll(false) + .resizable([true, false]) + .show(ctx, |ui| self.ui(ui)); + } +} + +impl super::View for CodeExample { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.scope(|ui| { + ui.spacing_mut().item_spacing = egui::vec2(8.0, 8.0); + self.code(ui); + }); ui.separator(); @@ -129,6 +139,12 @@ impl CodeExample { theme.ui(ui); theme.store_in_memory(ui.ctx()); }); + + ui.separator(); + + ui.vertical_centered(|ui| { + ui.add(crate::egui_github_link_file!()); + }); } } diff --git a/crates/egui_demo_lib/src/demo/context_menu.rs b/crates/egui_demo_lib/src/demo/context_menu.rs index ffcc73379d8..5195a2b0609 100644 --- a/crates/egui_demo_lib/src/demo/context_menu.rs +++ b/crates/egui_demo_lib/src/demo/context_menu.rs @@ -80,6 +80,8 @@ impl super::View for ContextMenus { ui.label("Right-click plot to edit it!"); ui.horizontal(|ui| { self.example_plot(ui).context_menu(|ui| { + ui.set_min_width(220.0); + ui.menu_button("Plot", |ui| { if ui.radio_value(&mut self.plot, Plot::Sin, "Sin").clicked() || ui @@ -95,13 +97,15 @@ impl super::View for ContextMenus { egui::Grid::new("button_grid").show(ui, |ui| { ui.add( egui::DragValue::new(&mut self.width) + .clamp_range(0.0..=f32::INFINITY) .speed(1.0) - .prefix("Width:"), + .prefix("Width: "), ); ui.add( egui::DragValue::new(&mut self.height) + .clamp_range(0.0..=f32::INFINITY) .speed(1.0) - .prefix("Height:"), + .prefix("Height: "), ); ui.end_row(); ui.checkbox(&mut self.show_axes[0], "x-Axis"); diff --git a/crates/egui_demo_lib/src/demo/dancing_strings.rs b/crates/egui_demo_lib/src/demo/dancing_strings.rs index 3beb323e75d..a2b560ee723 100644 --- a/crates/egui_demo_lib/src/demo/dancing_strings.rs +++ b/crates/egui_demo_lib/src/demo/dancing_strings.rs @@ -1,9 +1,11 @@ -use egui::{containers::*, *}; +use egui::{containers::*, epaint::PathStroke, *}; #[derive(Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] -pub struct DancingStrings {} +pub struct DancingStrings { + colors: bool, +} impl super::Demo for DancingStrings { fn name(&self) -> &'static str { @@ -28,6 +30,9 @@ impl super::View for DancingStrings { Color32::from_black_alpha(240) }; + ui.checkbox(&mut self.colors, "Colored") + .on_hover_text("Demonstrates how a path can have varying color across its length."); + Frame::canvas(ui.style()).show(ui, |ui| { ui.ctx().request_repaint(); let time = ui.input(|i| i.time); @@ -55,7 +60,24 @@ impl super::View for DancingStrings { .collect(); let thickness = 10.0 / mode as f32; - shapes.push(epaint::Shape::line(points, Stroke::new(thickness, color))); + shapes.push(epaint::Shape::line( + points, + if self.colors { + PathStroke::new_uv(thickness, move |rect, p| { + let t = remap(p.x, rect.x_range(), -1.0..=1.0).abs(); + let center_color = hex_color!("#5BCEFA"); + let outer_color = hex_color!("#F5A9B8"); + + Color32::from_rgb( + lerp(center_color.r() as f32..=outer_color.r() as f32, t) as u8, + lerp(center_color.g() as f32..=outer_color.g() as f32, t) as u8, + lerp(center_color.b() as f32..=outer_color.b() as f32, t) as u8, + ) + }) + } else { + PathStroke::new(thickness, color) + }, + )); } ui.painter().extend(shapes); diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index de1d96cddf4..0910df4ef56 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -29,6 +29,7 @@ impl Default for Demos { Box::::default(), Box::::default(), Box::::default(), + Box::::default(), Box::::default(), Box::::default(), Box::::default(), @@ -51,6 +52,15 @@ impl Default for Demos { impl Demos { pub fn from_demos(demos: Vec>) -> Self { let mut open = BTreeSet::new(); + + // Explains egui very well + open.insert( + super::code_example::CodeExample::default() + .name() + .to_owned(), + ); + + // Shows off the features open.insert( super::widget_gallery::WidgetGallery::default() .name() diff --git a/crates/egui_demo_lib/src/demo/frame_demo.rs b/crates/egui_demo_lib/src/demo/frame_demo.rs new file mode 100644 index 00000000000..0027a98d9da --- /dev/null +++ b/crates/egui_demo_lib/src/demo/frame_demo.rs @@ -0,0 +1,74 @@ +/// Shows off a table with dynamic layout +#[derive(PartialEq)] +pub struct FrameDemo { + frame: egui::Frame, +} + +impl Default for FrameDemo { + fn default() -> Self { + Self { + frame: egui::Frame { + inner_margin: 12.0.into(), + outer_margin: 24.0.into(), + rounding: 14.0.into(), + shadow: egui::Shadow { + offset: [8.0, 12.0].into(), + blur: 16.0, + spread: 0.0, + color: egui::Color32::from_black_alpha(180), + }, + fill: egui::Color32::from_rgba_unmultiplied(97, 0, 255, 128), + stroke: egui::Stroke::new(1.0, egui::Color32::GRAY), + }, + } + } +} + +impl super::Demo for FrameDemo { + fn name(&self) -> &'static str { + "▣ Frame" + } + + fn show(&mut self, ctx: &egui::Context, open: &mut bool) { + egui::Window::new(self.name()) + .open(open) + .resizable(false) + .show(ctx, |ui| { + use super::View as _; + self.ui(ui); + }); + } +} + +impl super::View for FrameDemo { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.add(&mut self.frame); + + ui.add_space(8.0); + ui.set_max_width(ui.min_size().x); + ui.vertical_centered(|ui| egui::reset_button(ui, self, "Reset")); + }); + + ui.separator(); + + ui.vertical(|ui| { + // We want to paint a background around the outer margin of the demonstration frame, so we use another frame around it: + egui::Frame::default() + .stroke(ui.visuals().widgets.noninteractive.bg_stroke) + .rounding(ui.visuals().widgets.noninteractive.rounding) + .show(ui, |ui| { + self.frame.show(ui, |ui| { + ui.style_mut().wrap = Some(false); + ui.label(egui::RichText::new("Content").color(egui::Color32::WHITE)); + }); + }); + }); + }); + + ui.set_max_width(ui.min_size().x); + ui.separator(); + ui.vertical_centered(|ui| ui.add(crate::egui_github_link_file!())); + } +} diff --git a/crates/egui_demo_lib/src/demo/misc_demo_window.rs b/crates/egui_demo_lib/src/demo/misc_demo_window.rs index b4b28511b62..f3c7a8dcfec 100644 --- a/crates/egui_demo_lib/src/demo/misc_demo_window.rs +++ b/crates/egui_demo_lib/src/demo/misc_demo_window.rs @@ -328,7 +328,7 @@ impl Default for ColorWidgets { impl ColorWidgets { fn ui(&mut self, ui: &mut Ui) { - egui::reset_button(ui, self); + egui::reset_button(ui, self, "Reset"); ui.label("egui lets you edit colors stored as either sRGBA or linear RGBA and with or without premultiplied alpha"); diff --git a/crates/egui_demo_lib/src/demo/mod.rs b/crates/egui_demo_lib/src/demo/mod.rs index d54498bbcf9..8a911c9d68b 100644 --- a/crates/egui_demo_lib/src/demo/mod.rs +++ b/crates/egui_demo_lib/src/demo/mod.rs @@ -13,6 +13,7 @@ pub mod demo_app_windows; pub mod drag_and_drop; pub mod extra_viewport; pub mod font_book; +pub mod frame_demo; pub mod highlighting; pub mod layout_test; pub mod misc_demo_window; diff --git a/crates/egui_demo_lib/src/demo/paint_bezier.rs b/crates/egui_demo_lib/src/demo/paint_bezier.rs index ad507de50a4..99d477b44ae 100644 --- a/crates/egui_demo_lib/src/demo/paint_bezier.rs +++ b/crates/egui_demo_lib/src/demo/paint_bezier.rs @@ -43,13 +43,27 @@ impl Default for PaintBezier { impl PaintBezier { pub fn ui_control(&mut self, ui: &mut egui::Ui) { ui.collapsing("Colors", |ui| { - ui.horizontal(|ui| { - ui.label("Fill color:"); - ui.color_edit_button_srgba(&mut self.fill); - }); - egui::stroke_ui(ui, &mut self.stroke, "Curve Stroke"); - egui::stroke_ui(ui, &mut self.aux_stroke, "Auxiliary Stroke"); - egui::stroke_ui(ui, &mut self.bounding_box_stroke, "Bounding Box Stroke"); + Grid::new("colors") + .num_columns(2) + .spacing([12.0, 8.0]) + .striped(true) + .show(ui, |ui| { + ui.label("Fill color"); + ui.color_edit_button_srgba(&mut self.fill); + ui.end_row(); + + ui.label("Curve Stroke"); + ui.add(&mut self.stroke); + ui.end_row(); + + ui.label("Auxiliary Stroke"); + ui.add(&mut self.aux_stroke); + ui.end_row(); + + ui.label("Bounding Box Stroke"); + ui.add(&mut self.bounding_box_stroke); + ui.end_row(); + }); }); ui.collapsing("Global tessellation options", |ui| { diff --git a/crates/egui_demo_lib/src/demo/painting.rs b/crates/egui_demo_lib/src/demo/painting.rs index 57e5f114da4..d95e8534653 100644 --- a/crates/egui_demo_lib/src/demo/painting.rs +++ b/crates/egui_demo_lib/src/demo/painting.rs @@ -20,7 +20,8 @@ impl Default for Painting { impl Painting { pub fn ui_control(&mut self, ui: &mut egui::Ui) -> egui::Response { ui.horizontal(|ui| { - egui::stroke_ui(ui, &mut self.stroke, "Stroke"); + ui.label("Stroke:"); + ui.add(&mut self.stroke); ui.separator(); if ui.button("Clear Painting").clicked() { self.lines.clear(); diff --git a/crates/egui_demo_lib/src/demo/pan_zoom.rs b/crates/egui_demo_lib/src/demo/pan_zoom.rs index 08829d6d5e6..40323346887 100644 --- a/crates/egui_demo_lib/src/demo/pan_zoom.rs +++ b/crates/egui_demo_lib/src/demo/pan_zoom.rs @@ -11,7 +11,7 @@ impl Eq for PanZoom {} impl super::Demo for PanZoom { fn name(&self) -> &'static str { - "🗖 Pan Zoom" + "🔍 Pan Zoom" } fn show(&mut self, ctx: &egui::Context, open: &mut bool) { diff --git a/crates/egui_demo_lib/src/demo/plot_demo.rs b/crates/egui_demo_lib/src/demo/plot_demo.rs index 0f8b8edcc13..12e45aa0f09 100644 --- a/crates/egui_demo_lib/src/demo/plot_demo.rs +++ b/crates/egui_demo_lib/src/demo/plot_demo.rs @@ -62,7 +62,7 @@ impl super::Demo for PlotDemo { impl super::View for PlotDemo { fn ui(&mut self, ui: &mut Ui) { ui.horizontal(|ui| { - egui::reset_button(ui, self); + egui::reset_button(ui, self, "Reset"); ui.collapsing("Instructions", |ui| { ui.label("Pan by dragging, or scroll (+ shift = horizontal)."); ui.label("Box zooming: Right click to zoom in and zoom out using a selection."); diff --git a/crates/egui_demo_lib/src/demo/scrolling.rs b/crates/egui_demo_lib/src/demo/scrolling.rs index 0530f2fee99..9c490d23d8a 100644 --- a/crates/egui_demo_lib/src/demo/scrolling.rs +++ b/crates/egui_demo_lib/src/demo/scrolling.rs @@ -336,7 +336,7 @@ impl super::View for ScrollTo { ui.separator(); ui.vertical_centered(|ui| { - egui::reset_button(ui, self); + egui::reset_button(ui, self, "Reset"); ui.add(crate::egui_github_link_file!()); }); } diff --git a/crates/egui_demo_lib/src/demo/sliders.rs b/crates/egui_demo_lib/src/demo/sliders.rs index d2b065403c5..4142c7af0e1 100644 --- a/crates/egui_demo_lib/src/demo/sliders.rs +++ b/crates/egui_demo_lib/src/demo/sliders.rs @@ -198,7 +198,7 @@ impl super::View for Sliders { ui.add_space(8.0); ui.vertical_centered(|ui| { - egui::reset_button(ui, self); + egui::reset_button(ui, self, "Reset"); ui.add(crate::egui_github_link_file!()); }); } diff --git a/crates/egui_demo_lib/src/demo/strip_demo.rs b/crates/egui_demo_lib/src/demo/strip_demo.rs index defeac8275a..091d4f01dac 100644 --- a/crates/egui_demo_lib/src/demo/strip_demo.rs +++ b/crates/egui_demo_lib/src/demo/strip_demo.rs @@ -8,7 +8,7 @@ pub struct StripDemo {} impl super::Demo for StripDemo { fn name(&self) -> &'static str { - "▣ Strip Demo" + "▣ Strip" } fn show(&mut self, ctx: &egui::Context, open: &mut bool) { diff --git a/crates/egui_demo_lib/src/demo/table_demo.rs b/crates/egui_demo_lib/src/demo/table_demo.rs index f15d2df9e86..c7029d44e49 100644 --- a/crates/egui_demo_lib/src/demo/table_demo.rs +++ b/crates/egui_demo_lib/src/demo/table_demo.rs @@ -40,7 +40,7 @@ impl Default for TableDemo { impl super::Demo for TableDemo { fn name(&self) -> &'static str { - "☰ Table Demo" + "☰ Table" } fn show(&mut self, ctx: &egui::Context, open: &mut bool) { @@ -136,6 +136,7 @@ impl TableDemo { .size .max(ui.spacing().interact_size.y); + let available_height = ui.available_height(); let mut table = TableBuilder::new(ui) .striped(self.striped) .resizable(self.resizable) @@ -145,7 +146,8 @@ impl TableDemo { .column(Column::initial(100.0).range(40.0..=300.0)) .column(Column::initial(100.0).at_least(40.0).clip(true)) .column(Column::remainder()) - .min_scrolled_height(0.0); + .min_scrolled_height(0.0) + .max_scroll_height(available_height); if self.clickable { table = table.sense(egui::Sense::click()); diff --git a/crates/egui_demo_lib/src/demo/tests.rs b/crates/egui_demo_lib/src/demo/tests.rs index 2dd6ea64a86..6a8348ac56a 100644 --- a/crates/egui_demo_lib/src/demo/tests.rs +++ b/crates/egui_demo_lib/src/demo/tests.rs @@ -131,7 +131,7 @@ impl super::Demo for ManualLayoutTest { impl super::View for ManualLayoutTest { fn ui(&mut self, ui: &mut egui::Ui) { - egui::reset_button(ui, self); + egui::reset_button(ui, self, "Reset"); let Self { widget_offset, @@ -298,7 +298,7 @@ impl super::View for TableTest { }); ui.vertical_centered(|ui| { - egui::reset_button(ui, self); + egui::reset_button(ui, self, "Reset"); ui.add(crate::egui_github_link_file!()); }); } @@ -372,7 +372,7 @@ impl super::Demo for InputTest { .default_width(800.0) .open(open) .resizable(true) - .scroll2(false) + .scroll(false) .show(ctx, |ui| { use super::View as _; self.ui(ui); @@ -466,26 +466,30 @@ fn response_summary(response: &egui::Response, show_hovers: bool) -> String { // These are in inverse logical/chonological order, because we show them in the ui that way: if response.triple_clicked_by(button) { - writeln!(new_info, "Triple-clicked{button_suffix}").ok(); + writeln!(new_info, "Triple_clicked_by{button_suffix}").ok(); } if response.double_clicked_by(button) { - writeln!(new_info, "Double-clicked{button_suffix}").ok(); + writeln!(new_info, "Double_clicked_by{button_suffix}").ok(); } if response.clicked_by(button) { - writeln!(new_info, "Clicked{button_suffix}").ok(); + writeln!(new_info, "Clicked_by{button_suffix}").ok(); } if response.drag_stopped_by(button) { - writeln!(new_info, "Drag stopped{button_suffix}").ok(); + writeln!(new_info, "Drag_stopped_by{button_suffix}").ok(); } if response.dragged_by(button) { - writeln!(new_info, "Dragged{button_suffix}").ok(); + writeln!(new_info, "Dragged_by{button_suffix}").ok(); } if response.drag_started_by(button) { - writeln!(new_info, "Drag started{button_suffix}").ok(); + writeln!(new_info, "Drag_started_by{button_suffix}").ok(); } } + if response.long_touched() { + writeln!(new_info, "Clicked with long-press").ok(); + } + new_info } diff --git a/crates/egui_demo_lib/src/demo/window_options.rs b/crates/egui_demo_lib/src/demo/window_options.rs index baa6eac652e..7d4241dd38e 100644 --- a/crates/egui_demo_lib/src/demo/window_options.rs +++ b/crates/egui_demo_lib/src/demo/window_options.rs @@ -67,7 +67,7 @@ impl super::Demo for WindowOptions { .constrain(constrain) .collapsible(collapsible) .title_bar(title_bar) - .scroll2(scroll2) + .scroll(scroll2) .enabled(enabled); if closable { window = window.open(open); @@ -146,7 +146,7 @@ impl super::View for WindowOptions { if ui.button("Disable for 2 seconds").clicked() { self.disabled_time = ui.input(|i| i.time); } - egui::reset_button(ui, self); + egui::reset_button(ui, self, "Reset"); ui.add(crate::egui_github_link_file!()); }); } diff --git a/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs b/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs index 0971abd45b2..be2650cb03e 100644 --- a/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs +++ b/crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs @@ -45,10 +45,10 @@ impl EasyMarkEditor { pub fn ui(&mut self, ui: &mut egui::Ui) { egui::Grid::new("controls").show(ui, |ui| { - let _ = ui.button("Hotkeys").on_hover_ui(nested_hotkeys_ui); + let _response = ui.button("Hotkeys").on_hover_ui(nested_hotkeys_ui); ui.checkbox(&mut self.show_rendered, "Show rendered"); ui.checkbox(&mut self.highlight_editor, "Highlight editor"); - egui::reset_button(ui, self); + egui::reset_button(ui, self, "Reset"); ui.end_row(); }); ui.separator(); @@ -261,7 +261,7 @@ The style characters are chosen to be similar to what they are representing: `$` = $small$ `^` = ^raised^ -# TODO +# To do - Sub-headers (`## h2`, `### h3` etc) - Hotkey Editor - International keyboard algorithm for non-letter keys diff --git a/crates/egui_demo_lib/src/easy_mark/easy_mark_highlighter.rs b/crates/egui_demo_lib/src/easy_mark/easy_mark_highlighter.rs index 7431accc504..5c6bc2ef086 100644 --- a/crates/egui_demo_lib/src/easy_mark/easy_mark_highlighter.rs +++ b/crates/egui_demo_lib/src/easy_mark/easy_mark_highlighter.rs @@ -14,7 +14,7 @@ impl MemoizedEasymarkHighlighter { pub fn highlight(&mut self, egui_style: &egui::Style, code: &str) -> egui::text::LayoutJob { if (&self.style, self.code.as_str()) != (egui_style, code) { self.style = egui_style.clone(); - self.code = code.to_owned(); + code.clone_into(&mut self.code); self.output = highlight_easymark(egui_style, code); } self.output.clone() diff --git a/crates/egui_demo_lib/src/lib.rs b/crates/egui_demo_lib/src/lib.rs index cf31ee8d43b..68bdc8eed85 100644 --- a/crates/egui_demo_lib/src/lib.rs +++ b/crates/egui_demo_lib/src/lib.rs @@ -10,15 +10,13 @@ #![allow(clippy::float_cmp)] #![allow(clippy::manual_range_contains)] -#![cfg_attr(feature = "puffin", deny(unsafe_code))] -#![cfg_attr(not(feature = "puffin"), forbid(unsafe_code))] -mod color_test; mod demo; pub mod easy_mark; +mod rendering_test; -pub use color_test::ColorTest; pub use demo::{DemoWindows, WidgetGallery}; +pub use rendering_test::ColorTest; /// View some Rust code with syntax highlighting and selection. pub(crate) fn rust_view_ui(ui: &mut egui::Ui, code: &str) { diff --git a/crates/egui_demo_lib/src/color_test.rs b/crates/egui_demo_lib/src/rendering_test.rs similarity index 93% rename from crates/egui_demo_lib/src/color_test.rs rename to crates/egui_demo_lib/src/rendering_test.rs index ebbb3076ffc..2aa6194eea1 100644 --- a/crates/egui_demo_lib/src/color_test.rs +++ b/crates/egui_demo_lib/src/rendering_test.rs @@ -31,17 +31,43 @@ impl Default for ColorTest { impl ColorTest { pub fn ui(&mut self, ui: &mut Ui) { - ui.set_max_width(680.0); - ui.vertical_centered(|ui| { ui.add(crate::egui_github_link_file!()); }); ui.horizontal_wrapped(|ui|{ - ui.label("This is made to test that the egui painter backend is set up correctly."); + ui.label("This is made to test that the egui rendering backend is set up correctly."); ui.add(egui::Label::new("❓").sense(egui::Sense::click())) .on_hover_text("The texture sampling should be sRGB-aware, and every other color operation should be done in gamma-space (sRGB). All colors should use pre-multiplied alpha"); }); + + ui.separator(); + + pixel_test(ui); + + ui.separator(); + + ui.collapsing("Color test", |ui| { + self.color_test(ui); + }); + + ui.separator(); + + ui.heading("Text rendering"); + + text_on_bg(ui, Color32::from_gray(200), Color32::from_gray(230)); // gray on gray + text_on_bg(ui, Color32::from_gray(140), Color32::from_gray(28)); // dark mode normal text + + // Matches Mac Font book (useful for testing): + text_on_bg(ui, Color32::from_gray(39), Color32::from_gray(255)); + text_on_bg(ui, Color32::from_gray(220), Color32::from_gray(30)); + + ui.separator(); + + blending_and_feathering_test(ui); + } + + fn color_test(&mut self, ui: &mut Ui) { ui.label("If the rendering is done right, all groups of gradients will look uniform."); ui.horizontal(|ui| { @@ -134,24 +160,6 @@ impl ColorTest { ui.label("Linear interpolation (texture sampling):"); self.show_gradients(ui, WHITE, (RED, GREEN), Interpolation::Linear); - - ui.separator(); - - pixel_test(ui); - - ui.separator(); - ui.label("Testing text rendering:"); - - text_on_bg(ui, Color32::from_gray(200), Color32::from_gray(230)); // gray on gray - text_on_bg(ui, Color32::from_gray(140), Color32::from_gray(28)); // dark mode normal text - - // Matches Mac Font book (useful for testing): - text_on_bg(ui, Color32::from_gray(39), Color32::from_gray(255)); - text_on_bg(ui, Color32::from_gray(220), Color32::from_gray(30)); - - ui.separator(); - - blending_and_feathering_test(ui); } fn show_gradients( @@ -385,8 +393,17 @@ impl TextureManager { } } -fn pixel_test(ui: &mut Ui) { - ui.label("Each subsequent square should be one physical pixel larger than the previous. They should be exactly one physical pixel apart. They should be perfectly aligned to the pixel grid."); +/// A visual test that the rendering is correctly aligned on the physical pixel grid. +/// +/// Requires eyes and a magnifying glass to verify. +pub fn pixel_test(ui: &mut Ui) { + ui.heading("Pixel alignment test"); + ui.label("The first square should be exactly one physical pixel big."); + ui.label("They should be exactly one physical pixel apart."); + ui.label("Each subsequent square should be one physical pixel larger than the previous."); + ui.label("They should be perfectly aligned to the physical pixel grid."); + ui.label("If these squares are blurry, everything will be blurry, including text."); + ui.label("You might need a magnifying glass to check this test."); let color = if ui.style().visuals.dark_mode { egui::Color32::WHITE @@ -395,7 +412,7 @@ fn pixel_test(ui: &mut Ui) { }; let pixels_per_point = ui.ctx().pixels_per_point(); - let num_squares: u32 = 8; + let num_squares = (pixels_per_point * 10.0).round().max(10.0) as u32; let size_pixels = vec2( ((num_squares + 1) * (num_squares + 2) / 2) as f32, num_squares as f32, @@ -422,6 +439,10 @@ fn pixel_test(ui: &mut Ui) { } fn blending_and_feathering_test(ui: &mut Ui) { + ui.label("The left side shows how lines of different widths look."); + ui.label("The right side tests text rendering at different opacities and sizes."); + ui.label("The top and bottom images should look symmetrical in their intensities."); + let size = vec2(512.0, 512.0); let (response, painter) = ui.allocate_painter(size, Sense::hover()); let rect = response.rect; diff --git a/crates/egui_extras/CHANGELOG.md b/crates/egui_extras/CHANGELOG.md index 5863612d76a..cc00e7e2324 100644 --- a/crates/egui_extras/CHANGELOG.md +++ b/crates/egui_extras/CHANGELOG.md @@ -5,6 +5,24 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.27.2 - 2024-04-02 +* Nothing new + + +## 0.27.1 - 2024-03-29 +* Nothing new + + +## 0.27.0 - 2024-03-26 +* Add scroll bar visibility option to `Table` widget [#3981](https://github.com/emilk/egui/pull/3981) (thanks [@richardhozak](https://github.com/richardhozak)!) +* Update `ehttp` to 0.5 [#4055](https://github.com/emilk/egui/pull/4055) +* Fix: assign a different id to each table cell, avoiding id clashes [#4076](https://github.com/emilk/egui/pull/4076) +* Fix interaction with widgets inside selectable rows of `Table` [#4077](https://github.com/emilk/egui/pull/4077) +* Fixed handling of `file://` protocol for images [#4107](https://github.com/emilk/egui/pull/4107) (thanks [@varphone](https://github.com/varphone)!) +* Option to change date picker format [#4180](https://github.com/emilk/egui/pull/4180) (thanks [@zaaarf](https://github.com/zaaarf)!) +* Added ability to disable highlighting of weekend days in `DatePickerPopup`. [#4151](https://github.com/emilk/egui/pull/4151) (thanks [@hiyosilver](https://github.com/hiyosilver)!) + + ## 0.26.2 - 2024-02-14 * Nothing new diff --git a/crates/egui_extras/Cargo.toml b/crates/egui_extras/Cargo.toml index be44ca07c6f..169e94c8b6c 100644 --- a/crates/egui_extras/Cargo.toml +++ b/crates/egui_extras/Cargo.toml @@ -17,6 +17,9 @@ categories = ["gui", "game-development"] keywords = ["gui", "imgui", "immediate", "portable", "gamedev"] include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +[lints] +workspace = true + [package.metadata.docs.rs] all-features = true diff --git a/crates/egui_extras/src/datepicker/button.rs b/crates/egui_extras/src/datepicker/button.rs index 5aa6eaeadfb..839adebfc47 100644 --- a/crates/egui_extras/src/datepicker/button.rs +++ b/crates/egui_extras/src/datepicker/button.rs @@ -16,6 +16,8 @@ pub struct DatePickerButton<'a> { calendar: bool, calendar_week: bool, show_icon: bool, + format: String, + highlight_weekends: bool, } impl<'a> DatePickerButton<'a> { @@ -28,6 +30,8 @@ impl<'a> DatePickerButton<'a> { calendar: true, calendar_week: true, show_icon: true, + format: "%Y-%m-%d".to_owned(), + highlight_weekends: true, } } @@ -73,6 +77,21 @@ impl<'a> DatePickerButton<'a> { self.show_icon = show_icon; self } + + /// Change the format shown on the button. (Default: %Y-%m-%d) + /// See [`chrono::format::strftime`] for valid formats. + #[inline] + pub fn format(mut self, format: impl Into) -> Self { + self.format = format.into(); + self + } + + /// Highlight weekend days. (Default: true) + #[inline] + pub fn highlight_weekends(mut self, highlight_weekends: bool) -> Self { + self.highlight_weekends = highlight_weekends; + self + } } impl<'a> Widget for DatePickerButton<'a> { @@ -83,9 +102,9 @@ impl<'a> Widget for DatePickerButton<'a> { .unwrap_or_default(); let mut text = if self.show_icon { - RichText::new(format!("{} 📆", self.selection.format("%Y-%m-%d"))) + RichText::new(format!("{} 📆", self.selection.format(&self.format))) } else { - RichText::new(format!("{}", self.selection.format("%Y-%m-%d"))) + RichText::new(format!("{}", self.selection.format(&self.format))) }; let visuals = ui.visuals().widgets.open; if button_state.picker_visible { @@ -138,6 +157,7 @@ impl<'a> Widget for DatePickerButton<'a> { arrows: self.arrows, calendar: self.calendar, calendar_week: self.calendar_week, + highlight_weekends: self.highlight_weekends, } .draw(ui) }) diff --git a/crates/egui_extras/src/datepicker/popup.rs b/crates/egui_extras/src/datepicker/popup.rs index a787d3c55ba..c5bd41d59f5 100644 --- a/crates/egui_extras/src/datepicker/popup.rs +++ b/crates/egui_extras/src/datepicker/popup.rs @@ -33,6 +33,7 @@ pub(crate) struct DatePickerPopup<'a> { pub arrows: bool, pub calendar: bool, pub calendar_week: bool, + pub highlight_weekends: bool, } impl<'a> DatePickerPopup<'a> { @@ -304,8 +305,9 @@ impl<'a> DatePickerPopup<'a> { && popup_state.day == day.day() { ui.visuals().selection.bg_fill - } else if day.weekday() == Weekday::Sat - || day.weekday() == Weekday::Sun + } else if (day.weekday() == Weekday::Sat + || day.weekday() == Weekday::Sun) + && self.highlight_weekends { if ui.visuals().dark_mode { Color32::DARK_RED diff --git a/crates/egui_extras/src/lib.rs b/crates/egui_extras/src/lib.rs index e899106cd45..381242c90b5 100644 --- a/crates/egui_extras/src/lib.rs +++ b/crates/egui_extras/src/lib.rs @@ -8,8 +8,6 @@ #![allow(clippy::float_cmp)] #![allow(clippy::manual_range_contains)] -#![cfg_attr(feature = "puffin", deny(unsafe_code))] -#![cfg_attr(not(feature = "puffin"), forbid(unsafe_code))] #[cfg(feature = "chrono")] mod datepicker; diff --git a/crates/egui_extras/src/loaders.rs b/crates/egui_extras/src/loaders.rs index 41ecfab7af2..d66ea483071 100644 --- a/crates/egui_extras/src/loaders.rs +++ b/crates/egui_extras/src/loaders.rs @@ -1,4 +1,4 @@ -// TODO: automatic cache eviction +// TODO(jprochazk): automatic cache eviction /// Installs a set of image loaders. /// diff --git a/crates/egui_extras/src/sizing.rs b/crates/egui_extras/src/sizing.rs index 1ff7a1a5363..2c380ae6179 100644 --- a/crates/egui_extras/src/sizing.rs +++ b/crates/egui_extras/src/sizing.rs @@ -32,7 +32,7 @@ impl Size { /// Relative size relative to all available space. Values must be in range `0.0..=1.0`. pub fn relative(fraction: f32) -> Self { - egui::egui_assert!(0.0 <= fraction && fraction <= 1.0); + debug_assert!(0.0 <= fraction && fraction <= 1.0); Self::Relative { fraction, range: Rangef::new(0.0, f32::INFINITY), diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index 5e9cf80fdcd..0cbf5e13b5f 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -131,7 +131,7 @@ impl Column { self } - /// Allowed range of movement (in points), if in a resizable [`Table`](crate::table::Table). + /// Allowed range of movement (in points), if in a resizable [`Table`]. #[inline] pub fn range(mut self, range: impl Into) -> Self { self.width_range = range.into(); diff --git a/crates/egui_glow/CHANGELOG.md b/crates/egui_glow/CHANGELOG.md index abb7a71292a..627b1c2ebdb 100644 --- a/crates/egui_glow/CHANGELOG.md +++ b/crates/egui_glow/CHANGELOG.md @@ -6,6 +6,19 @@ Changes since the last release can be found at ) -> Self { - use egui::NumExt; use glutin::context::NotCurrentGlContext; use glutin::display::GetGlDisplay; use glutin::display::GlDisplay; @@ -87,8 +91,8 @@ impl GlutinWindowContext { .expect("failed to finalize glutin window") }); 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 width = NonZeroU32::new(width).unwrap_or(NonZeroU32::MIN); + let height = NonZeroU32::new(height).unwrap_or(NonZeroU32::MIN); let surface_attributes = glutin::surface::SurfaceAttributesBuilder::::new() .build(window.raw_window_handle(), width, height); @@ -107,7 +111,7 @@ impl GlutinWindowContext { gl_surface .set_swap_interval( &gl_context, - glutin::surface::SwapInterval::Wait(std::num::NonZeroU32::new(1).unwrap()), + glutin::surface::SwapInterval::Wait(NonZeroU32::MIN), ) .unwrap(); diff --git a/crates/egui_glow/src/lib.rs b/crates/egui_glow/src/lib.rs index b12d2ff5f61..cfb95acd3d7 100644 --- a/crates/egui_glow/src/lib.rs +++ b/crates/egui_glow/src/lib.rs @@ -10,6 +10,7 @@ #![allow(clippy::float_cmp)] #![allow(clippy::manual_range_contains)] +#![allow(clippy::undocumented_unsafe_blocks)] pub mod painter; pub use glow; @@ -20,9 +21,9 @@ mod vao; pub use shader_version::ShaderVersion; -#[cfg(all(not(target_arch = "wasm32"), feature = "winit"))] +#[cfg(feature = "winit")] pub mod winit; -#[cfg(all(not(target_arch = "wasm32"), feature = "winit"))] +#[cfg(feature = "winit")] pub use winit::*; /// Check for OpenGL error and report it using `log::error`. diff --git a/crates/egui_glow/src/shader_version.rs b/crates/egui_glow/src/shader_version.rs index c59792a0aba..77c1e63cea9 100644 --- a/crates/egui_glow/src/shader_version.rs +++ b/crates/egui_glow/src/shader_version.rs @@ -1,4 +1,5 @@ #![allow(unsafe_code)] +#![allow(clippy::undocumented_unsafe_blocks)] use std::convert::TryInto; diff --git a/crates/egui_glow/src/winit.rs b/crates/egui_glow/src/winit.rs index 5d87d000416..0c407ccb75d 100644 --- a/crates/egui_glow/src/winit.rs +++ b/crates/egui_glow/src/winit.rs @@ -1,7 +1,7 @@ pub use egui_winit; pub use egui_winit::EventResponse; -use egui::{ViewportId, ViewportOutput}; +use egui::{ahash::HashSet, ViewportId, ViewportOutput}; use egui_winit::winit; use crate::shader_version::ShaderVersion; @@ -79,17 +79,17 @@ impl EguiGlow { log::warn!("Multiple viewports not yet supported by EguiGlow"); } for (_, ViewportOutput { commands, .. }) in viewport_output { - let mut screenshot_requested = false; + let mut actions_requested: HashSet = Default::default(); egui_winit::process_viewport_commands( &self.egui_ctx, &mut self.viewport_info, commands, window, true, - &mut screenshot_requested, + &mut actions_requested, ); - if screenshot_requested { - log::warn!("Screenshot not yet supported by EguiGlow"); + for action in actions_requested { + log::warn!("{:?} not yet supported by EguiGlow", action); } } diff --git a/crates/egui_plot/CHANGELOG.md b/crates/egui_plot/CHANGELOG.md index 19095a416bd..46f3d76fa78 100644 --- a/crates/egui_plot/CHANGELOG.md +++ b/crates/egui_plot/CHANGELOG.md @@ -5,6 +5,23 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.27.2 - 2024-04-02 +* Allow zoom/pan a plot as long as it contains the mouse cursor [#4292](https://github.com/emilk/egui/pull/4292) +* Prevent plot from resetting one axis while zooming/dragging the other [#4252](https://github.com/emilk/egui/pull/4252) (thanks [@YgorSouza](https://github.com/YgorSouza)!) +* egui_plot: Fix the same plot tick label being painted multiple times [#4307](https://github.com/emilk/egui/pull/4307) + + +## 0.27.1 - 2024-03-29 +* Nothing new + + +## 0.27.0 - 2024-03-26 +* Add `sense` option to `Plot` [#4052](https://github.com/emilk/egui/pull/4052) (thanks [@AmesingFlank](https://github.com/AmesingFlank)!) +* Plot widget - allow disabling scroll for x and y separately [#4051](https://github.com/emilk/egui/pull/4051) (thanks [@YgorSouza](https://github.com/YgorSouza)!) +* Fix panic when the base step size is set to 0 [#4078](https://github.com/emilk/egui/pull/4078) (thanks [@abey79](https://github.com/abey79)!) +* Expose `PlotGeometry` in public API [#4193](https://github.com/emilk/egui/pull/4193) (thanks [@dwuertz](https://github.com/dwuertz)!) + + ## 0.26.2 - 2024-02-14 * Nothing new diff --git a/crates/egui_plot/Cargo.toml b/crates/egui_plot/Cargo.toml index 61faf7432c0..91c4b3a50fc 100644 --- a/crates/egui_plot/Cargo.toml +++ b/crates/egui_plot/Cargo.toml @@ -17,6 +17,9 @@ categories = ["visualization", "gui"] keywords = ["egui", "plot", "plotting"] include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +[lints] +workspace = true + [package.metadata.docs.rs] all-features = true diff --git a/crates/egui_plot/src/axis.rs b/crates/egui_plot/src/axis.rs index df182631c1b..3c2972e6d87 100644 --- a/crates/egui_plot/src/axis.rs +++ b/crates/egui_plot/src/axis.rs @@ -106,7 +106,7 @@ pub struct AxisHints { pub(super) label_spacing: Rangef, } -// TODO: this just a guess. It might cease to work if a user changes font size. +// TODO(JohannesProgrammiert): this just a guess. It might cease to work if a user changes font size. const LINE_HEIGHT: f32 = 12.0; impl AxisHints { @@ -366,7 +366,7 @@ impl AxisWidget { match HPlacement::from(self.hints.placement) { HPlacement::Left => { - let angle = 0.0; // TODO: allow users to rotate text + let angle = 0.0; // TODO(emilk): allow users to rotate text if angle == 0.0 { let x = self.rect.max.x - galley.size().x; diff --git a/crates/egui_plot/src/items/mod.rs b/crates/egui_plot/src/items/mod.rs index 122189d2264..56098b36699 100644 --- a/crates/egui_plot/src/items/mod.rs +++ b/crates/egui_plot/src/items/mod.rs @@ -3,17 +3,18 @@ use std::ops::RangeInclusive; -use epaint::{emath::Rot2, util::FloatOrd, Mesh}; +use epaint::{emath::Rot2, Mesh}; use crate::*; use super::{Cursor, LabelFormatter, PlotBounds, PlotTransform}; use rect_elem::*; -use values::{ClosestElem, PlotGeometry}; pub use bar::Bar; pub use box_elem::{BoxElem, BoxSpread}; -pub use values::{LineStyle, MarkerShape, Orientation, PlotPoint, PlotPoints}; +pub use values::{ + ClosestElem, LineStyle, MarkerShape, Orientation, PlotGeometry, PlotPoint, PlotPoints, +}; mod bar; mod box_elem; @@ -45,6 +46,9 @@ pub trait PlotItem { fn highlighted(&self) -> bool; + /// Can the user hover this item? + fn allow_hover(&self) -> bool; + fn geometry(&self) -> PlotGeometry<'_>; fn bounds(&self) -> PlotBounds; @@ -121,6 +125,7 @@ pub struct HLine { pub(super) stroke: Stroke, pub(super) name: String, pub(super) highlight: bool, + pub(super) allow_hover: bool, pub(super) style: LineStyle, id: Option, } @@ -132,6 +137,7 @@ impl HLine { stroke: Stroke::new(1.0, Color32::TRANSPARENT), name: String::default(), highlight: false, + allow_hover: true, style: LineStyle::Solid, id: None, } @@ -144,6 +150,13 @@ impl HLine { self } + /// Allowed hovering this item in the plot. Default: `true`. + #[inline] + pub fn allow_hover(mut self, hovering: bool) -> Self { + self.allow_hover = hovering; + self + } + /// Add a stroke. #[inline] pub fn stroke(mut self, stroke: impl Into) -> Self { @@ -233,6 +246,10 @@ impl PlotItem for HLine { self.highlight } + fn allow_hover(&self) -> bool { + self.allow_hover + } + fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::None } @@ -256,6 +273,7 @@ pub struct VLine { pub(super) stroke: Stroke, pub(super) name: String, pub(super) highlight: bool, + pub(super) allow_hover: bool, pub(super) style: LineStyle, id: Option, } @@ -267,6 +285,7 @@ impl VLine { stroke: Stroke::new(1.0, Color32::TRANSPARENT), name: String::default(), highlight: false, + allow_hover: true, style: LineStyle::Solid, id: None, } @@ -279,6 +298,13 @@ impl VLine { self } + /// Allowed hovering this item in the plot. Default: `true`. + #[inline] + pub fn allow_hover(mut self, hovering: bool) -> Self { + self.allow_hover = hovering; + self + } + /// Add a stroke. #[inline] pub fn stroke(mut self, stroke: impl Into) -> Self { @@ -368,6 +394,10 @@ impl PlotItem for VLine { self.highlight } + fn allow_hover(&self) -> bool { + self.allow_hover + } + fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::None } @@ -390,6 +420,7 @@ pub struct Line { pub(super) stroke: Stroke, pub(super) name: String, pub(super) highlight: bool, + pub(super) allow_hover: bool, pub(super) fill: Option, pub(super) style: LineStyle, id: Option, @@ -402,6 +433,7 @@ impl Line { stroke: Stroke::new(1.5, Color32::TRANSPARENT), // Note: a stroke of 1.0 (or less) can look bad on low-dpi-screens name: Default::default(), highlight: false, + allow_hover: true, fill: None, style: LineStyle::Solid, id: None, @@ -415,6 +447,13 @@ impl Line { self } + /// Allowed hovering this item in the plot. Default: `true`. + #[inline] + pub fn allow_hover(mut self, hovering: bool) -> Self { + self.allow_hover = hovering; + self + } + /// Add a stroke. #[inline] pub fn stroke(mut self, stroke: impl Into) -> Self { @@ -558,6 +597,10 @@ impl PlotItem for Line { self.highlight } + fn allow_hover(&self) -> bool { + self.allow_hover + } + fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::Points(self.series.points()) } @@ -577,6 +620,7 @@ pub struct Polygon { pub(super) stroke: Stroke, pub(super) name: String, pub(super) highlight: bool, + pub(super) allow_hover: bool, pub(super) fill_color: Option, pub(super) style: LineStyle, id: Option, @@ -589,6 +633,7 @@ impl Polygon { stroke: Stroke::new(1.0, Color32::TRANSPARENT), name: Default::default(), highlight: false, + allow_hover: true, fill_color: None, style: LineStyle::Solid, id: None, @@ -603,6 +648,13 @@ impl Polygon { self } + /// Allowed hovering this item in the plot. Default: `true`. + #[inline] + pub fn allow_hover(mut self, hovering: bool) -> Self { + self.allow_hover = hovering; + self + } + /// Add a custom stroke. #[inline] pub fn stroke(mut self, stroke: impl Into) -> Self { @@ -697,6 +749,10 @@ impl PlotItem for Polygon { self.highlight } + fn allow_hover(&self) -> bool { + self.allow_hover + } + fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::Points(self.series.points()) } @@ -717,6 +773,7 @@ pub struct Text { pub(super) position: PlotPoint, pub(super) name: String, pub(super) highlight: bool, + pub(super) allow_hover: bool, pub(super) color: Color32, pub(super) anchor: Align2, id: Option, @@ -729,6 +786,7 @@ impl Text { position, name: Default::default(), highlight: false, + allow_hover: true, color: Color32::TRANSPARENT, anchor: Align2::CENTER_CENTER, id: None, @@ -742,6 +800,13 @@ impl Text { self } + /// Allowed hovering this item in the plot. Default: `true`. + #[inline] + pub fn allow_hover(mut self, hovering: bool) -> Self { + self.allow_hover = hovering; + self + } + /// Text color. #[inline] pub fn color(mut self, color: impl Into) -> Self { @@ -822,6 +887,10 @@ impl PlotItem for Text { self.highlight } + fn allow_hover(&self) -> bool { + self.allow_hover + } + fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::None } @@ -856,6 +925,8 @@ pub struct Points { pub(super) highlight: bool, + pub(super) allow_hover: bool, + pub(super) stems: Option, id: Option, } @@ -870,6 +941,7 @@ impl Points { radius: 1.0, name: Default::default(), highlight: false, + allow_hover: true, stems: None, id: None, } @@ -889,6 +961,13 @@ impl Points { self } + /// Allowed hovering this item in the plot. Default: `true`. + #[inline] + pub fn allow_hover(mut self, hovering: bool) -> Self { + self.allow_hover = hovering; + self + } + /// Set the marker's color. #[inline] pub fn color(mut self, color: impl Into) -> Self { @@ -1087,6 +1166,10 @@ impl PlotItem for Points { self.highlight } + fn allow_hover(&self) -> bool { + self.allow_hover + } + fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::Points(self.series.points()) } @@ -1108,6 +1191,7 @@ pub struct Arrows { pub(super) color: Color32, pub(super) name: String, pub(super) highlight: bool, + pub(super) allow_hover: bool, id: Option, } @@ -1120,6 +1204,7 @@ impl Arrows { color: Color32::TRANSPARENT, name: Default::default(), highlight: false, + allow_hover: true, id: None, } } @@ -1131,6 +1216,13 @@ impl Arrows { self } + /// Allowed hovering this item in the plot. Default: `true`. + #[inline] + pub fn allow_hover(mut self, hovering: bool) -> Self { + self.allow_hover = hovering; + self + } + /// Set the length of the arrow tips #[inline] pub fn tip_length(mut self, tip_length: f32) -> Self { @@ -1232,6 +1324,10 @@ impl PlotItem for Arrows { self.highlight } + fn allow_hover(&self) -> bool { + self.allow_hover + } + fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::Points(self.origins.points()) } @@ -1256,6 +1352,7 @@ pub struct PlotImage { pub(super) bg_fill: Color32, pub(super) tint: Color32, pub(super) highlight: bool, + pub(super) allow_hover: bool, pub(super) name: String, id: Option, } @@ -1271,6 +1368,7 @@ impl PlotImage { position: center_position, name: Default::default(), highlight: false, + allow_hover: true, texture_id: texture_id.into(), uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), size: size.into(), @@ -1288,6 +1386,13 @@ impl PlotImage { self } + /// Allowed hovering this item in the plot. Default: `true`. + #[inline] + pub fn allow_hover(mut self, hovering: bool) -> Self { + self.allow_hover = hovering; + self + } + /// Select UV range. Default is (0,0) in top-left, (1,1) bottom right. #[inline] pub fn uv(mut self, uv: impl Into) -> Self { @@ -1407,6 +1512,10 @@ impl PlotItem for PlotImage { self.highlight } + fn allow_hover(&self) -> bool { + self.allow_hover + } + fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::None } @@ -1443,6 +1552,7 @@ pub struct BarChart { pub(super) element_formatter: Option String>>, highlight: bool, + allow_hover: bool, id: Option, } @@ -1455,6 +1565,7 @@ impl BarChart { name: String::new(), element_formatter: None, highlight: false, + allow_hover: true, id: None, } } @@ -1523,6 +1634,13 @@ impl BarChart { self } + /// Allowed hovering this item in the plot. Default: `true`. + #[inline] + pub fn allow_hover(mut self, hovering: bool) -> Self { + self.allow_hover = hovering; + self + } + /// Add a custom way to format an element. /// Can be used to display a set number of decimals or custom labels. #[inline] @@ -1591,6 +1709,10 @@ impl PlotItem for BarChart { self.highlight } + fn allow_hover(&self) -> bool { + self.allow_hover + } + fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::Rects } @@ -1636,6 +1758,7 @@ pub struct BoxPlot { pub(super) element_formatter: Option String>>, highlight: bool, + allow_hover: bool, id: Option, } @@ -1648,6 +1771,7 @@ impl BoxPlot { name: String::new(), element_formatter: None, highlight: false, + allow_hover: true, id: None, } } @@ -1709,6 +1833,13 @@ impl BoxPlot { self } + /// Allowed hovering this item in the plot. Default: `true`. + #[inline] + pub fn allow_hover(mut self, hovering: bool) -> Self { + self.allow_hover = hovering; + self + } + /// Add a custom way to format an element. /// Can be used to display a set number of decimals or custom labels. #[inline] @@ -1752,6 +1883,10 @@ impl PlotItem for BoxPlot { self.highlight } + fn allow_hover(&self) -> bool { + self.allow_hover + } + fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::Rects } diff --git a/crates/egui_plot/src/items/values.rs b/crates/egui_plot/src/items/values.rs index 8ed6d38f333..6e9bff096b6 100644 --- a/crates/egui_plot/src/items/values.rs +++ b/crates/egui_plot/src/items/values.rs @@ -123,12 +123,12 @@ impl LineStyle { } } -impl ToString for LineStyle { - fn to_string(&self) -> String { +impl std::fmt::Display for LineStyle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Solid => "Solid".into(), - Self::Dotted { spacing } => format!("Dotted{spacing}Px"), - Self::Dashed { length } => format!("Dashed{length}Px"), + Self::Solid => write!(f, "Solid"), + Self::Dotted { spacing } => write!(f, "Dotted({spacing} px)"), + Self::Dashed { length } => write!(f, "Dashed({length} px)"), } } } @@ -156,7 +156,7 @@ impl Default for Orientation { pub enum PlotPoints { Owned(Vec), Generator(ExplicitGenerator), - // Borrowed(&[PlotPoint]), // TODO: Lifetimes are tricky in this case. + // Borrowed(&[PlotPoint]), // TODO(EmbersArc): Lifetimes are tricky in this case. } impl Default for PlotPoints { @@ -426,9 +426,9 @@ impl ExplicitGenerator { /// Result of [`super::PlotItem::find_closest()`] search, identifies an element inside the item for immediate use pub struct ClosestElem { - /// Position of hovered-over value (or bar/box-plot/...) in PlotItem + /// Position of hovered-over value (or bar/box-plot/...) in `PlotItem` pub index: usize, - /// Squared distance from the mouse cursor (needed to compare against other PlotItems, which might be nearer) + /// Squared distance from the mouse cursor (needed to compare against other `PlotItems`, which might be nearer) pub dist_sq: f32, } diff --git a/crates/egui_plot/src/lib.rs b/crates/egui_plot/src/lib.rs index 4238c7b3258..6b9d622d6e3 100644 --- a/crates/egui_plot/src/lib.rs +++ b/crates/egui_plot/src/lib.rs @@ -13,17 +13,19 @@ mod memory; mod plot_ui; mod transform; -use std::{ops::RangeInclusive, sync::Arc}; +use std::{cmp::Ordering, ops::RangeInclusive, sync::Arc}; use egui::ahash::HashMap; use egui::*; -use epaint::{util::FloatOrd, Hsva}; +use emath::Float as _; +use epaint::Hsva; pub use crate::{ axis::{Axis, AxisHints, HPlacement, Placement, VPlacement}, items::{ - Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, HLine, Line, LineStyle, MarkerShape, - Orientation, PlotImage, PlotItem, PlotPoint, PlotPoints, Points, Polygon, Text, VLine, + Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, ClosestElem, HLine, Line, LineStyle, + MarkerShape, Orientation, PlotConfig, PlotGeometry, PlotImage, PlotItem, PlotPoint, + PlotPoints, Points, Polygon, Text, VLine, }, legend::{Corner, Legend}, memory::PlotMemory, @@ -740,7 +742,7 @@ impl Plot { margin_fraction, width, height, - min_size, + mut min_size, data_aspect, view_aspect, mut show_x, @@ -764,8 +766,17 @@ impl Plot { sense, } = self; + // Disable interaction if ui is disabled. + let allow_zoom = allow_zoom.and(ui.is_enabled()); + let allow_drag = allow_drag.and(ui.is_enabled()); + let allow_scroll = allow_scroll.and(ui.is_enabled()); + // Determine position of widget. let pos = ui.available_rect_before_wrap().min; + // Minimum values for screen protection + min_size.x = min_size.x.at_least(1.0); + min_size.y = min_size.y.at_least(1.0); + // Determine size of widget. let size = { let width = width @@ -799,7 +810,7 @@ impl Plot { let plot_id = id.unwrap_or_else(|| ui.make_persistent_id(id_source)); let ([x_axis_widgets, y_axis_widgets], plot_rect) = axis_widgets( - PlotMemory::load(ui.ctx(), plot_id).as_ref(), // TODO: avoid loading plot memory twice + PlotMemory::load(ui.ctx(), plot_id).as_ref(), // TODO(emilk): avoid loading plot memory twice show_axes, complete_rect, [&x_axes, &y_axes], @@ -1020,7 +1031,7 @@ impl Plot { delta.y = 0.0; } mem.transform.translate_bounds(delta); - mem.auto_bounds = !allow_drag; + mem.auto_bounds = mem.auto_bounds.and(!allow_drag); } // Zooming @@ -1075,8 +1086,13 @@ impl Plot { } } - let hover_pos = response.hover_pos(); - if let Some(hover_pos) = hover_pos { + // Note: we catch zoom/pan if the response contains the pointer, even if it isn't hovered. + // For instance: The user is painting another interactive widget on top of the plot + // but they still want to be able to pan/zoom the plot. + if let (true, Some(hover_pos)) = ( + response.contains_pointer, + ui.input(|i| i.pointer.hover_pos()), + ) { if allow_zoom.any() { let mut zoom_factor = if data_aspect.is_some() { Vec2::splat(ui.input(|i| i.zoom_delta())) @@ -1091,7 +1107,7 @@ impl Plot { } if zoom_factor != Vec2::splat(1.0) { mem.transform.zoom(zoom_factor, hover_pos); - mem.auto_bounds = !allow_zoom; + mem.auto_bounds = mem.auto_bounds.and(!allow_zoom); } } if allow_scroll.any() { @@ -1648,12 +1664,15 @@ impl PreparedPlot { let interact_radius_sq = (16.0_f32).powi(2); - let candidates = items.iter().filter_map(|item| { - let item = &**item; - let closest = item.find_closest(pointer, transform); + let candidates = items + .iter() + .filter(|entry| entry.allow_hover()) + .filter_map(|item| { + let item = &**item; + let closest = item.find_closest(pointer, transform); - Some(item).zip(closest) - }); + Some(item).zip(closest) + }); let closest = candidates .min_by_key(|(_, elem)| elem.dist_sq.ord()) @@ -1708,9 +1727,31 @@ fn generate_marks(step_sizes: [f64; 3], bounds: (f64, f64)) -> Vec { fill_marks_between(&mut steps, step_sizes[0], bounds); fill_marks_between(&mut steps, step_sizes[1], bounds); fill_marks_between(&mut steps, step_sizes[2], bounds); + + // Remove duplicates: + // This can happen because we have overlapping steps, e.g.: + // step_size[0] = 10 => [-10, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120] + // step_size[1] = 100 => [ 0, 100 ] + // step_size[2] = 1000 => [ 0 ] + + steps.sort_by(|a, b| match cmp_f64(a.value, b.value) { + // Keep the largest step size when we dedup later + Ordering::Equal => cmp_f64(b.step_size, a.step_size), + + ord => ord, + }); + steps.dedup_by(|a, b| a.value == b.value); + steps } +fn cmp_f64(a: f64, b: f64) -> Ordering { + match a.partial_cmp(&b) { + Some(ord) => ord, + None => a.is_nan().cmp(&b.is_nan()), + } +} + /// Fill in all values between [min, max] which are a multiple of `step_size` fn fill_marks_between(out: &mut Vec, step_size: f64, (min, max): (f64, f64)) { debug_assert!(max > min); diff --git a/crates/egui_plot/src/plot_ui.rs b/crates/egui_plot/src/plot_ui.rs index fb126298df5..c4a39a6d728 100644 --- a/crates/egui_plot/src/plot_ui.rs +++ b/crates/egui_plot/src/plot_ui.rs @@ -116,6 +116,11 @@ impl PlotUi { self.last_plot_transform.value_from_position(position) } + /// Add an arbitrary item. + pub fn add(&mut self, item: impl PlotItem + 'static) { + self.items.push(Box::new(item)); + } + /// Add a data line. pub fn line(&mut self, mut line: Line) { if line.series.is_empty() { diff --git a/crates/emath/Cargo.toml b/crates/emath/Cargo.toml index a48a9cbdbaf..99fc1b004b8 100644 --- a/crates/emath/Cargo.toml +++ b/crates/emath/Cargo.toml @@ -13,6 +13,9 @@ categories = ["mathematics", "gui"] keywords = ["math", "gui"] include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +[lints] +workspace = true + [package.metadata.docs.rs] all-features = true @@ -22,12 +25,6 @@ all-features = true [features] default = [] -## Enable additional checks if debug assertions are enabled (debug builds). -extra_debug_asserts = [] - -## Always enable additional checks. -extra_asserts = [] - [dependencies] #! ### Optional dependencies diff --git a/crates/emath/src/history.rs b/crates/emath/src/history.rs index d85a27a398a..6aafd0af145 100644 --- a/crates/emath/src/history.rs +++ b/crates/emath/src/history.rs @@ -126,7 +126,7 @@ where /// Values must be added with a monotonically increasing time, or at least not decreasing. pub fn add(&mut self, now: f64, value: T) { if let Some((last_time, _)) = self.values.back() { - crate::emath_assert!(now >= *last_time, "Time shouldn't move backwards"); + debug_assert!(*last_time <= now, "Time shouldn't move backwards"); } self.total_count += 1; self.values.push_back((now, value)); diff --git a/crates/emath/src/lib.rs b/crates/emath/src/lib.rs index 911fb52a044..6ad48bd2543 100644 --- a/crates/emath/src/lib.rs +++ b/crates/emath/src/lib.rs @@ -20,8 +20,6 @@ //! #![allow(clippy::float_cmp)] -#![cfg_attr(feature = "puffin", deny(unsafe_code))] -#![cfg_attr(not(feature = "puffin"), forbid(unsafe_code))] use std::ops::{Add, Div, Mul, RangeInclusive, Sub}; @@ -30,6 +28,7 @@ use std::ops::{Add, Div, Mul, RangeInclusive, Sub}; pub mod align; mod history; mod numeric; +mod ordered_float; mod pos2; mod range; mod rect; @@ -40,10 +39,11 @@ mod ts_transform; mod vec2; mod vec2b; -pub use { +pub use self::{ align::{Align, Align2}, history::History, numeric::*, + ordered_float::*, pos2::*, range::Rangef, rect::*, @@ -144,7 +144,7 @@ where { let from = from.into(); let to = to.into(); - crate::emath_assert!(from.start() != from.end()); + debug_assert!(from.start() != from.end()); let t = (x - *from.start()) / (*from.end() - *from.start()); lerp(to, t) } @@ -168,7 +168,7 @@ where } else if *from.end() <= x { *to.end() } else { - crate::emath_assert!(from.start() != from.end()); + debug_assert!(from.start() != from.end()); let t = (x - *from.start()) / (*from.end() - *from.start()); // Ensure no numerical inaccuracies sneak in: if T::ONE <= t { @@ -192,8 +192,8 @@ pub fn format_with_minimum_decimals(value: f64, decimals: usize) -> String { pub fn format_with_decimals_in_range(value: f64, decimal_range: RangeInclusive) -> String { let min_decimals = *decimal_range.start(); let max_decimals = *decimal_range.end(); - crate::emath_assert!(min_decimals <= max_decimals); - crate::emath_assert!(max_decimals < 100); + debug_assert!(min_decimals <= max_decimals); + debug_assert!(max_decimals < 100); let max_decimals = max_decimals.min(16); let min_decimals = min_decimals.min(max_decimals); @@ -428,19 +428,3 @@ pub fn ease_in_ease_out(t: f32) -> f32 { let t = t.clamp(0.0, 1.0); (3.0 * t * t - 2.0 * t * t * t).clamp(0.0, 1.0) } - -// ---------------------------------------------------------------------------- - -/// An assert that is only active when `emath` is compiled with the `extra_asserts` feature -/// or with the `extra_debug_asserts` feature in debug builds. -#[macro_export] -macro_rules! emath_assert { - ($($arg: tt)*) => { - if cfg!(any( - feature = "extra_asserts", - all(feature = "extra_debug_asserts", debug_assertions), - )) { - assert!($($arg)*); - } - } -} diff --git a/crates/epaint/src/util/ordered_float.rs b/crates/emath/src/ordered_float.rs similarity index 79% rename from crates/epaint/src/util/ordered_float.rs rename to crates/emath/src/ordered_float.rs index 72f06a20a39..950ec16ac2c 100644 --- a/crates/epaint/src/util/ordered_float.rs +++ b/crates/emath/src/ordered_float.rs @@ -7,9 +7,12 @@ use std::hash::{Hash, Hasher}; /// Wraps a floating-point value to add total order and hash. /// Possible types for `T` are `f32` and `f64`. /// -/// See also [`FloatOrd`]. +/// All NaNs are considered equal to each other. +/// The size of zero is ignored. +/// +/// See also [`Float`]. #[derive(Clone, Copy)] -pub struct OrderedFloat(T); +pub struct OrderedFloat(pub T); impl OrderedFloat { #[inline] @@ -68,44 +71,34 @@ impl From for OrderedFloat { /// /// Example with `f64`: /// ``` -/// use epaint::util::FloatOrd; +/// use emath::Float as _; /// /// let array = [1.0, 2.5, 2.0]; /// let max = array.iter().max_by_key(|val| val.ord()); /// /// assert_eq!(max, Some(&2.5)); /// ``` -pub trait FloatOrd { +pub trait Float: PartialOrd + PartialEq + private::FloatImpl { /// Type to provide total order, useful as key in sorted contexts. fn ord(self) -> OrderedFloat where Self: Sized; } -impl FloatOrd for f32 { +impl Float for f32 { #[inline] fn ord(self) -> OrderedFloat { OrderedFloat(self) } } -impl FloatOrd for f64 { +impl Float for f64 { #[inline] fn ord(self) -> OrderedFloat { OrderedFloat(self) } } -// ---------------------------------------------------------------------------- - -/// Internal abstraction over floating point types -#[doc(hidden)] -pub trait Float: PartialOrd + PartialEq + private::FloatImpl {} - -impl Float for f32 {} - -impl Float for f64 {} - // Keep this trait in private module, to avoid exposing its methods as extensions in user code mod private { use super::*; @@ -124,7 +117,13 @@ mod private { #[inline] fn hash(&self, state: &mut H) { - crate::f32_hash(state, *self); + if *self == 0.0 { + state.write_u8(0); + } else if self.is_nan() { + state.write_u8(1); + } else { + self.to_bits().hash(state); + } } } @@ -136,7 +135,13 @@ mod private { #[inline] fn hash(&self, state: &mut H) { - crate::f64_hash(state, *self); + if *self == 0.0 { + state.write_u8(0); + } else if self.is_nan() { + state.write_u8(1); + } else { + self.to_bits().hash(state); + } } } } diff --git a/crates/emath/src/pos2.rs b/crates/emath/src/pos2.rs index f65efaef43d..6e0ef212ba1 100644 --- a/crates/emath/src/pos2.rs +++ b/crates/emath/src/pos2.rs @@ -1,3 +1,4 @@ +use std::fmt; use std::ops::{Add, AddAssign, Sub, SubAssign}; use crate::*; @@ -316,8 +317,19 @@ impl Div for Pos2 { } } -impl std::fmt::Debug for Pos2 { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Debug for Pos2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "[{:.1} {:.1}]", self.x, self.y) } } + +impl fmt::Display for Pos2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("[")?; + self.x.fmt(f)?; + f.write_str(" ")?; + self.y.fmt(f)?; + f.write_str("]")?; + Ok(()) + } +} diff --git a/crates/emath/src/rect.rs b/crates/emath/src/rect.rs index 4e6ee362c25..c1a7f603ecb 100644 --- a/crates/emath/src/rect.rs +++ b/crates/emath/src/rect.rs @@ -1,4 +1,5 @@ use std::f32::INFINITY; +use std::fmt; use crate::*; @@ -605,12 +606,49 @@ impl Rect { } } -impl std::fmt::Debug for Rect { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Rect { + /// Does this Rect intersect the given ray (where `d` is normalized)? + pub fn intersects_ray(&self, o: Pos2, d: Vec2) -> bool { + let mut tmin = -f32::INFINITY; + let mut tmax = f32::INFINITY; + + if d.x != 0.0 { + let tx1 = (self.min.x - o.x) / d.x; + let tx2 = (self.max.x - o.x) / d.x; + + tmin = tmin.max(tx1.min(tx2)); + tmax = tmax.min(tx1.max(tx2)); + } + + if d.y != 0.0 { + let ty1 = (self.min.y - o.y) / d.y; + let ty2 = (self.max.y - o.y) / d.y; + + tmin = tmin.max(ty1.min(ty2)); + tmax = tmax.min(ty1.max(ty2)); + } + + tmin <= tmax + } +} + +impl fmt::Debug for Rect { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "[{:?} - {:?}]", self.min, self.max) } } +impl fmt::Display for Rect { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("[")?; + self.min.fmt(f)?; + f.write_str(" - ")?; + self.max.fmt(f)?; + f.write_str("]")?; + Ok(()) + } +} + /// from (min, max) or (left top, right bottom) impl From<[Pos2; 2]> for Rect { #[inline] @@ -654,3 +692,22 @@ impl Div for Rect { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_rect() { + let r = Rect::from_min_max(pos2(10.0, 10.0), pos2(20.0, 20.0)); + assert_eq!(r.distance_sq_to_pos(pos2(15.0, 15.0)), 0.0); + assert_eq!(r.distance_sq_to_pos(pos2(10.0, 15.0)), 0.0); + assert_eq!(r.distance_sq_to_pos(pos2(10.0, 10.0)), 0.0); + + assert_eq!(r.distance_sq_to_pos(pos2(5.0, 15.0)), 25.0); // left of + assert_eq!(r.distance_sq_to_pos(pos2(25.0, 15.0)), 25.0); // right of + assert_eq!(r.distance_sq_to_pos(pos2(15.0, 5.0)), 25.0); // above + assert_eq!(r.distance_sq_to_pos(pos2(15.0, 25.0)), 25.0); // below + assert_eq!(r.distance_sq_to_pos(pos2(25.0, 5.0)), 50.0); // right and above + } +} diff --git a/crates/emath/src/rot2.rs b/crates/emath/src/rot2.rs index da67acb0c9c..f9e0ae6462c 100644 --- a/crates/emath/src/rot2.rs +++ b/crates/emath/src/rot2.rs @@ -18,10 +18,10 @@ use super::Vec2; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))] pub struct Rot2 { - /// angle.sin() + /// `angle.sin()` s: f32, - /// angle.cos() + /// `angle.cos()` c: f32, } @@ -84,7 +84,7 @@ impl Rot2 { c: self.c / l, s: self.s / l, }; - crate::emath_assert!(ret.is_finite()); + debug_assert!(ret.is_finite()); ret } } diff --git a/crates/emath/src/smart_aim.rs b/crates/emath/src/smart_aim.rs index bd0f7b66197..23e34abf701 100644 --- a/crates/emath/src/smart_aim.rs +++ b/crates/emath/src/smart_aim.rs @@ -33,7 +33,7 @@ pub fn best_in_range_f64(min: f64, max: f64) -> f64 { if !max.is_finite() { return min; } - crate::emath_assert!(min.is_finite() && max.is_finite()); + debug_assert!(min.is_finite() && max.is_finite()); let min_exponent = min.log10(); let max_exponent = max.log10(); @@ -82,7 +82,7 @@ fn is_integer(f: f64) -> bool { } fn to_decimal_string(v: f64) -> [i32; NUM_DECIMALS] { - crate::emath_assert!(v < 10.0, "{:?}", v); + debug_assert!(v < 10.0, "{v:?}"); let mut digits = [0; NUM_DECIMALS]; let mut v = v.abs(); for r in &mut digits { @@ -104,7 +104,7 @@ fn from_decimal_string(s: &[i32]) -> f64 { /// Find the simplest integer in the range [min, max] fn simplest_digit_closed_range(min: i32, max: i32) -> i32 { - crate::emath_assert!(1 <= min && min <= max && max <= 9); + debug_assert!(1 <= min && min <= max && max <= 9); if min <= 5 && 5 <= max { 5 } else { diff --git a/crates/emath/src/vec2.rs b/crates/emath/src/vec2.rs index 0cab00fe62f..99f7d0c05c7 100644 --- a/crates/emath/src/vec2.rs +++ b/crates/emath/src/vec2.rs @@ -1,3 +1,4 @@ +use std::fmt; use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; use crate::Vec2b; @@ -464,12 +465,23 @@ impl Div for Vec2 { } } -impl std::fmt::Debug for Vec2 { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Debug for Vec2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "[{:.1} {:.1}]", self.x, self.y) } } +impl fmt::Display for Vec2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("[")?; + self.x.fmt(f)?; + f.write_str(" ")?; + self.y.fmt(f)?; + f.write_str("]")?; + Ok(()) + } +} + #[test] fn test_vec2() { macro_rules! almost_eq { diff --git a/crates/epaint/CHANGELOG.md b/crates/epaint/CHANGELOG.md index 02c2bc66641..a4f935a3883 100644 --- a/crates/epaint/CHANGELOG.md +++ b/crates/epaint/CHANGELOG.md @@ -5,6 +5,24 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.27.2 - 2024-04-02 +* Nothing new + + +## 0.27.1 - 2024-03-29 +* Fix visual glitch on the right side of highly rounded rectangles [#4244](https://github.com/emilk/egui/pull/4244) +* Prevent visual glitch when shadow blur width is very high [#4245](https://github.com/emilk/egui/pull/4245) + + +## 0.27.0 - 2024-03-26 +* Add `ColorImage::from_gray_iter` [#3536](https://github.com/emilk/egui/pull/3536) (thanks [@wangxiaochuTHU](https://github.com/wangxiaochuTHU)!) +* Convenience const fn for `Margin`, `Rounding` and `Shadow` [#4080](https://github.com/emilk/egui/pull/4080) (thanks [@0Qwel](https://github.com/0Qwel)!) +* Added `Shape::{scale,translate}` wrappers [#4090](https://github.com/emilk/egui/pull/4090) (thanks [@varphone](https://github.com/varphone)!) +* Add `EllipseShape` [#4122](https://github.com/emilk/egui/pull/4122) (thanks [@TheTacBanana](https://github.com/TheTacBanana)!) +* Add `Margin` to `epaint` [#4231](https://github.com/emilk/egui/pull/4231) +* CSS-like `Shadow` with offset, spread, and blur [#4232](https://github.com/emilk/egui/pull/4232) + + ## 0.26.2 - 2024-02-14 * Nothing new diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml index a24a299b8ed..852066d046c 100644 --- a/crates/epaint/Cargo.toml +++ b/crates/epaint/Cargo.toml @@ -22,6 +22,9 @@ include = [ "fonts/UFL.txt", ] +[lints] +workspace = true + [package.metadata.docs.rs] all-features = true @@ -49,14 +52,6 @@ deadlock_detection = ["dep:backtrace"] ## If you plan on specifying your own fonts you may disable this feature. default_fonts = [] -## Enable additional checks if debug assertions are enabled (debug builds). -extra_debug_asserts = [ - "emath/extra_debug_asserts", - "ecolor/extra_debug_asserts", -] -## Always enable additional checks. -extra_asserts = ["emath/extra_asserts", "ecolor/extra_asserts"] - ## Turn on the `log` feature, that makes egui log some errors using the [`log`](https://docs.rs/log) crate. log = ["dep:log"] diff --git a/crates/epaint/benches/benchmark.rs b/crates/epaint/benches/benchmark.rs index 709adbfae9c..6323137fa50 100644 --- a/crates/epaint/benches/benchmark.rs +++ b/crates/epaint/benches/benchmark.rs @@ -1,6 +1,6 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use epaint::*; +use epaint::{tessellator::Path, *}; fn single_dashed_lines(c: &mut Criterion) { c.bench_function("single_dashed_lines", move |b| { @@ -72,10 +72,166 @@ fn tessellate_circles(c: &mut Criterion) { }); } +fn thick_line_solid(c: &mut Criterion) { + c.bench_function("thick_solid_line", move |b| { + let line = [pos2(0.0, 0.0), pos2(50.0, 0.0), pos2(100.0, 1.0)]; + let mut path = Path::default(); + path.add_open_points(&line); + + b.iter(|| { + let mut mesh = Mesh::default(); + path.stroke_closed(1.5, &Stroke::new(2.0, Color32::RED).into(), &mut mesh); + + black_box(mesh); + }); + }); +} + +fn thick_large_line_solid(c: &mut Criterion) { + c.bench_function("thick_large_solid_line", move |b| { + let line = (0..1000).map(|i| pos2(i as f32, 10.0)).collect::>(); + let mut path = Path::default(); + path.add_open_points(&line); + + b.iter(|| { + let mut mesh = Mesh::default(); + path.stroke_closed(1.5, &Stroke::new(2.0, Color32::RED).into(), &mut mesh); + + black_box(mesh); + }); + }); +} + +fn thin_line_solid(c: &mut Criterion) { + c.bench_function("thin_solid_line", move |b| { + let line = [pos2(0.0, 0.0), pos2(50.0, 0.0), pos2(100.0, 1.0)]; + let mut path = Path::default(); + path.add_open_points(&line); + + b.iter(|| { + let mut mesh = Mesh::default(); + path.stroke_closed(1.5, &Stroke::new(0.5, Color32::RED).into(), &mut mesh); + + black_box(mesh); + }); + }); +} + +fn thin_large_line_solid(c: &mut Criterion) { + c.bench_function("thin_large_solid_line", move |b| { + let line = (0..1000).map(|i| pos2(i as f32, 10.0)).collect::>(); + let mut path = Path::default(); + path.add_open_points(&line); + + b.iter(|| { + let mut mesh = Mesh::default(); + path.stroke_closed(1.5, &Stroke::new(0.5, Color32::RED).into(), &mut mesh); + + black_box(mesh); + }); + }); +} + +fn thick_line_uv(c: &mut Criterion) { + c.bench_function("thick_uv_line", move |b| { + let line = [pos2(0.0, 0.0), pos2(50.0, 0.0), pos2(100.0, 1.0)]; + let mut path = Path::default(); + path.add_open_points(&line); + + b.iter(|| { + let mut mesh = Mesh::default(); + path.stroke_closed( + 1.5, + &PathStroke::new_uv(2.0, |_, p| { + black_box(p * 2.0); + Color32::RED + }), + &mut mesh, + ); + + black_box(mesh); + }); + }); +} + +fn thick_large_line_uv(c: &mut Criterion) { + c.bench_function("thick_large_uv_line", move |b| { + let line = (0..1000).map(|i| pos2(i as f32, 10.0)).collect::>(); + let mut path = Path::default(); + path.add_open_points(&line); + + b.iter(|| { + let mut mesh = Mesh::default(); + path.stroke_closed( + 1.5, + &PathStroke::new_uv(2.0, |_, p| { + black_box(p * 2.0); + Color32::RED + }), + &mut mesh, + ); + + black_box(mesh); + }); + }); +} + +fn thin_line_uv(c: &mut Criterion) { + c.bench_function("thin_uv_line", move |b| { + let line = [pos2(0.0, 0.0), pos2(50.0, 0.0), pos2(100.0, 1.0)]; + let mut path = Path::default(); + path.add_open_points(&line); + + b.iter(|| { + let mut mesh = Mesh::default(); + path.stroke_closed( + 1.5, + &PathStroke::new_uv(2.0, |_, p| { + black_box(p * 2.0); + Color32::RED + }), + &mut mesh, + ); + + black_box(mesh); + }); + }); +} + +fn thin_large_line_uv(c: &mut Criterion) { + c.bench_function("thin_large_uv_line", move |b| { + let line = (0..1000).map(|i| pos2(i as f32, 10.0)).collect::>(); + let mut path = Path::default(); + path.add_open_points(&line); + + b.iter(|| { + let mut mesh = Mesh::default(); + path.stroke_closed( + 1.5, + &PathStroke::new_uv(2.0, |_, p| { + black_box(p * 2.0); + Color32::RED + }), + &mut mesh, + ); + + black_box(mesh); + }); + }); +} + criterion_group!( benches, single_dashed_lines, many_dashed_lines, - tessellate_circles + tessellate_circles, + thick_line_solid, + thick_large_line_solid, + thin_line_solid, + thin_large_line_solid, + thick_line_uv, + thick_large_line_uv, + thin_line_uv, + thin_large_line_uv ); criterion_main!(benches); diff --git a/crates/epaint/src/bezier.rs b/crates/epaint/src/bezier.rs index 3da99f33b65..6f61feb0ac3 100644 --- a/crates/epaint/src/bezier.rs +++ b/crates/epaint/src/bezier.rs @@ -3,7 +3,7 @@ use std::ops::Range; -use crate::{shape::Shape, Color32, PathShape, Stroke}; +use crate::{shape::Shape, Color32, PathShape, PathStroke}; use emath::*; // ---------------------------------------------------------------------------- @@ -11,7 +11,7 @@ use emath::*; /// A cubic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve). /// /// See also [`QuadraticBezierShape`]. -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct CubicBezierShape { /// The first point is the starting point and the last one is the ending point of the curve. @@ -20,7 +20,7 @@ pub struct CubicBezierShape { pub closed: bool, pub fill: Color32, - pub stroke: Stroke, + pub stroke: PathStroke, } impl CubicBezierShape { @@ -32,7 +32,7 @@ impl CubicBezierShape { points: [Pos2; 4], closed: bool, fill: Color32, - stroke: impl Into, + stroke: impl Into, ) -> Self { Self { points, @@ -52,7 +52,7 @@ impl CubicBezierShape { points, closed: self.closed, fill: self.fill, - stroke: self.stroke, + stroke: self.stroke.clone(), } } @@ -69,7 +69,7 @@ impl CubicBezierShape { points, closed: self.closed, fill: self.fill, - stroke: self.stroke, + stroke: self.stroke.clone(), }; pathshapes.push(pathshape); } @@ -141,8 +141,8 @@ impl CubicBezierShape { /// split the original cubic curve into a new one within a range. pub fn split_range(&self, t_range: Range) -> Self { - crate::epaint_assert!( - t_range.start >= 0.0 && t_range.end <= 1.0 && t_range.start <= t_range.end, + debug_assert!( + 0.0 <= t_range.start && t_range.end <= 1.0 && t_range.start <= t_range.end, "range should be in [0.0,1.0]" ); @@ -156,7 +156,7 @@ impl CubicBezierShape { points: [d_from, d_ctrl, d_to], closed: self.closed, fill: self.fill, - stroke: self.stroke, + stroke: self.stroke.clone(), }; let delta_t = t_range.end - t_range.start; let q_start = q.sample(t_range.start); @@ -168,7 +168,7 @@ impl CubicBezierShape { points: [from, ctrl1, ctrl2, to], closed: self.closed, fill: self.fill, - stroke: self.stroke, + stroke: self.stroke.clone(), } } @@ -178,7 +178,7 @@ impl CubicBezierShape { // https://scholarsarchive.byu.edu/cgi/viewcontent.cgi?article=1000&context=facpub#section.10.6 // and the error metric from the caffein owl blog post http://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html pub fn num_quadratics(&self, tolerance: f32) -> u32 { - crate::epaint_assert!(tolerance > 0.0, "the tolerance should be positive"); + debug_assert!(tolerance > 0.0, "the tolerance should be positive"); let x = self.points[0].x - 3.0 * self.points[1].x + 3.0 * self.points[2].x - self.points[3].x; @@ -273,7 +273,7 @@ impl CubicBezierShape { /// [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves) /// pub fn sample(&self, t: f32) -> Pos2 { - crate::epaint_assert!( + debug_assert!( t >= 0.0 && t <= 1.0, "the sample value should be in [0.0,1.0]" ); @@ -375,7 +375,7 @@ impl From for Shape { /// A quadratic [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve). /// /// See also [`CubicBezierShape`]. -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct QuadraticBezierShape { /// The first point is the starting point and the last one is the ending point of the curve. @@ -384,7 +384,7 @@ pub struct QuadraticBezierShape { pub closed: bool, pub fill: Color32, - pub stroke: Stroke, + pub stroke: PathStroke, } impl QuadraticBezierShape { @@ -397,7 +397,7 @@ impl QuadraticBezierShape { points: [Pos2; 3], closed: bool, fill: Color32, - stroke: impl Into, + stroke: impl Into, ) -> Self { Self { points, @@ -417,7 +417,7 @@ impl QuadraticBezierShape { points, closed: self.closed, fill: self.fill, - stroke: self.stroke, + stroke: self.stroke.clone(), } } @@ -429,7 +429,7 @@ impl QuadraticBezierShape { points, closed: self.closed, fill: self.fill, - stroke: self.stroke, + stroke: self.stroke.clone(), } } @@ -496,7 +496,7 @@ impl QuadraticBezierShape { /// [Bézier Curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_B.C3.A9zier_curves) /// pub fn sample(&self, t: f32) -> Pos2 { - crate::epaint_assert!( + debug_assert!( t >= 0.0 && t <= 1.0, "the sample value should be in [0.0,1.0]" ); @@ -688,7 +688,7 @@ fn single_curve_approximation(curve: &CubicBezierShape) -> QuadraticBezierShape points: [curve.points[0], c, curve.points[3]], closed: curve.closed, fill: curve.fill, - stroke: curve.stroke, + stroke: curve.stroke.clone(), } } diff --git a/crates/epaint/src/color.rs b/crates/epaint/src/color.rs new file mode 100644 index 00000000000..54106c10d3f --- /dev/null +++ b/crates/epaint/src/color.rs @@ -0,0 +1,48 @@ +use std::{fmt::Debug, sync::Arc}; + +use ecolor::Color32; +use emath::{Pos2, Rect}; + +/// How paths will be colored. +#[derive(Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum ColorMode { + /// The entire path is one solid color, this is the default. + Solid(Color32), + + /// Provide a callback which takes in the path's bounding box and a position and converts it to a color. + /// When used with a path, the bounding box will have a margin of [`TessellationOptions::feathering_size_in_pixels`](`crate::tessellator::TessellationOptions::feathering_size_in_pixels`) + /// + /// **This cannot be serialized** + #[cfg_attr(feature = "serde", serde(skip))] + UV(Arc Color32 + Send + Sync>), +} + +impl Default for ColorMode { + fn default() -> Self { + Self::Solid(Color32::TRANSPARENT) + } +} + +impl Debug for ColorMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Solid(arg0) => f.debug_tuple("Solid").field(arg0).finish(), + Self::UV(_arg0) => f.debug_tuple("UV").field(&"").finish(), + } + } +} + +impl PartialEq for ColorMode { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Solid(l0), Self::Solid(r0)) => l0 == r0, + (Self::UV(_l0), Self::UV(_r0)) => false, + _ => false, + } + } +} + +impl ColorMode { + pub const TRANSPARENT: Self = Self::Solid(Color32::TRANSPARENT); +} diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index c83be860a2a..d5e7055813b 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -22,11 +22,11 @@ #![allow(clippy::float_cmp)] #![allow(clippy::manual_range_contains)] -#![cfg_attr(feature = "puffin", deny(unsafe_code))] -#![cfg_attr(not(feature = "puffin"), forbid(unsafe_code))] mod bezier; +pub mod color; pub mod image; +mod margin; mod mesh; pub mod mutex; mod shadow; @@ -41,17 +41,19 @@ mod texture_handle; pub mod textures; pub mod util; -pub use { +pub use self::{ bezier::{CubicBezierShape, QuadraticBezierShape}, + color::ColorMode, image::{ColorImage, FontImage, ImageData, ImageDelta}, + margin::Margin, mesh::{Mesh, Mesh16, Vertex}, shadow::Shadow, shape::{ - CircleShape, PaintCallback, PaintCallbackInfo, PathShape, RectShape, Rounding, Shape, - TextShape, + CircleShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape, RectShape, + Rounding, Shape, TextShape, }, stats::PaintStats, - stroke::Stroke, + stroke::{PathStroke, Stroke}, tessellator::{TessellationOptions, Tessellator}, text::{FontFamily, FontId, Fonts, Galley}, texture_atlas::TextureAtlas, @@ -134,48 +136,6 @@ pub enum Primitive { Callback(PaintCallback), } -// ---------------------------------------------------------------------------- - -/// An assert that is only active when `epaint` is compiled with the `extra_asserts` feature -/// or with the `extra_debug_asserts` feature in debug builds. -#[macro_export] -macro_rules! epaint_assert { - ($($arg: tt)*) => { - if cfg!(any( - feature = "extra_asserts", - all(feature = "extra_debug_asserts", debug_assertions), - )) { - assert!($($arg)*); - } - } -} - -// ---------------------------------------------------------------------------- - -#[inline(always)] -pub(crate) fn f32_hash(state: &mut H, f: f32) { - if f == 0.0 { - state.write_u8(0); - } else if f.is_nan() { - state.write_u8(1); - } else { - use std::hash::Hash; - f.to_bits().hash(state); - } -} - -#[inline(always)] -pub(crate) fn f64_hash(state: &mut H, f: f64) { - if f == 0.0 { - state.write_u8(0); - } else if f.is_nan() { - state.write_u8(1); - } else { - use std::hash::Hash; - f.to_bits().hash(state); - } -} - // --------------------------------------------------------------------------- /// Was epaint compiled with the `rayon` feature? diff --git a/crates/epaint/src/margin.rs b/crates/epaint/src/margin.rs new file mode 100644 index 00000000000..e2ade58f9c2 --- /dev/null +++ b/crates/epaint/src/margin.rs @@ -0,0 +1,271 @@ +use emath::{vec2, Rect, Vec2}; + +/// A value for all four sides of a rectangle, +/// often used to express padding or spacing. +/// +/// Can be added and subtracted to/from [`Rect`]s. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct Margin { + pub left: f32, + pub right: f32, + pub top: f32, + pub bottom: f32, +} + +impl Margin { + pub const ZERO: Self = Self { + left: 0.0, + right: 0.0, + top: 0.0, + bottom: 0.0, + }; + + /// The same margin on every side. + #[doc(alias = "symmetric")] + #[inline] + pub const fn same(margin: f32) -> Self { + Self { + left: margin, + right: margin, + top: margin, + bottom: margin, + } + } + + /// Margins with the same size on opposing sides + #[inline] + pub const fn symmetric(x: f32, y: f32) -> Self { + Self { + left: x, + right: x, + top: y, + bottom: y, + } + } + + /// Total margins on both sides + #[inline] + pub fn sum(&self) -> Vec2 { + vec2(self.left + self.right, self.top + self.bottom) + } + + #[inline] + pub const fn left_top(&self) -> Vec2 { + vec2(self.left, self.top) + } + + #[inline] + pub const fn right_bottom(&self) -> Vec2 { + vec2(self.right, self.bottom) + } + + /// Are the margin on every side the same? + #[doc(alias = "symmetric")] + #[inline] + pub fn is_same(&self) -> bool { + self.left == self.right && self.left == self.top && self.left == self.bottom + } + + #[deprecated = "Use `rect + margin` instead"] + #[inline] + pub fn expand_rect(&self, rect: Rect) -> Rect { + Rect::from_min_max(rect.min - self.left_top(), rect.max + self.right_bottom()) + } + + #[deprecated = "Use `rect - margin` instead"] + #[inline] + pub fn shrink_rect(&self, rect: Rect) -> Rect { + Rect::from_min_max(rect.min + self.left_top(), rect.max - self.right_bottom()) + } +} + +impl From for Margin { + #[inline] + fn from(v: f32) -> Self { + Self::same(v) + } +} + +impl From for Margin { + #[inline] + fn from(v: Vec2) -> Self { + Self::symmetric(v.x, v.y) + } +} + +/// `Margin + Margin` +impl std::ops::Add for Margin { + type Output = Self; + + #[inline] + fn add(self, other: Self) -> Self { + Self { + left: self.left + other.left, + right: self.right + other.right, + top: self.top + other.top, + bottom: self.bottom + other.bottom, + } + } +} + +/// `Margin + f32` +impl std::ops::Add for Margin { + type Output = Self; + + #[inline] + fn add(self, v: f32) -> Self { + Self { + left: self.left + v, + right: self.right + v, + top: self.top + v, + bottom: self.bottom + v, + } + } +} + +/// `Margind += f32` +impl std::ops::AddAssign for Margin { + #[inline] + fn add_assign(&mut self, v: f32) { + self.left += v; + self.right += v; + self.top += v; + self.bottom += v; + } +} + +/// `Margin * f32` +impl std::ops::Mul for Margin { + type Output = Self; + + #[inline] + fn mul(self, v: f32) -> Self { + Self { + left: self.left * v, + right: self.right * v, + top: self.top * v, + bottom: self.bottom * v, + } + } +} + +/// `Margin *= f32` +impl std::ops::MulAssign for Margin { + #[inline] + fn mul_assign(&mut self, v: f32) { + self.left *= v; + self.right *= v; + self.top *= v; + self.bottom *= v; + } +} + +/// `Margin / f32` +impl std::ops::Div for Margin { + type Output = Self; + + #[inline] + fn div(self, v: f32) -> Self { + Self { + left: self.left / v, + right: self.right / v, + top: self.top / v, + bottom: self.bottom / v, + } + } +} + +/// `Margin /= f32` +impl std::ops::DivAssign for Margin { + #[inline] + fn div_assign(&mut self, v: f32) { + self.left /= v; + self.right /= v; + self.top /= v; + self.bottom /= v; + } +} + +/// `Margin - Margin` +impl std::ops::Sub for Margin { + type Output = Self; + + #[inline] + fn sub(self, other: Self) -> Self { + Self { + left: self.left - other.left, + right: self.right - other.right, + top: self.top - other.top, + bottom: self.bottom - other.bottom, + } + } +} + +/// `Margin - f32` +impl std::ops::Sub for Margin { + type Output = Self; + + #[inline] + fn sub(self, v: f32) -> Self { + Self { + left: self.left - v, + right: self.right - v, + top: self.top - v, + bottom: self.bottom - v, + } + } +} + +/// `Margin -= f32` +impl std::ops::SubAssign for Margin { + #[inline] + fn sub_assign(&mut self, v: f32) { + self.left -= v; + self.right -= v; + self.top -= v; + self.bottom -= v; + } +} + +/// `Rect + Margin` +impl std::ops::Add for Rect { + type Output = Self; + + #[inline] + fn add(self, margin: Margin) -> Self { + Self::from_min_max( + self.min - margin.left_top(), + self.max + margin.right_bottom(), + ) + } +} + +/// `Rect += Margin` +impl std::ops::AddAssign for Rect { + #[inline] + fn add_assign(&mut self, margin: Margin) { + *self = *self + margin; + } +} + +/// `Rect - Margin` +impl std::ops::Sub for Rect { + type Output = Self; + + #[inline] + fn sub(self, margin: Margin) -> Self { + Self::from_min_max( + self.min + margin.left_top(), + self.max - margin.right_bottom(), + ) + } +} + +/// `Rect -= Margin` +impl std::ops::SubAssign for Rect { + #[inline] + fn sub_assign(&mut self, margin: Margin) { + *self = *self - margin; + } +} diff --git a/crates/epaint/src/mesh.rs b/crates/epaint/src/mesh.rs index 7d5a51965d9..9dd919274e6 100644 --- a/crates/epaint/src/mesh.rs +++ b/crates/epaint/src/mesh.rs @@ -109,7 +109,7 @@ impl Mesh { /// Append all the indices and vertices of `other` to `self`. pub fn append(&mut self, other: Self) { crate::profile_function!(); - crate::epaint_assert!(other.is_valid()); + debug_assert!(other.is_valid()); if self.is_empty() { *self = other; @@ -121,7 +121,7 @@ impl Mesh { /// Append all the indices and vertices of `other` to `self` without /// taking ownership. pub fn append_ref(&mut self, other: &Self) { - crate::epaint_assert!(other.is_valid()); + debug_assert!(other.is_valid()); if self.is_empty() { self.texture_id = other.texture_id; @@ -140,7 +140,7 @@ impl Mesh { #[inline(always)] pub fn colored_vertex(&mut self, pos: Pos2, color: Color32) { - crate::epaint_assert!(self.texture_id == TextureId::default()); + debug_assert!(self.texture_id == TextureId::default()); self.vertices.push(Vertex { pos, uv: WHITE_UV, @@ -203,7 +203,7 @@ impl Mesh { /// Uniformly colored rectangle. #[inline(always)] pub fn add_colored_rect(&mut self, rect: Rect, color: Color32) { - crate::epaint_assert!(self.texture_id == TextureId::default()); + debug_assert!(self.texture_id == TextureId::default()); self.add_rect_with_uv(rect, [WHITE_UV, WHITE_UV].into(), color); } @@ -212,7 +212,7 @@ impl Mesh { /// Splits this mesh into many smaller meshes (if needed) /// where the smaller meshes have 16-bit indices. pub fn split_to_u16(self) -> Vec { - crate::epaint_assert!(self.is_valid()); + debug_assert!(self.is_valid()); const MAX_SIZE: u32 = std::u16::MAX as u32; @@ -265,7 +265,7 @@ impl Mesh { vertices: self.vertices[(min_vindex as usize)..=(max_vindex as usize)].to_vec(), texture_id: self.texture_id, }; - crate::epaint_assert!(mesh.is_valid()); + debug_assert!(mesh.is_valid()); output.push(mesh); } output diff --git a/crates/epaint/src/mutex.rs b/crates/epaint/src/mutex.rs index 2c61f038f9c..157701c2be0 100644 --- a/crates/epaint/src/mutex.rs +++ b/crates/epaint/src/mutex.rs @@ -133,14 +133,16 @@ mod rw_lock_impl { /// the feature `deadlock_detection` is turned enabled, in which case /// extra checks are added to detect deadlocks. #[derive(Default)] - pub struct RwLock(parking_lot::RwLock); + pub struct RwLock(parking_lot::RwLock); impl RwLock { #[inline(always)] pub fn new(val: T) -> Self { Self(parking_lot::RwLock::new(val)) } + } + impl RwLock { #[inline(always)] pub fn read(&self) -> RwLockReadGuard<'_, T> { parking_lot::RwLockReadGuard::map(self.0.read(), |v| v) diff --git a/crates/epaint/src/shadow.rs b/crates/epaint/src/shadow.rs index fb6a9a4ab24..fc7de267b9a 100644 --- a/crates/epaint/src/shadow.rs +++ b/crates/epaint/src/shadow.rs @@ -1,91 +1,70 @@ use super::*; /// The color and fuzziness of a fuzzy shape. +/// /// Can be used for a rectangular shadow with a soft penumbra. +/// +/// Very similar to a box-shadow in CSS. #[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Shadow { - /// The shadow extends this much outside the rect. - /// The size of the fuzzy penumbra. - pub extrusion: f32, + /// Move the shadow by this much. + /// + /// For instance, a value of `[1.0, 2.0]` will move the shadow 1 point to the right and 2 points down, + /// causing a drop-shadow effet. + pub offset: Vec2, + + /// The width of the blur, i.e. the width of the fuzzy penumbra. + /// + /// A value of 0.0 means a sharp shadow. + pub blur: f32, + + /// Expand the shadow in all directions by this much. + pub spread: f32, /// Color of the opaque center of the shadow. pub color: Color32, } impl Shadow { + /// No shadow at all. pub const NONE: Self = Self { - extrusion: 0.0, + offset: Vec2::ZERO, + blur: 0.0, + spread: 0.0, color: Color32::TRANSPARENT, }; - pub const fn new(extrusion: f32, color: Color32) -> Self { - Self { extrusion, color } - } + /// The argument is the rectangle of the shadow caster. + pub fn as_shape(&self, rect: Rect, rounding: impl Into) -> RectShape { + // tessellator.clip_rect = clip_rect; // TODO(emilk): culling - /// Tooltips, menus, …, for dark mode. - pub const fn small_dark() -> Self { - Self { - extrusion: 16.0, - color: Color32::from_black_alpha(96), - } - } + let Self { + offset, + blur, + spread, + color, + } = *self; - /// Tooltips, menus, …, for light mode. - pub const fn small_light() -> Self { - Self { - extrusion: 16.0, - color: Color32::from_black_alpha(20), - } - } + let rect = rect.translate(offset).expand(spread); + let rounding = rounding.into() + Rounding::same(spread.abs()); - /// Used for egui windows in dark mode. - pub const fn big_dark() -> Self { - Self { - extrusion: 32.0, - color: Color32::from_black_alpha(96), - } + RectShape::filled(rect, rounding, color).with_blur_width(blur) } - /// Used for egui windows in light mode. - pub const fn big_light() -> Self { - Self { - extrusion: 32.0, - color: Color32::from_black_alpha(16), + /// How much larger than the parent rect are we in each direction? + pub fn margin(&self) -> Margin { + let Self { + offset, + blur, + spread, + color: _, + } = *self; + Margin { + left: spread + 0.5 * blur - offset.x, + right: spread + 0.5 * blur + offset.x, + top: spread + 0.5 * blur - offset.y, + bottom: spread + 0.5 * blur + offset.y, } } - - pub fn tessellate(&self, rect: Rect, rounding: impl Into) -> Mesh { - // tessellator.clip_rect = clip_rect; // TODO(emilk): culling - - let Self { extrusion, color } = *self; - - let rounding: Rounding = rounding.into(); - let half_ext = 0.5 * extrusion; - - let ext_rounding = Rounding { - nw: rounding.nw + half_ext, - ne: rounding.ne + half_ext, - sw: rounding.sw + half_ext, - se: rounding.se + half_ext, - }; - - use crate::tessellator::*; - let rect = RectShape::filled(rect.expand(half_ext), ext_rounding, color); - let pixels_per_point = 1.0; // doesn't matter here - let font_tex_size = [1; 2]; // unused size we are not tessellating text. - let mut tessellator = Tessellator::new( - pixels_per_point, - TessellationOptions { - feathering: true, - feathering_size_in_pixels: extrusion * pixels_per_point, - ..Default::default() - }, - font_tex_size, - vec![], - ); - let mut mesh = Mesh::default(); - tessellator.tessellate_rect(&rect, &mut mesh); - mesh - } } diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 0aeec5525a5..70f9a8b5cfd 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -3,6 +3,7 @@ use std::{any::Any, sync::Arc}; use crate::{ + stroke::PathStroke, text::{FontId, Fonts, Galley}, Color32, Mesh, Stroke, TextureId, }; @@ -30,8 +31,14 @@ pub enum Shape { /// Circle with optional outline and fill. Circle(CircleShape), + /// Ellipse with optional outline and fill. + Ellipse(EllipseShape), + /// A line between two points. - LineSegment { points: [Pos2; 2], stroke: Stroke }, + LineSegment { + points: [Pos2; 2], + stroke: PathStroke, + }, /// A series of lines between points. /// The path can have a stroke and/or fill (if closed). @@ -85,7 +92,7 @@ impl Shape { /// A line between two points. /// More efficient than calling [`Self::line`]. #[inline] - pub fn line_segment(points: [Pos2; 2], stroke: impl Into) -> Self { + pub fn line_segment(points: [Pos2; 2], stroke: impl Into) -> Self { Self::LineSegment { points, stroke: stroke.into(), @@ -93,7 +100,7 @@ impl Shape { } /// A horizontal line. - pub fn hline(x: impl Into, y: f32, stroke: impl Into) -> Self { + pub fn hline(x: impl Into, y: f32, stroke: impl Into) -> Self { let x = x.into(); Self::LineSegment { points: [pos2(x.min, y), pos2(x.max, y)], @@ -102,7 +109,7 @@ impl Shape { } /// A vertical line. - pub fn vline(x: f32, y: impl Into, stroke: impl Into) -> Self { + pub fn vline(x: f32, y: impl Into, stroke: impl Into) -> Self { let y = y.into(); Self::LineSegment { points: [pos2(x, y.min), pos2(x, y.max)], @@ -114,13 +121,13 @@ impl Shape { /// /// Use [`Self::line_segment`] instead if your line only connects two points. #[inline] - pub fn line(points: Vec, stroke: impl Into) -> Self { + pub fn line(points: Vec, stroke: impl Into) -> Self { Self::Path(PathShape::line(points, stroke)) } /// A line that closes back to the start point again. #[inline] - pub fn closed_line(points: Vec, stroke: impl Into) -> Self { + pub fn closed_line(points: Vec, stroke: impl Into) -> Self { Self::Path(PathShape::closed_line(points, stroke)) } @@ -221,7 +228,7 @@ impl Shape { pub fn convex_polygon( points: Vec, fill: impl Into, - stroke: impl Into, + stroke: impl Into, ) -> Self { Self::Path(PathShape::convex_polygon(points, fill, stroke)) } @@ -236,6 +243,16 @@ impl Shape { Self::Circle(CircleShape::stroke(center, radius, stroke)) } + #[inline] + pub fn ellipse_filled(center: Pos2, radius: Vec2, fill_color: impl Into) -> Self { + Self::Ellipse(EllipseShape::filled(center, radius, fill_color)) + } + + #[inline] + pub fn ellipse_stroke(center: Pos2, radius: Vec2, stroke: impl Into) -> Self { + Self::Ellipse(EllipseShape::stroke(center, radius, stroke)) + } + #[inline] pub fn rect_filled( rect: Rect, @@ -296,7 +313,7 @@ impl Shape { #[inline] pub fn mesh(mesh: Mesh) -> Self { - crate::epaint_assert!(mesh.is_valid()); + debug_assert!(mesh.is_valid()); Self::Mesh(mesh) } @@ -324,6 +341,7 @@ impl Shape { rect } Self::Circle(circle_shape) => circle_shape.visual_bounding_rect(), + Self::Ellipse(ellipse_shape) => ellipse_shape.visual_bounding_rect(), Self::LineSegment { points, stroke } => { if stroke.is_empty() { Rect::NOTHING @@ -388,6 +406,11 @@ impl Shape { circle_shape.radius *= transform.scaling; circle_shape.stroke.width *= transform.scaling; } + Self::Ellipse(ellipse_shape) => { + ellipse_shape.center = transform * ellipse_shape.center; + ellipse_shape.radius *= transform.scaling; + ellipse_shape.stroke.width *= transform.scaling; + } Self::LineSegment { points, stroke } => { for p in points { *p = transform * *p; @@ -403,6 +426,7 @@ impl Shape { Self::Rect(rect_shape) => { rect_shape.rect = transform * rect_shape.rect; rect_shape.stroke.width *= transform.scaling; + rect_shape.rounding *= transform.scaling; } Self::Text(text_shape) => { text_shape.pos = transform * text_shape.pos; @@ -496,6 +520,61 @@ impl From for Shape { // ---------------------------------------------------------------------------- +/// How to paint an ellipse. +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct EllipseShape { + pub center: Pos2, + + /// Radius is the vector (a, b) where the width of the Ellipse is 2a and the height is 2b + pub radius: Vec2, + pub fill: Color32, + pub stroke: Stroke, +} + +impl EllipseShape { + #[inline] + pub fn filled(center: Pos2, radius: Vec2, fill_color: impl Into) -> Self { + Self { + center, + radius, + fill: fill_color.into(), + stroke: Default::default(), + } + } + + #[inline] + pub fn stroke(center: Pos2, radius: Vec2, stroke: impl Into) -> Self { + Self { + center, + radius, + fill: Default::default(), + stroke: stroke.into(), + } + } + + /// The visual bounding rectangle (includes stroke width) + pub fn visual_bounding_rect(&self) -> Rect { + if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() { + Rect::NOTHING + } else { + Rect::from_center_size( + self.center, + self.radius * 2.0 + Vec2::splat(self.stroke.width), + ) + } + } +} + +impl From for Shape { + #[inline(always)] + fn from(shape: EllipseShape) -> Self { + Self::Ellipse(shape) + } +} + +// ---------------------------------------------------------------------------- + /// A path which can be stroked and/or filled (if closed). #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -511,7 +590,7 @@ pub struct PathShape { pub fill: Color32, /// Color and thickness of the line. - pub stroke: Stroke, + pub stroke: PathStroke, // TODO(emilk): Add texture support either by supplying uv for each point, // or by some transform from points to uv (e.g. a callback or a linear transform matrix). } @@ -521,7 +600,7 @@ impl PathShape { /// /// Use [`Shape::line_segment`] instead if your line only connects two points. #[inline] - pub fn line(points: Vec, stroke: impl Into) -> Self { + pub fn line(points: Vec, stroke: impl Into) -> Self { Self { points, closed: false, @@ -532,7 +611,7 @@ impl PathShape { /// A line that closes back to the start point again. #[inline] - pub fn closed_line(points: Vec, stroke: impl Into) -> Self { + pub fn closed_line(points: Vec, stroke: impl Into) -> Self { Self { points, closed: true, @@ -548,7 +627,7 @@ impl PathShape { pub fn convex_polygon( points: Vec, fill: impl Into, - stroke: impl Into, + stroke: impl Into, ) -> Self { Self { points, @@ -593,6 +672,14 @@ pub struct RectShape { /// The thickness and color of the outline. pub stroke: Stroke, + /// If larger than zero, the edges of the rectangle + /// (for both fill and stroke) will be blurred. + /// + /// This can be used to produce shadows and glow effects. + /// + /// The blur is currently implemented using a simple linear blur in sRGBA gamma space. + pub blur_width: f32, + /// If the rect should be filled with a texture, which one? /// /// The texture is multiplied with [`Self::fill`]. @@ -620,6 +707,7 @@ impl RectShape { rounding: rounding.into(), fill: fill_color.into(), stroke: stroke.into(), + blur_width: 0.0, fill_texture_id: Default::default(), uv: Rect::ZERO, } @@ -636,6 +724,7 @@ impl RectShape { rounding: rounding.into(), fill: fill_color.into(), stroke: Default::default(), + blur_width: 0.0, fill_texture_id: Default::default(), uv: Rect::ZERO, } @@ -648,18 +737,32 @@ impl RectShape { rounding: rounding.into(), fill: Default::default(), stroke: stroke.into(), + blur_width: 0.0, fill_texture_id: Default::default(), uv: Rect::ZERO, } } + /// If larger than zero, the edges of the rectangle + /// (for both fill and stroke) will be blurred. + /// + /// This can be used to produce shadows and glow effects. + /// + /// The blur is currently implemented using a simple linear blur in `sRGBA` gamma space. + #[inline] + pub fn with_blur_width(mut self, blur_width: f32) -> Self { + self.blur_width = blur_width; + self + } + /// The visual bounding rectangle (includes stroke width) #[inline] pub fn visual_bounding_rect(&self) -> Rect { if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() { Rect::NOTHING } else { - self.rect.expand(self.stroke.width / 2.0) + self.rect + .expand((self.stroke.width + self.blur_width) / 2.0) } } } @@ -1190,12 +1293,7 @@ impl std::fmt::Debug for PaintCallback { impl std::cmp::PartialEq for PaintCallback { fn eq(&self, other: &Self) -> bool { - // As I understand it, the problem this clippy is trying to protect against - // can only happen if we do dynamic casts back and forth on the pointers, and we don't do that. - #[allow(clippy::vtable_address_comparisons)] - { - self.rect.eq(&other.rect) && Arc::ptr_eq(&self.callback, &other.callback) - } + self.rect.eq(&other.rect) && Arc::ptr_eq(&self.callback, &other.callback) } } diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index c8edff1fcaf..d0ab91536d1 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -1,7 +1,12 @@ +use std::sync::Arc; + use crate::*; /// Remember to handle [`Color32::PLACEHOLDER`] specially! -pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { +pub fn adjust_colors( + shape: &mut Shape, + adjust_color: impl Fn(&mut Color32) + Send + Sync + Copy + 'static, +) { #![allow(clippy::match_same_arms)] match shape { Shape::Noop => {} @@ -10,8 +15,48 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { adjust_colors(shape, adjust_color); } } - Shape::LineSegment { stroke, points: _ } => { - adjust_color(&mut stroke.color); + Shape::LineSegment { stroke, points: _ } => match &stroke.color { + color::ColorMode::Solid(mut col) => adjust_color(&mut col), + color::ColorMode::UV(callback) => { + let callback = callback.clone(); + stroke.color = color::ColorMode::UV(Arc::new(Box::new(move |rect, pos| { + let mut col = callback(rect, pos); + adjust_color(&mut col); + col + }))); + } + }, + + Shape::Path(PathShape { + points: _, + closed: _, + fill, + stroke, + }) + | Shape::QuadraticBezier(QuadraticBezierShape { + points: _, + closed: _, + fill, + stroke, + }) + | Shape::CubicBezier(CubicBezierShape { + points: _, + closed: _, + fill, + stroke, + }) => { + adjust_color(fill); + match &stroke.color { + color::ColorMode::Solid(mut col) => adjust_color(&mut col), + color::ColorMode::UV(callback) => { + let callback = callback.clone(); + stroke.color = color::ColorMode::UV(Arc::new(Box::new(move |rect, pos| { + let mut col = callback(rect, pos); + adjust_color(&mut col); + col + }))); + } + } } Shape::Circle(CircleShape { @@ -20,9 +65,9 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { fill, stroke, }) - | Shape::Path(PathShape { - points: _, - closed: _, + | Shape::Ellipse(EllipseShape { + center: _, + radius: _, fill, stroke, }) @@ -31,20 +76,9 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { rounding: _, fill, stroke, + blur_width: _, fill_texture_id: _, uv: _, - }) - | Shape::QuadraticBezier(QuadraticBezierShape { - points: _, - closed: _, - fill, - stroke, - }) - | Shape::CubicBezier(CubicBezierShape { - points: _, - closed: _, - fill, - stroke, }) => { adjust_color(fill); adjust_color(&mut stroke.color); diff --git a/crates/epaint/src/stats.rs b/crates/epaint/src/stats.rs index 56eaa24ac7a..c63d5a0ddec 100644 --- a/crates/epaint/src/stats.rs +++ b/crates/epaint/src/stats.rs @@ -192,7 +192,7 @@ impl PaintStats { fn add(&mut self, shape: &Shape) { match shape { Shape::Vec(shapes) => { - // self += PaintStats::from_shapes(&shapes); // TODO + // self += PaintStats::from_shapes(&shapes); // TODO(emilk) self.shapes += AllocInfo::from_slice(shapes); self.shape_vec += AllocInfo::from_slice(shapes); for shape in shapes { @@ -201,6 +201,7 @@ impl PaintStats { } Shape::Noop | Shape::Circle { .. } + | Shape::Ellipse { .. } | Shape::LineSegment { .. } | Shape::Rect { .. } | Shape::CubicBezier(_) diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 15ddd231f1b..36ecac253d3 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -1,5 +1,7 @@ #![allow(clippy::derived_hash_with_manual_eq)] // We need to impl Hash for f32, but we don't implement Eq, which is fine +use std::{fmt::Debug, sync::Arc}; + use super::*; /// Describes the width and color of a line. @@ -48,7 +50,72 @@ impl std::hash::Hash for Stroke { #[inline(always)] fn hash(&self, state: &mut H) { let Self { width, color } = *self; - crate::f32_hash(state, width); + emath::OrderedFloat(width).hash(state); color.hash(state); } } + +/// Describes the width and color of paths. The color can either be solid or provided by a callback. For more information, see [`ColorMode`] +/// +/// The default stroke is the same as [`Stroke::NONE`]. +#[derive(Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct PathStroke { + pub width: f32, + pub color: ColorMode, +} + +impl PathStroke { + /// Same as [`PathStroke::default`]. + pub const NONE: Self = Self { + width: 0.0, + color: ColorMode::TRANSPARENT, + }; + + #[inline] + pub fn new(width: impl Into, color: impl Into) -> Self { + Self { + width: width.into(), + color: ColorMode::Solid(color.into()), + } + } + + /// Create a new `PathStroke` with a UV function + /// + /// The bounding box passed to the callback will have a margin of [`TessellationOptions::feathering_size_in_pixels`](`crate::tessellator::TessellationOptions::feathering_size_in_pixels`) + #[inline] + pub fn new_uv( + width: impl Into, + callback: impl Fn(Rect, Pos2) -> Color32 + Send + Sync + 'static, + ) -> Self { + Self { + width: width.into(), + color: ColorMode::UV(Arc::new(callback)), + } + } + + /// True if width is zero or color is solid and transparent + #[inline] + pub fn is_empty(&self) -> bool { + self.width <= 0.0 || self.color == ColorMode::TRANSPARENT + } +} + +impl From<(f32, Color)> for PathStroke +where + Color: Into, +{ + #[inline(always)] + fn from((width, color): (f32, Color)) -> Self { + Self::new(width, color) + } +} + +impl From for PathStroke { + fn from(value: Stroke) -> Self { + Self { + width: value.width, + color: ColorMode::Solid(value.color), + } + } +} diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index c753b7374df..71a4b780515 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -9,6 +9,9 @@ use crate::texture_atlas::PreparedDisc; use crate::*; use emath::*; +use self::color::ColorMode; +use self::stroke::PathStroke; + // ---------------------------------------------------------------------------- #[allow(clippy::approx_constant)] @@ -471,16 +474,22 @@ impl Path { } /// Open-ended. - pub fn stroke_open(&self, feathering: f32, stroke: Stroke, out: &mut Mesh) { + pub fn stroke_open(&self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) { stroke_path(feathering, &self.0, PathType::Open, stroke, out); } /// A closed path (returning to the first point). - pub fn stroke_closed(&self, feathering: f32, stroke: Stroke, out: &mut Mesh) { + pub fn stroke_closed(&self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) { stroke_path(feathering, &self.0, PathType::Closed, stroke, out); } - pub fn stroke(&self, feathering: f32, path_type: PathType, stroke: Stroke, out: &mut Mesh) { + pub fn stroke( + &self, + feathering: f32, + path_type: PathType, + stroke: &PathStroke, + out: &mut Mesh, + ) { stroke_path(feathering, &self.0, path_type, stroke, out); } @@ -520,7 +529,7 @@ pub mod path { let min = rect.min; let max = rect.max; - let r = clamp_radius(rounding, rect); + let r = clamp_rounding(rounding, rect); if r == Rounding::ZERO { let min = rect.min; @@ -531,11 +540,33 @@ pub mod path { path.push(pos2(max.x, max.y)); // right bottom path.push(pos2(min.x, max.y)); // left bottom } else { - add_circle_quadrant(path, pos2(max.x - r.se, max.y - r.se), r.se, 0.0); - add_circle_quadrant(path, pos2(min.x + r.sw, max.y - r.sw), r.sw, 1.0); - add_circle_quadrant(path, pos2(min.x + r.nw, min.y + r.nw), r.nw, 2.0); - add_circle_quadrant(path, pos2(max.x - r.ne, min.y + r.ne), r.ne, 3.0); - path.dedup(); // We get duplicates for thin rectangles, producing visual artifats + // We need to avoid duplicated vertices, because that leads to visual artifacts later. + // Duplicated vertices can happen when one side is all rounding, with no straight edge between. + let eps = f32::EPSILON * rect.size().max_elem(); + + add_circle_quadrant(path, pos2(max.x - r.se, max.y - r.se), r.se, 0.0); // south east + + if rect.width() <= r.se + r.sw + eps { + path.pop(); // avoid duplicated vertex + } + + add_circle_quadrant(path, pos2(min.x + r.sw, max.y - r.sw), r.sw, 1.0); // south west + + if rect.height() <= r.sw + r.nw + eps { + path.pop(); // avoid duplicated vertex + } + + add_circle_quadrant(path, pos2(min.x + r.nw, min.y + r.nw), r.nw, 2.0); // north west + + if rect.width() <= r.nw + r.ne + eps { + path.pop(); // avoid duplicated vertex + } + + add_circle_quadrant(path, pos2(max.x - r.ne, min.y + r.ne), r.ne, 3.0); // north east + + if rect.height() <= r.ne + r.se + eps { + path.pop(); // avoid duplicated vertex + } } } @@ -589,7 +620,7 @@ pub mod path { } // Ensures the radius of each corner is within a valid range - fn clamp_radius(rounding: Rounding, rect: Rect) -> Rounding { + fn clamp_rounding(rounding: Rounding, rect: Rect) -> Rounding { let half_width = rect.width() * 0.5; let half_height = rect.height() * 0.5; let max_cr = half_width.min(half_height); @@ -622,7 +653,7 @@ pub struct TessellationOptions { /// Default: `true`. pub feathering: bool, - /// The size of the the feathering, in physical pixels. + /// The size of the feathering, in physical pixels. /// /// The default, and suggested, value for this is `1.0`. /// If you use a larger value, edges will appear blurry. @@ -842,19 +873,28 @@ fn stroke_path( feathering: f32, path: &[PathPoint], path_type: PathType, - stroke: Stroke, + stroke: &PathStroke, out: &mut Mesh, ) { let n = path.len() as u32; - if stroke.width <= 0.0 || stroke.color == Color32::TRANSPARENT || n < 2 { + if stroke.width <= 0.0 || stroke.color == ColorMode::TRANSPARENT || n < 2 { return; } let idx = out.vertices.len() as u32; + // expand the bounding box to include the thickness of the path + let bbox = Rect::from_points(&path.iter().map(|p| p.pos).collect::>()) + .expand((stroke.width / 2.0) + feathering); + + let get_color = |col: &ColorMode, pos: Pos2| match col { + ColorMode::Solid(col) => *col, + ColorMode::UV(fun) => fun(bbox, pos), + }; + if feathering > 0.0 { - let color_inner = stroke.color; + let color_inner = &stroke.color; let color_outer = Color32::TRANSPARENT; let thin_line = stroke.width <= feathering; @@ -867,9 +907,11 @@ fn stroke_path( */ // Fade out as it gets thinner: - let color_inner = mul_color(color_inner, stroke.width / feathering); - if color_inner == Color32::TRANSPARENT { - return; + if let ColorMode::Solid(col) = color_inner { + let color_inner = mul_color(*col, stroke.width / feathering); + if color_inner == Color32::TRANSPARENT { + return; + } } out.reserve_triangles(4 * n as usize); @@ -882,7 +924,10 @@ fn stroke_path( let p = p1.pos; let n = p1.normal; out.colored_vertex(p + n * feathering, color_outer); - out.colored_vertex(p, color_inner); + out.colored_vertex( + p, + mul_color(get_color(color_inner, p), stroke.width / feathering), + ); out.colored_vertex(p - n * feathering, color_outer); if connect_with_previous { @@ -921,8 +966,14 @@ fn stroke_path( let p = p1.pos; let n = p1.normal; out.colored_vertex(p + n * outer_rad, color_outer); - out.colored_vertex(p + n * inner_rad, color_inner); - out.colored_vertex(p - n * inner_rad, color_inner); + out.colored_vertex( + p + n * inner_rad, + get_color(color_inner, p + n * inner_rad), + ); + out.colored_vertex( + p - n * inner_rad, + get_color(color_inner, p - n * inner_rad), + ); out.colored_vertex(p - n * outer_rad, color_outer); out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0); @@ -961,8 +1012,14 @@ fn stroke_path( let n = end.normal; let back_extrude = n.rot90() * feathering; out.colored_vertex(p + n * outer_rad + back_extrude, color_outer); - out.colored_vertex(p + n * inner_rad, color_inner); - out.colored_vertex(p - n * inner_rad, color_inner); + out.colored_vertex( + p + n * inner_rad, + get_color(color_inner, p + n * inner_rad), + ); + out.colored_vertex( + p - n * inner_rad, + get_color(color_inner, p - n * inner_rad), + ); out.colored_vertex(p - n * outer_rad + back_extrude, color_outer); out.add_triangle(idx + 0, idx + 1, idx + 2); @@ -975,8 +1032,14 @@ fn stroke_path( let p = point.pos; let n = point.normal; out.colored_vertex(p + n * outer_rad, color_outer); - out.colored_vertex(p + n * inner_rad, color_inner); - out.colored_vertex(p - n * inner_rad, color_inner); + out.colored_vertex( + p + n * inner_rad, + get_color(color_inner, p + n * inner_rad), + ); + out.colored_vertex( + p - n * inner_rad, + get_color(color_inner, p - n * inner_rad), + ); out.colored_vertex(p - n * outer_rad, color_outer); out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0); @@ -998,8 +1061,14 @@ fn stroke_path( let n = end.normal; let back_extrude = -n.rot90() * feathering; out.colored_vertex(p + n * outer_rad + back_extrude, color_outer); - out.colored_vertex(p + n * inner_rad, color_inner); - out.colored_vertex(p - n * inner_rad, color_inner); + out.colored_vertex( + p + n * inner_rad, + get_color(color_inner, p + n * inner_rad), + ); + out.colored_vertex( + p - n * inner_rad, + get_color(color_inner, p - n * inner_rad), + ); out.colored_vertex(p - n * outer_rad + back_extrude, color_outer); out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0); @@ -1045,19 +1114,39 @@ fn stroke_path( if thin_line { // Fade out thin lines rather than making them thinner let radius = feathering / 2.0; - let color = mul_color(stroke.color, stroke.width / feathering); - if color == Color32::TRANSPARENT { - return; + if let ColorMode::Solid(color) = stroke.color { + let color = mul_color(color, stroke.width / feathering); + if color == Color32::TRANSPARENT { + return; + } } for p in path { - out.colored_vertex(p.pos + radius * p.normal, color); - out.colored_vertex(p.pos - radius * p.normal, color); + out.colored_vertex( + p.pos + radius * p.normal, + mul_color( + get_color(&stroke.color, p.pos + radius * p.normal), + stroke.width / feathering, + ), + ); + out.colored_vertex( + p.pos - radius * p.normal, + mul_color( + get_color(&stroke.color, p.pos - radius * p.normal), + stroke.width / feathering, + ), + ); } } else { let radius = stroke.width / 2.0; for p in path { - out.colored_vertex(p.pos + radius * p.normal, stroke.color); - out.colored_vertex(p.pos - radius * p.normal, stroke.color); + out.colored_vertex( + p.pos + radius * p.normal, + get_color(&stroke.color, p.pos + radius * p.normal), + ); + out.colored_vertex( + p.pos - radius * p.normal, + get_color(&stroke.color, p.pos - radius * p.normal), + ); } } } @@ -1215,11 +1304,14 @@ impl Tessellator { Shape::Circle(circle) => { self.tessellate_circle(circle, out); } + Shape::Ellipse(ellipse) => { + self.tessellate_ellipse(ellipse, out); + } Shape::Mesh(mesh) => { crate::profile_scope!("mesh"); if self.options.validate_meshes && !mesh.is_valid() { - crate::epaint_assert!(false, "Invalid Mesh in Shape::Mesh"); + debug_assert!(false, "Invalid Mesh in Shape::Mesh"); return; } // note: `append` still checks if the mesh is valid if extra asserts are enabled. @@ -1250,9 +1342,9 @@ impl Tessellator { self.tessellate_text(&text_shape, out); } Shape::QuadraticBezier(quadratic_shape) => { - self.tessellate_quadratic_bezier(quadratic_shape, out); + self.tessellate_quadratic_bezier(&quadratic_shape, out); } - Shape::CubicBezier(cubic_shape) => self.tessellate_cubic_bezier(cubic_shape, out), + Shape::CubicBezier(cubic_shape) => self.tessellate_cubic_bezier(&cubic_shape, out), Shape::Callback(_) => { panic!("Shape::Callback passed to Tessellator"); } @@ -1312,7 +1404,74 @@ impl Tessellator { self.scratchpad_path.add_circle(center, radius); self.scratchpad_path.fill(self.feathering, fill, out); self.scratchpad_path - .stroke_closed(self.feathering, stroke, out); + .stroke_closed(self.feathering, &stroke.into(), out); + } + + /// Tessellate a single [`EllipseShape`] into a [`Mesh`]. + /// + /// * `shape`: the ellipse to tessellate. + /// * `out`: triangles are appended to this. + pub fn tessellate_ellipse(&mut self, shape: EllipseShape, out: &mut Mesh) { + let EllipseShape { + center, + radius, + fill, + stroke, + } = shape; + + if radius.x <= 0.0 || radius.y <= 0.0 { + return; + } + + if self.options.coarse_tessellation_culling + && !self + .clip_rect + .expand2(radius + Vec2::splat(stroke.width)) + .contains(center) + { + return; + } + + // Get the max pixel radius + let max_radius = (radius.max_elem() * self.pixels_per_point) as u32; + + // Ensure there is at least 8 points in each quarter of the ellipse + let num_points = u32::max(8, max_radius / 16); + + // Create an ease ratio based the ellipses a and b + let ratio = ((radius.y / radius.x) / 2.0).clamp(0.0, 1.0); + + // Generate points between the 0 to pi/2 + let quarter: Vec = (1..num_points) + .map(|i| { + let percent = i as f32 / num_points as f32; + + // Ease the percent value, concentrating points around tight bends + let eased = 2.0 * (percent - percent.powf(2.0)) * ratio + percent.powf(2.0); + + // Scale the ease to the quarter + let t = eased * std::f32::consts::FRAC_PI_2; + Vec2::new(radius.x * f32::cos(t), radius.y * f32::sin(t)) + }) + .collect(); + + // Build the ellipse from the 4 known vertices filling arcs between + // them by mirroring the points between 0 and pi/2 + let mut points = Vec::new(); + points.push(center + Vec2::new(radius.x, 0.0)); + points.extend(quarter.iter().map(|p| center + *p)); + points.push(center + Vec2::new(0.0, radius.y)); + points.extend(quarter.iter().rev().map(|p| center + Vec2::new(-p.x, p.y))); + points.push(center + Vec2::new(-radius.x, 0.0)); + points.extend(quarter.iter().map(|p| center - *p)); + points.push(center + Vec2::new(0.0, -radius.y)); + points.extend(quarter.iter().rev().map(|p| center + Vec2::new(p.x, -p.y))); + + self.scratchpad_path.clear(); + self.scratchpad_path.add_line_loop(&points); + self.scratchpad_path.fill(self.feathering, fill, out); + self.scratchpad_path + .stroke_closed(self.feathering, &stroke.into(), out); } /// Tessellate a single [`Mesh`] into a [`Mesh`]. @@ -1321,7 +1480,7 @@ impl Tessellator { /// * `out`: triangles are appended to this. pub fn tessellate_mesh(&mut self, mesh: &Mesh, out: &mut Mesh) { if !mesh.is_valid() { - crate::epaint_assert!(false, "Invalid Mesh in Shape::Mesh"); + debug_assert!(false, "Invalid Mesh in Shape::Mesh"); return; } @@ -1338,7 +1497,13 @@ impl Tessellator { /// /// * `shape`: the mesh to tessellate. /// * `out`: triangles are appended to this. - pub fn tessellate_line(&mut self, points: [Pos2; 2], stroke: Stroke, out: &mut Mesh) { + pub fn tessellate_line( + &mut self, + points: [Pos2; 2], + stroke: impl Into, + out: &mut Mesh, + ) { + let stroke = stroke.into(); if stroke.is_empty() { return; } @@ -1354,7 +1519,7 @@ impl Tessellator { self.scratchpad_path.clear(); self.scratchpad_path.add_line_segment(points); self.scratchpad_path - .stroke_open(self.feathering, stroke, out); + .stroke_open(self.feathering, &stroke, out); } /// Tessellate a single [`PathShape`] into a [`Mesh`]. @@ -1389,7 +1554,7 @@ impl Tessellator { } if *fill != Color32::TRANSPARENT { - crate::epaint_assert!( + debug_assert!( closed, "You asked to fill a path that is not closed. That makes no sense." ); @@ -1401,7 +1566,7 @@ impl Tessellator { PathType::Open }; self.scratchpad_path - .stroke(self.feathering, typ, *stroke, out); + .stroke(self.feathering, typ, stroke, out); } /// Tessellate a single [`Rect`] into a [`Mesh`]. @@ -1411,9 +1576,10 @@ impl Tessellator { pub fn tessellate_rect(&mut self, rect: &RectShape, out: &mut Mesh) { let RectShape { mut rect, - rounding, + mut rounding, fill, stroke, + mut blur_width, fill_texture_id, uv, } = *rect; @@ -1432,6 +1598,29 @@ impl Tessellator { rect.min = rect.min.at_least(pos2(-1e7, -1e7)); rect.max = rect.max.at_most(pos2(1e7, 1e7)); + let old_feathering = self.feathering; + + if old_feathering < blur_width { + // We accomplish the blur by using a larger-than-normal feathering. + // Feathering is usually used to make the edges of a shape softer for anti-aliasing. + + // The tessellator can't handle blurring/feathering larger than the smallest side of the rect. + // Thats because the tessellator approximate very thin rectangles as line segments, + // and these line segments don't have rounded corners. + // When the feathering is small (the size of a pixel), this is usually fine, + // but here we have a huge feathering to simulate blur, + // so we need to avoid this optimization in the tessellator, + // which is also why we add this rather big epsilon: + let eps = 0.1; + blur_width = blur_width + .at_most(rect.size().min_elem() - eps) + .at_least(0.0); + + rounding += Rounding::same(0.5 * blur_width); + + self.feathering = self.feathering.max(blur_width); + } + if rect.width() < self.feathering { // Very thin - approximate by a vertical line-segment: let line = [rect.center_top(), rect.center_bottom()]; @@ -1472,8 +1661,10 @@ impl Tessellator { path.fill(self.feathering, fill, out); } - path.stroke_closed(self.feathering, stroke, out); + path.stroke_closed(self.feathering, &stroke.into(), out); } + + self.feathering = old_feathering; // restore } /// Tessellate a single [`TextShape`] into a [`Mesh`]. @@ -1569,7 +1760,7 @@ impl Tessellator { color = color.gamma_multiply(*opacity_factor); } - crate::epaint_assert!(color != Color32::PLACEHOLDER, "A placeholder color made it to the tessellator. You forgot to set a fallback color."); + debug_assert!(color != Color32::PLACEHOLDER, "A placeholder color made it to the tessellator. You forgot to set a fallback color."); let offset = if *angle == 0.0 { pos.to_vec2() @@ -1589,8 +1780,11 @@ impl Tessellator { self.scratchpad_path.clear(); self.scratchpad_path .add_line_segment([row_rect.left_bottom(), row_rect.right_bottom()]); - self.scratchpad_path - .stroke_open(self.feathering, *underline, out); + self.scratchpad_path.stroke_open( + self.feathering, + &PathStroke::from(*underline), + out, + ); } } } @@ -1601,7 +1795,7 @@ impl Tessellator { /// * `out`: triangles are appended to this. pub fn tessellate_quadratic_bezier( &mut self, - quadratic_shape: QuadraticBezierShape, + quadratic_shape: &QuadraticBezierShape, out: &mut Mesh, ) { let options = &self.options; @@ -1619,7 +1813,7 @@ impl Tessellator { &points, quadratic_shape.fill, quadratic_shape.closed, - quadratic_shape.stroke, + &quadratic_shape.stroke, out, ); } @@ -1628,7 +1822,7 @@ impl Tessellator { /// /// * `cubic_shape`: the shape to tessellate. /// * `out`: triangles are appended to this. - pub fn tessellate_cubic_bezier(&mut self, cubic_shape: CubicBezierShape, out: &mut Mesh) { + pub fn tessellate_cubic_bezier(&mut self, cubic_shape: &CubicBezierShape, out: &mut Mesh) { let options = &self.options; let clip_rect = self.clip_rect; if options.coarse_tessellation_culling @@ -1645,7 +1839,7 @@ impl Tessellator { &points, cubic_shape.fill, cubic_shape.closed, - cubic_shape.stroke, + &cubic_shape.stroke, out, ); } @@ -1656,7 +1850,7 @@ impl Tessellator { points: &[Pos2], fill: Color32, closed: bool, - stroke: Stroke, + stroke: &PathStroke, out: &mut Mesh, ) { if points.len() < 2 { @@ -1670,7 +1864,7 @@ impl Tessellator { self.scratchpad_path.add_open_points(points); } if fill != Color32::TRANSPARENT { - crate::epaint_assert!( + debug_assert!( closed, "You asked to fill a path that is not closed. That makes no sense." ); @@ -1752,7 +1946,7 @@ impl Tessellator { for clipped_primitive in &clipped_primitives { if let Primitive::Mesh(mesh) = &clipped_primitive.primitive { - crate::epaint_assert!(mesh.is_valid(), "Tessellator generated invalid Mesh"); + debug_assert!(mesh.is_valid(), "Tessellator generated invalid Mesh"); } } @@ -1776,7 +1970,7 @@ impl Tessellator { Shape::Path(path_shape) => 32 < path_shape.points.len(), - Shape::QuadraticBezier(_) | Shape::CubicBezier(_) => true, + Shape::QuadraticBezier(_) | Shape::CubicBezier(_) | Shape::Ellipse(_) => true, Shape::Noop | Shape::Text(_) @@ -1794,7 +1988,7 @@ impl Tessellator { .filter(|(_, clipped_shape)| should_parallelize(&clipped_shape.shape)) .map(|(index, clipped_shape)| { crate::profile_scope!("tessellate_big_shape"); - // TODO: reuse tessellator in a thread local + // TODO(emilk): reuse tessellator in a thread local let mut tessellator = (*self).clone(); let mut mesh = Mesh::default(); tessellator.tessellate_shape(clipped_shape.shape.clone(), &mut mesh); @@ -1867,3 +2061,48 @@ fn test_tessellator() { assert_eq!(primitives.len(), 2); } + +#[test] +fn path_bounding_box() { + use crate::*; + + for i in 1..=100 { + let width = i as f32; + + let rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(10.0, 10.0)); + let expected_rect = rect.expand((width / 2.0) + 1.5); + + let mut mesh = Mesh::default(); + + let mut path = Path::default(); + path.add_open_points(&[ + pos2(0.0, 0.0), + pos2(2.0, 0.0), + pos2(5.0, 5.0), + pos2(0.0, 5.0), + pos2(0.0, 7.0), + pos2(10.0, 10.0), + ]); + + path.stroke( + 1.5, + PathType::Closed, + &PathStroke::new_uv(width, move |r, p| { + assert_eq!(r, expected_rect); + // see https://github.com/emilk/egui/pull/4353#discussion_r1573879940 for why .contains() isn't used here. + // TL;DR rounding errors. + assert!( + r.distance_to_pos(p) <= 0.55, + "passed rect {r:?} didn't contain point {p:?} (distance: {})", + r.distance_to_pos(p) + ); + assert!( + expected_rect.distance_to_pos(p) <= 0.55, + "expected rect {expected_rect:?} didn't contain point {p:?}" + ); + Color32::WHITE + }), + &mut mesh, + ); + } +} diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 557d71733c8..394b898291e 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -8,7 +8,7 @@ use crate::{ }, TextureAtlas, }; -use emath::NumExt as _; +use emath::{NumExt as _, OrderedFloat}; // ---------------------------------------------------------------------------- @@ -56,7 +56,7 @@ impl std::hash::Hash for FontId { #[inline(always)] fn hash(&self, state: &mut H) { let Self { size, family } = self; - crate::f32_hash(state, *size); + emath::OrderedFloat(*size).hash(state); family.hash(state); } } @@ -567,21 +567,6 @@ impl FontsAndCache { // ---------------------------------------------------------------------------- -#[derive(Clone, Copy, Debug, PartialEq)] -struct HashableF32(f32); - -#[allow(clippy::derived_hash_with_manual_eq)] -impl std::hash::Hash for HashableF32 { - #[inline(always)] - fn hash(&self, state: &mut H) { - crate::f32_hash(state, self.0); - } -} - -impl Eq for HashableF32 {} - -// ---------------------------------------------------------------------------- - /// The collection of fonts used by `epaint`. /// /// Required in order to paint text. @@ -591,7 +576,7 @@ pub struct FontsImpl { definitions: FontDefinitions, atlas: Arc>, font_impl_cache: FontImplCache, - sized_family: ahash::HashMap<(HashableF32, FontFamily), Font>, + sized_family: ahash::HashMap<(OrderedFloat, FontFamily), Font>, } impl FontsImpl { @@ -641,7 +626,7 @@ impl FontsImpl { let FontId { size, family } = font_id; self.sized_family - .entry((HashableF32(*size), family.clone())) + .entry((OrderedFloat(*size), family.clone())) .or_insert_with(|| { let fonts = &self.definitions.families.get(family); let fonts = fonts diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 9b055edc577..276f3f9e2a3 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use emath::*; -use crate::{text::font::Font, Color32, Mesh, Stroke, Vertex}; +use crate::{stroke::PathStroke, text::font::Font, Color32, Mesh, Stroke, Vertex}; use super::{FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, Row, RowVisuals}; @@ -296,7 +296,7 @@ fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec, e // Start a new row: row_start_idx = last_kept_index + 1; row_start_x = paragraph.glyphs[row_start_idx].pos.x; - row_break_candidates = Default::default(); + row_break_candidates.forget_before_idx(row_start_idx); } else { // Found no place to break, so we have to overrun wrap_width. } @@ -853,7 +853,7 @@ fn add_hline(point_scale: PointScale, [start, stop]: [Pos2; 2], stroke: Stroke, let mut path = crate::tessellator::Path::default(); // TODO(emilk): reuse this to avoid re-allocations. path.add_line_segment([start, stop]); let feathering = 1.0 / point_scale.pixels_per_point(); - path.stroke_open(feathering, stroke, mesh); + path.stroke_open(feathering, &PathStroke::from(stroke), mesh); } else { // Thin lines often lost, so this is a bad idea @@ -943,6 +943,35 @@ impl RowBreakCandidates { .or(self.any) } } + + fn forget_before_idx(&mut self, index: usize) { + let Self { + space, + cjk, + pre_cjk, + dash, + punctuation, + any, + } = self; + if space.map_or(false, |s| s < index) { + *space = None; + } + if cjk.map_or(false, |s| s < index) { + *cjk = None; + } + if pre_cjk.map_or(false, |s| s < index) { + *pre_cjk = None; + } + if dash.map_or(false, |s| s < index) { + *dash = None; + } + if punctuation.map_or(false, |s| s < index) { + *punctuation = None; + } + if any.map_or(false, |s| s < index) { + *any = None; + } + } } #[inline] @@ -960,7 +989,7 @@ fn is_kana(c: char) -> bool { #[inline] fn is_cjk(c: char) -> bool { - // TODO: Add support for Korean Hangul. + // TODO(bigfarts): Add support for Korean Hangul. is_cjk_ideograph(c) || is_kana(c) } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 70317f771f6..dec2cb29057 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -185,7 +185,7 @@ impl std::hash::Hash for LayoutJob { text.hash(state); sections.hash(state); wrap.hash(state); - crate::f32_hash(state, *first_row_min_height); + emath::OrderedFloat(*first_row_min_height).hash(state); break_on_newline.hash(state); halign.hash(state); justify.hash(state); @@ -214,7 +214,7 @@ impl std::hash::Hash for LayoutSection { byte_range, format, } = self; - crate::f32_hash(state, *leading_space); + OrderedFloat(*leading_space).hash(state); byte_range.hash(state); format.hash(state); } @@ -293,9 +293,9 @@ impl std::hash::Hash for TextFormat { valign, } = self; font_id.hash(state); - crate::f32_hash(state, *extra_letter_spacing); + emath::OrderedFloat(*extra_letter_spacing).hash(state); if let Some(line_height) = *line_height { - crate::f32_hash(state, line_height); + emath::OrderedFloat(line_height).hash(state); } color.hash(state); background.hash(state); @@ -375,7 +375,7 @@ impl std::hash::Hash for TextWrapping { break_anywhere, overflow_character, } = self; - crate::f32_hash(state, *max_width); + emath::OrderedFloat(*max_width).hash(state); max_rows.hash(state); break_anywhere.hash(state); overflow_character.hash(state); @@ -453,9 +453,9 @@ pub struct Galley { /// `rect.top()` is always 0.0. /// /// With [`LayoutJob::halign`]: - /// * [`Align::LEFT`]: rect.left() == 0.0 - /// * [`Align::Center`]: rect.center() == 0.0 - /// * [`Align::RIGHT`]: rect.right() == 0.0 + /// * [`Align::LEFT`]: `rect.left() == 0.0` + /// * [`Align::Center`]: `rect.center() == 0.0` + /// * [`Align::RIGHT`]: `rect.right() == 0.0` pub rect: Rect, /// Tight bounding box around all the meshes in all the rows. @@ -890,7 +890,7 @@ impl Galley { pcursor_it.offset += row.char_count_including_newline(); } } - crate::epaint_assert!(ccursor_it == self.end().ccursor); + debug_assert!(ccursor_it == self.end().ccursor); Cursor { ccursor: ccursor_it, // clamp rcursor: self.end_rcursor(), diff --git a/crates/epaint/src/textures.rs b/crates/epaint/src/textures.rs index e4661ff02b7..4244c6871a7 100644 --- a/crates/epaint/src/textures.rs +++ b/crates/epaint/src/textures.rs @@ -49,7 +49,7 @@ impl TextureManager { pub fn set(&mut self, id: TextureId, delta: ImageDelta) { if let Some(meta) = self.metas.get_mut(&id) { if let Some(pos) = delta.pos { - crate::epaint_assert!( + debug_assert!( pos[0] + delta.image.width() <= meta.size[0] && pos[1] + delta.image.height() <= meta.size[1], "Partial texture update is outside the bounds of texture {id:?}", @@ -63,7 +63,7 @@ impl TextureManager { } self.delta.set.push((id, delta)); } else { - crate::epaint_assert!(false, "Tried setting texture {id:?} which is not allocated"); + debug_assert!(false, "Tried setting texture {id:?} which is not allocated"); } } @@ -77,7 +77,7 @@ impl TextureManager { self.delta.free.push(id); } } else { - crate::epaint_assert!(false, "Tried freeing texture {id:?} which is not allocated"); + debug_assert!(false, "Tried freeing texture {id:?} which is not allocated"); } } @@ -88,7 +88,7 @@ impl TextureManager { if let Some(meta) = self.metas.get_mut(&id) { meta.retain_count += 1; } else { - crate::epaint_assert!( + debug_assert!( false, "Tried retaining texture {id:?} which is not allocated", ); diff --git a/crates/epaint/src/util/mod.rs b/crates/epaint/src/util/mod.rs index 2aa70ee041e..383b945671c 100644 --- a/crates/epaint/src/util/mod.rs +++ b/crates/epaint/src/util/mod.rs @@ -1,6 +1,5 @@ -mod ordered_float; - -pub use ordered_float::*; +#[deprecated = "Use emath::OrderedFloat instead"] +pub use emath::OrderedFloat; /// Hash the given value with a predictable hasher. #[inline] diff --git a/deny.toml b/deny.toml index 17c47772833..02a9cdb04a3 100644 --- a/deny.toml +++ b/deny.toml @@ -1,7 +1,17 @@ -# https://embarkstudios.github.io/cargo-deny/ +# Copied from https://github.com/rerun-io/rerun_template +# +# https://github.com/EmbarkStudios/cargo-deny +# +# cargo-deny checks our dependency tree for copy-left licenses, +# duplicate dependencies, and rustsec advisories (https://rustsec.org/advisories). +# +# Install: `cargo install cargo-deny` +# Check: `cargo deny check`. + # Note: running just `cargo deny check` without a `--target` can result in # false positives due to https://github.com/EmbarkStudios/cargo-deny/issues/324 +[graph] targets = [ { triple = "aarch64-apple-darwin" }, { triple = "i686-pc-windows-gnu" }, @@ -15,26 +25,30 @@ targets = [ { triple = "x86_64-unknown-linux-musl" }, { triple = "x86_64-unknown-redox" }, ] +all-features = true + [advisories] -vulnerability = "deny" -unmaintained = "warn" -yanked = "deny" +version = 2 ignore = [ - "RUSTSEC-2020-0071", # https://rustsec.org/advisories/RUSTSEC-2020-0071 - chrono/time: Potential segfault in the time crate + "RUSTSEC-2024-0320", # unmaintaines yaml-rust pulled in by syntect + { name = "async-process" }, # yanked crated pulled in by old accesskit ] + [bans] multiple-versions = "deny" -wildcards = "allow" # at least until https://github.com/EmbarkStudios/cargo-deny/issues/241 is fixed +wildcards = "deny" deny = [ - { name = "cmake" }, # Lord no - { name = "openssl-sys" }, # prefer rustls - { name = "openssl" }, # prefer rustls + { name = "cmake", reason = "It has hurt me too much" }, + { name = "openssl-sys", reason = "Use rustls" }, + { name = "openssl", reason = "Use rustls" }, ] skip = [ { name = "bitflags" }, # old 1.0 version via glutin, png, spirv, … + { name = "block2" }, # old version via glutin->icrate + { name = "event-listener" }, # TODO(emilk): rustls pulls in two versions of this 😭 { name = "libloading" }, # wgpu-hal itself depends on 0.8 while some of its dependencies, like ash and d3d12, depend on 0.7 { name = "memoffset" }, # tiny dependency { name = "quick-xml" }, # old version via wayland-scanner @@ -42,13 +56,13 @@ skip = [ { name = "spin" }, # old version via ring through rusttls and other libraries, newer for wgpu. { name = "time" }, # old version pulled in by unmaintianed crate 'chrono' { name = "windows" }, # old version via accesskit_windows - { name = "x11rb" }, # old version via arboard { name = "x11rb-protocol" }, # old version via arboard + { name = "x11rb" }, # old version via arboard ] skip-tree = [ { name = "criterion" }, # dev-dependency { name = "fastrand" }, # old version via accesskit_unix - { name = "foreign-types" }, # small crate. Old version via cocoa and core-graphics (winit). + { name = "foreign-types" }, # small crate. Old version via core-graphics (winit). { name = "objc2" }, # old version via accesskit_macos { name = "polling" }, # old version via accesskit_unix { name = "rfd" }, # example dependency @@ -56,10 +70,9 @@ skip-tree = [ [licenses] -unlicensed = "deny" -allow-osi-fsf-free = "neither" -confidence-threshold = 0.92 # We want really high confidence when inferring licenses from text -copyleft = "deny" +version = 2 +private = { ignore = true } +confidence-threshold = 0.93 # We want really high confidence when inferring licenses from text allow = [ "Apache-2.0 WITH LLVM-exception", # https://spdx.org/licenses/LLVM-exception.html "Apache-2.0", # https://tldrlegal.com/license/apache-license-2.0-(apache-2.0) @@ -67,15 +80,17 @@ allow = [ "BSD-3-Clause", # https://tldrlegal.com/license/bsd-3-clause-license-(revised) "BSL-1.0", # https://tldrlegal.com/license/boost-software-license-1.0-explained "CC0-1.0", # https://creativecommons.org/publicdomain/zero/1.0/ - "ISC", # https://tldrlegal.com/license/-isc-license - "LicenseRef-UFL-1.0", # https://tldrlegal.com/license/ubuntu-font-license,-1.0 - no official SPDX, see https://github.com/emilk/egui/issues/2321 + "ISC", # https://www.tldrlegal.com/license/isc-license + "LicenseRef-UFL-1.0", # no official SPDX, see https://github.com/emilk/egui/issues/2321 + "MIT-0", # https://choosealicense.com/licenses/mit-0/ "MIT", # https://tldrlegal.com/license/mit-license - "MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11 + "MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11. Used by webpki-roots on Linux. "OFL-1.1", # https://spdx.org/licenses/OFL-1.1.html - "OpenSSL", # https://www.openssl.org/source/license.html + "OpenSSL", # https://www.openssl.org/source/license.html - used on Linux "Unicode-DFS-2016", # https://spdx.org/licenses/Unicode-DFS-2016.html "Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib) ] +exceptions = [] [[licenses.clarify]] name = "webpki" @@ -86,3 +101,8 @@ license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] name = "ring" expression = "MIT AND ISC AND OpenSSL" license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] + + +[sources] +unknown-registry = "deny" +unknown-git = "deny" diff --git a/examples/confirm_exit/Cargo.toml b/examples/confirm_exit/Cargo.toml index 09ad2c4f606..b1cab21a1bb 100644 --- a/examples/confirm_exit/Cargo.toml +++ b/examples/confirm_exit/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + [dependencies] eframe = { workspace = true, features = [ diff --git a/examples/confirm_exit/src/main.rs b/examples/confirm_exit/src/main.rs index 3a09da59d91..14816e1bdea 100644 --- a/examples/confirm_exit/src/main.rs +++ b/examples/confirm_exit/src/main.rs @@ -1,4 +1,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui; diff --git a/examples/custom_3d_glow/Cargo.toml b/examples/custom_3d_glow/Cargo.toml index d7e70401a47..3b8c9a45383 100644 --- a/examples/custom_3d_glow/Cargo.toml +++ b/examples/custom_3d_glow/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + [dependencies] eframe = { workspace = true, features = [ diff --git a/examples/custom_3d_glow/src/main.rs b/examples/custom_3d_glow/src/main.rs index a1f6fa26946..241124b7c17 100644 --- a/examples/custom_3d_glow/src/main.rs +++ b/examples/custom_3d_glow/src/main.rs @@ -1,5 +1,7 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example #![allow(unsafe_code)] +#![allow(clippy::undocumented_unsafe_blocks)] use eframe::{egui, egui_glow, glow}; diff --git a/examples/custom_font/Cargo.toml b/examples/custom_font/Cargo.toml index db633431531..d6021c12425 100644 --- a/examples/custom_font/Cargo.toml +++ b/examples/custom_font/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + [dependencies] eframe = { workspace = true, features = [ diff --git a/examples/custom_font/src/main.rs b/examples/custom_font/src/main.rs index 8a852319dd1..f42c5763150 100644 --- a/examples/custom_font/src/main.rs +++ b/examples/custom_font/src/main.rs @@ -1,4 +1,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui; diff --git a/examples/custom_font_style/Cargo.toml b/examples/custom_font_style/Cargo.toml index 51990ee919d..65b5045238c 100644 --- a/examples/custom_font_style/Cargo.toml +++ b/examples/custom_font_style/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["tami5 "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + [dependencies] eframe = { workspace = true, features = [ diff --git a/examples/custom_font_style/src/main.rs b/examples/custom_font_style/src/main.rs index 89117b4f01a..c1a61e3e69c 100644 --- a/examples/custom_font_style/src/main.rs +++ b/examples/custom_font_style/src/main.rs @@ -1,4 +1,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui; use egui::{FontFamily, FontId, RichText, TextStyle}; diff --git a/examples/custom_keypad/Cargo.toml b/examples/custom_keypad/Cargo.toml new file mode 100644 index 00000000000..7d000fd0d46 --- /dev/null +++ b/examples/custom_keypad/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "custom_keypad" +version = "0.1.0" +authors = ["Varphone Wong "] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.76" +publish = false + +[lints] +workspace = true + + +[dependencies] +eframe = { workspace = true, features = [ + "default", + "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO +] } + +# For image support: +egui_extras = { workspace = true, features = ["default", "image"] } + +env_logger = { version = "0.10", default-features = false, features = [ + "auto-color", + "humantime", +] } diff --git a/examples/custom_keypad/README.md b/examples/custom_keypad/README.md new file mode 100644 index 00000000000..9e5cdf7e8e9 --- /dev/null +++ b/examples/custom_keypad/README.md @@ -0,0 +1,7 @@ +Example showing how to implements a custom keypad. + +```sh +cargo run -p custom_keypad +``` + +![](screenshot.png) diff --git a/examples/custom_keypad/screenshot.png b/examples/custom_keypad/screenshot.png new file mode 100644 index 00000000000..632459e51aa Binary files /dev/null and b/examples/custom_keypad/screenshot.png differ diff --git a/examples/custom_keypad/src/keypad.rs b/examples/custom_keypad/src/keypad.rs new file mode 100644 index 00000000000..d4d0cb0d855 --- /dev/null +++ b/examples/custom_keypad/src/keypad.rs @@ -0,0 +1,255 @@ +use eframe::egui::{self, pos2, vec2, Button, Ui, Vec2}; + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +enum Transition { + #[default] + None, + CloseOnNextFrame, + CloseImmediately, +} + +#[derive(Clone, Debug)] +struct State { + open: bool, + closable: bool, + close_on_next_frame: bool, + start_pos: egui::Pos2, + focus: Option, + events: Option>, +} + +impl State { + fn new() -> Self { + Self { + open: false, + closable: false, + close_on_next_frame: false, + start_pos: pos2(100.0, 100.0), + focus: None, + events: None, + } + } + + fn queue_char(&mut self, c: char) { + let events = self.events.get_or_insert(vec![]); + if let Some(key) = egui::Key::from_name(&c.to_string()) { + events.push(egui::Event::Key { + key, + physical_key: Some(key), + pressed: true, + repeat: false, + modifiers: Default::default(), + }); + } + events.push(egui::Event::Text(c.to_string())); + } + + fn queue_key(&mut self, key: egui::Key) { + let events = self.events.get_or_insert(vec![]); + events.push(egui::Event::Key { + key, + physical_key: Some(key), + pressed: true, + repeat: false, + modifiers: Default::default(), + }); + } +} + +impl Default for State { + fn default() -> Self { + Self::new() + } +} + +/// A simple keypad widget. +pub struct Keypad { + id: egui::Id, +} + +impl Keypad { + pub fn new() -> Self { + Self { + id: egui::Id::new("keypad"), + } + } + + pub fn bump_events(&self, ctx: &egui::Context, raw_input: &mut egui::RawInput) { + let events = ctx.memory_mut(|m| { + m.data + .get_temp_mut_or_default::(self.id) + .events + .take() + }); + if let Some(mut events) = events { + events.append(&mut raw_input.events); + raw_input.events = events; + } + } + + fn buttons(ui: &mut Ui, state: &mut State) -> Transition { + let mut trans = Transition::None; + ui.vertical(|ui| { + let window_margin = ui.spacing().window_margin; + let size_1x1 = vec2(32.0, 26.0); + let _size_1x2 = vec2(32.0, 52.0 + window_margin.top); + let _size_2x1 = vec2(64.0 + window_margin.left, 26.0); + + ui.spacing_mut().item_spacing = Vec2::splat(window_margin.left); + + ui.horizontal(|ui| { + if ui.add_sized(size_1x1, Button::new("1")).clicked() { + state.queue_char('1'); + } + if ui.add_sized(size_1x1, Button::new("2")).clicked() { + state.queue_char('2'); + } + if ui.add_sized(size_1x1, Button::new("3")).clicked() { + state.queue_char('3'); + } + if ui.add_sized(size_1x1, Button::new("⏮")).clicked() { + state.queue_key(egui::Key::Home); + } + if ui.add_sized(size_1x1, Button::new("🔙")).clicked() { + state.queue_key(egui::Key::Backspace); + } + }); + ui.horizontal(|ui| { + if ui.add_sized(size_1x1, Button::new("4")).clicked() { + state.queue_char('4'); + } + if ui.add_sized(size_1x1, Button::new("5")).clicked() { + state.queue_char('5'); + } + if ui.add_sized(size_1x1, Button::new("6")).clicked() { + state.queue_char('6'); + } + if ui.add_sized(size_1x1, Button::new("⏭")).clicked() { + state.queue_key(egui::Key::End); + } + if ui.add_sized(size_1x1, Button::new("⎆")).clicked() { + state.queue_key(egui::Key::Enter); + trans = Transition::CloseOnNextFrame; + } + }); + ui.horizontal(|ui| { + if ui.add_sized(size_1x1, Button::new("7")).clicked() { + state.queue_char('7'); + } + if ui.add_sized(size_1x1, Button::new("8")).clicked() { + state.queue_char('8'); + } + if ui.add_sized(size_1x1, Button::new("9")).clicked() { + state.queue_char('9'); + } + if ui.add_sized(size_1x1, Button::new("⏶")).clicked() { + state.queue_key(egui::Key::ArrowUp); + } + if ui.add_sized(size_1x1, Button::new("⌨")).clicked() { + trans = Transition::CloseImmediately; + } + }); + ui.horizontal(|ui| { + if ui.add_sized(size_1x1, Button::new("0")).clicked() { + state.queue_char('0'); + } + if ui.add_sized(size_1x1, Button::new(".")).clicked() { + state.queue_char('.'); + } + if ui.add_sized(size_1x1, Button::new("⏴")).clicked() { + state.queue_key(egui::Key::ArrowLeft); + } + if ui.add_sized(size_1x1, Button::new("⏷")).clicked() { + state.queue_key(egui::Key::ArrowDown); + } + if ui.add_sized(size_1x1, Button::new("⏵")).clicked() { + state.queue_key(egui::Key::ArrowRight); + } + }); + }); + + trans + } + + pub fn show(&self, ctx: &egui::Context) { + let (focus, mut state) = ctx.memory(|m| { + ( + m.focused(), + m.data.get_temp::(self.id).unwrap_or_default(), + ) + }); + + let mut is_first_show = false; + if ctx.wants_keyboard_input() && state.focus != focus { + let y = ctx.style().spacing.interact_size.y * 1.25; + state.open = true; + state.start_pos = ctx.input(|i| { + i.pointer + .hover_pos() + .map_or(pos2(100.0, 100.0), |p| p + vec2(0.0, y)) + }); + state.focus = focus; + is_first_show = true; + } + + if state.close_on_next_frame { + state.open = false; + state.close_on_next_frame = false; + state.focus = None; + } + + let mut open = state.open; + + let win = egui::Window::new("⌨ Keypad"); + let win = if is_first_show { + win.current_pos(state.start_pos) + } else { + win.default_pos(state.start_pos) + }; + let resp = win + .movable(true) + .resizable(false) + .open(&mut open) + .show(ctx, |ui| Self::buttons(ui, &mut state)); + + state.open = open; + + if let Some(resp) = resp { + match resp.inner { + Some(Transition::CloseOnNextFrame) => { + state.close_on_next_frame = true; + } + Some(Transition::CloseImmediately) => { + state.open = false; + state.focus = None; + } + _ => {} + } + if !state.closable && resp.response.hovered() { + state.closable = true; + } + if state.closable && resp.response.clicked_elsewhere() { + state.open = false; + state.closable = false; + state.focus = None; + } + if is_first_show { + ctx.move_to_top(resp.response.layer_id); + } + } + + if let (true, Some(focus)) = (state.open, state.focus) { + ctx.memory_mut(|m| { + m.request_focus(focus); + }); + } + + ctx.memory_mut(|m| m.data.insert_temp(self.id, state)); + } +} + +impl Default for Keypad { + fn default() -> Self { + Self::new() + } +} diff --git a/examples/custom_keypad/src/main.rs b/examples/custom_keypad/src/main.rs new file mode 100644 index 00000000000..654de25fa44 --- /dev/null +++ b/examples/custom_keypad/src/main.rs @@ -0,0 +1,69 @@ +// #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example +use eframe::egui; + +mod keypad; +use keypad::Keypad; + +fn main() -> Result<(), eframe::Error> { + env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size([640.0, 480.0]), + ..Default::default() + }; + eframe::run_native( + "Custom Keypad App", + options, + Box::new(|cc| { + // Use the dark theme + cc.egui_ctx.set_visuals(egui::Visuals::dark()); + // This gives us image support: + egui_extras::install_image_loaders(&cc.egui_ctx); + + Box::::default() + }), + ) +} + +struct MyApp { + name: String, + age: u32, + keypad: Keypad, +} + +impl MyApp {} + +impl Default for MyApp { + fn default() -> Self { + Self { + name: "Arthur".to_owned(), + age: 42, + keypad: Keypad::new(), + } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::Window::new("Custom Keypad") + .default_pos([100.0, 100.0]) + .title_bar(true) + .show(ctx, |ui| { + ui.horizontal(|ui| { + ui.label("Your name: "); + ui.text_edit_singleline(&mut self.name); + }); + ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age")); + if ui.button("Increment").clicked() { + self.age += 1; + } + ui.label(format!("Hello '{}', age {}", self.name, self.age)); + }); + + self.keypad.show(ctx); + } + + fn raw_input_hook(&mut self, ctx: &egui::Context, raw_input: &mut egui::RawInput) { + self.keypad.bump_events(ctx, raw_input); + } +} diff --git a/examples/custom_plot_manipulation/Cargo.toml b/examples/custom_plot_manipulation/Cargo.toml index 3df5de96f3d..fc7b66c5ccc 100644 --- a/examples/custom_plot_manipulation/Cargo.toml +++ b/examples/custom_plot_manipulation/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["Ygor Souza "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + [dependencies] eframe = { workspace = true, features = [ diff --git a/examples/custom_plot_manipulation/src/main.rs b/examples/custom_plot_manipulation/src/main.rs index 4dffaf18ed1..e423d890fcb 100644 --- a/examples/custom_plot_manipulation/src/main.rs +++ b/examples/custom_plot_manipulation/src/main.rs @@ -1,5 +1,6 @@ //! This example shows how to implement custom gestures to pan and zoom in the plot #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui::{self, DragValue, Event, Vec2}; use egui_plot::{Legend, Line, PlotPoints}; diff --git a/examples/custom_window_frame/Cargo.toml b/examples/custom_window_frame/Cargo.toml index e163bda492a..6b800d0a0af 100644 --- a/examples/custom_window_frame/Cargo.toml +++ b/examples/custom_window_frame/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + [dependencies] eframe = { workspace = true, features = [ diff --git a/examples/custom_window_frame/src/main.rs b/examples/custom_window_frame/src/main.rs index 87daf82e06a..bb098652cbf 100644 --- a/examples/custom_window_frame/src/main.rs +++ b/examples/custom_window_frame/src/main.rs @@ -1,6 +1,7 @@ //! Show a custom window frame instead of the default OS window chrome decorations. #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui::{self, ViewportCommand}; diff --git a/examples/file_dialog/Cargo.toml b/examples/file_dialog/Cargo.toml index ceafd1be28a..9684a423256 100644 --- a/examples/file_dialog/Cargo.toml +++ b/examples/file_dialog/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + [dependencies] eframe = { workspace = true, features = [ diff --git a/examples/file_dialog/src/main.rs b/examples/file_dialog/src/main.rs index 4077c6de4a2..267c22e111c 100644 --- a/examples/file_dialog/src/main.rs +++ b/examples/file_dialog/src/main.rs @@ -1,4 +1,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui; @@ -77,7 +78,7 @@ impl eframe::App for MyApp { // Collect dropped files: ctx.input(|i| { if !i.raw.dropped_files.is_empty() { - self.dropped_files = i.raw.dropped_files.clone(); + self.dropped_files.clone_from(&i.raw.dropped_files); } }); } diff --git a/examples/hello_world/Cargo.toml b/examples/hello_world/Cargo.toml index e9e113c6722..bdd728a61e1 100644 --- a/examples/hello_world/Cargo.toml +++ b/examples/hello_world/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + [dependencies] eframe = { workspace = true, features = [ diff --git a/examples/hello_world/src/main.rs b/examples/hello_world/src/main.rs index b3fda5a5810..1c74f4c4992 100644 --- a/examples/hello_world/src/main.rs +++ b/examples/hello_world/src/main.rs @@ -1,4 +1,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui; diff --git a/examples/hello_world_par/Cargo.toml b/examples/hello_world_par/Cargo.toml index 442f671e330..e64cdd36108 100644 --- a/examples/hello_world_par/Cargo.toml +++ b/examples/hello_world_par/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["Maxim Osipenko "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + [dependencies] eframe = { workspace = true, default-features = false, features = [ diff --git a/examples/hello_world_par/src/main.rs b/examples/hello_world_par/src/main.rs index 617e840dc4c..2896f24843f 100644 --- a/examples/hello_world_par/src/main.rs +++ b/examples/hello_world_par/src/main.rs @@ -1,6 +1,7 @@ //! This example shows that you can use egui in parallel from multiple threads. #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example use std::sync::mpsc; use std::thread::JoinHandle; diff --git a/examples/hello_world_simple/Cargo.toml b/examples/hello_world_simple/Cargo.toml index 5b93eb686c7..c88382b9f6d 100644 --- a/examples/hello_world_simple/Cargo.toml +++ b/examples/hello_world_simple/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + [dependencies] eframe = { workspace = true, features = [ diff --git a/examples/hello_world_simple/src/main.rs b/examples/hello_world_simple/src/main.rs index 5f0ed31a49f..4e48feeff90 100644 --- a/examples/hello_world_simple/src/main.rs +++ b/examples/hello_world_simple/src/main.rs @@ -1,4 +1,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui; diff --git a/examples/images/Cargo.toml b/examples/images/Cargo.toml index 6e4740841f8..57d552e3be4 100644 --- a/examples/images/Cargo.toml +++ b/examples/images/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["Jan Procházka "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + [dependencies] eframe = { workspace = true, features = [ diff --git a/examples/images/src/ferris.svg b/examples/images/src/ferris.svg index c7f240dd97a..fe1589d912c 100644 --- a/examples/images/src/ferris.svg +++ b/examples/images/src/ferris.svg @@ -3,7 +3,7 @@ - + diff --git a/examples/images/src/main.rs b/examples/images/src/main.rs index efb088b6727..8a124e051a5 100644 --- a/examples/images/src/main.rs +++ b/examples/images/src/main.rs @@ -1,4 +1,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui; diff --git a/examples/keyboard_events/Cargo.toml b/examples/keyboard_events/Cargo.toml index 5428bc6a3ae..f7b12047062 100644 --- a/examples/keyboard_events/Cargo.toml +++ b/examples/keyboard_events/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["Jose Palazon "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + [dependencies] eframe = { workspace = true, features = [ diff --git a/examples/keyboard_events/src/main.rs b/examples/keyboard_events/src/main.rs index 581ae11eb74..bf0b3389981 100644 --- a/examples/keyboard_events/src/main.rs +++ b/examples/keyboard_events/src/main.rs @@ -1,4 +1,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui; use egui::*; diff --git a/examples/multiple_viewports/Cargo.toml b/examples/multiple_viewports/Cargo.toml index 37b27cb5f96..7aad28550fc 100644 --- a/examples/multiple_viewports/Cargo.toml +++ b/examples/multiple_viewports/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + [features] wgpu = ["eframe/wgpu"] diff --git a/examples/multiple_viewports/src/main.rs b/examples/multiple_viewports/src/main.rs index 0ef06e7815f..262002e2c5b 100644 --- a/examples/multiple_viewports/src/main.rs +++ b/examples/multiple_viewports/src/main.rs @@ -1,4 +1,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example use std::sync::{ atomic::{AtomicBool, Ordering}, diff --git a/examples/puffin_profiler/Cargo.toml b/examples/puffin_profiler/Cargo.toml index 021c05651a6..d787eeb9168 100644 --- a/examples/puffin_profiler/Cargo.toml +++ b/examples/puffin_profiler/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + [features] wgpu = ["eframe/wgpu"] diff --git a/examples/puffin_profiler/src/main.rs b/examples/puffin_profiler/src/main.rs index 521184f506b..8fe30d679ff 100644 --- a/examples/puffin_profiler/src/main.rs +++ b/examples/puffin_profiler/src/main.rs @@ -1,4 +1,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example use std::sync::{ atomic::{AtomicBool, Ordering}, diff --git a/examples/save_plot/Cargo.toml b/examples/save_plot/Cargo.toml index b0131067187..d20c927b058 100644 --- a/examples/save_plot/Cargo.toml +++ b/examples/save_plot/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["hacknus "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + [dependencies] eframe = { workspace = true, features = [ "default", diff --git a/examples/save_plot/src/main.rs b/examples/save_plot/src/main.rs index 3f13d3f28b6..61657faae86 100644 --- a/examples/save_plot/src/main.rs +++ b/examples/save_plot/src/main.rs @@ -1,4 +1,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui; use egui_plot::{Legend, Line, Plot, PlotPoints}; diff --git a/examples/screenshot/Cargo.toml b/examples/screenshot/Cargo.toml index 1da2e46cb0d..84399e4bc24 100644 --- a/examples/screenshot/Cargo.toml +++ b/examples/screenshot/Cargo.toml @@ -7,9 +7,12 @@ authors = [ ] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + [dependencies] eframe = { workspace = true, features = [ diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 91f2f92cfc0..c0c627e86a7 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -1,4 +1,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example use std::sync::Arc; diff --git a/examples/serial_windows/Cargo.toml b/examples/serial_windows/Cargo.toml index ee0f7b78d25..eebd08957af 100644 --- a/examples/serial_windows/Cargo.toml +++ b/examples/serial_windows/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + [dependencies] eframe = { workspace = true, features = [ diff --git a/examples/serial_windows/src/main.rs b/examples/serial_windows/src/main.rs index 57dccaa8b0e..04bed8006ae 100644 --- a/examples/serial_windows/src/main.rs +++ b/examples/serial_windows/src/main.rs @@ -1,4 +1,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui; diff --git a/examples/test_inline_glow_paint/Cargo.toml b/examples/test_inline_glow_paint/Cargo.toml index f1ff645ac27..ae66b6abdd3 100644 --- a/examples/test_inline_glow_paint/Cargo.toml +++ b/examples/test_inline_glow_paint/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/examples/test_inline_glow_paint/src/main.rs b/examples/test_inline_glow_paint/src/main.rs index 1710ee154a6..7851ad63b8b 100644 --- a/examples/test_inline_glow_paint/src/main.rs +++ b/examples/test_inline_glow_paint/src/main.rs @@ -1,3 +1,7 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example +#![allow(clippy::undocumented_unsafe_blocks)] + // Test that we can paint to the screen using glow directly. use eframe::egui; diff --git a/examples/test_viewports/Cargo.toml b/examples/test_viewports/Cargo.toml index cd5b2be0e18..142bafe2293 100644 --- a/examples/test_viewports/Cargo.toml +++ b/examples/test_viewports/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["konkitoman"] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + [features] wgpu = ["eframe/wgpu"] diff --git a/examples/test_viewports/src/main.rs b/examples/test_viewports/src/main.rs index b407ff5a12c..8bbb1b22a07 100644 --- a/examples/test_viewports/src/main.rs +++ b/examples/test_viewports/src/main.rs @@ -1,3 +1,6 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example + use std::sync::Arc; use eframe::egui; @@ -192,8 +195,11 @@ fn generic_child_ui(ui: &mut egui::Ui, vp_state: &mut ViewportState) { ui.horizontal(|ui| { ui.label("Title:"); if ui.text_edit_singleline(&mut vp_state.title).changed() { - // Title changes happen at the parent level: - ui.ctx().request_repaint_of(ui.ctx().parent_viewport_id()); + // Title changes + ui.ctx().send_viewport_cmd_to( + vp_state.id, + egui::ViewportCommand::Title(vp_state.title.clone()), + ); } }); @@ -438,7 +444,7 @@ fn drag_source( } } -// TODO: Update to be more like `crates/egui_demo_lib/src/debo/drag_and_drop.rs` +// TODO(emilk): Update to be more like `crates/egui_demo_lib/src/debo/drag_and_drop.rs` fn drop_target( ui: &mut egui::Ui, body: impl FnOnce(&mut egui::Ui) -> R, diff --git a/examples/user_attention/Cargo.toml b/examples/user_attention/Cargo.toml index 02a776477dd..6c3c91cdc25 100644 --- a/examples/user_attention/Cargo.toml +++ b/examples/user_attention/Cargo.toml @@ -4,9 +4,12 @@ version = "0.1.0" authors = ["TicClick "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.72" +rust-version = "1.76" publish = false +[lints] +workspace = true + [dependencies] eframe = { workspace = true, features = [ "default", diff --git a/examples/user_attention/src/main.rs b/examples/user_attention/src/main.rs index 4625abefa93..cbe27b6d5bc 100644 --- a/examples/user_attention/src/main.rs +++ b/examples/user_attention/src/main.rs @@ -1,3 +1,6 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example + use eframe::{egui, CreationContext, NativeOptions}; use egui::{Button, CentralPanel, Context, UserAttentionType}; diff --git a/rust-toolchain b/rust-toolchain index 694e5af99e5..871f562485d 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -5,6 +5,6 @@ # to the user in the error, instead of "error: invalid channel name '[toolchain]'". [toolchain] -channel = "1.72.0" +channel = "1.76.0" components = ["rustfmt", "clippy"] targets = ["wasm32-unknown-unknown"] diff --git a/scripts/cargo_deny.sh b/scripts/cargo_deny.sh index 560b8efd1bb..bf811e85204 100755 --- a/scripts/cargo_deny.sh +++ b/scripts/cargo_deny.sh @@ -1,21 +1,3 @@ #!/usr/bin/env bash -set -eu -script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) -cd "$script_path/.." -set -x - -cargo install --quiet cargo-deny - -cargo deny --all-features --log-level error --target aarch64-apple-darwin check -cargo deny --all-features --log-level error --target aarch64-linux-android check -cargo deny --all-features --log-level error --target i686-pc-windows-gnu check -cargo deny --all-features --log-level error --target i686-pc-windows-msvc check -cargo deny --all-features --log-level error --target i686-unknown-linux-gnu check -cargo deny --all-features --log-level error --target wasm32-unknown-unknown check -cargo deny --all-features --log-level error --target x86_64-apple-darwin check -cargo deny --all-features --log-level error --target x86_64-pc-windows-gnu check -cargo deny --all-features --log-level error --target x86_64-pc-windows-msvc check -cargo deny --all-features --log-level error --target x86_64-unknown-linux-gnu check -cargo deny --all-features --log-level error --target x86_64-unknown-linux-musl check -cargo deny --all-features --log-level error --target x86_64-unknown-redox check +cargo xtask deny diff --git a/scripts/check.sh b/scripts/check.sh index c1852a2d81a..5b802f6c606 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -9,7 +9,6 @@ set -x # Checks all tests, lints etc. # Basically does what the CI does. -cargo install --quiet cargo-cranky # Uses lints defined in Cranky.toml. See https://github.com/ericseppanen/cargo-cranky cargo +1.75.0 install --quiet typos-cli # web_sys_unstable_apis is required to enable the web_sys clipboard API which eframe web uses, @@ -24,7 +23,7 @@ cargo fmt --all -- --check cargo doc --quiet --lib --no-deps --all-features cargo doc --quiet --document-private-items --no-deps --all-features -cargo cranky --quiet --all-targets --all-features -- -D warnings +cargo clippy --quiet --all-targets --all-features -- -D warnings ./scripts/clippy_wasm.sh cargo check --quiet --all-targets diff --git a/scripts/clippy_wasm.sh b/scripts/clippy_wasm.sh index 72f6cc0e53e..3d80bd0f703 100755 --- a/scripts/clippy_wasm.sh +++ b/scripts/clippy_wasm.sh @@ -10,4 +10,4 @@ set -x # Use scripts/clippy_wasm/clippy.toml export CLIPPY_CONF_DIR="scripts/clippy_wasm" -cargo cranky --quiet --all-features --target wasm32-unknown-unknown --target-dir target_wasm -p egui_demo_app --lib -- --deny warnings +cargo clippy --quiet --all-features --target wasm32-unknown-unknown --target-dir target_wasm -p egui_demo_app --lib -- --deny warnings diff --git a/scripts/clippy_wasm/clippy.toml b/scripts/clippy_wasm/clippy.toml index aadfe75d83c..943444cfc50 100644 --- a/scripts/clippy_wasm/clippy.toml +++ b/scripts/clippy_wasm/clippy.toml @@ -6,7 +6,7 @@ # ----------------------------------------------------------------------------- # Section identical to the root clippy.toml: -msrv = "1.72" +msrv = "1.76" allow-unwrap-in-tests = true @@ -38,6 +38,7 @@ disallowed-methods = [ disallowed-types = [ { path = "instant::SystemTime", reason = "Known bugs. Use web-time." }, { path = "std::thread::Builder", reason = "Cannot spawn threads on wasm" }, + { path = "std::time::Instant", reason = "Use web-time instead." }, # { path = "std::path::PathBuf", reason = "Can't read/write files on web" }, // TODO(emilk): consider banning Path on wasm ] diff --git a/scripts/generate_changelog.py b/scripts/generate_changelog.py index 17b44f49be5..6b0fc71eb85 100755 --- a/scripts/generate_changelog.py +++ b/scripts/generate_changelog.py @@ -115,18 +115,22 @@ def print_section(crate: str, items: List[str]) -> None: print() +def changelog_filepath(crate: str) -> str: + scripts_dirpath = os.path.dirname(os.path.realpath(__file__)) + if crate == "egui": + file_path = f"{scripts_dirpath}/../CHANGELOG.md" + else: + file_path = f"{scripts_dirpath}/../crates/{crate}/CHANGELOG.md" + return os.path.normpath(file_path) + + def add_to_changelog_file(crate: str, items: List[str], version: str) -> None: insert_text = f"\n## {version} - {date.today()}\n" for item in items: insert_text += f"* {item}\n" insert_text += "\n" - scripts_dirpath = os.path.dirname(os.path.realpath(__file__)) - if crate == "egui": - file_path = f"{scripts_dirpath}/../CHANGELOG.md" - else: - file_path = f"{scripts_dirpath}/../crates/{crate}/CHANGELOG.md" - file_path = os.path.normpath(file_path) + file_path = changelog_filepath(crate) with open(file_path, 'r') as file: content = file.read() @@ -151,6 +155,28 @@ def main() -> None: print("ERROR: --version is required when --write is used") sys.exit(1) + crate_names = [ + "ecolor", + "eframe", + "egui_extras", + "egui_plot", + "egui_glow", + "egui-wgpu", + "egui-winit", + "egui", + "epaint", + ] + + # We read all existing changelogs to remove duplicate entries. + # For instance: the PRs that were part of 0.27.2 would also show up in the diff for `0.27.0..HEAD` + # when its time for a 0.28 release. We can't do `0.27.2..HEAD` because we would miss PRs that were + # merged before in `0.27.0..0.27.2` that were not cherry-picked into `0.27.2`. + all_changelogs = "" + for crate in crate_names: + file_path = changelog_filepath(crate) + with open(file_path, 'r') as file: + all_changelogs += file.read() + repo = Repo(".") commits = list(repo.iter_commits(args.commit_range)) commits.reverse() # Most recent last @@ -167,17 +193,6 @@ def main() -> None: ignore_labels = ["CI", "dependencies"] - crate_names = [ - "ecolor", - "eframe", - "egui_extras", - "egui_plot", - "egui_glow", - "egui-wgpu", - "egui-winit", - "egui", - "epaint", - ] sections = {} unsorted_prs = [] unsorted_commits = [] @@ -193,6 +208,10 @@ def main() -> None: summary = f"{title} [{hexsha[:7]}](https://github.com/{OWNER}/{REPO}/commit/{hexsha})" unsorted_commits.append(summary) else: + if f"[#{pr_number}]" in all_changelogs: + print(f"Ignoring PR that is already in the changelog: #{pr_number}") + continue + # We prefer the PR title if available title = pr_info.pr_title if pr_info else title labels = pr_info.labels if pr_info else [] diff --git a/scripts/lint.py b/scripts/lint.py index 59322ad2819..221d980d079 100755 --- a/scripts/lint.py +++ b/scripts/lint.py @@ -36,7 +36,7 @@ def lint_lines(filepath, lines_in): for line_nr, line in enumerate(lines_in): line_nr = line_nr + 1 - # TODO: only # and /// on lines before a keyword + # TODO(emilk): only # and /// on lines before a keyword pattern = ( r"^\s*((///)|((pub(\(\w*\))? )?((impl|fn|struct|enum|union|trait)\b))).*$" @@ -66,6 +66,12 @@ def lint_lines(filepath, lines_in): ) lines_out.append("#[inline]") + + if re.search(r"TODO[^(]", line): + errors.append( + f"{filepath}:{line_nr}: write 'TODO(username):' instead" + ) + if ( "(target_os" in line and filepath.startswith("./crates/egui/") diff --git a/scripts/setup_web.sh b/scripts/setup_web.sh index 36d39128b6e..51e53d4be4a 100755 --- a/scripts/setup_web.sh +++ b/scripts/setup_web.sh @@ -7,4 +7,4 @@ cd "$script_path/.." rustup target add wasm32-unknown-unknown # For generating JS bindings: -cargo install --quiet wasm-bindgen-cli --version 0.2.90 +cargo install --quiet wasm-bindgen-cli --version 0.2.92 diff --git a/web_demo/index.html b/web_demo/index.html index afe4cc646e1..142355b48f8 100644 --- a/web_demo/index.html +++ b/web_demo/index.html @@ -37,7 +37,12 @@ width: 100%; } - /* Position canvas in center-top: */ + /* Position canvas in center-top. + This is rather arbitrarily chosen. + In particular, it seems like both Chromium and Firefox will still align + the canvas on the physical pixel grid, which is required to get + pixel-perfect (non-blurry) rendering in egui. + See https://github.com/emilk/egui/issues/4241 for more */ canvas { margin-right: auto; margin-left: auto; diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 00000000000..26bdbb47e59 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "xtask" +edition.workspace = true +license.workspace = true +rust-version.workspace = true +version.workspace = true +publish = false + +[lints] +workspace = true + +[dependencies] diff --git a/xtask/README.md b/xtask/README.md new file mode 100644 index 00000000000..bb5e9277762 --- /dev/null +++ b/xtask/README.md @@ -0,0 +1,12 @@ +## xtask - Task automation + +This crate is meant to automate common tasks on the repository. It serves as a +replacement for shell scripts that is more portable across host operating +systems (namely Windows) and hopefully also easier to work with for +contributors who are already familiar with Rust (and not necessarily with shell +scripting). + +The executable can be invoked via the subcommand `cargo xtask`, thanks to an +alias defined in `.cargo/config.toml`. + +For more information, see . diff --git a/xtask/src/deny.rs b/xtask/src/deny.rs new file mode 100644 index 00000000000..5c24949bd43 --- /dev/null +++ b/xtask/src/deny.rs @@ -0,0 +1,60 @@ +//! Run `cargo deny` +//! +//! Also installs the subcommand if it is not already installed. + +use std::process::Command; + +use super::DynError; + +pub fn deny(args: &[&str]) -> Result<(), DynError> { + if !args.is_empty() { + return Err(format!("Invalid arguments: {args:?}").into()); + } + install_cargo_deny()?; + let targets = [ + "aarch64-apple-darwin", + "aarch64-linux-android", + "i686-pc-windows-gnu", + "i686-pc-windows-msvc", + "i686-unknown-linux-gnu", + "wasm32-unknown-unknown", + "x86_64-apple-darwin", + "x86_64-pc-windows-gnu", + "x86_64-pc-windows-msvc", + "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", + "x86_64-unknown-redox", + ]; + for target in targets { + let mut cmd = Command::new("cargo"); + cmd.args([ + "deny", + "--all-features", + "--log-level", + "error", + "--target", + target, + "check", + ]); + super::utils::print_cmd(&cmd); + let status = cmd.status()?; + if !status.success() { + return Err(status.to_string().into()); + } + } + Ok(()) +} + +fn install_cargo_deny() -> Result<(), DynError> { + let already_installed = Command::new("cargo") + .args(["deny", "--version"]) + .output() + .is_ok_and(|out| out.status.success()); + if already_installed { + return Ok(()); + } + let mut cmd = Command::new("cargo"); + cmd.args(["+stable", "install", "--quiet", "--locked", "cargo-deny"]); + let reason = "install cargo-deny"; + super::utils::ask_to_run(cmd, true, reason) +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 00000000000..0f16c545427 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,42 @@ +//! Helper crate for running scripts within the `egui` repo + +#![allow(clippy::print_stdout)] +#![allow(clippy::print_stderr)] +#![allow(clippy::exit)] + +mod deny; +pub(crate) mod utils; + +type DynError = Box; + +fn main() { + if let Err(e) = try_main() { + eprintln!("{e}"); + std::process::exit(-1); + } +} + +fn try_main() -> Result<(), DynError> { + let arg_strings: Vec<_> = std::env::args().skip(1).collect(); + let args: Vec<_> = arg_strings.iter().map(String::as_str).collect(); + + match args.as_slice() { + &[] | &["-h"] | &["--help"] => print_help(), + &["deny", ..] => deny::deny(&args[1..])?, + c => Err(format!("Invalid arguments {c:?}"))?, + } + Ok(()) +} + +fn print_help() { + let help = " + xtask help + + Subcommands + deny: Run cargo-deny for all targets + + Options + -h, --help: print help and exit + "; + println!("{help}"); +} diff --git a/xtask/src/utils.rs b/xtask/src/utils.rs new file mode 100644 index 00000000000..760c27f8f14 --- /dev/null +++ b/xtask/src/utils.rs @@ -0,0 +1,45 @@ +use std::{ + env, + io::{self, Write as _}, + process::Command, +}; + +use super::DynError; + +/// Print the command and its arguments as if the user had typed them +pub fn print_cmd(cmd: &Command) { + print!("{} ", cmd.get_program().to_string_lossy()); + for arg in cmd.get_args() { + print!("{} ", arg.to_string_lossy()); + } + println!(); +} + +/// Prompt user before running a command +/// +/// Adapted from [miri](https://github.com/rust-lang/miri/blob/dba35d2be72f4b78343d1a0f0b4737306f310672/cargo-miri/src/util.rs#L181-L204) +pub fn ask_to_run(mut cmd: Command, ask: bool, reason: &str) -> Result<(), DynError> { + // Disable interactive prompts in CI (GitHub Actions, Travis, AppVeyor, etc). + // Azure doesn't set `CI` though (nothing to see here, just Microsoft being Microsoft), + // so we also check their `TF_BUILD`. + let is_ci = env::var_os("CI").is_some() || env::var_os("TF_BUILD").is_some(); + if ask && !is_ci { + let mut buf = String::new(); + print!("The script is going to run: \n\n`{cmd:?}`\n\n To {reason}.\nProceed? [Y/n] ",); + io::stdout().flush().unwrap(); + io::stdin().read_line(&mut buf).unwrap(); + match buf.trim().to_lowercase().as_ref() { + "" | "y" | "yes" => {} + "n" | "no" => return Err("Aborting as per your request".into()), + a => return Err(format!("Invalid answer `{a}`").into()), + }; + } else { + eprintln!("Running `{cmd:?}` to {reason}."); + } + + let status = cmd.status()?; + if !status.success() { + return Err(format!("failed to {reason}: {status}").into()); + } + Ok(()) +}