diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c00eac197b4..5578a9fa2bd 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,7 +9,7 @@ Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/ * Open the PR as a draft until you have self-reviewed it and run `./scripts/check.sh`. * When you have addressed a PR comment, mark it as resolved. -Please be patient! I will review you PR, but my time is limited! +Please be patient! I will review your PR, but my time is limited! --> Closes . diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index f2252a31e3f..d3f5cd14e5d 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -29,4 +29,4 @@ jobs: with: mode: minimum count: 1 - labels: "CI, dependencies, docs and examples, ecolor, eframe, egui_extras, egui_glow, egui_plot, egui-wgpu, egui-winit, egui, epaint, typo" + labels: "CI, dependencies, docs and examples, ecolor, eframe, egui_extras, egui_glow, egui_plot, egui-wgpu, egui-winit, egui, epaint, exclude from changelog, typo" diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 471301af70b..167d18dbb9d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -49,7 +49,7 @@ jobs: run: cargo check --locked --all-features --all-targets - name: check egui_extras --all-features - run: cargo check --locked --all-features --all-targets -p egui_extras + run: cargo check --locked --all-features -p egui_extras - name: check default features run: cargo check --locked --all-targets @@ -57,11 +57,14 @@ jobs: - name: check --no-default-features run: cargo check --locked --no-default-features --lib --all-targets - - name: check epaint --no-default-features - run: cargo check --locked --no-default-features --lib --all-targets -p epaint - - name: check eframe --no-default-features - run: cargo check --locked --no-default-features --features x11 --lib --all-targets -p eframe + run: cargo check --locked --no-default-features --features x11 --lib -p eframe + + - name: check egui_extras --no-default-features + run: cargo check --locked --no-default-features --lib -p egui_extras + + - name: check epaint --no-default-features + run: cargo check --locked --no-default-features --lib -p epaint - name: Test doc-tests run: cargo test --doc --all-features @@ -78,6 +81,9 @@ jobs: - name: Cranky run: cargo cranky --all-targets --all-features -- -D warnings + - name: Cranky release + run: cargo cranky --all-targets --all-features --release -- -D warnings + # --------------------------------------------------------------------------- check_wasm: @@ -90,7 +96,7 @@ jobs: toolchain: 1.70.0 targets: wasm32-unknown-unknown - - run: sudo apt-get update && sudo apt-get install libgtk-3-dev + - run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev - name: Set up cargo cache uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/spelling_and_links.yml b/.github/workflows/spelling_and_links.yml new file mode 100644 index 00000000000..2b4c8de14d8 --- /dev/null +++ b/.github/workflows/spelling_and_links.yml @@ -0,0 +1,38 @@ +name: Check spelling and links +on: [pull_request] + +jobs: + typos: + # https://github.com/crate-ci/typos + # Add exceptions to _typos.toml + # install and run locally: cargo install typos-cli && typos + name: typos + runs-on: ubuntu-latest + steps: + - name: Checkout Actions Repository + uses: actions/checkout@v4 + + - name: Check spelling of entire workspace + uses: crate-ci/typos@master + # Disabled: too many names of crates and user-names etc + # spellcheck: + # name: Spellcheck + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: streetsidesoftware/cspell-action@v2 + # with: + # files: "**/*.md" + linkinator: + name: linkinator + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: jprochazk/linkinator-action@main + with: + linksToSkip: "https://crates.io/crates/.*, http://localhost:.*" # Avoid crates.io rate-limiting + retry: true + retryErrors: true + retryErrorsCount: 5 + retryErrorsJitter: 2000 + diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml deleted file mode 100644 index 6ed55693688..00000000000 --- a/.github/workflows/typos.yml +++ /dev/null @@ -1,17 +0,0 @@ -# https://github.com/crate-ci/typos -# Add exceptions to _typos.toml -# install and run locally: cargo install typos-cli && typos - -name: Spell Check -on: [pull_request] - -jobs: - run: - name: Spell Check - runs-on: ubuntu-latest - steps: - - name: Checkout Actions Repository - uses: actions/checkout@v4 - - - name: Check spelling of entire workspace - uses: crate-ci/typos@master diff --git a/CHANGELOG.md b/CHANGELOG.md index ae62045fa13..8e4da41862c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,91 @@ This file is updated upon each release. Changes since the last release can be found by running the `scripts/generate_changelog.py` script. +## 0.23.0 - 2023-09-27 - New image API +This release contains a simple and powerful image API: + +```rs +// Load from web: +ui.image("https://www.example.com/some_image.png"); + +// Include image in the binary using `include_bytes`: +ui.image(egui::include_image!("../assets/ferris.svg")); + +// With options: +ui.add( + egui::Image::new("file://path/to/image.jpg") + .max_width(200.0) + .rounding(10.0), +); +``` + +The API is based on a plugin-system, where you can tell `egui` how to load the images, and from where. + +`egui_extras` comes with loaders for you, so all you need to do is add the following to your `Cargo.toml`: + +```toml +egui_extras = { version = "0.23", features = ["all_loaders"] } +image = { version = "0.24", features = ["jpeg", "png"] } # Add the types you want support for +``` + +And this to your code: + +```rs +egui_extras::install_image_loaders(egui_ctx); +``` + +### ⚠️ BREAKING +* Update MSRV to Rust 1.70.0 [#3310](https://github.com/emilk/egui/pull/3310) +* Break out plotting to own crate `egui_plot` [#3282](https://github.com/emilk/egui/pull/3282) + +### ⭐ Added +* A new image API [#3297](https://github.com/emilk/egui/pull/3297) [#3315](https://github.com/emilk/egui/pull/3315) [#3328](https://github.com/emilk/egui/pull/3328) [#3338](https://github.com/emilk/egui/pull/3338) [#3342](https://github.com/emilk/egui/pull/3342) [#3343](https://github.com/emilk/egui/pull/3343) [#3402](https://github.com/emilk/egui/pull/3402) (thanks [@jprochazk](https://github.com/jprochazk)!) +* Add option to truncate text at some width [#3244](https://github.com/emilk/egui/pull/3244) +* Add control of line height and letter spacing [#3302](https://github.com/emilk/egui/pull/3302) +* Support images with rounded corners [#3257](https://github.com/emilk/egui/pull/3257) +* Change focused widget with arrow keys [#3272](https://github.com/emilk/egui/pull/3272) (thanks [@TimonPost](https://github.com/TimonPost)!) +* Add opt-in `puffin` feature to egui [#3298](https://github.com/emilk/egui/pull/3298) +* Add debug-option to show a callstack to the widget under the mouse [#3391](https://github.com/emilk/egui/pull/3391) +* Add `Context::open_url` and `Context::copy_text` [#3380](https://github.com/emilk/egui/pull/3380) +* Add `Area::constrain_to` and `Window::constrain_to` [#3396](https://github.com/emilk/egui/pull/3396) +* Add `Memory::area_rect` [#3161](https://github.com/emilk/egui/pull/3161) (thanks [@tosti007](https://github.com/tosti007)!) +* Add `Margin::expand_rect` and `shrink_rect` [#3214](https://github.com/emilk/egui/pull/3214) +* Provide `into_inner()` for `egui::mutex::{Mutex, RwLock}` [#3110](https://github.com/emilk/egui/pull/3110) (thanks [@KmolYuan](https://github.com/KmolYuan)!) +* Support multi-threaded Wasm [#3236](https://github.com/emilk/egui/pull/3236) +* Change touch force to be `Option` instead of `f32` [#3240](https://github.com/emilk/egui/pull/3240) (thanks [@lucasmerlin](https://github.com/lucasmerlin)!) +* Add option to always open hyperlink in a new browser tab [#3242](https://github.com/emilk/egui/pull/3242) (thanks [@FreddyFunk](https://github.com/FreddyFunk)!) +* Add `Window::drag_to_scroll` [#3118](https://github.com/emilk/egui/pull/3118) (thanks [@KYovchevski](https://github.com/KYovchevski)!) +* Add `CollapsingState::remove` to clear stored state [#3252](https://github.com/emilk/egui/pull/3252) (thanks [@dmackdev](https://github.com/dmackdev)!) +* Add tooltip_delay option [#3245](https://github.com/emilk/egui/pull/3245) (thanks [@YgorSouza](https://github.com/YgorSouza)!) +* Added `Context::is_context_menu_open()` [#3267](https://github.com/emilk/egui/pull/3267) (thanks [@dmlary](https://github.com/dmlary)!) +* Add `mime` field to `DroppedFile` [#3273](https://github.com/emilk/egui/pull/3273) (thanks [@abey79](https://github.com/abey79)!) +* Allow setting the progress bar height [#3183](https://github.com/emilk/egui/pull/3183) (thanks [@s-nie](https://github.com/s-nie)!) +* Add `scroll_area::State::velocity` [#3300](https://github.com/emilk/egui/pull/3300) (thanks [@Barugon](https://github.com/Barugon)!) +* Add `Visuals::interact_cursor` [#3312](https://github.com/emilk/egui/pull/3312) (thanks [@zkldi](https://github.com/zkldi)!) +* Add method to `RichText` making it easier to construct layout jobs [#3319](https://github.com/emilk/egui/pull/3319) (thanks [@OmegaJak](https://github.com/OmegaJak)!) +* Add `Context::style_mut` [#3359](https://github.com/emilk/egui/pull/3359) +* `std::borrow::Cow<'_, str>` now implements `TextBuffer` [#3164](https://github.com/emilk/egui/pull/3164) (thanks [@burtonageo](https://github.com/burtonageo)!) + +### 🔧 Changed +* Separate text cursor from selection visuals [#3181](https://github.com/emilk/egui/pull/3181) (thanks [@lampsitter](https://github.com/lampsitter)!) +* `DragValue`: update value on each key press by default [#2880](https://github.com/emilk/egui/pull/2880) (thanks [@Barugon](https://github.com/Barugon)!) +* Replace uses of `RangeInclusive` with `emath::Rangef` [#3221](https://github.com/emilk/egui/pull/3221) +* Implement `Send + Sync` for `ColorPickerFn` and `Ui` (#3148) [#3233](https://github.com/emilk/egui/pull/3233) (thanks [@idanarye](https://github.com/idanarye)!) +* Use the minus character instead of "dash" [#3271](https://github.com/emilk/egui/pull/3271) +* Changing `menu_image_button` to use `ImageButton` builder [#3288](https://github.com/emilk/egui/pull/3288) (thanks [@v-kat](https://github.com/v-kat)!) +* Prune old egui memory data when reaching some limit [#3299](https://github.com/emilk/egui/pull/3299) + +### 🐛 Fixed +* Fix TextEdit's character limit [#3173](https://github.com/emilk/egui/pull/3173) (thanks [@Serverator](https://github.com/Serverator)!) +* Set the correct unicode character for "ctrl" shortcuts [#3186](https://github.com/emilk/egui/pull/3186) (thanks [@abey79](https://github.com/abey79)!) +* Fix crash in `DragValue` when only setting `min_decimals` [#3231](https://github.com/emilk/egui/pull/3231) +* Fix clipping issued with `ScrollArea` [#2860](https://github.com/emilk/egui/pull/2860) (thanks [@Barugon](https://github.com/Barugon)!) +* Fix moving slider with arrow keys [#3354](https://github.com/emilk/egui/pull/3354) +* Fix problems with tabs in text [#3355](https://github.com/emilk/egui/pull/3355) +* Fix interaction with moved color-picker [#3395](https://github.com/emilk/egui/pull/3395) + + + ## 0.22.0 - 2023-05-23 - A plethora of small improvements ### ⭐ Added * Scroll bar visibility options [#2729](https://github.com/emilk/egui/pull/2729) (thanks [@IVAN-MK7](https://github.com/IVAN-MK7)!) @@ -244,7 +329,7 @@ Changes since the last release can be found by running the `scripts/generate_cha ### Contributors 🙏 * [4JX](https://github.com/4JX) -* [AlexxxRu](https://github.com/AlexxxRu) +* [a-liashenko](https://github.com/a-liashenko) * [ascclemens](https://github.com/ascclemens) * [awaken1ng](https://github.com/awaken1ng) * [bigfarts](https://github.com/bigfarts) @@ -340,7 +425,7 @@ Changes since the last release can be found by running the `scripts/generate_cha * [4JX](https://github.com/4JX) * [55nknown](https://github.com/55nknown) * [AlanRace](https://github.com/AlanRace) -* [AlexxxRu](https://github.com/AlexxxRu) +* [a-liashenko](https://github.com/a-liashenko) * [awaken1ng](https://github.com/awaken1ng) * [BctfN0HUK7Yg](https://github.com/BctfN0HUK7Yg) * [Bromeon](https://github.com/Bromeon) diff --git a/Cargo.lock b/Cargo.lock index a877ba759ce..1500d9e29c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,9 +20,9 @@ checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" [[package]] name = "accesskit" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c98a5d094590335462354da402d754fe2cb78f0e6ce5024611c28ed539c1de" +checksum = "b0cc53b7e5d8f45ebe687178cf91af0f45fdba6e78fedf94f0269c5be5b9f296" dependencies = [ "enumn", "serde", @@ -30,18 +30,18 @@ dependencies = [ [[package]] name = "accesskit_consumer" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca541e0fdb600916d196a940228df99b86d804fd2e6ef13894d7814f2799db43" +checksum = "39dfcfd32eb0c1b525daaf4b02adcd2fa529c22cd713491e15bf002a01a714f5" dependencies = [ "accesskit", ] [[package]] name = "accesskit_macos" -version = "0.7.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfea17e5bb5dcbfcf5b256ab2f5889a3e6f6582de78b9db9b6689adad3b002f3" +checksum = "89c7e8406319ac3149d7b59983637984f0864bbf738319b1c443976268b6426c" dependencies = [ "accesskit", "accesskit_consumer", @@ -51,38 +51,40 @@ dependencies = [ [[package]] name = "accesskit_unix" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4d1517421278cc8e67422d0786a18cf4291093ebe49eadf1cf989ff80e57f90" +checksum = "0b0c84552a7995c981d5f22e2d4b24ba9a55718bb12fba883506d6d7344acaf1" dependencies = [ "accesskit", "accesskit_consumer", "async-channel", + "async-once-cell", "atspi", "futures-lite", + "once_cell", "serde", "zbus", ] [[package]] name = "accesskit_windows" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11c7f177739f23bd19bb856e4a64fdd96eb8638ec0a6a6dde9a7019a9e91c53" +checksum = "314d4a797fc82d182b04f4f0665a368924fb556ad9557fccd2d39d38dc8c1c1b" dependencies = [ "accesskit", "accesskit_consumer", - "arrayvec", "once_cell", "paste", - "windows 0.44.0", + "static_assertions", + "windows 0.48.0", ] [[package]] name = "accesskit_winit" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f741b54fba827e49a73d55fdd43e8d3d5133aa7710a48581013c7802f232b83" +checksum = "88e39fcec2e10971e188730b7a76bab60647dacc973d4591855ebebcadfaa738" dependencies = [ "accesskit", "accesskit_macos", @@ -93,9 +95,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.19.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -108,36 +110,32 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "ahash" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", "once_cell", "serde", "version_check", + "zerocopy", ] [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "android-activity" version = "0.4.1" @@ -162,6 +160,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -179,22 +183,21 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arboard" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6041616acea41d67c4a984709ddab1587fd0b10efe5cc563fee954d2f011854" +checksum = "ac57f2b058a76363e357c056e4f74f1945bf734d37b8b3ef49066c4787dde0fc" dependencies = [ "clipboard-win", "log", "objc", "objc-foundation", "objc_id", - "once_cell", "parking_lot", "thiserror", "winapi", @@ -209,9 +212,9 @@ checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" [[package]] name = "arrayvec" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "ash" @@ -228,35 +231,47 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" dependencies = [ - "event-listener", + "event-listener 2.5.3", "futures-core", ] [[package]] name = "async-channel" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ "concurrent-queue", - "event-listener", + "event-listener 2.5.3", "futures-core", ] [[package]] name = "async-executor" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" +checksum = "78f2db9467baa66a700abce2a18c5ad793f6f83310aca1284796fc3921d113fd" dependencies = [ "async-lock", "async-task", "concurrent-queue", - "fastrand", + "fastrand 2.0.1", "futures-lite", "slab", ] +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock", + "autocfg", + "blocking", + "futures-lite", +] + [[package]] name = "async-io" version = "1.13.0" @@ -271,7 +286,7 @@ dependencies = [ "log", "parking", "polling", - "rustix", + "rustix 0.37.23", "slab", "socket2", "waker-fn", @@ -279,39 +294,81 @@ dependencies = [ [[package]] name = "async-lock" -version = "2.7.0" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-once-cell" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9338790e78aa95a416786ec8389546c4b6a1dfc3dc36071ed9518a9413a542eb" + +[[package]] +name = "async-process" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +checksum = "bf012553ce51eb7aa6dc2143804cc8252bd1cb681a1c5cb7fa94ca88682dee1d" dependencies = [ - "event-listener", + "async-io", + "async-lock", + "async-signal", + "blocking", + "cfg-if", + "event-listener 3.0.0", + "futures-lite", + "rustix 0.38.14", + "windows-sys 0.48.0", ] [[package]] name = "async-recursion" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" +checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", +] + +[[package]] +name = "async-signal" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4af361a844928cb7d36590d406709473a1b574f443094422ef166daa3b493208" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "concurrent-queue", + "futures-core", + "futures-io", + "libc", + "signal-hook-registry", + "slab", + "windows-sys 0.48.0", ] [[package]] name = "async-task" -version = "4.4.0" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" +checksum = "b9441c6b2fe128a7c2bf680a44c34d0df31ce09e5b7e401fcca3faa483dbc921" [[package]] name = "async-trait" -version = "0.1.68" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] @@ -326,31 +383,58 @@ dependencies = [ "system-deps", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atspi" -version = "0.10.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "674e7a3376837b2e7d12d34d58ac47073c491dc3bf6f71a7adaf687d4d817faa" +checksum = "6059f350ab6f593ea00727b334265c4dfc7fd442ee32d264794bd9bdc68e87ca" +dependencies = [ + "atspi-common", + "atspi-connection", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92af95f966d2431f962bc632c2e68eda7777330158bf640c4af4249349b2cdf5" dependencies = [ - "async-recursion", - "async-trait", - "atspi-macros", "enumflags2", - "futures-lite", "serde", - "tracing", + "static_assertions", "zbus", "zbus_names", + "zvariant", ] [[package]] -name = "atspi-macros" -version = "0.2.0" +name = "atspi-connection" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb4870a32c0eaa17e35bca0e6b16020635157121fb7d45593d242c295bc768" +checksum = "a0c65e7d70f86d4c0e3b2d585d9bf3f979f0b19d635a336725a88d279f76b939" dependencies = [ - "quote", - "syn 1.0.109", + "atspi-common", + "atspi-proxies", + "futures-lite", + "zbus", +] + +[[package]] +name = "atspi-proxies" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6495661273703e7a229356dcbe8c8f38223d697aacfaf0e13590a9ac9977bb52" +dependencies = [ + "atspi-common", + "serde", + "zbus", ] [[package]] @@ -372,9 +456,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.67" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -391,6 +475,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" + [[package]] name = "bincode" version = "1.3.3" @@ -402,11 +492,11 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.65.1" +version = "0.68.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "cexpr", "clang-sys", "lazy_static", @@ -419,7 +509,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.16", + "syn 2.0.37", "which", ] @@ -446,9 +536,12 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +dependencies = [ + "serde", +] [[package]] name = "block" @@ -484,30 +577,46 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "blocking" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94c4ef1f913d78636d78d538eec1f18de81e481f44b1be0a81060090530846e1" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "fastrand 2.0.1", + "futures-io", + "futures-lite", + "piper", + "tracing", +] + [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "bytemuck" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdde5c9cd29ebd706ce1b35600920a33550e402fc998a2e53ad3b42c3c47a192" +checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] @@ -518,9 +627,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cairo-sys-rs" @@ -534,10 +643,11 @@ dependencies = [ [[package]] name = "calloop" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a59225be45a478d772ce015d9743e49e92798ece9e34eda9a6aa2a6a7f40192" +checksum = "52e0d00eb1ea24371a97d2da6201c6747a633dc6dc1988ef503403b4c59504a8" dependencies = [ + "bitflags 1.3.2", "log", "nix 0.25.1", "slotmap", @@ -553,11 +663,12 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -577,9 +688,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.15.1" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8790cf1286da485c72cf5fc7aeba308438800036ec67d89425924c4807268c9" +checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" dependencies = [ "smallvec", "target-lexicon", @@ -608,17 +719,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", - "time 0.1.45", "wasm-bindgen", - "winapi", + "windows-targets 0.48.5", ] [[package]] @@ -673,7 +783,7 @@ checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "bitflags 1.3.2", "clap_lex", - "indexmap", + "indexmap 1.9.3", "textwrap", ] @@ -715,15 +825,14 @@ dependencies = [ [[package]] name = "cocoa-foundation" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "931d3837c286f56e3c58423ce4eba12d08db2374461a785c86f672b08b5650d6" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" dependencies = [ "bitflags 1.3.2", "block", "core-foundation", "core-graphics-types", - "foreign-types 0.3.2", "libc", "objc", ] @@ -768,9 +877,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" dependencies = [ "crossbeam-utils", ] @@ -814,21 +923,20 @@ dependencies = [ [[package]] name = "core-graphics-types" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" +checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33" dependencies = [ "bitflags 1.3.2", "core-foundation", - "foreign-types 0.3.2", "libc", ] [[package]] name = "cpufeatures" -version = "0.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" dependencies = [ "libc", ] @@ -888,9 +996,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if", ] @@ -945,7 +1053,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16e44ab292b1dddfdaf7be62cfd8877df52f2f3fde5858d95bab606be259f20" dependencies = [ - "bitflags 2.3.1", + "bitflags 2.4.0", "libloading 0.8.0", "winapi", ] @@ -991,6 +1099,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5" +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" + [[package]] name = "derivative" version = "2.2.0" @@ -1022,26 +1136,6 @@ dependencies = [ "dirs-sys-next", ] -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -1061,11 +1155,11 @@ checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] name = "dlib" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.7.4", + "libloading 0.8.0", ] [[package]] @@ -1083,16 +1177,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" -[[package]] -name = "download_image" -version = "0.1.0" -dependencies = [ - "eframe", - "egui_extras", - "env_logger", - "image", -] - [[package]] name = "dyn-clonable" version = "0.9.0" @@ -1116,13 +1200,13 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" +checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" [[package]] name = "ecolor" -version = "0.22.0" +version = "0.23.0" dependencies = [ "bytemuck", "cint", @@ -1133,7 +1217,7 @@ dependencies = [ [[package]] name = "eframe" -version = "0.22.0" +version = "0.23.0" dependencies = [ "bytemuck", "cocoa", @@ -1170,10 +1254,11 @@ dependencies = [ [[package]] name = "egui" -version = "0.22.0" +version = "0.23.0" dependencies = [ "accesskit", - "ahash 0.8.3", + "ahash", + "backtrace", "document-features", "epaint", "log", @@ -1185,7 +1270,7 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.22.0" +version = "0.23.0" dependencies = [ "bytemuck", "document-features", @@ -1200,7 +1285,7 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.22.0" +version = "0.23.0" dependencies = [ "accesskit_winit", "arboard", @@ -1218,7 +1303,7 @@ dependencies = [ [[package]] name = "egui_demo_app" -version = "0.22.0" +version = "0.23.0" dependencies = [ "bytemuck", "chrono", @@ -1231,6 +1316,7 @@ dependencies = [ "image", "log", "poll-promise", + "rfd", "serde", "wasm-bindgen", "wasm-bindgen-futures", @@ -1239,7 +1325,7 @@ dependencies = [ [[package]] name = "egui_demo_lib" -version = "0.22.0" +version = "0.23.0" dependencies = [ "chrono", "criterion", @@ -1247,33 +1333,34 @@ dependencies = [ "egui", "egui_extras", "egui_plot", - "enum-map", "log", "serde", - "syntect", "unicode_names2", ] [[package]] name = "egui_extras" -version = "0.22.0" +version = "0.23.0" dependencies = [ "chrono", "document-features", "egui", "ehttp", + "enum-map", "image", "log", + "mime_guess2", "puffin", "resvg", "serde", + "syntect", "tiny-skia", "usvg", ] [[package]] name = "egui_glow" -version = "0.22.0" +version = "0.23.0" dependencies = [ "bytemuck", "document-features", @@ -1283,7 +1370,7 @@ dependencies = [ "glutin", "glutin-winit", "log", - "memoffset", + "memoffset 0.6.5", "puffin", "raw-window-handle", "wasm-bindgen", @@ -1292,7 +1379,7 @@ dependencies = [ [[package]] name = "egui_plot" -version = "0.22.0" +version = "0.23.0" dependencies = [ "document-features", "egui", @@ -1301,9 +1388,9 @@ dependencies = [ [[package]] name = "ehttp" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e4525e883dd283d12b755ab3ad71d7c8dea2ee8e8a062b9f4c4f84637ed681" +checksum = "f88f45662356f96afc7d9e2bc9910ad8352ee01417f7c69b8b16a53c8767a75d" dependencies = [ "document-features", "js-sys", @@ -1315,13 +1402,13 @@ dependencies = [ [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "emath" -version = "0.22.0" +version = "0.23.0" dependencies = [ "bytemuck", "document-features", @@ -1331,9 +1418,9 @@ dependencies = [ [[package]] name = "enum-map" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "988f0d17a0fa38291e5f41f71ea8d46a5d5497b9054d5a759fae2cbb819f2356" +checksum = "c188012f8542dee7b3996e44dd89461d64aa471b0a7c71a1ae2f595d259e96e5" dependencies = [ "enum-map-derive", "serde", @@ -1341,20 +1428,20 @@ dependencies = [ [[package]] name = "enum-map-derive" -version = "0.11.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4da76b3b6116d758c7ba93f7ec6a35d2e2cf24feda76c6e38a375f4d5c59f2" +checksum = "04d0b288e3bb1d861c4403c1774a6f7a798781dfc519b3647df2a3dd4ae95f25" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.37", ] [[package]] name = "enumflags2" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c041f5090df68b32bcd905365fd51769c8b9d553fe87fde0b683534f10c01bd2" +checksum = "5998b4f30320c9d93aed72f63af821bfdac50465b75428fce77b48ec482c3939" dependencies = [ "enumflags2_derive", "serde", @@ -1362,24 +1449,24 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" +checksum = "f95e2801cd355d4a1a3e3953ce6ee5ae9603a5c833455343a8bfe3f44d418246" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] name = "enumn" -version = "0.1.8" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48016319042fb7c87b78d2993084a831793a897a5cd1a2a67cab9d1eeb4b7d76" +checksum = "c2ad8cef1d801a4686bfd8919f0b30eac4c8e48968c437a6405ded4fb5272d2b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] @@ -1397,10 +1484,10 @@ dependencies = [ [[package]] name = "epaint" -version = "0.22.0" +version = "0.23.0" dependencies = [ "ab_glyph", - "ahash 0.8.3", + "ahash", "backtrace", "bytemuck", "criterion", @@ -1413,11 +1500,17 @@ dependencies = [ "serde", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ "errno-dragonfly", "libc", @@ -1450,11 +1543,22 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29e56284f00d94c1bc7fd3c77027b4623c88c1f53d8d2394c6199f2921dea325" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + [[package]] name = "fancy-regex" -version = "0.7.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6b8560a05112eb52f04b00e5d3790c0dd75d9d980eb8a122fb23b92a623ccf" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" dependencies = [ "bit-set", "regex", @@ -1469,6 +1573,21 @@ dependencies = [ "instant", ] +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + [[package]] name = "file_dialog" version = "0.1.0" @@ -1480,9 +1599,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.25" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", "miniz_oxide", @@ -1527,7 +1646,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] @@ -1544,9 +1663,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -1569,7 +1688,7 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ - "fastrand", + "fastrand 1.9.0", "futures-core", "futures-io", "memchr", @@ -1658,20 +1777,20 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "gimli" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "gio-sys" @@ -1727,9 +1846,9 @@ dependencies = [ [[package]] name = "glutin" -version = "0.30.8" +version = "0.30.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f9b771a65f0a1e3ddb6aa16f867d87dc73c922411c255e6c4ab7f6d45c7327" +checksum = "8fc93b03242719b8ad39fb26ed2b01737144ce7bd4bfc7adadcef806596760fe" dependencies = [ "bitflags 1.3.2", "cfg_aliases", @@ -1762,9 +1881,9 @@ dependencies = [ [[package]] name = "glutin_egl_sys" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b3bcbddc51573b977fc6dca5d93867e4f29682cdbaf5d13e48f4fa4346d4d87" +checksum = "af784eb26c5a68ec85391268e074f0aa618c096eadb5d6330b0911cf34fe57c5" dependencies = [ "gl_generator", "windows-sys 0.45.0", @@ -1806,7 +1925,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.3.1", + "bitflags 2.4.0", "gpu-alloc-types", ] @@ -1816,7 +1935,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.3.1", + "bitflags 2.4.0", ] [[package]] @@ -1834,22 +1953,22 @@ dependencies = [ [[package]] name = "gpu-descriptor" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0c02e1ba0bdb14e965058ca34e09c020f8e507a760df1121728e0aef68d57a" +checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "gpu-descriptor-types", - "hashbrown", + "hashbrown 0.14.0", ] [[package]] name = "gpu-descriptor-types" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "363e3677e55ad168fef68cf9de3a4a310b53124c5e784c53a1d70e92d23f2126" +checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", ] [[package]] @@ -1881,8 +2000,15 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" dependencies = [ - "ahash 0.7.6", + "ahash", + "allocator-api2", ] [[package]] @@ -1911,6 +2037,7 @@ name = "hello_world" version = "0.1.0" dependencies = [ "eframe", + "egui_extras", "env_logger", ] @@ -1941,9 +2068,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -1974,9 +2101,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "iana-time-zone" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2003,9 +2130,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -2013,9 +2140,9 @@ dependencies = [ [[package]] name = "image" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" dependencies = [ "bytemuck", "byteorder", @@ -2026,6 +2153,16 @@ dependencies = [ "png", ] +[[package]] +name = "images" +version = "0.1.0" +dependencies = [ + "eframe", + "egui_extras", + "env_logger", + "image", +] + [[package]] name = "imagesize" version = "0.10.1" @@ -2039,7 +2176,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", ] [[package]] @@ -2056,24 +2203,23 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.1", + "hermit-abi 0.3.3", "libc", "windows-sys 0.48.0", ] [[package]] name = "is-terminal" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", - "rustix", + "hermit-abi 0.3.3", + "rustix 0.38.14", "windows-sys 0.48.0", ] @@ -2088,9 +2234,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jni" @@ -2186,9 +2332,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.144" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "libloading" @@ -2231,6 +2377,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +[[package]] +name = "linux-raw-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" + [[package]] name = "litrs" version = "0.2.3" @@ -2239,9 +2391,9 @@ checksum = "f9275e0933cf8bb20f008924c0cb07a0692fe54d8064996520bf998de9eb79aa" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", @@ -2249,12 +2401,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "lz4_flex" @@ -2273,9 +2422,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "memmap2" @@ -2295,13 +2444,22 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "metal" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "623b5e6cefd76e58f774bd3cc0c6f5c7615c58c03a97815245a25c3c9bdee318" dependencies = [ - "bitflags 2.3.1", + "bitflags 2.4.0", "block", "core-graphics-types", "foreign-types 0.5.0", @@ -2310,6 +2468,22 @@ dependencies = [ "paste", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess2" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a3333bb1609500601edc766a39b4c1772874a4ce26022f4d866854dc020c41" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2318,11 +2492,12 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", + "simd-adler32", ] [[package]] @@ -2333,14 +2508,14 @@ checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" [[package]] name = "mio" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.45.0", + "wasi", + "windows-sys 0.48.0", ] [[package]] @@ -2350,10 +2525,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1ceaaa4eedaece7e4ec08c55c640ba03dbb73fb812a6570a59bcf1930d0f70e" dependencies = [ "bit-set", - "bitflags 2.3.1", + "bitflags 2.4.0", "codespan-reporting", "hexf-parse", - "indexmap", + "indexmap 1.9.3", "log", "num-traits", "rustc-hash", @@ -2430,7 +2605,7 @@ dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] @@ -2443,8 +2618,19 @@ dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", - "memoffset", - "pin-utils", + "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", ] [[package]] @@ -2486,9 +2672,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", ] @@ -2581,18 +2767,18 @@ dependencies = [ [[package]] name = "object" -version = "0.30.3" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "oorandom" @@ -2602,9 +2788,9 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "orbclient" -version = "0.3.45" +version = "0.3.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "221d488cd70617f1bd599ed8ceb659df2147d9393717954d82a0f5e8032a6ab1" +checksum = "8378ac0dfbd4e7895f2d2c1f1345cab3836910baf3a300b000d04250f0c8428f" dependencies = [ "redox_syscall 0.3.5", ] @@ -2621,9 +2807,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.5.0" +version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" +checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" [[package]] name = "owned_ttf_parser" @@ -2654,9 +2840,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" +checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" [[package]] name = "parking_lot" @@ -2670,22 +2856,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", + "redox_syscall 0.3.5", "smallvec", - "windows-sys 0.45.0", + "windows-targets 0.48.5", ] [[package]] name = "paste" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "peeking_take_while" @@ -2695,9 +2881,9 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pico-args" @@ -2707,9 +2893,9 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -2717,6 +2903,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand 2.0.1", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.27" @@ -2725,36 +2922,38 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "plist" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5329b8f106a176ab0dce4aae5da86bfcb139bb74fb00882859e03745011f3635" +checksum = "bdc0001cfea3db57a2e24bc0d818e9e20e554b5f97fabb9bc231dc240269ae06" dependencies = [ - "base64", - "indexmap", + "base64 0.21.4", + "indexmap 1.9.3", "line-wrap", "quick-xml", "serde", - "time 0.3.21", + "time", ] [[package]] name = "png" -version = "0.17.7" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" +checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" dependencies = [ "bitflags 1.3.2", "crc32fast", + "fdeflate", "flate2", "miniz_oxide", ] [[package]] name = "poll-promise" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf2a02372dfae23c9c01267fb296b8a3413bb4e45fbd589c3ac73c6dcfbb305" +checksum = "5f6a58fecbf9da8965bcdb20ce4fd29788d1acee68ddbb64f0ba1b81bccdb7df" dependencies = [ + "document-features", "static_assertions", ] @@ -2788,12 +2987,12 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "prettyplease" -version = "0.2.5" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617feabb81566b593beb4886fb8c1f38064169dae4dccad0e3220160c3b37203" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] @@ -2808,18 +3007,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.58" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] [[package]] name = "profiling" -version = "1.0.8" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332cd62e95873ea4f41f3dfd6bbbfc5b52aec892d7e8d534197c4720a0bbbab2" +checksum = "f89dff0959d98c9758c88826cc002e2c3d0b9dfac4139711d1f30de442f1139b" [[package]] name = "puffin" @@ -2862,18 +3061,18 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.27" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -2957,26 +3156,32 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.2" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1a59b5d8e97dee33696bf13c5ba8ab85341c002922fba050069326b9c498974" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.2", + "regex-automata", + "regex-syntax", ] [[package]] -name = "regex-syntax" -version = "0.6.29" +name = "regex-automata" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "renderdoc-sys" @@ -2998,16 +3203,6 @@ dependencies = [ "usvg", ] -[[package]] -name = "retained_image" -version = "0.1.0" -dependencies = [ - "eframe", - "egui_extras", - "env_logger", - "image", -] - [[package]] name = "rfd" version = "0.11.4" @@ -3059,13 +3254,14 @@ dependencies = [ [[package]] name = "ron" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300a51053b1cb55c80b7a9fde4120726ddf25ca241a1cbb926626f62fb136bff" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ - "base64", - "bitflags 1.3.2", + "base64 0.21.4", + "bitflags 2.4.0", "serde", + "serde_derive", ] [[package]] @@ -3091,35 +3287,58 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.37.19" +version = "0.37.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys 0.4.7", "windows-sys 0.48.0", ] [[package]] name = "rustls" -version = "0.20.8" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ "log", "ring", + "rustls-webpki", "sct", - "webpki", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +dependencies = [ + "ring", + "untrusted", ] [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "safemem" @@ -3155,9 +3374,9 @@ checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "screenshot" @@ -3193,29 +3412,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.163" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "itoa", "ryu", @@ -3224,20 +3443,20 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.12" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" +checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] name = "serde_spanned" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" dependencies = [ "serde", ] @@ -3252,9 +3471,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -3263,9 +3482,24 @@ dependencies = [ [[package]] name = "shlex" -version = "1.1.0" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "simplecss" @@ -3278,15 +3512,15 @@ dependencies = [ [[package]] name = "siphasher" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] @@ -3302,15 +3536,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.10.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "smithay-client-toolkit" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f307c47d32d2715eb2e0ece5589057820e0e5e70d07c247d1063e844e107f454" +checksum = "870427e30b8f2cbe64bf43ec4b86e88fe39b0a84b3f15efd9c9c2d020bc86eb9" dependencies = [ "bitflags 1.3.2", "calloop", @@ -3395,9 +3629,9 @@ checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" [[package]] name = "strict-num" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df65f20698aeed245efdde3628a6b559ea1239bbb871af1b6e3b58c413b2bd1" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" dependencies = [ "float-cmp", ] @@ -3408,15 +3642,6 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -[[package]] -name = "svg" -version = "0.1.0" -dependencies = [ - "eframe", - "egui_extras", - "env_logger", -] - [[package]] name = "svgtypes" version = "0.8.2" @@ -3439,9 +3664,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.16" +version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", @@ -3450,21 +3675,19 @@ dependencies = [ [[package]] name = "syntect" -version = "5.0.0" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6c454c27d9d7d9a84c7803aaa3c50cd088d2906fe3c6e42da3209aa623576a8" +checksum = "e02b4b303bf8d08bfeb0445cba5068a3d306b6baece1d5582171a9bf49188f91" dependencies = [ "bincode", "bitflags 1.3.2", "fancy-regex", "flate2", "fnv", - "lazy_static", "once_cell", "plist", - "regex-syntax 0.6.29", + "regex-syntax", "serde", - "serde_derive", "serde_json", "thiserror", "walkdir", @@ -3473,9 +3696,9 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.1.0" +version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5fa6fb9ee296c0dc2df41a656ca7948546d061958115ddb0bcaae43ad0d17d2" +checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3" dependencies = [ "cfg-expr", "heck", @@ -3486,28 +3709,28 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.7" +version = "0.12.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5" +checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" [[package]] name = "tempfile" -version = "3.5.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", - "fastrand", + "fastrand 2.0.1", "redox_syscall 0.3.5", - "rustix", - "windows-sys 0.45.0", + "rustix 0.38.14", + "windows-sys 0.48.0", ] [[package]] name = "termcolor" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" dependencies = [ "winapi-util", ] @@ -3520,41 +3743,31 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", -] - -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", + "syn 2.0.37", ] [[package]] name = "time" -version = "0.3.21" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" dependencies = [ + "deranged", "itoa", "serde", "time-core", @@ -3563,15 +3776,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.9" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] @@ -3628,9 +3841,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" -version = "0.7.4" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" dependencies = [ "serde", "serde_spanned", @@ -3640,20 +3853,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.9" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d964908cec0d030b812013af25a0e57fddfadb1e066ecc6681d86253129d4f" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap", + "indexmap 2.0.0", "serde", "serde_spanned", "toml_datetime", @@ -3674,13 +3887,13 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", ] [[package]] @@ -3694,15 +3907,15 @@ dependencies = [ [[package]] name = "ttf-parser" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44dcf002ae3b32cd25400d6df128c5babec3927cd1eb7ce813cfff20eb6c3746" +checksum = "a464a4b34948a5f67fddd2b823c62d9d92e44be75058b99939eae6c5b6960b33" [[package]] name = "tts" -version = "0.25.5" +version = "0.25.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e339627916d1e1425f806c68b57d7eb6f9486ef0500829b1324556bef3b4fa2d" +checksum = "aee57eae77c7059f02e9ae166cd3ef4973e62c859b1eeaf9a738032a5b1c38e4" dependencies = [ "cocoa-foundation", "core-foundation", @@ -3719,7 +3932,7 @@ dependencies = [ "thiserror", "wasm-bindgen", "web-sys", - "windows 0.48.0", + "windows 0.51.1", ] [[package]] @@ -3733,9 +3946,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uds_windows" @@ -3747,6 +3960,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -3755,9 +3977,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -3770,9 +3992,9 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "unicode-xid" @@ -3794,25 +4016,25 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "ureq" -version = "2.6.2" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d" +checksum = "f5ccd538d4a604753ebc2f17cd9946e89b77bf87f6a8e2309667c6f2e87855e3" dependencies = [ - "base64", + "base64 0.21.4", "flate2", "log", "once_cell", "rustls", + "rustls-webpki", "url", - "webpki", "webpki-roots", ] [[package]] name = "url" -version = "2.3.1" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna", @@ -3833,7 +4055,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b5b7c2b30845b3348c067ca3d09e20cc6e327c288f0ca4c48698712abf432e9" dependencies = [ - "base64", + "base64 0.13.1", "data-url", "flate2", "imagesize", @@ -3867,26 +4089,20 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "waker-fn" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" [[package]] name = "walkdir" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ "same-file", "winapi-util", ] -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3914,15 +4130,15 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.36" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ "cfg-if", "js-sys", @@ -3948,7 +4164,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.37", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4067,9 +4283,9 @@ dependencies = [ [[package]] name = "webbrowser" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd222aa310eb7532e3fd427a5d7db7e44bc0b0cf1c1e21139c345325511a85b6" +checksum = "b2c79b77f525a2d670cb40619d7d9c673d09e0666f72c591ebd7861f84a87e57" dependencies = [ "core-foundation", "home", @@ -4082,24 +4298,11 @@ dependencies = [ "web-sys", ] -[[package]] -name = "webpki" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] name = "wgpu" @@ -4133,7 +4336,7 @@ checksum = "ecf7454d9386f602f7399225c92dd2fbdcde52c519bc8fb0bd6fbeb388075dc2" dependencies = [ "arrayvec", "bit-vec", - "bitflags 2.3.1", + "bitflags 2.4.0", "codespan-reporting", "log", "naga", @@ -4158,7 +4361,7 @@ dependencies = [ "arrayvec", "ash", "bit-set", - "bitflags 2.3.1", + "bitflags 2.4.0", "block", "core-graphics-types", "d3d12", @@ -4195,20 +4398,21 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee64d7398d0c2f9ca48922c902ef69c42d000c759f3db41e355f4a570b052b67" dependencies = [ - "bitflags 2.3.1", + "bitflags 2.4.0", "js-sys", "web-sys", ] [[package]] name = "which" -version = "4.4.0" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ "either", - "libc", + "home", "once_cell", + "rustix 0.38.14", ] [[package]] @@ -4235,9 +4439,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] @@ -4263,8 +4467,6 @@ version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" dependencies = [ - "windows-implement", - "windows-interface", "windows-targets 0.42.2", ] @@ -4274,14 +4476,35 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.0", + "windows-implement", + "windows-interface", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", ] [[package]] name = "windows-implement" -version = "0.44.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce87ca8e3417b02dc2a8a22769306658670ec92d78f1bd420d6310a67c245c6" +checksum = "5e2ee588991b9e7e6c8338edf3333fbe4da35dc72092643958ebb43f0ab2c49c" dependencies = [ "proc-macro2", "quote", @@ -4290,9 +4513,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.44.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "853f69a591ecd4f810d29f17e902d40e349fb05b0b11fff63b08b826bfe39c7f" +checksum = "e6fb8df20c9bcaa8ad6ab513f7b40104840c8867d5751126e4df3b08388d0cc7" dependencies = [ "proc-macro2", "quote", @@ -4314,7 +4537,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.5", ] [[package]] @@ -4334,17 +4557,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -4355,9 +4578,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" @@ -4367,9 +4590,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" @@ -4379,9 +4602,9 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" @@ -4391,9 +4614,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" @@ -4403,9 +4626,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" @@ -4415,9 +4638,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" @@ -4427,9 +4650,9 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winit" @@ -4468,9 +4691,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.4.6" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ "memchr", ] @@ -4517,11 +4740,21 @@ dependencies = [ "nom", ] +[[package]] +name = "xdg-home" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2769203cd13a0c6015d515be729c526d041e9cf2c0cc478d57faee85f40c6dcd" +dependencies = [ + "nix 0.26.4", + "winapi", +] + [[package]] name = "xml-rs" -version = "0.8.15" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a56c84a8ccd4258aed21c92f70c0f6dea75356b6892ae27c24139da456f9336" +checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" [[package]] name = "xmlparser" @@ -4540,27 +4773,29 @@ dependencies = [ [[package]] name = "zbus" -version = "3.10.0" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f770930448dd412a4a7131dd968a8e6df0064db4d7916fbbd2d6c3f26b566938" +checksum = "31de390a2d872e4cd04edd71b425e29853f786dc99317ed72d73d6fcf5ebb948" dependencies = [ "async-broadcast", "async-executor", + "async-fs", "async-io", "async-lock", + "async-process", "async-recursion", "async-task", "async-trait", + "blocking", "byteorder", "derivative", - "dirs", "enumflags2", - "event-listener", + "event-listener 2.5.3", "futures-core", "futures-sink", "futures-util", "hex", - "nix 0.25.1", + "nix 0.26.4", "once_cell", "ordered-stream", "rand", @@ -4571,6 +4806,7 @@ dependencies = [ "tracing", "uds_windows", "winapi", + "xdg-home", "zbus_macros", "zbus_names", "zvariant", @@ -4578,33 +4814,54 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "3.10.0" +version = "3.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4832059b438689017db7340580ebabba07f114eab91bf990c6e55052408b40d8" +checksum = "41d1794a946878c0e807f55a397187c11fc7a038ba5d868e7db4f3bd7760bc9d" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "regex", "syn 1.0.109", + "zvariant_utils", ] [[package]] name = "zbus_names" -version = "2.5.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82441e6033be0a741157a72951a3e4957d519698f3a824439cc131c5ba77ac2a" +checksum = "fb80bb776dbda6e23d705cf0123c3b95df99c4ebeaec6c2599d4a5419902b4a9" dependencies = [ "serde", "static_assertions", "zvariant", ] +[[package]] +name = "zerocopy" +version = "0.7.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686b7e407015242119c33dab17b8f61ba6843534de936d94368856528eae4dcc" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020f3dfe25dfc38dfea49ce62d5d45ecdd7f0d8a724fa63eb36b6eba4ec76806" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "zvariant" -version = "3.14.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622cc473f10cef1b0d73b7b34a266be30ebdcfaea40ec297dd8cbda088f9f93c" +checksum = "44b291bee0d960c53170780af148dca5fa260a63cdd24f1962fa82e03e53338c" dependencies = [ "byteorder", "enumflags2", @@ -4616,9 +4873,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "3.14.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d9c1b57352c25b778257c661f3c4744b7cefb7fc09dd46909a153cce7773da2" +checksum = "934d7a7dfc310d6ee06c87ffe88ef4eca7d3e37bb251dece2ef93da8f17d8ecd" dependencies = [ "proc-macro-crate", "proc-macro2", diff --git a/Cranky.toml b/Cranky.toml index d67d3e75c53..4099a187c4f 100644 --- a/Cranky.toml +++ b/Cranky.toml @@ -95,7 +95,6 @@ warn = [ "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", @@ -138,6 +137,8 @@ warn = [ allow = [ "clippy::manual_range_contains", # this one is just worse imho + "clippy::significant_drop_tightening", # False positives + # TODO(emilk): enable more of these lints: "clippy::let_underscore_untyped", "clippy::missing_assert_message", diff --git a/README.md b/README.md index ae9cf01fe0a..520304b14b7 100644 --- a/README.md +++ b/README.md @@ -9,29 +9,23 @@ [![Apache](https://img.shields.io/badge/license-Apache-blue.svg)](https://github.com/emilk/egui/blob/master/LICENSE-APACHE) [![Discord](https://img.shields.io/discord/900275882684477440?label=egui%20discord)](https://discord.gg/JFcEma9bJq) -👉 [Click to run the web demo](https://www.egui.rs/#demo) 👈 -egui (pronounced "e-gooey") is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and [in your favorite game engine](#integrations) (or will soon). +
+ -egui aims to be the easiest-to-use Rust GUI library, and the simplest way to make a web app in Rust. +egui development is sponsored by [Rerun](https://www.rerun.io/), a startup building
+an SDK for visualizing streams of multimodal data. +
-egui can be used anywhere you can draw textured triangles, which means you can easily integrate it into your game engine of choice. +--- -Sections: +👉 [Click to run the web demo](https://www.egui.rs/#demo) 👈 -* [Example](#example) -* [Quick start](#quick-start) -* [Demo](#demo) -* [Goals](#goals) -* [Who is egui for?](#who-is-egui-for) -* [State / features](#state) -* [Integrations](#integrations) -* [Why immediate mode](#why-immediate-mode) -* [FAQ](#faq) -* [Other](#other) -* [Credits](#credits) +egui (pronounced "e-gooey") is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and [in your favorite game engine](#integrations). -([egui 的中文翻译文档 / chinese translation](https://github.com/Re-Ch-Love/egui-doc-cn/blob/main/README_zh-hans.md)) +egui aims to be the easiest-to-use Rust GUI library, and the simplest way to make a web app in Rust. + +egui can be used anywhere you can draw textured triangles, which means you can easily integrate it into your game engine of choice. ## Example @@ -46,10 +40,29 @@ if ui.button("Click each year").clicked() { age += 1; } ui.label(format!("Hello '{name}', age {age}")); +ui.image(egui::include_image!("ferris.png")); ``` +## Sections: + +* [Example](#example) +* [Quick start](#quick-start) +* [Demo](#demo) +* [Goals](#goals) +* [State / features](#state) +* [Dependencies](#dependencies) +* [Who is egui for?](#who-is-egui-for) +* [Integrations](#integrations) +* [Why immediate mode](#why-immediate-mode) +* [FAQ](#faq) +* [Other](#other) +* [Credits](#credits) + +([egui 的中文翻译文档 / chinese translation](https://github.com/Re-Ch-Love/egui-doc-cn/blob/main/README_zh-hans.md)) + + ## Quick start There are simple examples in [the `examples/` folder](https://github.com/emilk/egui/blob/master/examples/). If you want to write a web app, then go to and follow the instructions. The official docs are at . For inspiration and more examples, check out the [the egui web demo](https://www.egui.rs/#demo) and follow the links in it to its source code. @@ -87,7 +100,7 @@ On Fedora Rawhide you need to run: * Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs) * Modular: You should be able to use small parts of egui and combine them in new ways * Safe: there is no `unsafe` code in egui -* Minimal dependencies: [`ab_glyph`](https://crates.io/crates/ab_glyph) [`ahash`](https://crates.io/crates/ahash) [`nohash-hasher`](https://crates.io/crates/nohash-hasher) [`parking_lot`](https://crates.io/crates/parking_lot) +* Minimal dependencies egui is *not* a framework. egui is a library you call into, not an environment you program for. @@ -99,6 +112,47 @@ egui is *not* a framework. egui is a library you call into, not an environment y * Native looking interface * Advanced and flexible layouts (that's fundamentally incompatible with immediate mode) +## State + +egui is in active development. It works well for what it does, but it lacks many features and the interfaces are still in flux. New releases will have breaking changes. + +Still, egui can be used to create professional looking applications, like [the Rerun Viewer](https://app.rerun.io/). + +### Features + +* Widgets: label, text button, hyperlink, checkbox, radio button, slider, draggable value, text editing, combo box, color picker, spinner +* Images +* Layouts: horizontal, vertical, columns, automatic wrapping +* Text editing: multiline, copy/paste, undo, emoji supports +* Windows: move, resize, name, minimize and close. Automatically sized and positioned. +* Regions: resizing, vertical scrolling, collapsing headers (sections) +* Rendering: Anti-aliased rendering of lines, circles, text and convex polygons. +* Tooltips on hover +* Accessibility via [AccessKit](https://accesskit.dev/) +* More + + + +Light Theme: + + + + +## Dependencies +`egui` has a minimal set of default dependencies: + +* [`ab_glyph`](https://crates.io/crates/ab_glyph) +* [`ahash`](https://crates.io/crates/ahash) +* [`nohash-hasher`](https://crates.io/crates/nohash-hasher) +* [`parking_lot`](https://crates.io/crates/parking_lot) + +Heavier dependencies are kept out of `egui`, even as opt-in. +No code that isn't fully Wasm-friendly is part of `egui`. + +To load images into `egui` you can use the official [`egui_extras`](https://github.com/emilk/egui/tree/master/crates/egui_extras) crate. + +[`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) on the other hand has a lot of dependencies, including [`winit`](https://crates.io/crates/winit), [`image`](https://crates.io/crates/image), graphics crates, clipboard crates, etc, + ## Who is egui for? egui aims to be the best choice when you want a simple way to create a GUI, or you want to add a GUI to a game engine. @@ -127,27 +181,6 @@ So in summary: * egui: pure Rust, new, exciting, work in progress * Dear ImGui: feature rich, well tested, cumbersome Rust integration -## State - -egui is in active development. It works well for what it does, but it lacks many features and the interfaces are still in flux. New releases will have breaking changes. - -### Features - -* Widgets: label, text button, hyperlink, checkbox, radio button, slider, draggable value, text editing, combo box, color picker -* Layouts: horizontal, vertical, columns, automatic wrapping -* Text editing: multiline, copy/paste, undo, emoji supports -* Windows: move, resize, name, minimize and close. Automatically sized and positioned. -* Regions: resizing, vertical scrolling, collapsing headers (sections) -* Rendering: Anti-aliased rendering of lines, circles, text and convex polygons. -* Tooltips on hover -* More - - - -Light Theme: - - - ## Integrations egui is built to be easy to integrate into any existing game engine or platform you are working on. @@ -198,55 +231,7 @@ These are the official egui integrations: Missing an integration for the thing you're working on? Create one, it's easy! ### Writing your own egui integration - -You need to collect [`egui::RawInput`](https://docs.rs/egui/latest/egui/struct.RawInput.html) and handle [`egui::FullOutput`](https://docs.rs/egui/latest/egui/struct.FullOutput.html). The basic structure is this: - -``` rust -let mut egui_ctx = egui::CtxRef::default(); - -// Game loop: -loop { - // Gather input (mouse, touches, keyboard, screen size, etc): - let raw_input: egui::RawInput = my_integration.gather_input(); - let full_output = egui_ctx.run(raw_input, |egui_ctx| { - my_app.ui(egui_ctx); // add panels, windows and widgets to `egui_ctx` here - }); - let clipped_primitives = egui_ctx.tessellate(full_output.shapes); // creates triangles to paint - - my_integration.paint(&full_output.textures_delta, clipped_primitives); - - let platform_output = full_output.platform_output; - my_integration.set_cursor_icon(platform_output.cursor_icon); - if !platform_output.copied_text.is_empty() { - my_integration.set_clipboard_text(platform_output.copied_text); - } - // See `egui::FullOutput` and `egui::PlatformOutput` for more -} -``` - -For a reference OpenGL backend, see [the `egui_glium` painter](https://github.com/emilk/egui/blob/master/crates/egui_glium/src/painter.rs) or [the `egui_glow` painter](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs). - -### Debugging your integration - -#### Things look jagged - -* Turn off backface culling. - -#### My text is blurry - -* Make sure you set the proper `pixels_per_point` in the input to egui. -* Make sure the texture sampler is not off by half a pixel. Try nearest-neighbor sampler to check. - -#### My windows are too transparent or too dark - -* egui uses premultiplied alpha, so make sure your blending function is `(ONE, ONE_MINUS_SRC_ALPHA)`. -* Make sure your texture sampler is clamped (`GL_CLAMP_TO_EDGE`). -* egui prefers linear color spaces for all blending so: - * Use an sRGBA-aware texture if available (e.g. `GL_SRGB8_ALPHA8`). - * Otherwise: remember to decode gamma in the fragment shader. - * Decode the gamma of the incoming vertex colors in your vertex shader. - * Turn on sRGBA/linear framebuffer if available (`GL_FRAMEBUFFER_SRGB`). - * Otherwise: gamma-encode the colors before you write them again. +See . ## Why immediate mode @@ -288,7 +273,7 @@ For "atomic" widgets (e.g. a button) `egui` knows the size before showing it, so #### CPU usage Since an immediate mode GUI does a full layout each frame, the layout code needs to be quick. If you have a very complex GUI this can tax the CPU. In particular, having a very large UI in a scroll area (with very long scrollback) can be slow, as the content needs to be laid out each frame. -If you design the GUI with this in mind and refrain from huge scroll areas (or only lay out the part that is in view) then the performance hit is generally pretty small. For most cases you can expect `egui` to take up 1-2 ms per frame, but `egui` still has a lot of room for optimization (it's not something I've focused on yet). You can also set up `egui` to only repaint when there is interaction (e.g. mouse movement). +If you design the GUI with this in mind and refrain from huge scroll areas (or only lay out the part that is in view) then the performance hit is generally pretty small. For most cases you can expect `egui` to take up 1-2 ms per frame, but `egui` still has a lot of room for optimization (it's not something I've focused on yet). `egui` only repaints when there is interaction (e.g. mouse movement) or an animation, so if your app is idle, no CPU is wasted. If your GUI is highly interactive, then immediate mode may actually be more performant compared to retained mode. Go to any web page and resize the browser window, and you'll notice that the browser is very slow to do the layout and eats a lot of CPU doing it. Resize a window in `egui` by contrast, and you'll get smooth 60 FPS at no extra CPU cost. @@ -311,7 +296,9 @@ Yes! But you need to install your own font (`.ttf` or `.otf`) using `Context::se ### Can I customize the look of egui? Yes! You can customize the colors, spacing, fonts and sizes of everything using `Context::set_style`. -Here is an example (from https://github.com/AlexxxRu/TinyPomodoro): +This is not yet as powerful as say CSS, [but this is going to improve soon](https://github.com/emilk/egui/issues/3284). + +Here is an example (from https://github.com/a-liashenko/TinyPomodoro): @@ -319,7 +306,7 @@ Here is an example (from https://github.com/AlexxxRu/TinyPomodoro): If you call `.await` in your GUI code, the UI will freeze, which is very bad UX. Instead, keep the GUI thread non-blocking and communicate with any concurrent tasks (`async` tasks or other threads) with something like: * Channels (e.g. [`std::sync::mpsc::channel`](https://doc.rust-lang.org/std/sync/mpsc/fn.channel.html)). Make sure to use [`try_recv`](https://doc.rust-lang.org/std/sync/mpsc/struct.Receiver.html#method.try_recv) so you don't block the gui thread! * `Arc>` (background thread sets a value; GUI thread reads it) -* [`poll_promise::Promise`](https://docs.rs/poll-promise) (example: [`examples/download_image/`](https://github.com/emilk/egui/blob/master/examples/download_image/)) +* [`poll_promise::Promise`](https://docs.rs/poll-promise) * [`eventuals::Eventual`](https://docs.rs/eventuals/latest/eventuals/struct.Eventual.html) * [`tokio::sync::watch::channel`](https://docs.rs/tokio/latest/tokio/sync/watch/fn.channel.html) @@ -345,7 +332,7 @@ If you want to embed 3D into an egui view there are two options. #### `Shape::Callback` Example: -* +* `Shape::Callback` will call your code when egui gets painted, to show anything using whatever the background rendering context is. When using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe) this will be [`glow`](https://github.com/grovesNL/glow). Other integrations will give you other rendering contexts, if they support `Shape::Callback` at all. @@ -399,6 +386,9 @@ Notable contributions by: * [@t18b219k](https://github.com/t18b219k): [Port glow painter to web](https://github.com/emilk/egui/pull/868). * [@danielkeller](https://github.com/danielkeller): [`Context` refactor](https://github.com/emilk/egui/pull/1050). * [@MaximOsipenko](https://github.com/MaximOsipenko): [`Context` lock refactor](https://github.com/emilk/egui/pull/2625). +* [@mwcampbell](https://github.com/mwcampbell): [AccessKit](https://github.com/AccessKit/accesskit) [integration](https://github.com/emilk/egui/pull/2294). +* [@hasenbanck](https://github.com/hasenbanck), [@s-nie](https://github.com/s-nie), [@Wumpf](https://github.com/Wumpf): [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu) +* [@jprochazk](https://github.com/jprochazk): [egui image API](https://github.com/emilk/egui/issues/3291) * And [many more](https://github.com/emilk/egui/graphs/contributors?type=a). egui is licensed under [MIT](LICENSE-MIT) OR [Apache-2.0](LICENSE-APACHE). @@ -415,8 +405,8 @@ Default fonts: ---
- + -egui development is sponsored by [Rerun](https://www.rerun.io/), a startup doing
-visualizations for computer vision and robotics. +egui development is sponsored by [Rerun](https://www.rerun.io/), a startup building
+an SDK for visualizing streams of multimodal data.
diff --git a/crates/ecolor/Cargo.toml b/crates/ecolor/Cargo.toml index 2e821516e4e..8acafe7bda0 100644 --- a/crates/ecolor/Cargo.toml +++ b/crates/ecolor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ecolor" -version = "0.22.0" +version = "0.23.0" authors = [ "Emil Ernerfeldt ", "Andreas Reich ", diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index 09e68116f09..56b96589256 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -16,14 +16,14 @@ pub struct Color32(pub(crate) [u8; 4]); impl std::ops::Index for Color32 { type Output = u8; - #[inline(always)] + #[inline] fn index(&self, index: usize) -> &u8 { &self.0[index] } } impl std::ops::IndexMut for Color32 { - #[inline(always)] + #[inline] fn index_mut(&mut self, index: usize) -> &mut u8 { &mut self.0[index] } @@ -63,23 +63,24 @@ impl Color32 { /// An ugly color that is planned to be replaced before making it to the screen. pub const TEMPORARY_COLOR: Color32 = Color32::from_rgb(64, 254, 0); - #[inline(always)] + #[inline] pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self { Self([r, g, b, 255]) } - #[inline(always)] + #[inline] pub const fn from_rgb_additive(r: u8, g: u8, b: u8) -> Self { Self([r, g, b, 0]) } /// From `sRGBA` with premultiplied alpha. - #[inline(always)] + #[inline] pub const fn from_rgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { Self([r, g, b, a]) } /// From `sRGBA` WITHOUT premultiplied alpha. + #[inline] pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { if a == 255 { Self::from_rgb(r, g, b) // common-case optimization @@ -99,74 +100,83 @@ impl Color32 { } } - #[inline(always)] + #[inline] pub const fn from_gray(l: u8) -> Self { Self([l, l, l, 255]) } - #[inline(always)] + #[inline] pub const fn from_black_alpha(a: u8) -> Self { Self([0, 0, 0, a]) } + #[inline] pub fn from_white_alpha(a: u8) -> Self { Rgba::from_white_alpha(linear_f32_from_linear_u8(a)).into() } - #[inline(always)] + #[inline] pub const fn from_additive_luminance(l: u8) -> Self { Self([l, l, l, 0]) } - #[inline(always)] + #[inline] pub const fn is_opaque(&self) -> bool { self.a() == 255 } - #[inline(always)] + #[inline] pub const fn r(&self) -> u8 { self.0[0] } - #[inline(always)] + #[inline] pub const fn g(&self) -> u8 { self.0[1] } - #[inline(always)] + #[inline] pub const fn b(&self) -> u8 { self.0[2] } - #[inline(always)] + #[inline] pub const fn a(&self) -> u8 { self.0[3] } /// Returns an opaque version of self + #[inline] pub fn to_opaque(self) -> Self { Rgba::from(self).to_opaque().into() } /// Returns an additive version of self - #[inline(always)] + #[inline] pub const fn additive(self) -> Self { let [r, g, b, _] = self.to_array(); Self([r, g, b, 0]) } + /// Is the alpha=0 ? + #[inline] + pub fn is_additive(self) -> bool { + self.a() == 0 + } + /// Premultiplied RGBA - #[inline(always)] + #[inline] pub const fn to_array(&self) -> [u8; 4] { [self.r(), self.g(), self.b(), self.a()] } /// Premultiplied RGBA - #[inline(always)] + #[inline] pub const fn to_tuple(&self) -> (u8, u8, u8, u8) { (self.r(), self.g(), self.b(), self.a()) } + #[inline] pub fn to_srgba_unmultiplied(&self) -> [u8; 4] { Rgba::from(*self).to_srgba_unmultiplied() } @@ -192,6 +202,7 @@ impl Color32 { /// /// This is using linear space, which is not perceptually even. /// You may want to use [`Self::gamma_multiply`] instead. + #[inline] pub fn linear_multiply(self, factor: f32) -> Color32 { crate::ecolor_assert!(0.0 <= factor && factor <= 1.0); // As an unfortunate side-effect of using premultiplied alpha diff --git a/crates/ecolor/src/hsva.rs b/crates/ecolor/src/hsva.rs index 8a68cb93502..1fa54a24e10 100644 --- a/crates/ecolor/src/hsva.rs +++ b/crates/ecolor/src/hsva.rs @@ -21,11 +21,13 @@ pub struct Hsva { } impl Hsva { + #[inline] pub fn new(h: f32, s: f32, v: f32, a: f32) -> Self { Self { h, s, v, a } } /// From `sRGBA` with premultiplied alpha + #[inline] pub fn from_srgba_premultiplied(srgba: [u8; 4]) -> Self { Self::from_rgba_premultiplied( linear_f32_from_gamma_u8(srgba[0]), @@ -36,6 +38,7 @@ impl Hsva { } /// From `sRGBA` without premultiplied alpha + #[inline] pub fn from_srgba_unmultiplied(srgba: [u8; 4]) -> Self { Self::from_rgba_unmultiplied( linear_f32_from_gamma_u8(srgba[0]), @@ -46,6 +49,7 @@ impl Hsva { } /// From linear RGBA with premultiplied alpha + #[inline] pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { #![allow(clippy::many_single_char_names)] if a == 0.0 { @@ -61,12 +65,14 @@ impl Hsva { } /// From linear RGBA without premultiplied alpha + #[inline] pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { #![allow(clippy::many_single_char_names)] let (h, s, v) = hsv_from_rgb([r, g, b]); Hsva { h, s, v, a } } + #[inline] pub fn from_additive_rgb(rgb: [f32; 3]) -> Self { let (h, s, v) = hsv_from_rgb(rgb); Hsva { @@ -77,11 +83,13 @@ impl Hsva { } } + #[inline] pub fn from_rgb(rgb: [f32; 3]) -> Self { let (h, s, v) = hsv_from_rgb(rgb); Hsva { h, s, v, a: 1.0 } } + #[inline] pub fn from_srgb([r, g, b]: [u8; 3]) -> Self { Self::from_rgb([ linear_f32_from_gamma_u8(r), @@ -92,14 +100,17 @@ impl Hsva { // ------------------------------------------------------------------------ + #[inline] pub fn to_opaque(self) -> Self { Self { a: 1.0, ..self } } + #[inline] pub fn to_rgb(&self) -> [f32; 3] { rgb_from_hsv((self.h, self.s, self.v)) } + #[inline] pub fn to_srgb(&self) -> [u8; 3] { let [r, g, b] = self.to_rgb(); [ @@ -109,6 +120,7 @@ impl Hsva { ] } + #[inline] pub fn to_rgba_premultiplied(&self) -> [f32; 4] { let [r, g, b, a] = self.to_rgba_unmultiplied(); let additive = a < 0.0; @@ -120,12 +132,14 @@ impl Hsva { } /// Represents additive colors using a negative alpha. + #[inline] pub fn to_rgba_unmultiplied(&self) -> [f32; 4] { let Hsva { h, s, v, a } = *self; let [r, g, b] = rgb_from_hsv((h, s, v)); [r, g, b, a] } + #[inline] pub fn to_srgba_premultiplied(&self) -> [u8; 4] { let [r, g, b, a] = self.to_rgba_premultiplied(); [ @@ -136,6 +150,7 @@ impl Hsva { ] } + #[inline] pub fn to_srgba_unmultiplied(&self) -> [u8; 4] { let [r, g, b, a] = self.to_rgba_unmultiplied(); [ @@ -148,30 +163,35 @@ impl Hsva { } impl From for Rgba { + #[inline] fn from(hsva: Hsva) -> Rgba { Rgba(hsva.to_rgba_premultiplied()) } } impl From for Hsva { + #[inline] fn from(rgba: Rgba) -> Hsva { Self::from_rgba_premultiplied(rgba.0[0], rgba.0[1], rgba.0[2], rgba.0[3]) } } impl From for Color32 { + #[inline] fn from(hsva: Hsva) -> Color32 { Color32::from(Rgba::from(hsva)) } } impl From for Hsva { + #[inline] fn from(srgba: Color32) -> Hsva { Hsva::from(Rgba::from(srgba)) } } /// All ranges in 0-1, rgb is linear. +#[inline] pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) { #![allow(clippy::many_single_char_names)] let min = r.min(g.min(b)); @@ -195,6 +215,7 @@ pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) { } /// All ranges in 0-1, rgb is linear. +#[inline] pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] { #![allow(clippy::many_single_char_names)] let h = (h.fract() + 1.0).fract(); // wrap diff --git a/crates/ecolor/src/rgba.rs b/crates/ecolor/src/rgba.rs index 38bbaa3212a..b40f70e3e51 100644 --- a/crates/ecolor/src/rgba.rs +++ b/crates/ecolor/src/rgba.rs @@ -13,20 +13,20 @@ pub struct Rgba(pub(crate) [f32; 4]); impl std::ops::Index for Rgba { type Output = f32; - #[inline(always)] + #[inline] fn index(&self, index: usize) -> &f32 { &self.0[index] } } impl std::ops::IndexMut for Rgba { - #[inline(always)] + #[inline] fn index_mut(&mut self, index: usize) -> &mut f32 { &mut self.0[index] } } -#[inline(always)] +#[inline] pub(crate) fn f32_hash(state: &mut H, f: f32) { if f == 0.0 { state.write_u8(0); @@ -57,17 +57,17 @@ impl Rgba { pub const GREEN: Rgba = Rgba::from_rgb(0.0, 1.0, 0.0); pub const BLUE: Rgba = Rgba::from_rgb(0.0, 0.0, 1.0); - #[inline(always)] + #[inline] pub const fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { Self([r, g, b, a]) } - #[inline(always)] + #[inline] pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { Self([r * a, g * a, b * a, a]) } - #[inline(always)] + #[inline] pub fn from_srgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { let r = linear_f32_from_gamma_u8(r); let g = linear_f32_from_gamma_u8(g); @@ -76,7 +76,7 @@ impl Rgba { Self::from_rgba_premultiplied(r, g, b, a) } - #[inline(always)] + #[inline] pub fn from_srgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { let r = linear_f32_from_gamma_u8(r); let g = linear_f32_from_gamma_u8(g); @@ -85,16 +85,17 @@ impl Rgba { Self::from_rgba_premultiplied(r * a, g * a, b * a, a) } - #[inline(always)] + #[inline] pub const fn from_rgb(r: f32, g: f32, b: f32) -> Self { Self([r, g, b, 1.0]) } - #[inline(always)] + #[inline] pub const fn from_gray(l: f32) -> Self { Self([l, l, l, 1.0]) } + #[inline] pub fn from_luminance_alpha(l: f32, a: f32) -> Self { crate::ecolor_assert!(0.0 <= l && l <= 1.0); crate::ecolor_assert!(0.0 <= a && a <= 1.0); @@ -102,28 +103,34 @@ impl Rgba { } /// Transparent black - #[inline(always)] + #[inline] pub fn from_black_alpha(a: f32) -> Self { crate::ecolor_assert!(0.0 <= a && a <= 1.0); Self([0.0, 0.0, 0.0, a]) } /// Transparent white - #[inline(always)] + #[inline] pub fn from_white_alpha(a: f32) -> Self { crate::ecolor_assert!(0.0 <= a && a <= 1.0, "a: {}", a); Self([a, a, a, a]) } /// Return an additive version of this color (alpha = 0) - #[inline(always)] + #[inline] pub fn additive(self) -> Self { let [r, g, b, _] = self.0; Self([r, g, b, 0.0]) } + /// Is the alpha=0 ? + #[inline] + pub fn is_additive(self) -> bool { + self.a() == 0.0 + } + /// Multiply with e.g. 0.5 to make us half transparent - #[inline(always)] + #[inline] pub fn multiply(self, alpha: f32) -> Self { Self([ alpha * self[0], @@ -133,22 +140,22 @@ impl Rgba { ]) } - #[inline(always)] + #[inline] pub fn r(&self) -> f32 { self.0[0] } - #[inline(always)] + #[inline] pub fn g(&self) -> f32 { self.0[1] } - #[inline(always)] + #[inline] pub fn b(&self) -> f32 { self.0[2] } - #[inline(always)] + #[inline] pub fn a(&self) -> f32 { self.0[3] } @@ -160,6 +167,7 @@ impl Rgba { } /// Returns an opaque version of self + #[inline] pub fn to_opaque(&self) -> Self { if self.a() == 0.0 { // Additive or fully transparent black. @@ -175,18 +183,19 @@ impl Rgba { } /// Premultiplied RGBA - #[inline(always)] + #[inline] pub fn to_array(&self) -> [f32; 4] { [self.r(), self.g(), self.b(), self.a()] } /// Premultiplied RGBA - #[inline(always)] + #[inline] pub fn to_tuple(&self) -> (f32, f32, f32, f32) { (self.r(), self.g(), self.b(), self.a()) } /// unmultiply the alpha + #[inline] pub fn to_rgba_unmultiplied(&self) -> [f32; 4] { let a = self.a(); if a == 0.0 { @@ -198,6 +207,7 @@ impl Rgba { } /// unmultiply the alpha + #[inline] pub fn to_srgba_unmultiplied(&self) -> [u8; 4] { let [r, g, b, a] = self.to_rgba_unmultiplied(); [ @@ -212,7 +222,7 @@ impl Rgba { impl std::ops::Add for Rgba { type Output = Rgba; - #[inline(always)] + #[inline] fn add(self, rhs: Rgba) -> Rgba { Rgba([ self[0] + rhs[0], @@ -226,7 +236,7 @@ impl std::ops::Add for Rgba { impl std::ops::Mul for Rgba { type Output = Rgba; - #[inline(always)] + #[inline] fn mul(self, other: Rgba) -> Rgba { Rgba([ self[0] * other[0], @@ -240,7 +250,7 @@ impl std::ops::Mul for Rgba { impl std::ops::Mul for Rgba { type Output = Rgba; - #[inline(always)] + #[inline] fn mul(self, factor: f32) -> Rgba { Rgba([ self[0] * factor, @@ -254,7 +264,7 @@ impl std::ops::Mul for Rgba { impl std::ops::Mul for f32 { type Output = Rgba; - #[inline(always)] + #[inline] fn mul(self, rgba: Rgba) -> Rgba { Rgba([ self * rgba[0], diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index 072a54e8cd9..2a0b5136eaf 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -6,6 +6,39 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C This file is updated upon each release. Changes since the last release can be found by running the `scripts/generate_changelog.py` script. +## 0.23.0 - 2023-09-27 +* Update MSRV to Rust 1.70.0 [#3310](https://github.com/emilk/egui/pull/3310) +* Update to puffin 0.16 [#3144](https://github.com/emilk/egui/pull/3144) +* Update to `wgpu` 0.17.0 [#3170](https://github.com/emilk/egui/pull/3170) (thanks [@Aaron1011](https://github.com/Aaron1011)!) +* Improved wgpu callbacks [#3253](https://github.com/emilk/egui/pull/3253) (thanks [@Wumpf](https://github.com/Wumpf)!) +* Improve documentation of `eframe`, especially for wasm32 [#3295](https://github.com/emilk/egui/pull/3295) +* `eframe::Frame::info` returns a reference [#3301](https://github.com/emilk/egui/pull/3301) (thanks [@Barugon](https://github.com/Barugon)!) +* Move `App::persist_window` to `NativeOptions` and `App::max_size_points` to `WebOptions` [#3397](https://github.com/emilk/egui/pull/3397) + +#### Desktop/Native: +* Only show on-screen-keyboard and IME when editing text [#3362](https://github.com/emilk/egui/pull/3362) (thanks [@Barugon](https://github.com/Barugon)!) +* Add `eframe::storage_dir` [#3286](https://github.com/emilk/egui/pull/3286) +* Add `NativeOptions::window_builder` for more customization [#3390](https://github.com/emilk/egui/pull/3390) (thanks [@twop](https://github.com/twop)!) +* Better restore Window position on Mac when on secondary monitor [#3239](https://github.com/emilk/egui/pull/3239) +* Fix iOS support in `eframe` [#3241](https://github.com/emilk/egui/pull/3241) (thanks [@lucasmerlin](https://github.com/lucasmerlin)!) +* Speed up `eframe` state storage [#3353](https://github.com/emilk/egui/pull/3353) (thanks [@sebbert](https://github.com/sebbert)!) +* Allow users to opt-out of default `winit` features [#3228](https://github.com/emilk/egui/pull/3228) +* Expose Raw Window and Display Handles [#3073](https://github.com/emilk/egui/pull/3073) (thanks [@bash](https://github.com/bash)!) +* Use window title as fallback when app_id is not set [#3107](https://github.com/emilk/egui/pull/3107) (thanks [@jacekpoz](https://github.com/jacekpoz)!) +* Sleep a bit only when minimized [#3139](https://github.com/emilk/egui/pull/3139) (thanks [@icedrocket](https://github.com/icedrocket)!) +* Prevent text from being cleared when selected due to winit IME [#3376](https://github.com/emilk/egui/pull/3376) (thanks [@YgorSouza](https://github.com/YgorSouza)!) +* Fix android app quit on resume with glow backend [#3080](https://github.com/emilk/egui/pull/3080) (thanks [@tkkcc](https://github.com/tkkcc)!) +* Fix panic with persistence without window [#3167](https://github.com/emilk/egui/pull/3167) (thanks [@sagebind](https://github.com/sagebind)!) +* Only call `run_return` twice on Windows [#3053](https://github.com/emilk/egui/pull/3053) (thanks [@pan93412](https://github.com/pan93412)!) +* Gracefully catch error saving state to disk [#3230](https://github.com/emilk/egui/pull/3230) +* Recognize numpad enter/plus/minus [#3285](https://github.com/emilk/egui/pull/3285) +* Add more puffin profile scopes to `eframe` [#3330](https://github.com/emilk/egui/pull/3330) [#3332](https://github.com/emilk/egui/pull/3332) + +#### Web: +* Update to wasm-bindgen 0.2.87 [#3237](https://github.com/emilk/egui/pull/3237) +* Remove `Function()` invocation from eframe text_agent to bypass "unsafe-eval" restrictions in Chrome browser extensions. [#3349](https://github.com/emilk/egui/pull/3349) (thanks [@aspect](https://github.com/aspect)!) +* Fix docs about web [#3026](https://github.com/emilk/egui/pull/3026) (thanks [@kerryeon](https://github.com/kerryeon)!) + ## 0.22.0 - 2023-05-23 * Fix: `request_repaint_after` works even when called from background thread [#2939](https://github.com/emilk/egui/pull/2939) diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 619ae8e579e..0faca5602ce 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "eframe" -version = "0.22.0" +version = "0.23.0" authors = ["Emil Ernerfeldt "] description = "egui framework - write GUI apps that compiles to web and/or natively" edition = "2021" @@ -39,6 +39,16 @@ default = [ ## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/). accesskit = ["egui/accesskit", "egui-winit/accesskit"] +# Allow crates to choose an android-activity backend via Winit +# - It's important that most applications should not have to depend on android-activity directly, and can +# rely on Winit to pull in a suitable version (unlike most Rust crates, any version conflicts won't link) +# - It's also important that we don't impose an android-activity backend by taking this choice away from applications. + +## Enable the `game-activity` backend via `egui-winit` on Android +android-game-activity = ["egui-winit/android-game-activity"] +## Enable the `native-activity` backend via `egui-winit` on Android +android-native-activity = ["egui-winit/android-native-activity"] + ## If set, egui will use `include_bytes!` to bundle some fonts. ## If you plan on specifying your own fonts you may disable this feature. default_fonts = ["egui/default_fonts"] @@ -46,12 +56,6 @@ default_fonts = ["egui/default_fonts"] ## Use [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow). glow = ["dep:glow", "dep:egui_glow", "dep:glutin", "dep:glutin-winit"] -## Enables wayland support and fixes clipboard issue. -wayland = ["egui-winit/wayland"] - -## Enables compiling for x11. -x11 = ["egui-winit/x11"] - ## Enable saving app state to disk. persistence = [ "directories-next", @@ -63,35 +67,32 @@ persistence = [ ## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. ## -## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. ## `eframe` will call `puffin::GlobalProfiler::lock().new_frame()` for you +## +## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. puffin = ["dep:puffin", "egui/puffin", "egui_glow?/puffin", "egui-wgpu?/puffin"] +## Enables wayland support and fixes clipboard issue. +wayland = ["egui-winit/wayland"] + ## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`) on web. ## -## For other platforms, use the "accesskit" feature instead. +## For other platforms, use the `accesskit` feature instead. web_screen_reader = ["tts"] -## If set, eframe will look for the env-var `EFRAME_SCREENSHOT_TO` and write a screenshot to that location, and then quit. -## This is used to generate images for the examples. -__screenshot = [] - ## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)). ## This overrides the `glow` feature. wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster", "dep:raw-window-handle"] -# Allow crates to choose an android-activity backend via Winit -# - It's important that most applications should not have to depend on android-activity directly, and can -# rely on Winit to pull in a suitable version (unlike most Rust crates, any version conflicts won't link) -# - It's also important that we don't impose an android-activity backend by taking this choice away from applications. +## Enables compiling for x11. +x11 = ["egui-winit/x11"] -## Enable the `native-activity` backend via `egui-winit` on Android -android-native-activity = ["egui-winit/android-native-activity"] -## Enable the `game-activity` backend via `egui-winit` on Android -android-game-activity = ["egui-winit/android-game-activity"] +## If set, eframe will look for the env-var `EFRAME_SCREENSHOT_TO` and write a screenshot to that location, and then quit. +## This is used to generate images for examples. +__screenshot = [] [dependencies] -egui = { version = "0.22.0", path = "../egui", default-features = false, features = [ +egui = { version = "0.23.0", path = "../egui", default-features = false, features = [ "bytemuck", "log", ] } @@ -104,7 +105,7 @@ thiserror.workspace = true ## Enable this when generating docs. document-features = { version = "0.2", optional = true } -egui_glow = { version = "0.22.0", path = "../egui_glow", optional = true, default-features = false } +egui_glow = { version = "0.23.0", path = "../egui_glow", optional = true, default-features = false } glow = { version = "0.12", optional = true } ron = { version = "0.8", optional = true, features = ["integer128"] } serde = { version = "1", optional = true, features = ["derive"] } @@ -112,7 +113,7 @@ serde = { version = "1", optional = true, features = ["derive"] } # ------------------------------------------- # native: [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -egui-winit = { version = "0.22.0", path = "../egui-winit", default-features = false, features = [ +egui-winit = { version = "0.23.0", path = "../egui-winit", default-features = false, features = [ "clipboard", "links", ] } @@ -124,7 +125,7 @@ winit = { version = "0.28.1", default-features = false } # optional native: directories-next = { version = "2", optional = true } -egui-wgpu = { version = "0.22.0", path = "../egui-wgpu", optional = true, features = [ +egui-wgpu = { version = "0.23.0", path = "../egui-wgpu", optional = true, features = [ "winit", ] } # if wgpu is used, use it with winit pollster = { version = "0.3", optional = true } # needed for wgpu @@ -138,7 +139,7 @@ wgpu = { workspace = true, optional = true } # mac: [target.'cfg(any(target_os = "macos"))'.dependencies] -cocoa = "0.24.1" +cocoa = "0.24.1" # Stuck on old version until we update to winit 0.29 objc = "0.2.7" # windows: @@ -198,7 +199,7 @@ web-sys = { version = "0.3.58", features = [ ] } # optional web: -egui-wgpu = { version = "0.22.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit +egui-wgpu = { version = "0.23.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit raw-window-handle = { version = "0.5.2", optional = true } tts = { version = "0.25", optional = true, default-features = false } wgpu = { workspace = true, optional = true } diff --git a/crates/eframe/README.md b/crates/eframe/README.md index 55993943d07..15cdf72d39a 100644 --- a/crates/eframe/README.md +++ b/crates/eframe/README.md @@ -28,6 +28,7 @@ You need to either use `edition = "2021"`, or set `resolver = "2"` in the `[work You can opt-in to the using [`egui_wgpu`](https://github.com/emilk/egui/tree/master/crates/egui_wgpu) for rendering by enabling the `wgpu` feature and setting `NativeOptions::renderer` to `Renderer::Wgpu`. +To get copy-paste working on web, you need to compile with `export RUSTFLAGS=--cfg=web_sys_unstable_apis`. ## Alternatives `eframe` is not the only way to write an app using `egui`! You can also try [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad), [`bevy_egui`](https://github.com/mvlabat/bevy_egui), [`egui_sdl2_gl`](https://github.com/ArjunNair/egui_sdl2_gl), and others. @@ -60,4 +61,4 @@ Not all rust crates work when compiled to WASM, but here are some useful crates ## Name -The _frame_ in `eframe` stands both for the frame in which your `egui` app resides and also for "framework" (`frame` is a framework, `egui` is a library). +The _frame_ in `eframe` stands both for the frame in which your `egui` app resides and also for "framework" (`eframe` is a framework, `egui` is a library). diff --git a/crates/eframe/src/epi/icon_data.rs b/crates/eframe/src/epi/icon_data.rs index c62b6a689a1..078b57714f1 100644 --- a/crates/eframe/src/epi/icon_data.rs +++ b/crates/eframe/src/epi/icon_data.rs @@ -14,6 +14,15 @@ pub struct IconData { pub height: u32, } +impl std::fmt::Debug for IconData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("IconData") + .field("width", &self.width) + .field("height", &self.height) + .finish_non_exhaustive() + } +} + impl IconData { /// Convert into [`image::RgbaImage`] /// diff --git a/crates/eframe/src/epi/mod.rs b/crates/eframe/src/epi/mod.rs index 597a9122ca6..db0d4686d7a 100644 --- a/crates/eframe/src/epi/mod.rs +++ b/crates/eframe/src/epi/mod.rs @@ -28,7 +28,7 @@ use static_assertions::assert_not_impl_any; #[cfg(not(target_arch = "wasm32"))] #[cfg(any(feature = "glow", feature = "wgpu"))] -pub use winit::event_loop::EventLoopBuilder; +pub use winit::{event_loop::EventLoopBuilder, window::WindowBuilder}; /// Hook into the building of an event loop before it is run /// @@ -38,6 +38,14 @@ pub use winit::event_loop::EventLoopBuilder; #[cfg(any(feature = "glow", feature = "wgpu"))] pub type EventLoopBuilderHook = Box)>; +/// Hook into the building of a the native window. +/// +/// You can configure any platform specific details required on top of the default configuration +/// done by `eframe`. +#[cfg(not(target_arch = "wasm32"))] +#[cfg(any(feature = "glow", feature = "wgpu"))] +pub type WindowBuilderHook = Box WindowBuilder>; + /// This is how your app is created. /// /// You can use the [`CreationContext`] to setup egui, restore state, setup OpenGL things, etc. @@ -182,13 +190,6 @@ pub trait App { std::time::Duration::from_secs(30) } - /// The size limit of the web app canvas. - /// - /// By default the max size is [`egui::Vec2::INFINITY`], i.e. unlimited. - fn max_size_points(&self) -> egui::Vec2 { - egui::Vec2::INFINITY - } - /// Background color values for the app, e.g. what is sent to `gl.clearColor`. /// /// This is the background of your windows if you don't set a central panel. @@ -208,12 +209,6 @@ pub trait App { // _visuals.window_fill() would also be a natural choice } - /// Controls whether or not the native window position and size will be - /// persisted (only if the "persistence" feature is enabled). - fn persist_native_window(&self) -> bool { - true - } - /// Controls whether or not the egui memory (window positions etc) will be /// persisted (only if the "persistence" feature is enabled). fn persist_egui_memory(&self) -> bool { @@ -313,6 +308,7 @@ pub struct NativeOptions { pub resizable: bool, /// On desktop: make the window transparent. + /// /// You control the transparency with [`App::clear_color()`]. /// You should avoid having a [`egui::CentralPanel`], or make sure its frame is also transparent. pub transparent: bool, @@ -397,6 +393,15 @@ pub struct NativeOptions { #[cfg(any(feature = "glow", feature = "wgpu"))] pub event_loop_builder: Option, + /// Hook into the building of a window. + /// + /// Specify a callback here in case you need to make platform specific changes to the + /// window appearance. + /// + /// Note: A [`NativeOptions`] clone will not include any `window_builder` hook. + #[cfg(any(feature = "glow", feature = "wgpu"))] + pub window_builder: Option, + #[cfg(feature = "glow")] /// Needed for cross compiling for VirtualBox VMSVGA driver with OpenGL ES 2.0 and OpenGL 2.1 which doesn't support SRGB texture. /// See . @@ -455,6 +460,10 @@ pub struct NativeOptions { /// } /// ``` pub app_id: Option, + + /// Controls whether or not the native window position and size will be + /// persisted (only if the "persistence" feature is enabled). + pub persist_window: bool, } #[cfg(not(target_arch = "wasm32"))] @@ -466,6 +475,9 @@ impl Clone for NativeOptions { #[cfg(any(feature = "glow", feature = "wgpu"))] event_loop_builder: None, // Skip any builder callbacks if cloning + #[cfg(any(feature = "glow", feature = "wgpu"))] + window_builder: None, // Skip any builder callbacks if cloning + #[cfg(feature = "wgpu")] wgpu_options: self.wgpu_options.clone(), @@ -520,6 +532,9 @@ impl Default for NativeOptions { #[cfg(any(feature = "glow", feature = "wgpu"))] event_loop_builder: None, + #[cfg(any(feature = "glow", feature = "wgpu"))] + window_builder: None, + #[cfg(feature = "glow")] shader_version: None, @@ -529,6 +544,8 @@ impl Default for NativeOptions { wgpu_options: egui_wgpu::WgpuConfiguration::default(), app_id: None, + + persist_window: true, } } } @@ -566,6 +583,11 @@ pub struct WebOptions { /// Configures wgpu instance/device/adapter/surface creation and renderloop. #[cfg(feature = "wgpu")] pub wgpu_options: egui_wgpu::WgpuConfiguration, + + /// The size limit of the web app canvas. + /// + /// By default the max size is [`egui::Vec2::INFINITY`], i.e. unlimited. + pub max_size_points: egui::Vec2, } #[cfg(target_arch = "wasm32")] @@ -581,6 +603,8 @@ impl Default for WebOptions { #[cfg(feature = "wgpu")] wgpu_options: egui_wgpu::WgpuConfiguration::default(), + + max_size_points: egui::Vec2::INFINITY, } } } @@ -1158,12 +1182,14 @@ impl Storage for DummyStorage { /// Get and deserialize the [RON](https://github.com/ron-rs/ron) stored at the given key. #[cfg(feature = "ron")] pub fn get_value(storage: &dyn Storage, key: &str) -> Option { + crate::profile_function!(key); storage .get_string(key) .and_then(|value| match ron::from_str(&value) { Ok(value) => Some(value), Err(err) => { - log::warn!("Failed to decode RON: {err}"); + // This happens on when we break the format, e.g. when updating egui. + log::debug!("Failed to decode RON: {err}"); None } }) @@ -1172,6 +1198,7 @@ pub fn get_value(storage: &dyn Storage, key: &st /// Serialize the given value as [RON](https://github.com/ron-rs/ron) and store with the given key. #[cfg(feature = "ron")] pub fn set_value(storage: &mut dyn Storage, key: &str, value: &T) { + crate::profile_function!(key); match ron::ser::to_string(value) { Ok(string) => storage.set_string(key, string), Err(err) => log::error!("eframe failed to encode data using ron: {}", err), diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 2972d672740..2c36d428782 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -280,7 +280,7 @@ pub fn run_simple_native( update_fun: U, } - impl App for SimpleApp { + impl App for SimpleApp { fn update(&mut self, ctx: &egui::Context, frame: &mut Frame) { (self.update_fun)(ctx, frame); } @@ -324,7 +324,6 @@ pub type Result = std::result::Result; // --------------------------------------------------------------------------- -#[cfg(not(target_arch = "wasm32"))] mod profiling_scopes { #![allow(unused_macros)] #![allow(unused_imports)] @@ -333,6 +332,7 @@ mod profiling_scopes { macro_rules! profile_function { ($($arg: tt)*) => { #[cfg(feature = "puffin")] + #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. puffin::profile_function!($($arg)*); }; } @@ -342,11 +342,12 @@ mod profiling_scopes { macro_rules! profile_scope { ($($arg: tt)*) => { #[cfg(feature = "puffin")] + #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. puffin::profile_scope!($($arg)*); }; } pub(crate) use profile_scope; } -#[cfg(not(target_arch = "wasm32"))] +#[allow(unused_imports)] pub(crate) use profiling_scopes::*; diff --git a/crates/eframe/src/native/app_icon.rs b/crates/eframe/src/native/app_icon.rs index 20a49a6e27c..61713055c78 100644 --- a/crates/eframe/src/native/app_icon.rs +++ b/crates/eframe/src/native/app_icon.rs @@ -191,6 +191,8 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus { #[cfg(target_os = "macos")] #[allow(unsafe_code)] fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus { + crate::profile_function!(); + use cocoa::{ appkit::{NSApp, NSApplication, NSImage, NSMenu, NSWindow}, base::{id, nil}, @@ -221,12 +223,15 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS png_bytes.len() as u64, ); let app_icon = NSImage::initWithData_(NSImage::alloc(nil), data); + + crate::profile_scope!("setApplicationIconImage_"); app.setApplicationIconImage_(app_icon); } // Change the title in the top bar - for python processes this would be again "python" otherwise. let main_menu = app.mainMenu(); let app_menu: id = msg_send![main_menu.itemAtIndex_(0), submenu]; + crate::profile_scope!("setTitle_"); app_menu.setTitle_(NSString::alloc(nil).init_str(title)); // The title in the Dock apparently can't be changed. diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 1204521d64b..357e3984890 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -75,7 +75,7 @@ pub fn read_window_info( pub fn window_builder( event_loop: &EventLoopWindowTarget, title: &str, - native_options: &epi::NativeOptions, + native_options: &mut epi::NativeOptions, window_settings: Option, ) -> winit::window::WindowBuilder { let epi::NativeOptions { @@ -179,7 +179,10 @@ pub fn window_builder( } } - window_builder + match std::mem::take(&mut native_options.window_builder) { + Some(hook) => hook(window_builder), + None => window_builder, + } } pub fn apply_native_options_to_window( @@ -187,6 +190,7 @@ pub fn apply_native_options_to_window( native_options: &crate::NativeOptions, window_settings: Option, ) { + crate::profile_function!(); use winit::window::WindowLevel; window.set_window_level(if native_options.always_on_top { WindowLevel::AlwaysOnTop @@ -353,6 +357,8 @@ pub struct EpiIntegration { can_drag_window: bool, window_state: WindowState, follow_system_theme: bool, + #[cfg(feature = "persistence")] + persist_window: bool, app_icon_setter: super::app_icon::AppTitleIconSetter, } @@ -421,6 +427,8 @@ impl EpiIntegration { can_drag_window: false, window_state, follow_system_theme: native_options.follow_system_theme, + #[cfg(feature = "persistence")] + persist_window: native_options.persist_window, app_icon_setter, } } @@ -465,6 +473,8 @@ impl EpiIntegration { app: &mut dyn epi::App, event: &winit::event::WindowEvent<'_>, ) -> EventResponse { + crate::profile_function!(); + use winit::event::{ElementState, MouseButton, WindowEvent}; match event { @@ -590,7 +600,7 @@ impl EpiIntegration { crate::profile_function!(); if let Some(window) = _window { - if _app.persist_native_window() { + if self.persist_window { crate::profile_scope!("native_window"); epi::set_value( storage, @@ -622,6 +632,7 @@ const STORAGE_EGUI_MEMORY_KEY: &str = "egui"; const STORAGE_WINDOW_KEY: &str = "window"; pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option { + crate::profile_function!(); #[cfg(feature = "persistence")] { epi::get_value(_storage?, STORAGE_WINDOW_KEY) @@ -631,6 +642,7 @@ pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option) -> Option { + crate::profile_function!(); #[cfg(feature = "persistence")] { epi::get_value(_storage?, STORAGE_EGUI_MEMORY_KEY) diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index eae742cff94..4c44430f819 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + io::Write, path::{Path, PathBuf}, }; @@ -31,6 +32,7 @@ pub struct FileStorage { impl Drop for FileStorage { fn drop(&mut self) { if let Some(join_handle) = self.last_save_join_handle.take() { + crate::profile_scope!("wait_for_save"); join_handle.join().ok(); } } @@ -39,6 +41,7 @@ impl Drop for FileStorage { impl FileStorage { /// Store the state in this .ron file. fn from_ron_filepath(ron_filepath: impl Into) -> Self { + crate::profile_function!(); let ron_filepath: PathBuf = ron_filepath.into(); log::debug!("Loading app state from {:?}…", ron_filepath); Self { @@ -51,6 +54,7 @@ impl FileStorage { /// Find a good place to put the files that the OS likes. pub fn from_app_id(app_id: &str) -> Option { + crate::profile_function!(app_id); if let Some(data_dir) = storage_dir(app_id) { if let Err(err) = std::fs::create_dir_all(&data_dir) { log::warn!( @@ -83,6 +87,7 @@ impl crate::Storage for FileStorage { fn flush(&mut self) { if self.dirty { + crate::profile_function!(); self.dirty = false; let file_path = self.ron_filepath.clone(); @@ -122,10 +127,14 @@ fn save_to_disk(file_path: &PathBuf, kv: &HashMap) { match std::fs::File::create(file_path) { Ok(file) => { + let mut writer = std::io::BufWriter::new(file); let config = Default::default(); - if let Err(err) = ron::ser::to_writer_pretty(file, &kv, config) { - log::warn!("Failed to serialize app state: {err}"); + crate::profile_scope!("ron::serialize"); + if let Err(err) = ron::ser::to_writer_pretty(&mut writer, &kv, config) + .and_then(|_| writer.flush().map_err(|err| err.into())) + { + log::warn!("Failed to serialize app state: {}", err); } else { log::trace!("Persisted to {:?}", file_path); } @@ -142,6 +151,7 @@ fn read_ron(ron_path: impl AsRef) -> Option where T: serde::de::DeserializeOwned, { + crate::profile_function!(); match std::fs::File::open(ron_path) { Ok(file) => { let reader = std::io::BufReader::new(file); diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 986a86d3fcc..cb85a303b59 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -92,6 +92,7 @@ trait WinitApp { fn create_event_loop_builder( native_options: &mut epi::NativeOptions, ) -> EventLoopBuilder { + crate::profile_function!(); let mut event_loop_builder = winit::event_loop::EventLoopBuilder::with_user_event(); if let Some(hook) = std::mem::take(&mut native_options.event_loop_builder) { @@ -101,6 +102,14 @@ fn create_event_loop_builder( event_loop_builder } +fn create_event_loop(native_options: &mut epi::NativeOptions) -> EventLoop { + crate::profile_function!(); + let mut builder = create_event_loop_builder(native_options); + + crate::profile_scope!("EventLoopBuilder::build"); + builder.build() +} + /// Access a thread-local event loop. /// /// We reuse the event-loop so we can support closing and opening an eframe window @@ -117,8 +126,7 @@ fn with_event_loop( // do that as part of the lazy thread local storage initialization and so we instead // create the event loop lazily here let mut event_loop = event_loop.borrow_mut(); - let event_loop = event_loop - .get_or_insert_with(|| create_event_loop_builder(&mut native_options).build()); + let event_loop = event_loop.get_or_insert_with(|| create_event_loop(&mut native_options)); f(event_loop, native_options) }) } @@ -137,6 +145,8 @@ fn run_and_return( let mut returned_result = Ok(()); event_loop.run_return(|event, event_loop, control_flow| { + crate::profile_scope!("winit_event", short_event_description(&event)); + let event_result = match &event { winit::event::Event::LoopDestroyed => { // On Mac, Cmd-Q we get here and then `run_return` doesn't return (despite its name), @@ -150,13 +160,11 @@ fn run_and_return( // Platform-dependent event handlers to workaround a winit bug // See: https://github.com/rust-windowing/winit/issues/987 // See: https://github.com/rust-windowing/winit/issues/1619 - #[cfg(target_os = "windows")] - winit::event::Event::RedrawEventsCleared => { + winit::event::Event::RedrawEventsCleared if cfg!(target_os = "windows") => { next_repaint_time = extremely_far_future(); winit_app.run_ui_and_paint() } - #[cfg(not(target_os = "windows"))] - winit::event::Event::RedrawRequested(_) => { + winit::event::Event::RedrawRequested(_) if !cfg!(target_os = "windows") => { next_repaint_time = extremely_far_future(); winit_app.run_ui_and_paint() } @@ -266,6 +274,8 @@ fn run_and_exit(event_loop: EventLoop, mut winit_app: impl WinitApp + let mut next_repaint_time = Instant::now(); event_loop.run(move |event, event_loop, control_flow| { + crate::profile_scope!("winit_event", short_event_description(&event)); + let event_result = match event { winit::event::Event::LoopDestroyed => { log::debug!("Received Event::LoopDestroyed"); @@ -420,6 +430,8 @@ mod glow_integration { native_options: &epi::NativeOptions, event_loop: &EventLoopWindowTarget, ) -> Result { + crate::profile_function!(); + use glutin::prelude::*; // convert native options to glutin options let hardware_acceleration = match native_options.hardware_acceleration { @@ -460,26 +472,35 @@ mod glow_integration { "trying to create glutin Display with config: {:?}", &config_template_builder ); - // create gl display. this may probably create a window too on most platforms. definitely on `MS windows`. never on android. - let (window, gl_config) = glutin_winit::DisplayBuilder::new() + + // Create GL display. This may probably create a window too on most platforms. Definitely on `MS windows`. Never on Android. + let display_builder = glutin_winit::DisplayBuilder::new() // we might want to expose this option to users in the future. maybe using an env var or using native_options. .with_preference(glutin_winit::ApiPrefence::FallbackEgl) // https://github.com/emilk/egui/issues/2520#issuecomment-1367841150 - .with_window_builder(Some(winit_window_builder.clone())) - .build( - event_loop, - config_template_builder.clone(), - |mut config_iterator| { - let config = config_iterator.next().expect( + .with_window_builder(Some(winit_window_builder.clone())); + + let (window, gl_config) = { + crate::profile_scope!("DisplayBuilder::build"); + + display_builder + .build( + event_loop, + config_template_builder.clone(), + |mut config_iterator| { + let config = config_iterator.next().expect( "failed to find a matching configuration for creating glutin config", ); - log::debug!( - "using the first config from config picker closure. config: {:?}", - &config - ); - config - }, - ) - .map_err(|e| crate::Error::NoGlutinConfigs(config_template_builder.build(), e))?; + log::debug!( + "using the first config from config picker closure. config: {:?}", + &config + ); + config + }, + ) + .map_err(|e| { + crate::Error::NoGlutinConfigs(config_template_builder.build(), e) + })? + }; let gl_display = gl_config.display(); log::debug!( @@ -499,10 +520,15 @@ mod glow_integration { let fallback_context_attributes = glutin::context::ContextAttributesBuilder::new() .with_context_api(glutin::context::ContextApi::Gles(None)) .build(raw_window_handle); - let gl_context = match gl_config - .display() - .create_context(&gl_config, &context_attributes) - { + + let gl_context_result = { + crate::profile_scope!("create_context"); + gl_config + .display() + .create_context(&gl_config, &context_attributes) + }; + + let gl_context = match gl_context_result { Ok(it) => it, Err(err) => { log::warn!("failed to create context using default context attributes {context_attributes:?} due to error: {err}"); @@ -539,6 +565,8 @@ mod glow_integration { /// we presently assume that we will #[allow(unsafe_code)] fn on_resume(&mut self, event_loop: &EventLoopWindowTarget) -> Result<()> { + crate::profile_function!(); + if self.gl_surface.is_some() { log::warn!("on_resume called even thought we already have a surface. early return"); return Ok(()); @@ -656,6 +684,7 @@ mod glow_integration { native_options: epi::NativeOptions, app_creator: epi::AppCreator, ) -> Self { + crate::profile_function!(); Self { repaint_proxy: Arc::new(egui::mutex::Mutex::new(event_loop.create_proxy())), app_name: app_name.to_owned(), @@ -671,7 +700,7 @@ mod glow_integration { event_loop: &EventLoopWindowTarget, storage: Option<&dyn epi::Storage>, title: &str, - native_options: &NativeOptions, + native_options: &mut NativeOptions, ) -> Result<(GlutinWindowContext, glow::Context)> { crate::profile_function!(); @@ -693,6 +722,7 @@ mod glow_integration { } let gl = unsafe { + crate::profile_scope!("glow::Context::from_loader_function"); glow::Context::from_loader_function(|s| { let s = std::ffi::CString::new(s) .expect("failed to construct C string from string for gl proc address"); @@ -705,6 +735,7 @@ mod glow_integration { } fn init_run_state(&mut self, event_loop: &EventLoopWindowTarget) -> Result<()> { + crate::profile_function!(); let storage = epi_integration::create_storage( self.native_options .app_id @@ -716,7 +747,7 @@ mod glow_integration { event_loop, storage.as_deref(), &self.app_name, - &self.native_options, + &mut self.native_options, )?; let gl = Arc::new(gl); @@ -744,7 +775,6 @@ mod glow_integration { let theme = system_theme.unwrap_or(self.native_options.default_theme); integration.egui_ctx.set_visuals(theme.egui_visuals()); - gl_window.window().set_ime_allowed(true); if self.native_options.mouse_passthrough { gl_window.window().set_cursor_hittest(false).unwrap(); } @@ -814,6 +844,7 @@ mod glow_integration { fn save_and_destroy(&mut self) { if let Some(mut running) = self.running.take() { + crate::profile_function!(); running .integration .save(running.app.as_mut(), running.gl_window.window.as_ref()); @@ -823,125 +854,125 @@ mod glow_integration { } fn run_ui_and_paint(&mut self) -> EventResult { - if let Some(running) = &mut self.running { - if running.gl_window.window.is_none() { - return EventResult::Wait; - } + let Some(running) = &mut self.running else { + return EventResult::Wait; + }; - #[cfg(feature = "puffin")] - puffin::GlobalProfiler::lock().new_frame(); - crate::profile_scope!("frame"); + if running.gl_window.window.is_none() { + return EventResult::Wait; + } - let GlowWinitRunning { - gl_window, - gl, - app, - integration, - painter, - } = running; + #[cfg(feature = "puffin")] + puffin::GlobalProfiler::lock().new_frame(); + crate::profile_scope!("frame"); - let window = gl_window.window(); + let GlowWinitRunning { + gl_window, + gl, + app, + integration, + painter, + } = running; - let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); + let window = gl_window.window(); - egui_glow::painter::clear( - gl, - screen_size_in_pixels, - app.clear_color(&integration.egui_ctx.style().visuals), - ); + let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); - let egui::FullOutput { - platform_output, - repaint_after, - textures_delta, - shapes, - } = integration.update(app.as_mut(), window); - - integration.handle_platform_output(window, platform_output); - - let clipped_primitives = { - crate::profile_scope!("tessellate"); - integration.egui_ctx.tessellate(shapes) - }; - - painter.paint_and_update_textures( - screen_size_in_pixels, - integration.egui_ctx.pixels_per_point(), - &clipped_primitives, - &textures_delta, - ); + egui_glow::painter::clear( + gl, + screen_size_in_pixels, + app.clear_color(&integration.egui_ctx.style().visuals), + ); - let screenshot_requested = &mut integration.frame.output.screenshot_requested; + let egui::FullOutput { + platform_output, + repaint_after, + textures_delta, + shapes, + } = integration.update(app.as_mut(), window); - if *screenshot_requested { - *screenshot_requested = false; - let screenshot = painter.read_screen_rgba(screen_size_in_pixels); - integration.frame.screenshot.set(Some(screenshot)); - } + integration.handle_platform_output(window, platform_output); - integration.post_rendering(app.as_mut(), window); + let clipped_primitives = { + crate::profile_scope!("tessellate"); + integration.egui_ctx.tessellate(shapes) + }; - { - crate::profile_scope!("swap_buffers"); - gl_window.swap_buffers().unwrap(); - } + painter.paint_and_update_textures( + screen_size_in_pixels, + integration.egui_ctx.pixels_per_point(), + &clipped_primitives, + &textures_delta, + ); - integration.post_present(window); + let screenshot_requested = &mut integration.frame.output.screenshot_requested; - #[cfg(feature = "__screenshot")] - // give it time to settle: - if integration.egui_ctx.frame_nr() == 2 { - if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") { - assert!( - path.ends_with(".png"), - "Expected EFRAME_SCREENSHOT_TO to end with '.png', got {path:?}" - ); - let screenshot = painter.read_screen_rgba(screen_size_in_pixels); - image::save_buffer( - &path, - screenshot.as_raw(), - screenshot.width() as u32, - screenshot.height() as u32, - image::ColorType::Rgba8, - ) - .unwrap_or_else(|err| { - panic!("Failed to save screenshot to {path:?}: {err}"); - }); - eprintln!("Screenshot saved to {path:?}."); - std::process::exit(0); - } - } + if *screenshot_requested { + *screenshot_requested = false; + let screenshot = painter.read_screen_rgba(screen_size_in_pixels); + integration.frame.screenshot.set(Some(screenshot)); + } - let control_flow = if integration.should_close() { - EventResult::Exit - } else if repaint_after.is_zero() { - EventResult::RepaintNext - } else if let Some(repaint_after_instant) = - std::time::Instant::now().checked_add(repaint_after) - { - // if repaint_after is something huge and can't be added to Instant, - // we will use `ControlFlow::Wait` instead. - // technically, this might lead to some weird corner cases where the user *WANTS* - // winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own - // egui backend impl i guess. - EventResult::RepaintAt(repaint_after_instant) - } else { - EventResult::Wait - }; + integration.post_rendering(app.as_mut(), window); + + { + crate::profile_scope!("swap_buffers"); + gl_window.swap_buffers().unwrap(); + } - integration.maybe_autosave(app.as_mut(), window); + integration.post_present(window); - if window.is_minimized() == Some(true) { - // On Mac, a minimized Window uses up all CPU: - // https://github.com/emilk/egui/issues/325 - crate::profile_scope!("bg_sleep"); - std::thread::sleep(std::time::Duration::from_millis(10)); + #[cfg(feature = "__screenshot")] + // give it time to settle: + if integration.egui_ctx.frame_nr() == 2 { + if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") { + assert!( + path.ends_with(".png"), + "Expected EFRAME_SCREENSHOT_TO to end with '.png', got {path:?}" + ); + let screenshot = painter.read_screen_rgba(screen_size_in_pixels); + image::save_buffer( + &path, + screenshot.as_raw(), + screenshot.width() as u32, + screenshot.height() as u32, + image::ColorType::Rgba8, + ) + .unwrap_or_else(|err| { + panic!("Failed to save screenshot to {path:?}: {err}"); + }); + eprintln!("Screenshot saved to {path:?}."); + std::process::exit(0); } + } - control_flow + let control_flow = if integration.should_close() { + EventResult::Exit + } else if repaint_after.is_zero() { + EventResult::RepaintNext + } else if let Some(repaint_after_instant) = + std::time::Instant::now().checked_add(repaint_after) + { + // if repaint_after is something huge and can't be added to Instant, + // we will use `ControlFlow::Wait` instead. + // technically, this might lead to some weird corner cases where the user *WANTS* + // winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own + // egui backend impl i guess. + EventResult::RepaintAt(repaint_after_instant) } else { EventResult::Wait + }; + + integration.maybe_autosave(app.as_mut(), window); + + if window.is_minimized() == Some(true) { + // On Mac, a minimized Window uses up all CPU: + // https://github.com/emilk/egui/issues/325 + crate::profile_scope!("minimized_sleep"); + std::thread::sleep(std::time::Duration::from_millis(10)); } + + control_flow } fn on_event( @@ -949,6 +980,8 @@ mod glow_integration { event_loop: &EventLoopWindowTarget, event: &winit::event::Event<'_, UserEvent>, ) -> Result { + crate::profile_function!(); + Ok(match event { winit::event::Event::Resumed => { // first resume event. @@ -999,7 +1032,7 @@ mod glow_integration { // Resize with 0 width and height is used by winit to signal a minimize event on Windows. // See: https://github.com/rust-windowing/winit/issues/208 // This solves an issue where the app would panic when minimizing on Windows. - if physical_size.width > 0 && physical_size.height > 0 { + if 0 < physical_size.width && 0 < physical_size.height { running.gl_window.resize(*physical_size); } } @@ -1037,11 +1070,13 @@ mod glow_integration { EventResult::Wait } } + #[cfg(feature = "accesskit")] winit::event::Event::UserEvent(UserEvent::AccessKitActionRequest( accesskit_winit::ActionRequestEvent { request, .. }, )) => { if let Some(running) = &mut self.running { + crate::profile_scope!("on_accesskit_action_request"); running .integration .on_accesskit_action_request(request.clone()); @@ -1070,14 +1105,14 @@ mod glow_integration { run_and_return(event_loop, glow_eframe) }) } else { - let event_loop = create_event_loop_builder(&mut native_options).build(); + let event_loop = create_event_loop(&mut native_options); let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator); run_and_exit(event_loop, glow_eframe); } #[cfg(target_os = "ios")] { - let event_loop = create_event_loop_builder(&mut native_options).build(); + let event_loop = create_event_loop(&mut native_options); let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator); run_and_exit(event_loop, glow_eframe); } @@ -1125,6 +1160,7 @@ mod wgpu_integration { native_options: epi::NativeOptions, app_creator: epi::AppCreator, ) -> Self { + crate::profile_function!(); #[cfg(feature = "__screenshot")] assert!( std::env::var("EFRAME_SCREENSHOT_TO").is_err(), @@ -1146,12 +1182,17 @@ mod wgpu_integration { event_loop: &EventLoopWindowTarget, storage: Option<&dyn epi::Storage>, title: &str, - native_options: &NativeOptions, + native_options: &mut NativeOptions, ) -> std::result::Result { + crate::profile_function!(); + let window_settings = epi_integration::load_window_settings(storage); let window_builder = epi_integration::window_builder(event_loop, title, native_options, window_settings); - let window = window_builder.build(event_loop)?; + let window = { + crate::profile_scope!("WindowBuilder::build"); + window_builder.build(event_loop)? + }; epi_integration::apply_native_options_to_window( &window, native_options, @@ -1167,6 +1208,7 @@ mod wgpu_integration { ) -> std::result::Result<(), egui_wgpu::WgpuError> { self.window = Some(window); if let Some(running) = &mut self.running { + crate::profile_function!(); pollster::block_on(running.painter.set_window(self.window.as_ref()))?; } Ok(()) @@ -1188,6 +1230,8 @@ mod wgpu_integration { storage: Option>, window: winit::window::Window, ) -> std::result::Result<(), egui_wgpu::WgpuError> { + crate::profile_function!(); + #[allow(unsafe_code, unused_mut, unused_unsafe)] let mut painter = egui_wgpu::winit::Painter::new( self.native_options.wgpu_options.clone(), @@ -1222,8 +1266,6 @@ mod wgpu_integration { let theme = system_theme.unwrap_or(self.native_options.default_theme); integration.egui_ctx.set_visuals(theme.egui_visuals()); - window.set_ime_allowed(true); - { let event_loop_proxy = self.repaint_proxy.clone(); integration @@ -1241,7 +1283,7 @@ mod wgpu_integration { let app_creator = std::mem::take(&mut self.app_creator) .expect("Single-use AppCreator has unexpectedly already been taken"); - let mut app = app_creator(&epi::CreationContext { + let cc = epi::CreationContext { egui_ctx: integration.egui_ctx.clone(), integration_info: integration.frame.info().clone(), storage: integration.frame.storage(), @@ -1250,7 +1292,11 @@ mod wgpu_integration { wgpu_render_state, raw_display_handle: window.raw_display_handle(), raw_window_handle: window.raw_window_handle(), - }); + }; + let mut app = { + crate::profile_scope!("user_app_creator"); + app_creator(&cc) + }; if app.warm_up_enabled() { integration.warm_up(app.as_mut(), &window); @@ -1288,6 +1334,7 @@ mod wgpu_integration { fn save_and_destroy(&mut self) { if let Some(mut running) = self.running.take() { + crate::profile_function!(); running .integration .save(running.app.as_mut(), self.window.as_ref()); @@ -1303,76 +1350,76 @@ mod wgpu_integration { } fn run_ui_and_paint(&mut self) -> EventResult { - if let (Some(running), Some(window)) = (&mut self.running, &self.window) { - #[cfg(feature = "puffin")] - puffin::GlobalProfiler::lock().new_frame(); - crate::profile_scope!("frame"); - - let WgpuWinitRunning { - app, - integration, - painter, - } = running; - - let egui::FullOutput { - platform_output, - repaint_after, - textures_delta, - shapes, - } = integration.update(app.as_mut(), window); - - integration.handle_platform_output(window, platform_output); - - let clipped_primitives = { - crate::profile_scope!("tessellate"); - integration.egui_ctx.tessellate(shapes) - }; - - let screenshot_requested = &mut integration.frame.output.screenshot_requested; - - let screenshot = painter.paint_and_update_textures( - integration.egui_ctx.pixels_per_point(), - app.clear_color(&integration.egui_ctx.style().visuals), - &clipped_primitives, - &textures_delta, - *screenshot_requested, - ); - *screenshot_requested = false; - integration.frame.screenshot.set(screenshot); + let (Some(running), Some(window)) = (&mut self.running, &self.window) else { + return EventResult::Wait; + }; - integration.post_rendering(app.as_mut(), window); - integration.post_present(window); + #[cfg(feature = "puffin")] + puffin::GlobalProfiler::lock().new_frame(); + crate::profile_scope!("frame"); - let control_flow = if integration.should_close() { - EventResult::Exit - } else if repaint_after.is_zero() { - EventResult::RepaintNext - } else if let Some(repaint_after_instant) = - std::time::Instant::now().checked_add(repaint_after) - { - // if repaint_after is something huge and can't be added to Instant, - // we will use `ControlFlow::Wait` instead. - // technically, this might lead to some weird corner cases where the user *WANTS* - // winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own - // egui backend impl i guess. - EventResult::RepaintAt(repaint_after_instant) - } else { - EventResult::Wait - }; + let WgpuWinitRunning { + app, + integration, + painter, + } = running; - integration.maybe_autosave(app.as_mut(), window); + let egui::FullOutput { + platform_output, + repaint_after, + textures_delta, + shapes, + } = integration.update(app.as_mut(), window); - if window.is_minimized() == Some(true) { - // On Mac, a minimized Window uses up all CPU: - // https://github.com/emilk/egui/issues/325 - crate::profile_scope!("bg_sleep"); - std::thread::sleep(std::time::Duration::from_millis(10)); - } + integration.handle_platform_output(window, platform_output); + + let clipped_primitives = { + crate::profile_scope!("tessellate"); + integration.egui_ctx.tessellate(shapes) + }; + + let screenshot_requested = &mut integration.frame.output.screenshot_requested; + + let screenshot = painter.paint_and_update_textures( + integration.egui_ctx.pixels_per_point(), + app.clear_color(&integration.egui_ctx.style().visuals), + &clipped_primitives, + &textures_delta, + *screenshot_requested, + ); + *screenshot_requested = false; + integration.frame.screenshot.set(screenshot); - control_flow + integration.post_rendering(app.as_mut(), window); + integration.post_present(window); + + let control_flow = if integration.should_close() { + EventResult::Exit + } else if repaint_after.is_zero() { + EventResult::RepaintNext + } else if let Some(repaint_after_instant) = + std::time::Instant::now().checked_add(repaint_after) + { + // if repaint_after is something huge and can't be added to Instant, + // we will use `ControlFlow::Wait` instead. + // technically, this might lead to some weird corner cases where the user *WANTS* + // winit to use `WaitUntil(MAX_INSTANT)` explicitly. they can roll their own + // egui backend impl i guess. + EventResult::RepaintAt(repaint_after_instant) } else { EventResult::Wait + }; + + integration.maybe_autosave(app.as_mut(), window); + + if window.is_minimized() == Some(true) { + // On Mac, a minimized Window uses up all CPU: + // https://github.com/emilk/egui/issues/325 + crate::profile_scope!("minimized_sleep"); + std::thread::sleep(std::time::Duration::from_millis(10)); } + + control_flow } fn on_event( @@ -1380,6 +1427,8 @@ mod wgpu_integration { event_loop: &EventLoopWindowTarget, event: &winit::event::Event<'_, UserEvent>, ) -> Result { + crate::profile_function!(); + Ok(match event { winit::event::Event::Resumed => { if let Some(running) = &self.running { @@ -1388,7 +1437,7 @@ mod wgpu_integration { event_loop, running.integration.frame.storage(), &self.app_name, - &self.native_options, + &mut self.native_options, )?; self.set_window(window)?; } @@ -1403,7 +1452,7 @@ mod wgpu_integration { event_loop, storage.as_deref(), &self.app_name, - &self.native_options, + &mut self.native_options, )?; self.init_run_state(event_loop, storage, window)?; } @@ -1442,7 +1491,7 @@ mod wgpu_integration { // Resize with 0 width and height is used by winit to signal a minimize event on Windows. // See: https://github.com/rust-windowing/winit/issues/208 // This solves an issue where the app would panic when minimizing on Windows. - if physical_size.width > 0 && physical_size.height > 0 { + if 0 < physical_size.width && 0 < physical_size.height { running.painter.on_window_resized( physical_size.width, physical_size.height, @@ -1517,14 +1566,14 @@ mod wgpu_integration { run_and_return(event_loop, wgpu_eframe) }) } else { - let event_loop = create_event_loop_builder(&mut native_options).build(); + let event_loop = create_event_loop(&mut native_options); let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator); run_and_exit(event_loop, wgpu_eframe); } #[cfg(target_os = "ios")] { - let event_loop = create_event_loop_builder(&mut native_options).build(); + let event_loop = create_event_loop(&mut native_options); let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator); run_and_exit(event_loop, wgpu_eframe); } @@ -1551,3 +1600,67 @@ fn system_theme(window: &winit::window::Window, options: &NativeOptions) -> Opti fn extremely_far_future() -> std::time::Instant { std::time::Instant::now() + std::time::Duration::from_secs(10_000_000_000) } + +// For the puffin profiler! +#[allow(dead_code)] // Only used for profiling +fn short_event_description(event: &winit::event::Event<'_, UserEvent>) -> &'static str { + use winit::event::{DeviceEvent, Event, StartCause, WindowEvent}; + + match event { + Event::Suspended => "Event::Suspended", + Event::Resumed => "Event::Resumed", + Event::MainEventsCleared => "Event::MainEventsCleared", + Event::RedrawRequested(_) => "Event::RedrawRequested", + Event::RedrawEventsCleared => "Event::RedrawEventsCleared", + Event::LoopDestroyed => "Event::LoopDestroyed", + Event::UserEvent(user_event) => match user_event { + UserEvent::RequestRepaint { .. } => "UserEvent::RequestRepaint", + #[cfg(feature = "accesskit")] + UserEvent::AccessKitActionRequest(_) => "UserEvent::AccessKitActionRequest", + }, + Event::DeviceEvent { event, .. } => match event { + DeviceEvent::Added { .. } => "DeviceEvent::Added", + DeviceEvent::Removed { .. } => "DeviceEvent::Removed", + DeviceEvent::MouseMotion { .. } => "DeviceEvent::MouseMotion", + DeviceEvent::MouseWheel { .. } => "DeviceEvent::MouseWheel", + DeviceEvent::Motion { .. } => "DeviceEvent::Motion", + DeviceEvent::Button { .. } => "DeviceEvent::Button", + DeviceEvent::Key { .. } => "DeviceEvent::Key", + DeviceEvent::Text { .. } => "DeviceEvent::Text", + }, + Event::NewEvents(start_cause) => match start_cause { + StartCause::ResumeTimeReached { .. } => "NewEvents::ResumeTimeReached", + StartCause::WaitCancelled { .. } => "NewEvents::WaitCancelled", + StartCause::Poll => "NewEvents::Poll", + StartCause::Init => "NewEvents::Init", + }, + Event::WindowEvent { event, .. } => match event { + WindowEvent::Resized { .. } => "WindowEvent::Resized", + WindowEvent::Moved { .. } => "WindowEvent::Moved", + WindowEvent::CloseRequested { .. } => "WindowEvent::CloseRequested", + WindowEvent::Destroyed { .. } => "WindowEvent::Destroyed", + WindowEvent::DroppedFile { .. } => "WindowEvent::DroppedFile", + WindowEvent::HoveredFile { .. } => "WindowEvent::HoveredFile", + WindowEvent::HoveredFileCancelled { .. } => "WindowEvent::HoveredFileCancelled", + WindowEvent::ReceivedCharacter { .. } => "WindowEvent::ReceivedCharacter", + WindowEvent::Focused { .. } => "WindowEvent::Focused", + WindowEvent::KeyboardInput { .. } => "WindowEvent::KeyboardInput", + WindowEvent::ModifiersChanged { .. } => "WindowEvent::ModifiersChanged", + WindowEvent::Ime { .. } => "WindowEvent::Ime", + WindowEvent::CursorMoved { .. } => "WindowEvent::CursorMoved", + WindowEvent::CursorEntered { .. } => "WindowEvent::CursorEntered", + WindowEvent::CursorLeft { .. } => "WindowEvent::CursorLeft", + WindowEvent::MouseWheel { .. } => "WindowEvent::MouseWheel", + WindowEvent::MouseInput { .. } => "WindowEvent::MouseInput", + WindowEvent::TouchpadMagnify { .. } => "WindowEvent::TouchpadMagnify", + WindowEvent::SmartMagnify { .. } => "WindowEvent::SmartMagnify", + WindowEvent::TouchpadRotate { .. } => "WindowEvent::TouchpadRotate", + WindowEvent::TouchpadPressure { .. } => "WindowEvent::TouchpadPressure", + WindowEvent::AxisMotion { .. } => "WindowEvent::AxisMotion", + WindowEvent::Touch { .. } => "WindowEvent::Touch", + WindowEvent::ScaleFactorChanged { .. } => "WindowEvent::ScaleFactorChanged", + WindowEvent::ThemeChanged { .. } => "WindowEvent::ThemeChanged", + WindowEvent::Occluded { .. } => "WindowEvent::Occluded", + }, + } +} diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 6f4579b66ff..d8646df2bd5 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -6,6 +6,7 @@ use crate::{epi, App}; use super::{now_sec, web_painter::WebPainter, NeedRepaint}; pub struct AppRunner { + web_options: crate::WebOptions, pub(crate) frame: epi::Frame, egui_ctx: egui::Context, painter: super::ActiveWebPainter, @@ -98,6 +99,7 @@ impl AppRunner { } let mut runner = Self { + web_options, frame, egui_ctx, painter, @@ -174,7 +176,7 @@ impl AppRunner { pub fn logic(&mut self) -> (std::time::Duration, Vec) { let frame_start = now_sec(); - super::resize_canvas_to_screen_size(self.canvas_id(), self.app.max_size_points()); + 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()); let raw_input = self.input.new_frame(canvas_size); diff --git a/crates/eframe/src/web/text_agent.rs b/crates/eframe/src/web/text_agent.rs index 1688163b3fb..d4f3b5ca3c6 100644 --- a/crates/eframe/src/web/text_agent.rs +++ b/crates/eframe/src/web/text_agent.rs @@ -101,14 +101,13 @@ pub fn install_text_agent(runner_ref: &WebRunner) -> Result<(), JsValue> { // When input lost focus, focus on it again. // It is useful when user click somewhere outside canvas. + let input_refocus = input.clone(); runner_ref.add_event_listener(&input, "focusout", move |_event: web_sys::MouseEvent, _| { // Delay 10 ms, and focus again. - let func = js_sys::Function::new_no_args(&format!( - "document.getElementById('{AGENT_ID}').focus()" - )); - window - .set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10) - .unwrap(); + let input_refocus = input_refocus.clone(); + call_after_delay(std::time::Duration::from_millis(10), move || { + input_refocus.focus().ok(); + }); })?; body.append_child(&input)?; diff --git a/crates/egui-wgpu/CHANGELOG.md b/crates/egui-wgpu/CHANGELOG.md index d7cbee4d11e..6d8ba691eef 100644 --- a/crates/egui-wgpu/CHANGELOG.md +++ b/crates/egui-wgpu/CHANGELOG.md @@ -6,6 +6,13 @@ This file is updated upon each release. Changes since the last release can be found by running the `scripts/generate_changelog.py` script. +## 0.23.0 - 2023-09-27 +* Update to `wgpu` 0.17.0 [#3170](https://github.com/emilk/egui/pull/3170) (thanks [@Aaron1011](https://github.com/Aaron1011)!) +* Improved wgpu callbacks [#3253](https://github.com/emilk/egui/pull/3253) (thanks [@Wumpf](https://github.com/Wumpf)!) +* Fix depth texture init with multisampling [#3207](https://github.com/emilk/egui/pull/3207) (thanks [@mauliu](https://github.com/mauliu)!) +* Fix panic on wgpu GL backend due to new screenshot capability [#3078](https://github.com/emilk/egui/pull/3078) (thanks [@amfaber](https://github.com/amfaber)!) + + ## 0.22.0 - 2023-05-23 * Update to wgpu 0.16 [#2884](https://github.com/emilk/egui/pull/2884) (thanks [@niklaskorz](https://github.com/niklaskorz)!) * Device configuration is now dependent on adapter [#2951](https://github.com/emilk/egui/pull/2951) (thanks [@Wumpf](https://github.com/Wumpf)!) diff --git a/crates/egui-wgpu/Cargo.toml b/crates/egui-wgpu/Cargo.toml index 22d54cb0846..06bc638b3fb 100644 --- a/crates/egui-wgpu/Cargo.toml +++ b/crates/egui-wgpu/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "egui-wgpu" -version = "0.22.0" +version = "0.23.0" description = "Bindings for using egui natively using the wgpu library" authors = [ "Nils Hasenbanck ", @@ -36,7 +36,7 @@ winit = ["dep:winit"] [dependencies] -epaint = { version = "0.22.0", path = "../epaint", default-features = false, features = [ +epaint = { version = "0.23.0", path = "../epaint", default-features = false, features = [ "bytemuck", ] } diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 7d0a3506cf4..a46e185e883 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -123,6 +123,16 @@ pub struct WgpuConfiguration { pub on_surface_error: Arc SurfaceErrorAction>, } +impl std::fmt::Debug for WgpuConfiguration { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WgpuConfiguration") + .field("supported_backends", &self.supported_backends) + .field("present_mode", &self.present_mode) + .field("power_preference", &self.power_preference) + .finish_non_exhaustive() + } +} + impl Default for WgpuConfiguration { fn default() -> Self { Self { @@ -203,22 +213,30 @@ pub fn depth_format_from_bits(depth_buffer: u8, stencil_buffer: u8) -> Option { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] - puffin::profile_function!($($arg)*); - }; -} -pub(crate) use profile_function; - -/// Profiling macro for feature "puffin" -macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] - puffin::profile_scope!($($arg)*); - }; +mod profiling_scopes { + #![allow(unused_macros)] + #![allow(unused_imports)] + + /// Profiling macro for feature "puffin" + macro_rules! profile_function { + ($($arg: tt)*) => { + #[cfg(feature = "puffin")] + #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. + puffin::profile_function!($($arg)*); + }; + } + pub(crate) use profile_function; + + /// Profiling macro for feature "puffin" + macro_rules! profile_scope { + ($($arg: tt)*) => { + #[cfg(feature = "puffin")] + #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. + puffin::profile_scope!($($arg)*); + }; + } + pub(crate) use profile_scope; } -pub(crate) use profile_scope; + +#[allow(unused_imports)] +pub(crate) use profiling_scopes::*; diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index c97fba88230..62d9698332a 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -445,6 +445,13 @@ impl Renderer { needs_reset = true; + let info = PaintCallbackInfo { + viewport: callback.rect, + clip_rect: *clip_rect, + pixels_per_point, + screen_size_px: size_in_pixels, + }; + { // We're setting a default viewport for the render pass as a // courtesy for the user, so that they don't have to think about @@ -455,29 +462,19 @@ impl Renderer { // viewport during the paint callback, effectively overriding this // one. - let min = (callback.rect.min.to_vec2() * pixels_per_point).round(); - let max = (callback.rect.max.to_vec2() * pixels_per_point).round(); + let viewport_px = info.viewport_in_pixels(); render_pass.set_viewport( - min.x, - min.y, - max.x - min.x, - max.y - min.y, + viewport_px.left_px, + viewport_px.top_px, + viewport_px.width_px, + viewport_px.height_px, 0.0, 1.0, ); } - cbfn.0.paint( - PaintCallbackInfo { - viewport: callback.rect, - clip_rect: *clip_rect, - pixels_per_point, - screen_size_px: size_in_pixels, - }, - render_pass, - &self.callback_resources, - ); + cbfn.0.paint(info, render_pass, &self.callback_resources); } } } @@ -605,9 +602,8 @@ impl Renderer { /// Get the WGPU texture and bind group associated to a texture that has been allocated by egui. /// - /// This could be used by custom paint hooks to render images that have been added through with - /// [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html) - /// or [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture). + /// This could be used by custom paint hooks to render images that have been added through + /// [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture). pub fn texture( &self, id: &epaint::TextureId, diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 00500af111d..c01336f356e 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -137,6 +137,7 @@ impl Painter { render_state: &RenderState, present_mode: wgpu::PresentMode, ) { + crate::profile_function!(); let usage = if surface_state.supports_screenshot { wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST } else { @@ -181,6 +182,7 @@ impl Painter { &mut self, window: Option<&winit::window::Window>, ) -> Result<(), crate::WgpuError> { + crate::profile_function!(); match window { Some(window) => { let surface = unsafe { self.instance.create_surface(&window)? }; @@ -254,6 +256,7 @@ impl Painter { width_in_pixels: u32, height_in_pixels: u32, ) { + crate::profile_function!(); let render_state = self.render_state.as_ref().unwrap(); let surface_state = self.surface_state.as_mut().unwrap(); @@ -309,6 +312,7 @@ impl Painter { } pub fn on_window_resized(&mut self, width_in_pixels: u32, height_in_pixels: u32) { + crate::profile_function!(); if self.surface_state.is_some() { self.resize_and_generate_depth_texture_view_and_msaa_view( width_in_pixels, diff --git a/crates/egui-winit/CHANGELOG.md b/crates/egui-winit/CHANGELOG.md index d181b99b454..ecb1de76853 100644 --- a/crates/egui-winit/CHANGELOG.md +++ b/crates/egui-winit/CHANGELOG.md @@ -5,6 +5,13 @@ This file is updated upon each release. Changes since the last release can be found by running the `scripts/generate_changelog.py` script. +## 0.23.0 - 2023-09-27 +* Only show on-screen-keyboard and IME when editing text [#3362](https://github.com/emilk/egui/pull/3362) (thanks [@Barugon](https://github.com/Barugon)!) +* Replace `instant` with `web_time` [#3296](https://github.com/emilk/egui/pull/3296) +* Allow users to opt-out of default `winit` features [#3228](https://github.com/emilk/egui/pull/3228) +* Recognize numpad enter/plus/minus [#3285](https://github.com/emilk/egui/pull/3285) + + ## 0.22.0 - 2023-05-23 * Only use `wasm-bindgen` feature for `instant` when building for wasm32 [#2808](https://github.com/emilk/egui/pull/2808) (thanks [@gferon](https://github.com/gferon)!) * Fix unsafe API of `Clipboard::new` [#2765](https://github.com/emilk/egui/pull/2765) (thanks [@dhardy](https://github.com/dhardy)!) diff --git a/crates/egui-winit/Cargo.toml b/crates/egui-winit/Cargo.toml index 5f670007cbd..0c8434850c6 100644 --- a/crates/egui-winit/Cargo.toml +++ b/crates/egui-winit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "egui-winit" -version = "0.22.0" +version = "0.23.0" authors = ["Emil Ernerfeldt "] description = "Bindings for using egui with winit" edition = "2021" @@ -23,6 +23,15 @@ default = ["clipboard", "links", "wayland", "winit/default", "x11"] ## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/). accesskit = ["accesskit_winit", "egui/accesskit"] +# Allow crates to choose an android-activity backend via Winit +# - It's important that most applications should not have to depend on android-activity directly, and can +# rely on Winit to pull in a suitable version (unlike most Rust crates, any version conflicts won't link) +# - It's also important that we don't impose an android-activity backend by taking this choice away from applications. +## Enable the `game-activity` backend via Winit on Android +android-game-activity = ["winit/android-game-activity"] +## Enable the `native-activity` backend via Winit on Android +android-native-activity = ["winit/android-native-activity"] + ## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`egui::epaint::Vertex`], [`egui::Vec2`] etc to `&[u8]`. bytemuck = ["egui/bytemuck"] @@ -45,18 +54,8 @@ wayland = ["winit/wayland"] ## Enables compiling for x11. x11 = ["winit/x11"] -# Allow crates to choose an android-activity backend via Winit -# - It's important that most applications should not have to depend on android-activity directly, and can -# rely on Winit to pull in a suitable version (unlike most Rust crates, any version conflicts won't link) -# - It's also important that we don't impose an android-activity backend by taking this choice away from applications. - -## Enable the `native-activity` backend via Winit on Android -android-native-activity = ["winit/android-native-activity"] -## Enable the `game-activity` backend via Winit on Android -android-game-activity = ["winit/android-game-activity"] - [dependencies] -egui = { version = "0.22.0", path = "../egui", default-features = false, features = [ +egui = { version = "0.23.0", path = "../egui", default-features = false, features = [ "log", ] } log = { version = "0.4", features = ["std"] } @@ -67,7 +66,7 @@ winit = { version = "0.28", default-features = false } #! ### Optional dependencies # feature accesskit -accesskit_winit = { version = "0.14.0", optional = true } +accesskit_winit = { version = "0.15.0", optional = true } ## Enable this when generating docs. document-features = { version = "0.2", optional = true } diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 8da31e2c6b5..f9feae9a472 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -80,6 +80,8 @@ pub struct State { #[cfg(feature = "accesskit")] accesskit: Option, + + allow_ime: bool, } impl State { @@ -107,6 +109,8 @@ impl State { #[cfg(feature = "accesskit")] accesskit: None, + + allow_ime: false, } } @@ -190,6 +194,8 @@ impl State { egui_ctx: &egui::Context, event: &winit::event::WindowEvent<'_>, ) -> EventResponse { + crate::profile_function!(); + use winit::event::WindowEvent; match event { WindowEvent::ScaleFactorChanged { scale_factor, .. } => { @@ -285,7 +291,7 @@ impl State { .events .push(egui::Event::CompositionEnd(text.clone())); } - winit::event::Ime::Preedit(text, ..) => { + winit::event::Ime::Preedit(text, Some(_)) => { if !self.input_method_editor_started { self.input_method_editor_started = true; self.egui_input.events.push(egui::Event::CompositionStart); @@ -294,6 +300,7 @@ impl State { .events .push(egui::Event::CompositionUpdate(text.clone())); } + winit::event::Ime::Preedit(_, None) => {} }; EventResponse { @@ -661,6 +668,12 @@ impl State { self.clipboard.set(copied_text); } + let allow_ime = text_cursor_pos.is_some(); + if self.allow_ime != allow_ime { + self.allow_ime = allow_ime; + window.set_ime_allowed(allow_ime); + } + if let Some(egui::Pos2 { x, y }) = text_cursor_pos { window.set_ime_position(winit::dpi::LogicalPosition { x, y }); } @@ -894,26 +907,30 @@ fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option { - #[cfg(feature = "puffin")] - puffin::profile_function!($($arg)*); - }; -} +mod profiling_scopes { + #![allow(unused_macros)] + #![allow(unused_imports)] -#[allow(unused_imports)] -pub(crate) use profile_function; - -/// Profiling macro for feature "puffin" -#[allow(unused_macros)] -macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - puffin::profile_scope!($($arg)*); - }; + /// Profiling macro for feature "puffin" + macro_rules! profile_function { + ($($arg: tt)*) => { + #[cfg(feature = "puffin")] + #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. + puffin::profile_function!($($arg)*); + }; + } + pub(crate) use profile_function; + + /// Profiling macro for feature "puffin" + macro_rules! profile_scope { + ($($arg: tt)*) => { + #[cfg(feature = "puffin")] + #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. + puffin::profile_scope!($($arg)*); + }; + } + pub(crate) use profile_scope; } #[allow(unused_imports)] -pub(crate) use profile_scope; +pub(crate) use profiling_scopes::*; diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index 7ce890f4bab..145f1d63cf9 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "egui" -version = "0.22.0" +version = "0.23.0" authors = ["Emil Ernerfeldt "] description = "An easy-to-use immediate mode GUI that runs on both web and native" edition = "2021" @@ -22,9 +22,18 @@ all-features = true [features] default = ["default_fonts"] +## Exposes detailed accessibility implementation required by platform +## accessibility APIs. Also requires support in the egui integration. +accesskit = ["dep:accesskit"] + ## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`epaint::Vertex`], [`emath::Vec2`] etc to `&[u8]`. bytemuck = ["epaint/bytemuck"] +## Show a debug-ui on hover including the stacktrace to the hovered item. +## This is very useful in finding the code that creates a part of the UI. +## Does not work on web. +callstack = ["dep:backtrace"] + ## [`cint`](https://docs.rs/cint) enables interoperability with other color libraries. cint = ["epaint/cint"] @@ -67,7 +76,7 @@ unity = ["epaint/unity"] [dependencies] -epaint = { version = "0.22.0", path = "../epaint", default-features = false } +epaint = { version = "0.23.0", path = "../epaint", default-features = false } ahash = { version = "0.8.1", default-features = false, features = [ "no-rng", # we don't need DOS-protection, so we let users opt-in to it instead @@ -76,9 +85,9 @@ ahash = { version = "0.8.1", default-features = false, features = [ nohash-hasher = "0.2" #! ### Optional dependencies -## Exposes detailed accessibility implementation required by platform -## accessibility APIs. Also requires support in the egui integration. -accesskit = { version = "0.11", optional = true } +accesskit = { version = "0.12", optional = true } + +backtrace = { version = "0.3", optional = true } ## Enable this when generating docs. document-features = { version = "0.2", optional = true } diff --git a/crates/egui/assets/ferris.png b/crates/egui/assets/ferris.png new file mode 100644 index 00000000000..8741baa19d0 Binary files /dev/null and b/crates/egui/assets/ferris.png differ diff --git a/crates/egui/src/animation_manager.rs b/crates/egui/src/animation_manager.rs index be181507962..b7b7d18beff 100644 --- a/crates/egui/src/animation_manager.rs +++ b/crates/egui/src/animation_manager.rs @@ -25,7 +25,7 @@ struct ValueAnim { } impl AnimationManager { - /// See `Context::animate_bool` for documentation + /// See [`crate::Context::animate_bool`] for documentation pub fn animate_bool( &mut self, input: &InputState, diff --git a/crates/egui/src/callstack.rs b/crates/egui/src/callstack.rs new file mode 100644 index 00000000000..fac5ac5936e --- /dev/null +++ b/crates/egui/src/callstack.rs @@ -0,0 +1,186 @@ +#[derive(Clone)] +struct Frame { + /// `_main` is usually as the deepest depth. + depth: usize, + name: String, + file_and_line: String, +} + +/// Capture a callstack, skipping the frames that are not interesting. +/// +/// In particular: slips everything before `egui::Context::run`, +/// and skipping all frames in the `egui::` namespace. +pub fn capture() -> String { + let mut frames = vec![]; + let mut depth = 0; + + backtrace::trace(|frame| { + // Resolve this instruction pointer to a symbol name + backtrace::resolve_frame(frame, |symbol| { + let mut file_and_line = symbol.filename().map(shorten_source_file_path); + + if let Some(file_and_line) = &mut file_and_line { + if let Some(line_nr) = symbol.lineno() { + file_and_line.push_str(&format!(":{line_nr}")); + } + } + let file_and_line = file_and_line.unwrap_or_default(); + + let name = symbol + .name() + .map(|name| name.to_string()) + .unwrap_or_default(); + + frames.push(Frame { + depth, + name, + file_and_line, + }); + }); + + depth += 1; // note: we can resolve multiple symbols on the same frame. + + true // keep going to the next frame + }); + + if frames.is_empty() { + return Default::default(); + } + + // Inclusive: + let mut min_depth = 0; + let mut max_depth = frames.len() - 1; + + for frame in &frames { + if frame.name.starts_with("egui::callstack::capture") { + min_depth = frame.depth + 1; + } + if frame.name.starts_with("egui::context::Context::run") { + max_depth = frame.depth; + } + } + + // Remove frames that are uninteresting: + frames.retain(|frame| { + // Keep some special frames to give the user a sense of chronology: + if frame.name == "main" + || frame.name == "_main" + || frame.name.starts_with("egui::context::Context::run") + || frame.name.starts_with("eframe::run_native") + { + return true; + } + + if frame.depth < min_depth || max_depth < frame.depth { + return false; + } + + // Remove stuff that isn't user calls: + let skip_prefixes = [ + // "backtrace::", // not needed, since we cut at at egui::callstack::capture + "egui::", + "", + "egui_plot::", + "egui_extras::", + "core::ptr::drop_in_place::", + "eframe::", + "core::ops::function::FnOnce::call_once", + " as core::ops::function::FnOnce>::call_once", + ]; + for prefix in skip_prefixes { + if frame.name.starts_with(prefix) { + return false; + } + } + true + }); + + frames.reverse(); // main on top, i.e. chronological order. Same as Python. + + let mut deepest_depth = 0; + let mut widest_file_line = 0; + for frame in &frames { + deepest_depth = frame.depth.max(deepest_depth); + widest_file_line = frame.file_and_line.len().max(widest_file_line); + } + + let widest_depth = deepest_depth.to_string().len(); + + let mut formatted = String::new(); + + if !frames.is_empty() { + let mut last_depth = frames[0].depth; + + for frame in &frames { + let Frame { + depth, + name, + file_and_line, + } = frame; + + if frame.depth + 1 < last_depth || last_depth + 1 < frame.depth { + // Show that some frames were elided + formatted.push_str(&format!("{:widest_depth$} …\n", "")); + } + + formatted.push_str(&format!( + "{depth:widest_depth$}: {file_and_line:widest_file_line$} {name}\n" + )); + + last_depth = frame.depth; + } + } + + formatted +} + +/// Shorten a path to a Rust source file from a callstack. +/// +/// Example input: +/// * `/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs` +/// * `crates/rerun/src/main.rs` +/// * `/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs` +fn shorten_source_file_path(path: &std::path::Path) -> String { + // Look for `src` and strip everything up to it. + + let components: Vec<_> = path.iter().map(|path| path.to_string_lossy()).collect(); + + let mut src_idx = None; + for (i, c) in components.iter().enumerate() { + if c == "src" { + src_idx = Some(i); + } + } + + // Look for the last `src`: + if let Some(src_idx) = src_idx { + // Before `src` comes the name of the crate - let's include that: + let first_index = src_idx.saturating_sub(1); + + let mut output = components[first_index].to_string(); + for component in &components[first_index + 1..] { + output.push('/'); + output.push_str(component); + } + output + } else { + // No `src` directory found - weird! + path.display().to_string() + } +} + +#[test] +fn test_shorten_path() { + for (before, after) in [ + ("/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs", "tokio-1.24.1/src/runtime/runtime.rs"), + ("crates/rerun/src/main.rs", "rerun/src/main.rs"), + ("/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs", "core/src/ops/function.rs"), + ("/weird/path/file.rs", "/weird/path/file.rs"), + ] + { + use std::str::FromStr as _; + let before = std::path::PathBuf::from_str(before).unwrap(); + assert_eq!(shorten_source_file_path(&before), after); + } +} diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index f493c20cded..4f603717ec1 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -65,12 +65,12 @@ pub struct Area { interactable: bool, enabled: bool, constrain: bool, + constrain_rect: Option, order: Order, default_pos: Option, pivot: Align2, anchor: Option<(Align2, Vec2)>, new_pos: Option, - drag_bounds: Option, } impl Area { @@ -80,13 +80,13 @@ impl Area { movable: true, interactable: true, constrain: false, + constrain_rect: None, enabled: true, order: Order::Middle, default_pos: None, new_pos: None, pivot: Align2::LEFT_TOP, anchor: None, - drag_bounds: None, } } @@ -155,6 +155,21 @@ impl Area { self } + /// Constrain the movement of the window to the given rectangle. + /// + /// For instance: `.constrain_to(ctx.screen_rect())`. + pub fn constrain_to(mut self, constrain_rect: Rect) -> Self { + self.constrain = true; + self.constrain_rect = Some(constrain_rect); + self + } + + #[deprecated = "Use `constrain_to` instead"] + pub fn drag_bounds(mut self, constrain_rect: Rect) -> Self { + self.constrain_rect = Some(constrain_rect); + self + } + /// Where the "root" of the area is. /// /// For instance, if you set this to [`Align2::RIGHT_TOP`] @@ -189,12 +204,6 @@ impl Area { self.movable(false) } - /// Constrain the area up to which the window can be dragged. - pub fn drag_bounds(mut self, bounds: Rect) -> Self { - self.drag_bounds = Some(bounds); - self - } - pub(crate) fn get_pivot(&self) -> Align2 { if let Some((pivot, _)) = self.anchor { pivot @@ -209,7 +218,8 @@ pub(crate) struct Prepared { state: State, move_response: Response, enabled: bool, - drag_bounds: Option, + constrain: bool, + constrain_rect: Option, /// We always make windows invisible the first frame to hide "first-frame-jitters". /// @@ -243,8 +253,8 @@ impl Area { new_pos, pivot, anchor, - drag_bounds, constrain, + constrain_rect, } = self; let layer_id = LayerId::new(order, id); @@ -271,7 +281,7 @@ impl Area { } // interact right away to prevent frame-delay - let move_response = { + let mut move_response = { let interact_id = layer_id.id.with("move"); let sense = if movable { Sense::click_and_drag() @@ -291,16 +301,8 @@ impl Area { enabled, ); - // Important check - don't try to move e.g. a combobox popup! - if movable { - if move_response.dragged() { - state.pivot_pos += ctx.input(|i| i.pointer.delta()); - } - - state.set_left_top_pos( - ctx.constrain_window_rect_to_area(state.rect(), drag_bounds) - .min, - ); + if movable && move_response.dragged() { + state.pivot_pos += ctx.input(|i| i.pointer.delta()); } if (move_response.dragged() || move_response.clicked()) @@ -314,21 +316,25 @@ impl Area { move_response }; - state.set_left_top_pos(ctx.round_pos_to_pixels(state.left_top_pos())); - if constrain { state.set_left_top_pos( - ctx.constrain_window_rect_to_area(state.rect(), drag_bounds) - .left_top(), + ctx.constrain_window_rect_to_area(state.rect(), constrain_rect) + .min, ); } + state.set_left_top_pos(ctx.round_pos_to_pixels(state.left_top_pos())); + + // Update responsbe with posisbly moved/constrained rect: + move_response = move_response.with_new_rect(state.rect()); + Prepared { layer_id, state, move_response, enabled, - drag_bounds, + constrain, + constrain_rect, temporarily_invisible: is_new, } } @@ -371,15 +377,19 @@ impl Prepared { &mut self.state } - pub(crate) fn drag_bounds(&self) -> Option { - self.drag_bounds + pub(crate) fn constrain(&self) -> bool { + self.constrain + } + + pub(crate) fn constrain_rect(&self) -> Option { + self.constrain_rect } pub(crate) fn content_ui(&self, ctx: &Context) -> Ui { let screen_rect = ctx.screen_rect(); - let bounds = if let Some(bounds) = self.drag_bounds { - bounds.intersect(screen_rect) // protect against infinite bounds + let constrain_rect = if let Some(constrain_rect) = self.constrain_rect { + constrain_rect.intersect(screen_rect) // protect against infinite bounds } else { let central_area = ctx.available_rect(); @@ -393,7 +403,7 @@ impl Prepared { let max_rect = Rect::from_min_max( self.state.left_top_pos(), - bounds + constrain_rect .max .at_least(self.state.left_top_pos() + Vec2::splat(32.0)), ); @@ -401,9 +411,9 @@ impl Prepared { 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(), bounds.max) + let clip_rect = Rect::from_min_max(self.state.left_top_pos(), constrain_rect.max) .expand(clip_rect_margin) - .intersect(bounds); + .intersect(constrain_rect); let mut ui = Ui::new( ctx.clone(), @@ -424,7 +434,8 @@ impl Prepared { mut state, move_response, enabled: _, - drag_bounds: _, + constrain: _, + constrain_rect: _, temporarily_invisible: _, } = self; diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index 6ce5cbc408e..e92269751a3 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -127,8 +127,8 @@ impl Frame { } #[inline] - pub fn stroke(mut self, stroke: Stroke) -> Self { - self.stroke = stroke; + pub fn stroke(mut self, stroke: impl Into) -> Self { + self.stroke = stroke.into(); self } diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index e4ad1b1305f..78be080ba47 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -260,9 +260,8 @@ fn show_tooltip_area_dyn<'c, R>( Area::new(area_id) .order(Order::Tooltip) .fixed_pos(window_pos) - .constrain(true) + .constrain_to(ctx.screen_rect()) .interactable(false) - .drag_bounds(ctx.screen_rect()) .show(ctx, |ui| { Frame::popup(&ctx.style()) .show(ui, |ui| { diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 2f71a57c08b..7274b6c5add 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -314,6 +314,7 @@ impl Resize { state.store(ui.ctx(), id); + #[cfg(debug_assertions)] if ui.ctx().style().debug.show_resize { ui.ctx().debug_painter().debug_rect( Rect::from_min_size(content_ui.min_rect().left_top(), state.desired_size), @@ -331,15 +332,21 @@ impl Resize { use epaint::Stroke; -pub fn paint_resize_corner(ui: &mut Ui, response: &Response) { +pub fn paint_resize_corner(ui: &Ui, response: &Response) { let stroke = ui.style().interact(response).fg_stroke; paint_resize_corner_with_style(ui, &response.rect, stroke, Align2::RIGHT_BOTTOM); } -pub fn paint_resize_corner_with_style(ui: &mut Ui, rect: &Rect, stroke: Stroke, corner: Align2) { +pub fn paint_resize_corner_with_style( + ui: &Ui, + rect: &Rect, + stroke: impl Into, + corner: Align2, +) { let painter = ui.painter(); let cp = painter.round_pos_to_pixels(corner.pos_in_rect(rect)); let mut w = 2.0; + let stroke = stroke.into(); while w <= rect.width() && w <= rect.height() { painter.line_segment( diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index b14bcaea265..fff306911bb 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -1,8 +1,3 @@ -//! Coordinate system names: -//! * content: size of contents (generally large; that's why we want scroll bars) -//! * outer: size of scroll area including scroll bar(s) -//! * inner: excluding scroll bar(s). The area we clip the contents to. - #![allow(clippy::needless_range_loop)] use crate::*; @@ -20,6 +15,9 @@ pub struct State { /// The content were to large to fit large frame. content_is_too_large: [bool; 2], + /// Did the user interact (hover or drag) the scroll bars last frame? + scroll_bar_interaction: [bool; 2], + /// Momentum, used for kinetic scrolling #[cfg_attr(feature = "serde", serde(skip))] vel: Vec2, @@ -39,6 +37,7 @@ impl Default for State { offset: Vec2::ZERO, show_scroll: [false; 2], content_is_too_large: [false; 2], + scroll_bar_interaction: [false; 2], vel: Vec2::ZERO, scroll_start_offset_from_top_left: [None; 2], scroll_stuck_to_end: [true; 2], @@ -80,15 +79,61 @@ pub struct ScrollAreaOutput { } /// Indicate whether the horizontal and vertical scroll bars must be always visible, hidden or visible when needed. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum ScrollBarVisibility { - AlwaysVisible, - VisibleWhenNeeded, + /// Hide scroll bar even if they are needed. + /// + /// You can still scroll, with the scroll-wheel + /// and by dragging the contents, but there is no + /// visual indication of how far you have scrolled. AlwaysHidden, + + /// Show scroll bars only when the content size exceeds the container, + /// i.e. when there is any need to scroll. + /// + /// This is the default. + VisibleWhenNeeded, + + /// Always show the scroll bar, even if the contents fit in the container + /// and there is no need to scroll. + AlwaysVisible, +} + +impl Default for ScrollBarVisibility { + #[inline] + fn default() -> Self { + Self::VisibleWhenNeeded + } +} + +impl ScrollBarVisibility { + pub const ALL: [Self; 3] = [ + Self::AlwaysHidden, + Self::VisibleWhenNeeded, + Self::AlwaysVisible, + ]; } /// Add vertical and/or horizontal scrolling to a contained [`Ui`]. /// +/// By default, scroll bars only show up when needed, i.e. when the contents +/// is larger than the container. +/// This is controlled by [`Self::scroll_bar_visibility`]. +/// +/// There are two flavors of scroll areas: solid and floating. +/// Solid scroll bars use up space, reducing the amount of space available +/// to the contents. Floating scroll bars float on top of the contents, covering it. +/// You can change the scroll style by changing the [`crate::style::Spacing::scroll`]. +/// +/// ### Coordinate system +/// * content: size of contents (generally large; that's why we want scroll bars) +/// * outer: size of scroll area including scroll bar(s) +/// * inner: excluding scroll bar(s). The area we clip the contents to. +/// +/// If the floating scroll bars settings is turned on then `inner == outer`. +/// +/// ## Example /// ``` /// # egui::__run_test_ui(|ui| { /// egui::ScrollArea::vertical().show(ui, |ui| { @@ -101,8 +146,9 @@ pub enum ScrollBarVisibility { #[derive(Clone, Debug)] #[must_use = "You should call .show()"] pub struct ScrollArea { - /// Do we have horizontal/vertical scrolling? - has_bar: [bool; 2], + /// Do we have horizontal/vertical scrolling enabled? + scroll_enabled: [bool; 2], + auto_shrink: [bool; 2], max_size: Vec2, min_scrolled_size: Vec2, @@ -123,35 +169,39 @@ pub struct ScrollArea { impl ScrollArea { /// Create a horizontal scroll area. + #[inline] pub fn horizontal() -> Self { Self::new([true, false]) } /// Create a vertical scroll area. + #[inline] pub fn vertical() -> Self { Self::new([false, true]) } /// Create a bi-directional (horizontal and vertical) scroll area. + #[inline] pub fn both() -> Self { Self::new([true, true]) } /// Create a scroll area where both direction of scrolling is disabled. /// It's unclear why you would want to do this. + #[inline] pub fn neither() -> Self { Self::new([false, false]) } /// Create a scroll area where you decide which axis has scrolling enabled. /// For instance, `ScrollArea::new([true, false])` enables horizontal scrolling. - pub fn new(has_bar: [bool; 2]) -> Self { + pub fn new(scroll_enabled: [bool; 2]) -> Self { Self { - has_bar, + scroll_enabled, auto_shrink: [true; 2], max_size: Vec2::INFINITY, min_scrolled_size: Vec2::splat(64.0), - scroll_bar_visibility: ScrollBarVisibility::VisibleWhenNeeded, + scroll_bar_visibility: Default::default(), id_source: None, offset_x: None, offset_y: None, @@ -166,6 +216,7 @@ impl ScrollArea { /// Use `f32::INFINITY` if you want the scroll area to expand to fit the surrounding [`Ui`] (default). /// /// See also [`Self::auto_shrink`]. + #[inline] pub fn max_width(mut self, max_width: f32) -> Self { self.max_size.x = max_width; self @@ -176,6 +227,7 @@ impl ScrollArea { /// Use `f32::INFINITY` if you want the scroll area to expand to fit the surrounding [`Ui`] (default). /// /// See also [`Self::auto_shrink`]. + #[inline] pub fn max_height(mut self, max_height: f32) -> Self { self.max_size.y = max_height; self @@ -187,6 +239,7 @@ impl ScrollArea { /// (and so we don't require scroll bars). /// /// Default: `64.0`. + #[inline] pub fn min_scrolled_width(mut self, min_scrolled_width: f32) -> Self { self.min_scrolled_size.x = min_scrolled_width; self @@ -198,6 +251,7 @@ impl ScrollArea { /// (and so we don't require scroll bars). /// /// Default: `64.0`. + #[inline] pub fn min_scrolled_height(mut self, min_scrolled_height: f32) -> Self { self.min_scrolled_size.y = min_scrolled_height; self @@ -206,12 +260,14 @@ impl ScrollArea { /// Set the visibility of both horizontal and vertical scroll bars. /// /// With `ScrollBarVisibility::VisibleWhenNeeded` (default), the scroll bar will be visible only when needed. + #[inline] pub fn scroll_bar_visibility(mut self, scroll_bar_visibility: ScrollBarVisibility) -> Self { self.scroll_bar_visibility = scroll_bar_visibility; self } /// A source for the unique [`Id`], e.g. `.id_source("second_scroll_area")` or `.id_source(loop_index)`. + #[inline] pub fn id_source(mut self, id_source: impl std::hash::Hash) -> Self { self.id_source = Some(Id::new(id_source)); self @@ -224,6 +280,7 @@ impl ScrollArea { /// See also: [`Self::vertical_scroll_offset`], [`Self::horizontal_scroll_offset`], /// [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and /// [`Response::scroll_to_me`](crate::Response::scroll_to_me) + #[inline] pub fn scroll_offset(mut self, offset: Vec2) -> Self { self.offset_x = Some(offset.x); self.offset_y = Some(offset.y); @@ -236,6 +293,7 @@ impl ScrollArea { /// /// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and /// [`Response::scroll_to_me`](crate::Response::scroll_to_me) + #[inline] pub fn vertical_scroll_offset(mut self, offset: f32) -> Self { self.offset_y = Some(offset); self @@ -247,26 +305,30 @@ impl ScrollArea { /// /// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and /// [`Response::scroll_to_me`](crate::Response::scroll_to_me) + #[inline] pub fn horizontal_scroll_offset(mut self, offset: f32) -> Self { self.offset_x = Some(offset); self } /// Turn on/off scrolling on the horizontal axis. + #[inline] pub fn hscroll(mut self, hscroll: bool) -> Self { - self.has_bar[0] = hscroll; + self.scroll_enabled[0] = hscroll; self } /// Turn on/off scrolling on the vertical axis. + #[inline] pub fn vscroll(mut self, vscroll: bool) -> Self { - self.has_bar[1] = vscroll; + self.scroll_enabled[1] = vscroll; self } /// Turn on/off scrolling on the horizontal/vertical axes. - pub fn scroll2(mut self, has_bar: [bool; 2]) -> Self { - self.has_bar = has_bar; + #[inline] + pub fn scroll2(mut self, scroll_enabled: [bool; 2]) -> Self { + self.scroll_enabled = scroll_enabled; self } @@ -279,6 +341,7 @@ impl ScrollArea { /// is typing text in a [`TextEdit`] widget contained within the scroll area. /// /// This controls both scrolling directions. + #[inline] pub fn enable_scrolling(mut self, enable: bool) -> Self { self.scrolling_enabled = enable; self @@ -291,6 +354,7 @@ impl ScrollArea { /// If `true`, the [`ScrollArea`] will sense drags. /// /// Default: `true`. + #[inline] pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self { self.drag_to_scroll = drag_to_scroll; self @@ -302,13 +366,15 @@ impl ScrollArea { /// * If `false`, egui will add blank space inside the scroll area. /// /// Default: `[true; 2]`. + #[inline] pub fn auto_shrink(mut self, auto_shrink: [bool; 2]) -> Self { self.auto_shrink = auto_shrink; self } - pub(crate) fn has_any_bar(&self) -> bool { - self.has_bar[0] || self.has_bar[1] + /// Is any scrolling enabled? + pub(crate) fn is_any_scroll_enabled(&self) -> bool { + self.scroll_enabled[0] || self.scroll_enabled[1] } /// The scroll handle will stick to the rightmost position even while the content size @@ -317,6 +383,7 @@ impl ScrollArea { /// it will remain focused on whatever content viewport the user left it on. If the scroll /// handle is dragged all the way to the right it will again become stuck and remain there /// until manually pulled from the end position. + #[inline] pub fn stick_to_right(mut self, stick: bool) -> Self { self.stick_to_end[0] = stick; self @@ -328,6 +395,7 @@ impl ScrollArea { /// it will remain focused on whatever content viewport the user left it on. If the scroll /// handle is dragged to the bottom it will again become stuck and remain there until manually /// pulled from the end position. + #[inline] pub fn stick_to_bottom(mut self, stick: bool) -> Self { self.stick_to_end[1] = stick; self @@ -337,11 +405,24 @@ impl ScrollArea { struct Prepared { id: Id, state: State, - has_bar: [bool; 2], + auto_shrink: [bool; 2], + /// Does this `ScrollArea` have horizontal/vertical scrolling enabled? + scroll_enabled: [bool; 2], + + /// Smoothly interpolated boolean of whether or not to show the scroll bars. + show_bars_factor: Vec2, + /// How much horizontal and vertical space are used up by the /// width of the vertical bar, and the height of the horizontal bar? + /// + /// This is always zero for floating scroll bars. + /// + /// Note that this is a `yx` swizzling of [`Self::show_bars_factor`] + /// times the maximum bar with. + /// That's because horizontal scroll uses up vertical space, + /// and vice versa. current_bar_use: Vec2, scroll_bar_visibility: ScrollBarVisibility, @@ -362,7 +443,7 @@ struct Prepared { impl ScrollArea { fn begin(self, ui: &mut Ui) -> Prepared { let Self { - has_bar, + scroll_enabled, auto_shrink, max_size, min_scrolled_size, @@ -379,7 +460,7 @@ impl ScrollArea { let id_source = id_source.unwrap_or_else(|| Id::new("scroll_area")); let id = ui.make_persistent_id(id_source); - ui.ctx().check_for_id_clash( + ctx.check_for_id_clash( id, Rect::from_min_size(ui.available_rect_before_wrap().min, Vec2::ZERO), "ScrollArea", @@ -389,25 +470,18 @@ impl ScrollArea { state.offset.x = offset_x.unwrap_or(state.offset.x); state.offset.y = offset_y.unwrap_or(state.offset.y); - let max_scroll_bar_width = max_scroll_bar_width_with_margin(ui); - - let current_hscroll_bar_height = if !has_bar[0] { - 0.0 - } else if scroll_bar_visibility == ScrollBarVisibility::AlwaysVisible { - max_scroll_bar_width - } else { - max_scroll_bar_width * ui.ctx().animate_bool(id.with("h"), state.show_scroll[0]) + let show_bars: [bool; 2] = match scroll_bar_visibility { + ScrollBarVisibility::AlwaysHidden => [false; 2], + ScrollBarVisibility::VisibleWhenNeeded => state.show_scroll, + ScrollBarVisibility::AlwaysVisible => scroll_enabled, }; - let current_vscroll_bar_width = if !has_bar[1] { - 0.0 - } else if scroll_bar_visibility == ScrollBarVisibility::AlwaysVisible { - max_scroll_bar_width - } else { - max_scroll_bar_width * ui.ctx().animate_bool(id.with("v"), state.show_scroll[1]) - }; + let show_bars_factor = Vec2::new( + ctx.animate_bool(id.with("h"), show_bars[0]), + ctx.animate_bool(id.with("v"), show_bars[1]), + ); - let current_bar_use = vec2(current_vscroll_bar_width, current_hscroll_bar_height); + let current_bar_use = show_bars_factor.yx() * ui.spacing().scroll.allocated_width(); let available_outer = ui.available_rect_before_wrap(); @@ -421,7 +495,7 @@ impl ScrollArea { // one shouldn't collapse into nothingness. // See https://github.com/emilk/egui/issues/1097 for d in 0..2 { - if has_bar[d] { + if scroll_enabled[d] { inner_size[d] = inner_size[d].max(min_scrolled_size[d]); } } @@ -438,7 +512,7 @@ impl ScrollArea { } else { // Tell the inner Ui to use as much space as possible, we can scroll to see it! for d in 0..2 { - if has_bar[d] { + if scroll_enabled[d] { content_max_size[d] = f32::INFINITY; } } @@ -452,7 +526,7 @@ impl ScrollArea { let clip_rect_margin = ui.visuals().clip_rect_margin; let mut content_clip_rect = ui.clip_rect(); for d in 0..2 { - if has_bar[d] { + if scroll_enabled[d] { if state.content_is_too_large[d] { content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin; content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin; @@ -479,7 +553,7 @@ impl ScrollArea { if content_response.dragged() { for d in 0..2 { - if has_bar[d] { + if scroll_enabled[d] { ui.input(|input| { state.offset[d] -= input.pointer.delta()[d]; state.vel[d] = input.pointer.velocity()[d]; @@ -502,7 +576,7 @@ impl ScrollArea { // Offset has an inverted coordinate system compared to // the velocity, so we subtract it instead of adding it state.offset -= state.vel * dt; - ui.ctx().request_repaint(); + ctx.request_repaint(); } } } @@ -510,8 +584,9 @@ impl ScrollArea { Prepared { id, state, - has_bar, auto_shrink, + scroll_enabled, + show_bars_factor, current_bar_use, scroll_bar_visibility, inner_rect, @@ -621,9 +696,10 @@ impl Prepared { id, mut state, inner_rect, - has_bar, auto_shrink, - mut current_bar_use, + scroll_enabled, + mut show_bars_factor, + current_bar_use, scroll_bar_visibility, content_ui, viewport: _, @@ -634,7 +710,7 @@ impl Prepared { let content_size = content_ui.min_size(); for d in 0..2 { - if has_bar[d] { + if scroll_enabled[d] { // We take the scroll target so only this ScrollArea will use it: let scroll_target = content_ui .ctx() @@ -680,7 +756,7 @@ impl Prepared { let mut inner_size = inner_rect.size(); for d in 0..2 { - inner_size[d] = match (has_bar[d], auto_shrink[d]) { + inner_size[d] = match (scroll_enabled[d], auto_shrink[d]) { (true, true) => inner_size[d].min(content_size[d]), // shrink scroll area if content is small (true, false) => inner_size[d], // let scroll area be larger than content; fill with blank space (false, true) => content_size[d], // Follow the content (expand/contract to fit it). @@ -694,14 +770,15 @@ impl Prepared { let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use); let content_is_too_large = [ - content_size.x > inner_rect.width(), - content_size.y > inner_rect.height(), + scroll_enabled[0] && inner_rect.width() < content_size.x, + scroll_enabled[1] && inner_rect.height() < content_size.y, ]; let max_offset = content_size - inner_rect.size(); - if scrolling_enabled && ui.rect_contains_pointer(outer_rect) { + let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect); + if scrolling_enabled && is_hovering_outer_rect { for d in 0..2 { - if has_bar[d] { + if scroll_enabled[d] { let scroll_delta = ui.ctx().frame_state(|fs| fs.scroll_delta); let scrolling_up = state.offset[d] > 0.0 && scroll_delta[d] > 0.0; @@ -718,44 +795,74 @@ impl Prepared { } let show_scroll_this_frame = match scroll_bar_visibility { - ScrollBarVisibility::AlwaysVisible => [true, true], + ScrollBarVisibility::AlwaysHidden => [false, false], ScrollBarVisibility::VisibleWhenNeeded => { [content_is_too_large[0], content_is_too_large[1]] } - ScrollBarVisibility::AlwaysHidden => [false, false], + ScrollBarVisibility::AlwaysVisible => scroll_enabled, }; - let max_scroll_bar_width = max_scroll_bar_width_with_margin(ui); - // Avoid frame delay; start showing scroll bar right away: - if show_scroll_this_frame[0] && current_bar_use.y <= 0.0 { - current_bar_use.y = max_scroll_bar_width * ui.ctx().animate_bool(id.with("h"), true); + if show_scroll_this_frame[0] && show_bars_factor.x <= 0.0 { + show_bars_factor.x = ui.ctx().animate_bool(id.with("h"), true); } - if show_scroll_this_frame[1] && current_bar_use.x <= 0.0 { - current_bar_use.x = max_scroll_bar_width * ui.ctx().animate_bool(id.with("v"), true); + if show_scroll_this_frame[1] && show_bars_factor.y <= 0.0 { + show_bars_factor.y = ui.ctx().animate_bool(id.with("v"), true); } + let scroll_style = ui.spacing().scroll; + + // Paint the bars: for d in 0..2 { // maybe force increase in offset to keep scroll stuck to end position if stick_to_end[d] && state.scroll_stuck_to_end[d] { state.offset[d] = content_size[d] - inner_rect.size()[d]; } - let animation_t = current_bar_use[1 - d] / max_scroll_bar_width; - - if animation_t == 0.0 { + let show_factor = show_bars_factor[d]; + if show_factor == 0.0 { + state.scroll_bar_interaction[d] = false; continue; } - // margin on either side of the scroll bar - let inner_margin = animation_t * ui.spacing().scroll_bar_inner_margin; - let outer_margin = animation_t * ui.spacing().scroll_bar_outer_margin; - let mut min_cross = inner_rect.max[1 - d] + inner_margin; // left of vertical scroll (d == 1) - let mut max_cross = outer_rect.max[1 - d] - outer_margin; // right of vertical scroll (d == 1) - let min_main = inner_rect.min[d]; // top of vertical scroll (d == 1) - let max_main = inner_rect.max[d]; // bottom of vertical scroll (d == 1) + // left/right of a horizontal scroll (d==1) + // top/bottom of vertical scroll (d == 1) + let main_range = Rangef::new(inner_rect.min[d], inner_rect.max[d]); - if ui.clip_rect().max[1 - d] < max_cross + outer_margin { + // Margin on either side of the scroll bar: + let inner_margin = show_factor * scroll_style.bar_inner_margin; + let outer_margin = show_factor * scroll_style.bar_outer_margin; + + // top/bottom of a horizontal scroll (d==0). + // left/rigth of a vertical scroll (d==1). + let mut cross = if scroll_style.floating { + let max_bar_rect = if d == 0 { + outer_rect.with_min_y(outer_rect.max.y - scroll_style.allocated_width()) + } else { + outer_rect.with_min_x(outer_rect.max.x - scroll_style.allocated_width()) + }; + let is_hovering_bar_area = is_hovering_outer_rect + && ui.rect_contains_pointer(max_bar_rect) + || state.scroll_bar_interaction[d]; + let is_hovering_bar_area_t = ui + .ctx() + .animate_bool(id.with((d, "bar_hover")), is_hovering_bar_area); + let width = show_factor + * lerp( + scroll_style.floating_width..=scroll_style.bar_width, + is_hovering_bar_area_t, + ); + + let max_cross = outer_rect.max[1 - d] - outer_margin; + let min_cross = max_cross - width; + Rangef::new(min_cross, max_cross) + } else { + let min_cross = inner_rect.max[1 - d] + inner_margin; + let max_cross = outer_rect.max[1 - d] - outer_margin; + Rangef::new(min_cross, max_cross) + }; + + if ui.clip_rect().max[1 - d] < cross.max + outer_margin { // Move the scrollbar so it is visible. This is needed in some cases. // For instance: // * When we have a vertical-only scroll area in a top level panel, @@ -765,36 +872,35 @@ impl Prepared { // is outside the clip rectangle. // Really this should use the tighter clip_rect that ignores clip_rect_margin, but we don't store that. // clip_rect_margin is quite a hack. It would be nice to get rid of it. - let width = max_cross - min_cross; - max_cross = ui.clip_rect().max[1 - d] - outer_margin; - min_cross = max_cross - width; + let width = cross.max - cross.min; + cross.max = ui.clip_rect().max[1 - d] - outer_margin; + cross.min = cross.max - width; } let outer_scroll_rect = if d == 0 { Rect::from_min_max( - pos2(inner_rect.left(), min_cross), - pos2(inner_rect.right(), max_cross), + pos2(inner_rect.left(), cross.min), + pos2(inner_rect.right(), cross.max), ) } else { Rect::from_min_max( - pos2(min_cross, inner_rect.top()), - pos2(max_cross, inner_rect.bottom()), + pos2(cross.min, inner_rect.top()), + pos2(cross.max, inner_rect.bottom()), ) }; - let from_content = - |content| remap_clamp(content, 0.0..=content_size[d], min_main..=max_main); + let from_content = |content| remap_clamp(content, 0.0..=content_size[d], main_range); let handle_rect = if d == 0 { Rect::from_min_max( - pos2(from_content(state.offset.x), min_cross), - pos2(from_content(state.offset.x + inner_rect.width()), max_cross), + pos2(from_content(state.offset.x), cross.min), + pos2(from_content(state.offset.x + inner_rect.width()), cross.max), ) } else { Rect::from_min_max( - pos2(min_cross, from_content(state.offset.y)), + pos2(cross.min, from_content(state.offset.y)), pos2( - max_cross, + cross.max, from_content(state.offset.y + inner_rect.height()), ), ) @@ -808,22 +914,24 @@ impl Prepared { }; let response = ui.interact(outer_scroll_rect, interact_id, sense); + state.scroll_bar_interaction[d] = response.hovered() || response.dragged(); + if let Some(pointer_pos) = response.interact_pointer_pos() { let scroll_start_offset_from_top_left = state.scroll_start_offset_from_top_left[d] .get_or_insert_with(|| { if handle_rect.contains(pointer_pos) { pointer_pos[d] - handle_rect.min[d] } else { - let handle_top_pos_at_bottom = max_main - handle_rect.size()[d]; + let handle_top_pos_at_bottom = main_range.max - handle_rect.size()[d]; // Calculate the new handle top position, centering the handle on the mouse. let new_handle_top_pos = (pointer_pos[d] - handle_rect.size()[d] / 2.0) - .clamp(min_main, handle_top_pos_at_bottom); + .clamp(main_range.min, handle_top_pos_at_bottom); pointer_pos[d] - new_handle_top_pos } }); let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left; - state.offset[d] = remap(new_handle_top, min_main..=max_main, 0.0..=content_size[d]); + state.offset[d] = remap(new_handle_top, main_range, 0.0..=content_size[d]); // some manual action taken, scroll not stuck state.scroll_stuck_to_end[d] = false; @@ -843,19 +951,19 @@ impl Prepared { // Avoid frame-delay by calculating a new handle rect: let mut handle_rect = if d == 0 { Rect::from_min_max( - pos2(from_content(state.offset.x), min_cross), - pos2(from_content(state.offset.x + inner_rect.width()), max_cross), + pos2(from_content(state.offset.x), cross.min), + pos2(from_content(state.offset.x + inner_rect.width()), cross.max), ) } else { Rect::from_min_max( - pos2(min_cross, from_content(state.offset.y)), + pos2(cross.min, from_content(state.offset.y)), pos2( - max_cross, + cross.max, from_content(state.offset.y + inner_rect.height()), ), ) }; - let min_handle_size = ui.spacing().scroll_handle_min_length; + let min_handle_size = scroll_style.handle_min_length; if handle_rect.size()[d] < min_handle_size { handle_rect = Rect::from_center_size( handle_rect.center(), @@ -868,21 +976,76 @@ impl Prepared { } let visuals = if scrolling_enabled { - ui.style().interact(&response) + // Pick visuals based on interaction with the handle. + // Remember that the response is for the whole scroll bar! + let is_hovering_handle = response.hovered() + && ui.input(|i| { + i.pointer + .latest_pos() + .map_or(false, |p| handle_rect.contains(p)) + }); + let visuals = ui.visuals(); + if response.is_pointer_button_down_on() { + &visuals.widgets.active + } else if is_hovering_handle { + &visuals.widgets.hovered + } else { + &visuals.widgets.inactive + } + } else { + &ui.visuals().widgets.inactive + }; + + let handle_opacity = if scroll_style.floating { + if response.hovered() || response.dragged() { + scroll_style.interact_handle_opacity + } else { + let is_hovering_outer_rect_t = ui.ctx().animate_bool( + id.with((d, "is_hovering_outer_rect")), + is_hovering_outer_rect, + ); + lerp( + scroll_style.dormant_handle_opacity + ..=scroll_style.active_handle_opacity, + is_hovering_outer_rect_t, + ) + } + } else { + 1.0 + }; + + let background_opacity = if scroll_style.floating { + if response.hovered() || response.dragged() { + scroll_style.interact_background_opacity + } else if is_hovering_outer_rect { + scroll_style.active_background_opacity + } else { + scroll_style.dormant_background_opacity + } + } else { + 1.0 + }; + + let handle_color = if scroll_style.foreground_color { + visuals.fg_stroke.color } else { - &ui.style().visuals.widgets.inactive + visuals.bg_fill }; + // Background: ui.painter().add(epaint::Shape::rect_filled( outer_scroll_rect, visuals.rounding, - ui.visuals().extreme_bg_color, + ui.visuals() + .extreme_bg_color + .gamma_multiply(background_opacity), )); + // Handle: ui.painter().add(epaint::Shape::rect_filled( handle_rect, visuals.rounding, - visuals.bg_fill, + handle_color.gamma_multiply(handle_opacity), )); } } @@ -904,9 +1067,9 @@ impl Prepared { // has appropriate effect. state.scroll_stuck_to_end = [ (state.offset[0] == available_offset[0]) - || (self.stick_to_end[0] && available_offset[0] < 0.), + || (self.stick_to_end[0] && available_offset[0] < 0.0), (state.offset[1] == available_offset[1]) - || (self.stick_to_end[1] && available_offset[1] < 0.), + || (self.stick_to_end[1] && available_offset[1] < 0.0), ]; state.show_scroll = show_scroll_this_frame; @@ -917,10 +1080,3 @@ impl Prepared { (content_size, state) } } - -/// Width of a vertical scrollbar, or height of a horizontal scroll bar -fn max_scroll_bar_width_with_margin(ui: &Ui) -> f32 { - ui.spacing().scroll_bar_inner_margin - + ui.spacing().scroll_bar_width - + ui.spacing().scroll_bar_outer_margin -} diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index ca4f6190d24..af72ff98b9c 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -43,7 +43,7 @@ impl<'open> Window<'open> { /// If you need a changing title, you must call `window.id(…)` with a fixed id. pub fn new(title: impl Into) -> Self { let title = title.into().fallback_text_style(TextStyle::Heading); - let area = Area::new(Id::new(title.text())); + let area = Area::new(Id::new(title.text())).constrain(true); Self { title, open: None, @@ -146,11 +146,31 @@ impl<'open> Window<'open> { } /// Constrains this window to the screen bounds. + /// + /// To change the area to constrain to, use [`Self::constrain_to`]. + /// + /// Default: `true`. pub fn constrain(mut self, constrain: bool) -> Self { self.area = self.area.constrain(constrain); self } + /// Constrain the movement of the window to the given rectangle. + /// + /// For instance: `.constrain_to(ctx.screen_rect())`. + pub fn constrain_to(mut self, constrain_rect: Rect) -> Self { + self.area = self.area.constrain_to(constrain_rect); + self + } + + #[deprecated = "Use `constrain_to` instead"] + pub fn drag_bounds(mut self, constrain_rect: Rect) -> Self { + #![allow(deprecated)] + + self.area = self.area.drag_bounds(constrain_rect); + self + } + /// Where the "root" of the window is. /// /// For instance, if you set this to [`Align2::RIGHT_TOP`] @@ -276,12 +296,6 @@ impl<'open> Window<'open> { self.scroll = self.scroll.drag_to_scroll(drag_to_scroll); self } - - /// Constrain the area up to which the window can be dragged. - pub fn drag_bounds(mut self, bounds: Rect) -> Self { - self.area = self.area.drag_bounds(bounds); - self - } } impl<'open> Window<'open> { @@ -405,7 +419,7 @@ impl<'open> Window<'open> { ui.add_space(title_content_spacing); } - if scroll.has_any_bar() { + if scroll.is_any_scroll_enabled() { scroll.show(ui, add_contents).inner } else { add_contents(ui) @@ -415,7 +429,7 @@ impl<'open> Window<'open> { .map_or((None, None), |ir| (Some(ir.inner), Some(ir.response))); let outer_rect = frame.end(&mut area_content_ui).rect; - paint_resize_corner(&mut area_content_ui, &possible, outer_rect, frame_stroke); + paint_resize_corner(&area_content_ui, &possible, outer_rect, frame_stroke); // END FRAME -------------------------------- @@ -434,7 +448,7 @@ impl<'open> Window<'open> { if let Some(interaction) = interaction { paint_frame_interaction( - &mut area_content_ui, + &area_content_ui, outer_rect, interaction, ctx.style().visuals.widgets.active, @@ -442,7 +456,7 @@ impl<'open> Window<'open> { } else if let Some(hover_interaction) = hover_interaction { if ctx.input(|i| i.pointer.has_pointer()) { paint_frame_interaction( - &mut area_content_ui, + &area_content_ui, outer_rect, hover_interaction, ctx.style().visuals.widgets.hovered, @@ -452,13 +466,6 @@ impl<'open> Window<'open> { content_inner }; - { - let pos = ctx - .constrain_window_rect_to_area(area.state().rect(), area.drag_bounds()) - .left_top(); - area.state_mut().set_left_top_pos(pos); - } - let full_response = area.end(ctx, area_content_ui); let inner_response = InnerResponse { @@ -470,10 +477,10 @@ impl<'open> Window<'open> { } fn paint_resize_corner( - ui: &mut Ui, + ui: &Ui, possible: &PossibleInteractions, outer_rect: Rect, - stroke: Stroke, + stroke: impl Into, ) { let corner = if possible.resize_right && possible.resize_bottom { Align2::RIGHT_BOTTOM @@ -562,9 +569,11 @@ fn interact( resize_id: Id, ) -> Option { let new_rect = move_and_resize_window(ctx, &window_interaction)?; - let new_rect = ctx.round_rect_to_pixels(new_rect); + let mut new_rect = ctx.round_rect_to_pixels(new_rect); - let new_rect = ctx.constrain_window_rect_to_area(new_rect, area.drag_bounds()); + if area.constrain() { + new_rect = ctx.constrain_window_rect_to_area(new_rect, area.constrain_rect()); + } // TODO(emilk): add this to a Window state instead as a command "move here next frame" area.state_mut().set_left_top_pos(new_rect.left_top()); @@ -749,7 +758,7 @@ fn resize_hover( /// Fill in parts of the window frame when we resize by dragging that part fn paint_frame_interaction( - ui: &mut Ui, + ui: &Ui, rect: Rect, interaction: WindowInteraction, visuals: style::WidgetVisuals, diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 554d36f5b2c..168489752d0 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1,10 +1,13 @@ #![warn(missing_docs)] // Let's keep `Context` well-documented. +use std::borrow::Cow; use std::sync::Arc; +use crate::load::Bytes; +use crate::load::SizedTexture; use crate::{ animation_manager::AnimationManager, data::output::PlatformOutput, frame_state::FrameState, - input_state::*, layers::GraphicLayers, memory::Options, os::OperatingSystem, + input_state::*, layers::GraphicLayers, load::Loaders, memory::Options, os::OperatingSystem, output::FullOutput, util::IdTypeMap, TextureHandle, *, }; use epaint::{mutex::*, stats::*, text::Fonts, TessellationOptions, *}; @@ -167,7 +170,7 @@ struct ContextImpl { #[cfg(feature = "accesskit")] accesskit_node_classes: accesskit::NodeClassSet, - loaders: load::Loaders, + loaders: Arc, } impl ContextImpl { @@ -548,10 +551,6 @@ impl Context { ) -> R { self.write(move |ctx| writer(&mut ctx.memory.options.tessellation_options)) } -} - -impl Context { - // --------------------------------------------------------------------- /// If the given [`Id`] has been used previously the same frame at at different position, /// then an error will be printed on screen. @@ -663,6 +662,7 @@ impl Context { // This solves the problem of overlapping widgets. // Whichever widget is added LAST (=on top) gets the input: if interact_rect.is_positive() && sense.interactive() { + #[cfg(debug_assertions)] if self.style().debug.show_interactive_widgets { Self::layer_painter(self, LayerId::debug()).rect( interact_rect, @@ -671,6 +671,8 @@ impl Context { Stroke::new(1.0, Color32::YELLOW.additive().linear_multiply(0.05)), ); } + + #[cfg(debug_assertions)] let mut show_blocking_widget = None; self.write(|ctx| { @@ -691,6 +693,7 @@ impl Context { // Another interactive widget is covering us at the pointer position, // so we aren't hovered. + #[cfg(debug_assertions)] if ctx.memory.options.style.debug.show_blocking_widget { // Store the rects to use them outside the write() call to // avoid deadlock @@ -706,6 +709,7 @@ impl Context { } }); + #[cfg(debug_assertions)] if let Some((interact_rect, prev_rect)) = show_blocking_widget { Self::layer_painter(self, LayerId::debug()).debug_rect( interact_rect, @@ -916,6 +920,31 @@ impl Context { self.output_mut(|o| o.cursor_icon = cursor_icon); } + /// Open an URL in a browser. + /// + /// Equivalent to: + /// ``` + /// # let ctx = egui::Context::default(); + /// # let open_url = egui::OpenUrl::same_tab("http://www.example.com"); + /// ctx.output_mut(|o| o.open_url = Some(open_url)); + /// ``` + pub fn open_url(&self, open_url: crate::OpenUrl) { + self.output_mut(|o| o.open_url = Some(open_url)); + } + + /// Copy the given text to the system clipboard. + /// + /// Empty strings are ignored. + /// + /// Equivalent to: + /// ``` + /// # let ctx = egui::Context::default(); + /// ctx.output_mut(|o| o.copied_text = "Copy this".to_owned()); + /// ``` + pub fn copy_text(&self, text: String) { + self.output_mut(|o| o.copied_text = text); + } + /// Format the given shortcut in a human-readable way (e.g. `Ctrl+Shift+X`). /// /// Can be used to get the text for [`Button::shortcut_text`]. @@ -1046,17 +1075,24 @@ impl Context { self.options(|opt| opt.style.clone()) } - /// The [`Style`] used by all new windows, panels etc. - /// - /// You can also use [`Ui::style_mut`] to change the style of a single [`Ui`]. + /// Mutate the [`Style`] used by all subsequent windows, panels etc. /// /// Example: /// ``` /// # let mut ctx = egui::Context::default(); - /// let mut style: egui::Style = (*ctx.style()).clone(); - /// style.spacing.item_spacing = egui::vec2(10.0, 20.0); - /// ctx.set_style(style); + /// ctx.style_mut(|style| { + /// style.spacing.item_spacing = egui::vec2(10.0, 20.0); + /// }); /// ``` + pub fn style_mut(&self, mutate_style: impl FnOnce(&mut Style)) { + self.options_mut(|opt| mutate_style(std::sync::Arc::make_mut(&mut opt.style))); + } + + /// The [`Style`] used by all new windows, panels etc. + /// + /// You can also change this using [`Self::style_mut]` + /// + /// You can use [`Ui::style_mut`] to change the style of a single [`Ui`]. pub fn set_style(&self, style: impl Into>) { self.options_mut(|opt| opt.style = style.into()); } @@ -1118,9 +1154,16 @@ impl Context { /// Allocate a texture. /// + /// This is for advanced users. + /// Most users should use [`crate::Ui::image`] or [`Self::try_load_texture`] + /// instead. + /// /// In order to display an image you must convert it to a texture using this function. + /// The function will hand over the image data to the egui backend, which will + /// upload it to the GPU. /// - /// Make sure to only call this once for each image, i.e. NOT in your main GUI code. + /// ⚠️ Make sure to only call this ONCE for each image, i.e. NOT in your main GUI code. + /// The call is NOT immediate safe. /// /// The given name can be useful for later debugging, and will be visible if you call [`Self::texture_ui`]. /// @@ -1143,12 +1186,12 @@ impl Context { /// }); /// /// // Show the image: - /// ui.image(texture, texture.size_vec2()); + /// ui.image((texture.id(), texture.size_vec2())); /// } /// } /// ``` /// - /// Se also [`crate::ImageData`], [`crate::Ui::image`] and [`crate::ImageButton`]. + /// See also [`crate::ImageData`], [`crate::Ui::image`] and [`crate::Image`]. pub fn load_texture( &self, name: impl Into, @@ -1241,14 +1284,13 @@ impl Context { }); #[cfg_attr(not(feature = "accesskit"), allow(unused_mut))] - let mut platform_output: PlatformOutput = self.output_mut(|o| std::mem::take(o)); + let mut platform_output: PlatformOutput = self.output_mut(std::mem::take); #[cfg(feature = "accesskit")] { crate::profile_scope!("accesskit"); let state = self.frame_state_mut(|fs| fs.accesskit_state.take()); if let Some(state) = state { - let has_focus = self.input(|i| i.raw.focused); let root_id = crate::accesskit_root_id().accesskit_id(); let nodes = self.write(|ctx| { state @@ -1262,13 +1304,13 @@ impl Context { }) .collect() }); + let focus_id = self + .memory(|mem| mem.focus()) + .map_or(root_id, |id| id.accesskit_id()); platform_output.accesskit_update = Some(accesskit::TreeUpdate { nodes, tree: Some(accesskit::Tree::new(root_id)), - focus: has_focus.then(|| { - let focus_id = self.memory(|mem| mem.interaction.focus.id); - focus_id.map_or(root_id, |id| id.accesskit_id()) - }), + focus: focus_id, }); } } @@ -1490,15 +1532,15 @@ impl Context { // --------------------------------------------------------------------- /// Whether or not to debug widget layout on hover. + #[cfg(debug_assertions)] pub fn debug_on_hover(&self) -> bool { self.options(|opt| opt.style.debug.debug_on_hover) } /// Turn on/off whether or not to debug widget layout on hover. + #[cfg(debug_assertions)] pub fn set_debug_on_hover(&self, debug_on_hover: bool) { - let mut style = self.options(|opt| (*opt.style).clone()); - style.debug.debug_on_hover = debug_on_hover; - self.set_style(style); + self.style_mut(|style| style.debug.debug_on_hover = debug_on_hover); } } @@ -1581,7 +1623,6 @@ impl Context { /// Show the state of egui, including its input and output. pub fn inspection_ui(&self, ui: &mut Ui) { use crate::containers::*; - crate::trace!(ui); ui.label(format!("Is using pointer: {}", self.is_using_pointer())) .on_hover_text( @@ -1689,14 +1730,15 @@ impl Context { let mut size = vec2(w as f32, h as f32); size *= (max_preview_size.x / size.x).min(1.0); size *= (max_preview_size.y / size.y).min(1.0); - ui.image(texture_id, size).on_hover_ui(|ui| { - // show larger on hover - let max_size = 0.5 * ui.ctx().screen_rect().size(); - let mut size = vec2(w as f32, h as f32); - size *= max_size.x / size.x.max(max_size.x); - size *= max_size.y / size.y.max(max_size.y); - ui.image(texture_id, size); - }); + ui.image(SizedTexture::new(texture_id, size)) + .on_hover_ui(|ui| { + // show larger on hover + let max_size = 0.5 * ui.ctx().screen_rect().size(); + let mut size = vec2(w as f32, h as f32); + size *= max_size.x / size.x.max(max_size.x); + size *= max_size.y / size.y.max(max_size.y); + ui.image(SizedTexture::new(texture_id, size)); + }); ui.label(format!("{w} x {h}")); ui.label(format!("{:.3} MB", meta.bytes_used() as f64 * 1e-6)); @@ -1898,7 +1940,7 @@ impl Context { NodeBuilder::new(Role::Window).build(&mut ctx.accesskit_node_classes), )], tree: Some(Tree::new(root_id)), - focus: None, + focus: root_id, }) } } @@ -1907,60 +1949,93 @@ impl Context { impl Context { /// Associate some static bytes with a `uri`. /// - /// The same `uri` may be passed to [`Ui::image2`] later to load the bytes as an image. - pub fn include_static_bytes(&self, uri: &'static str, bytes: &'static [u8]) { - self.read(|ctx| ctx.loaders.include.insert_static(uri, bytes)); + /// The same `uri` may be passed to [`Ui::image`] later to load the bytes as an image. + /// + /// By convention, the `uri` should start with `bytes://`. + /// Following that convention will lead to better error messages. + pub fn include_bytes(&self, uri: impl Into>, bytes: impl Into) { + self.loaders().include.insert(uri, bytes); } - /// Associate some bytes with a `uri`. - /// - /// The same `uri` may be passed to [`Ui::image2`] later to load the bytes as an image. - pub fn include_bytes(&self, uri: &'static str, bytes: impl Into>) { - self.read(|ctx| ctx.loaders.include.insert_shared(uri, bytes)); + /// Returns `true` if the chain of bytes, image, or texture loaders + /// contains a loader with the given `id`. + pub fn is_loader_installed(&self, id: &str) -> bool { + let loaders = self.loaders(); + + loaders.bytes.lock().iter().any(|l| l.id() == id) + || loaders.image.lock().iter().any(|l| l.id() == id) + || loaders.texture.lock().iter().any(|l| l.id() == id) } - /// Append an entry onto the chain of bytes loaders. + /// Add a new bytes loader. + /// + /// It will be tried first, before any already installed loaders. /// /// See [`load`] for more information. pub fn add_bytes_loader(&self, loader: Arc) { - self.write(|ctx| ctx.loaders.bytes.push(loader)); + self.loaders().bytes.lock().push(loader); } - /// Append an entry onto the chain of image loaders. + /// Add a new image loader. + /// + /// It will be tried first, before any already installed loaders. /// /// See [`load`] for more information. pub fn add_image_loader(&self, loader: Arc) { - self.write(|ctx| ctx.loaders.image.push(loader)); + self.loaders().image.lock().push(loader); } - /// Append an entry onto the chain of texture loaders. + /// Add a new texture loader. + /// + /// It will be tried first, before any already installed loaders. /// /// See [`load`] for more information. pub fn add_texture_loader(&self, loader: Arc) { - self.write(|ctx| ctx.loaders.texture.push(loader)); + self.loaders().texture.lock().push(loader); } /// Release all memory and textures related to the given image URI. /// /// If you attempt to load the image again, it will be reloaded from scratch. pub fn forget_image(&self, uri: &str) { - self.write(|ctx| { - use crate::load::BytesLoader as _; + use load::BytesLoader as _; - ctx.loaders.include.forget(uri); + crate::profile_function!(); - for loader in &ctx.loaders.bytes { - loader.forget(uri); - } + let loaders = self.loaders(); - for loader in &ctx.loaders.image { - loader.forget(uri); - } + loaders.include.forget(uri); + for loader in loaders.bytes.lock().iter() { + loader.forget(uri); + } + for loader in loaders.image.lock().iter() { + loader.forget(uri); + } + for loader in loaders.texture.lock().iter() { + loader.forget(uri); + } + } - for loader in &ctx.loaders.texture { - loader.forget(uri); - } - }); + /// Release all memory and textures related to images used in [`Ui::image`] or [`Image`]. + /// + /// If you attempt to load any images again, they will be reloaded from scratch. + pub fn forget_all_images(&self) { + use load::BytesLoader as _; + + crate::profile_function!(); + + let loaders = self.loaders(); + + loaders.include.forget_all(); + for loader in loaders.bytes.lock().iter() { + loader.forget_all(); + } + for loader in loaders.image.lock().iter() { + loader.forget_all(); + } + for loader in loaders.texture.lock().iter() { + loader.forget_all(); + } } /// Try loading the bytes from the given uri using any available bytes loaders. @@ -1975,20 +2050,27 @@ impl Context { /// # Errors /// This may fail with: /// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`. - /// - [`LoadError::Custom`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed. + /// - [`LoadError::Loading`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed. + /// + /// ⚠ May deadlock if called from within a `BytesLoader`! /// /// [not_supported]: crate::load::LoadError::NotSupported - /// [custom]: crate::load::LoadError::Custom + /// [custom]: crate::load::LoadError::Loading pub fn try_load_bytes(&self, uri: &str) -> load::BytesLoadResult { + crate::profile_function!(); + let loaders = self.loaders(); - for loader in &loaders.bytes { + let bytes_loaders = loaders.bytes.lock(); + + // Try most recently added loaders first (hence `.rev()`) + for loader in bytes_loaders.iter().rev() { match loader.load(self, uri) { Err(load::LoadError::NotSupported) => continue, result => return result, } } - Err(load::LoadError::NotSupported) + Err(load::LoadError::NoMatchingBytesLoader) } /// Try loading the image from the given uri using any available image loaders. @@ -2002,21 +2084,33 @@ impl Context { /// /// # Errors /// This may fail with: + /// - [`LoadError::NoImageLoaders`][no_image_loaders] if tbere are no registered image loaders. /// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`. - /// - [`LoadError::Custom`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed. + /// - [`LoadError::Loading`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed. /// + /// ⚠ May deadlock if called from within an `ImageLoader`! + /// + /// [no_image_loaders]: crate::load::LoadError::NoImageLoaders /// [not_supported]: crate::load::LoadError::NotSupported - /// [custom]: crate::load::LoadError::Custom + /// [custom]: crate::load::LoadError::Loading pub fn try_load_image(&self, uri: &str, size_hint: load::SizeHint) -> load::ImageLoadResult { + crate::profile_function!(); + let loaders = self.loaders(); - for loader in &loaders.image { + let image_loaders = loaders.image.lock(); + if image_loaders.is_empty() { + return Err(load::LoadError::NoImageLoaders); + } + + // Try most recently added loaders first (hence `.rev()`) + for loader in image_loaders.iter().rev() { match loader.load(self, uri, size_hint) { Err(load::LoadError::NotSupported) => continue, result => return result, } } - Err(load::LoadError::NotSupported) + Err(load::LoadError::NoMatchingImageLoader) } /// Try loading the texture from the given uri using any available texture loaders. @@ -2031,31 +2125,38 @@ impl Context { /// # Errors /// This may fail with: /// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`. - /// - [`LoadError::Custom`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed. + /// - [`LoadError::Loading`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed. + /// + /// ⚠ May deadlock if called from within a `TextureLoader`! /// /// [not_supported]: crate::load::LoadError::NotSupported - /// [custom]: crate::load::LoadError::Custom + /// [custom]: crate::load::LoadError::Loading pub fn try_load_texture( &self, uri: &str, texture_options: TextureOptions, size_hint: load::SizeHint, ) -> load::TextureLoadResult { + crate::profile_function!(); + let loaders = self.loaders(); + let texture_loaders = loaders.texture.lock(); - for loader in &loaders.texture { + // Try most recently added loaders first (hence `.rev()`) + for loader in texture_loaders.iter().rev() { match loader.load(self, uri, texture_options, size_hint) { Err(load::LoadError::NotSupported) => continue, result => return result, } } - Err(load::LoadError::NotSupported) + Err(load::LoadError::NoMatchingTextureLoader) } - fn loaders(&self) -> load::Loaders { + /// The loaders of bytes, images, and textures. + pub fn loaders(&self) -> Arc { crate::profile_function!(); - self.read(|this| this.loaders.clone()) // TODO(emilk): something less slow + self.read(|this| this.loaders.clone()) } } diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 61eb152d8e2..4ed234c05df 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -455,6 +455,11 @@ impl Modifiers { !self.is_none() } + #[inline] + pub fn all(&self) -> bool { + self.alt && self.ctrl && self.shift && self.command + } + /// Is shift the only pressed button? #[inline] pub fn shift_only(&self) -> bool { @@ -1028,3 +1033,62 @@ impl From for TouchId { Self(id as u64) } } + +// ---------------------------------------------------------------------------- + +// TODO(emilk): generalize this to a proper event filter. +/// Controls which events that a focused widget will have exclusive access to. +/// +/// Currently this only controls a few special keyboard events, +/// but in the future this `struct` should be extended into a full callback thing. +/// +/// Any events not covered by the filter are given to the widget, but are not exclusive. +#[derive(Clone, Copy, Debug)] +pub struct EventFilter { + /// If `true`, pressing tab will act on the widget, + /// and NOT move focus away from the focused widget. + /// + /// Default: `false` + pub tab: bool, + + /// If `true`, pressing arrows will act on the widget, + /// and NOT move focus away from the focused widget. + /// + /// Default: `false` + pub arrows: bool, + + /// If `true`, pressing escape will act on the widget, + /// and NOT surrender focus from the focused widget. + /// + /// Default: `false` + pub escape: bool, +} + +#[allow(clippy::derivable_impls)] // let's be explicit +impl Default for EventFilter { + fn default() -> Self { + Self { + tab: false, + arrows: false, + escape: false, + } + } +} + +impl EventFilter { + pub fn matches(&self, event: &Event) -> bool { + if let Event::Key { key, .. } = event { + match key { + crate::Key::Tab => self.tab, + crate::Key::ArrowUp + | crate::Key::ArrowRight + | crate::Key::ArrowDown + | crate::Key::ArrowLeft => self.arrows, + crate::Key::Escape => self.escape, + _ => true, + } + } else { + true + } + } +} diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index 156a3492849..2e60db0d7e6 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -84,6 +84,8 @@ pub struct PlatformOutput { pub mutable_text_under_cursor: bool, /// Screen-space position of text edit cursor (used for IME). + /// + /// Iff `Some`, the user is editing text. pub text_cursor_pos: Option, #[cfg(feature = "accesskit")] @@ -92,7 +94,9 @@ pub struct PlatformOutput { impl PlatformOutput { /// Open the given url in a web browser. + /// /// If egui is running in a browser, the same tab will be reused. + #[deprecated = "Use Context::open_url instead"] pub fn open_url(&mut self, url: impl ToString) { self.open_url = Some(OpenUrl::same_tab(url)); } @@ -156,6 +160,8 @@ impl PlatformOutput { } /// What URL to open, and how. +/// +/// Use with [`crate::Context::open_url`]. #[derive(Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct OpenUrl { diff --git a/crates/egui/src/frame_state.rs b/crates/egui/src/frame_state.rs index f82b2e7e600..49c3a5f1077 100644 --- a/crates/egui/src/frame_state.rs +++ b/crates/egui/src/frame_state.rs @@ -54,6 +54,9 @@ pub(crate) struct FrameState { /// Highlight these widgets the next frame. Write to this. pub(crate) highlight_next_frame: IdSet, + + #[cfg(debug_assertions)] + pub(crate) has_debug_viewed_this_frame: bool, } impl Default for FrameState { @@ -70,6 +73,9 @@ impl Default for FrameState { accesskit_state: None, highlight_this_frame: Default::default(), highlight_next_frame: Default::default(), + + #[cfg(debug_assertions)] + has_debug_viewed_this_frame: false, } } } @@ -89,6 +95,9 @@ impl FrameState { accesskit_state, highlight_this_frame, highlight_next_frame, + + #[cfg(debug_assertions)] + has_debug_viewed_this_frame, } = self; used_ids.clear(); @@ -99,6 +108,11 @@ impl FrameState { *scroll_delta = input.scroll_delta; *scroll_target = [None, None]; + #[cfg(debug_assertions)] + { + *has_debug_viewed_this_frame = false; + } + #[cfg(feature = "accesskit")] { *accesskit_state = None; diff --git a/crates/egui/src/grid.rs b/crates/egui/src/grid.rs index 3f1dc592c8c..fe8f952bd28 100644 --- a/crates/egui/src/grid.rs +++ b/crates/egui/src/grid.rs @@ -187,24 +187,27 @@ impl GridLayout { } pub(crate) fn advance(&mut self, cursor: &mut Rect, _frame_rect: Rect, widget_rect: Rect) { - let debug_expand_width = self.style.debug.show_expand_width; - let debug_expand_height = self.style.debug.show_expand_height; - if debug_expand_width || debug_expand_height { - let rect = widget_rect; - let too_wide = rect.width() > self.prev_col_width(self.col); - let too_high = rect.height() > self.prev_row_height(self.row); - - if (debug_expand_width && too_wide) || (debug_expand_height && too_high) { - let painter = self.ctx.debug_painter(); - painter.rect_stroke(rect, 0.0, (1.0, Color32::LIGHT_BLUE)); - - let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0)); - let paint_line_seg = |a, b| painter.line_segment([a, b], stroke); - - if debug_expand_width && too_wide { - paint_line_seg(rect.left_top(), rect.left_bottom()); - paint_line_seg(rect.left_center(), rect.right_center()); - paint_line_seg(rect.right_top(), rect.right_bottom()); + #[cfg(debug_assertions)] + { + let debug_expand_width = self.style.debug.show_expand_width; + let debug_expand_height = self.style.debug.show_expand_height; + if debug_expand_width || debug_expand_height { + let rect = widget_rect; + let too_wide = rect.width() > self.prev_col_width(self.col); + let too_high = rect.height() > self.prev_row_height(self.row); + + if (debug_expand_width && too_wide) || (debug_expand_height && too_high) { + let painter = self.ctx.debug_painter(); + painter.rect_stroke(rect, 0.0, (1.0, Color32::LIGHT_BLUE)); + + let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0)); + let paint_line_seg = |a, b| painter.line_segment([a, b], stroke); + + if debug_expand_width && too_wide { + paint_line_seg(rect.left_top(), rect.left_bottom()); + paint_line_seg(rect.left_center(), rect.right_center()); + paint_line_seg(rect.right_top(), rect.right_bottom()); + } } } } @@ -218,7 +221,7 @@ impl GridLayout { self.col += 1; } - fn paint_row(&mut self, cursor: &mut Rect, painter: &Painter) { + fn paint_row(&mut self, cursor: &Rect, painter: &Painter) { // handle row color painting based on color-picker function let Some(color_picker) = self.color_picker.as_ref() else { return; @@ -430,9 +433,9 @@ impl Grid { // paint first incoming row if is_color { - let mut cursor = ui.cursor(); + let cursor = ui.cursor(); let painter = ui.painter(); - grid.paint_row(&mut cursor, painter); + grid.paint_row(&cursor, painter); } ui.set_grid(grid); diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index 612314312fd..75cc9f8568f 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -72,7 +72,7 @@ impl Id { #[cfg(feature = "accesskit")] pub(crate) fn accesskit_id(&self) -> accesskit::NodeId { - std::num::NonZeroU64::new(self.0).unwrap().into() + self.0.into() } } diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state.rs index 7f94cf16a2a..47b8544f2be 100644 --- a/crates/egui/src/input_state.rs +++ b/crates/egui/src/input_state.rs @@ -461,6 +461,15 @@ impl InputState { pub fn num_accesskit_action_requests(&self, id: crate::Id, action: accesskit::Action) -> usize { self.accesskit_action_requests(id, action).count() } + + /// Get all events that matches the given filter. + pub fn filtered_events(&self, filter: &EventFilter) -> Vec { + self.events + .iter() + .filter(|event| filter.matches(event)) + .cloned() + .collect() + } } // ---------------------------------------------------------------------------- diff --git a/crates/egui/src/layout.rs b/crates/egui/src/layout.rs index 8da23700a23..1dd77c21580 100644 --- a/crates/egui/src/layout.rs +++ b/crates/egui/src/layout.rs @@ -801,6 +801,7 @@ impl Layout { /// ## Debug stuff impl Layout { /// Shows where the next widget is going to be placed + #[cfg(debug_assertions)] pub(crate) fn paint_text_at_cursor( &self, painter: &crate::Painter, diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 590828ed4fd..1cd7273e69e 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -12,6 +12,10 @@ //! Then you add a [`Window`] or a [`SidePanel`] to get a [`Ui`], which is what you'll be using to add all the buttons and labels that you need. //! //! +//! ## Feature flags +#![cfg_attr(feature = "document-features", doc = document_features::document_features!())] +//! +//! //! # Using egui //! //! To see what is possible to build with egui you can check out the online demo at . @@ -84,7 +88,7 @@ //! ui.separator(); //! //! # let my_image = egui::TextureId::default(); -//! ui.image(my_image, [640.0, 480.0]); +//! ui.image((my_image, egui::Vec2::new(640.0, 480.0))); //! //! ui.collapsing("Click to see what is hidden!", |ui| { //! ui.label("Not much, as it turns out"); @@ -107,9 +111,9 @@ //! //! Most likely you are using an existing `egui` backend/integration such as [`eframe`](https://docs.rs/eframe), [`bevy_egui`](https://docs.rs/bevy_egui), //! or [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad), -//! but if you want to integrate `egui` into a new game engine, this is the section for you. +//! but if you want to integrate `egui` into a new game engine or graphics backend, this is the section for you. //! -//! To write your own integration for egui you need to do this: +//! You need to collect [`RawInput`] and handle [`FullOutput`]. The basic structure is this: //! //! ``` no_run //! # fn handle_platform_output(_: egui::PlatformOutput) {} @@ -135,10 +139,42 @@ //! } //! ``` //! +//! For a reference OpenGL renderer, see [the `egui_glow` painter](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs). +//! +//! +//! ### Debugging your renderer +//! +//! #### Things look jagged +//! +//! * Turn off backface culling. +//! +//! #### My text is blurry +//! +//! * Make sure you set the proper `pixels_per_point` in the input to egui. +//! * Make sure the texture sampler is not off by half a pixel. Try nearest-neighbor sampler to check. +//! +//! #### My windows are too transparent or too dark +//! +//! * egui uses premultiplied alpha, so make sure your blending function is `(ONE, ONE_MINUS_SRC_ALPHA)`. +//! * Make sure your texture sampler is clamped (`GL_CLAMP_TO_EDGE`). +//! * egui prefers linear color spaces for all blending so: +//! * Use an sRGBA-aware texture if available (e.g. `GL_SRGB8_ALPHA8`). +//! * Otherwise: remember to decode gamma in the fragment shader. +//! * Decode the gamma of the incoming vertex colors in your vertex shader. +//! * Turn on sRGBA/linear framebuffer if available (`GL_FRAMEBUFFER_SRGB`). +//! * Otherwise: gamma-encode the colors before you write them again. +//! //! //! # Understanding immediate mode //! -//! `egui` is an immediate mode GUI library. It is useful to fully grok what "immediate mode" implies. +//! `egui` is an immediate mode GUI library. +//! +//! Immediate mode has its roots in gaming, where everything on the screen is painted at the +//! display refresh rate, i.e. at 60+ frames per second. +//! In immediate mode GUIs, the entire interface is laid out and painted at the same high rate. +//! This makes immediate mode GUIs especially well suited for highly interactive applications. +//! +//! It is useful to fully grok what "immediate mode" implies. //! //! Here is an example to illustrate it: //! @@ -173,7 +209,7 @@ //! # }); //! ``` //! -//! Here egui will read `value` to display the slider, then look if the mouse is dragging the slider and if so change the `value`. +//! Here egui will read `value` (an `f32`) to display the slider, then look if the mouse is dragging the slider and if so change the `value`. //! Note that `egui` does not store the slider value for you - it only displays the current value, and changes it //! by how much the slider has been dragged in the previous few milliseconds. //! This means it is responsibility of the egui user to store the state (`value`) so that it persists between frames. @@ -293,10 +329,6 @@ //! }); // the temporary settings are reverted here //! # }); //! ``` -//! -//! ## Feature flags -#![cfg_attr(feature = "document-features", doc = document_features::document_features!())] -//! #![allow(clippy::float_cmp)] #![allow(clippy::manual_range_contains)] @@ -328,6 +360,10 @@ pub mod util; pub mod widget_text; pub mod widgets; +#[cfg(feature = "callstack")] +#[cfg(debug_assertions)] +mod callstack; + #[cfg(feature = "accesskit")] pub use accesskit; @@ -364,7 +400,9 @@ pub use { context::{Context, RequestRepaintInfo}, data::{ input::*, - output::{self, CursorIcon, FullOutput, PlatformOutput, UserAttentionType, WidgetInfo}, + output::{ + self, CursorIcon, FullOutput, OpenUrl, PlatformOutput, UserAttentionType, WidgetInfo, + }, }, grid::Grid, id::{Id, IdMap}, @@ -399,6 +437,35 @@ pub fn warn_if_debug_build(ui: &mut crate::Ui) { // ---------------------------------------------------------------------------- +/// Include an image in the binary. +/// +/// This is a wrapper over `include_bytes!`, and behaves in the same way. +/// +/// It produces an [`ImageSource`] which can be used directly in [`Ui::image`] or [`Image::new`]: +/// +/// ``` +/// # egui::__run_test_ui(|ui| { +/// ui.image(egui::include_image!("../assets/ferris.png")); +/// ui.add( +/// egui::Image::new(egui::include_image!("../assets/ferris.png")) +/// .max_width(200.0) +/// .rounding(10.0), +/// ); +/// +/// let image_source: egui::ImageSource = egui::include_image!("../assets/ferris.png"); +/// assert_eq!(image_source.uri(), Some("bytes://../assets/ferris.png")); +/// # }); +/// ``` +#[macro_export] +macro_rules! include_image { + ($path: literal) => { + $crate::ImageSource::Bytes { + uri: ::std::borrow::Cow::Borrowed(concat!("bytes://", $path)), + bytes: $crate::load::Bytes::Static(include_bytes!($path)), + } + }; +} + /// Create a [`Hyperlink`](crate::Hyperlink) to the current [`file!()`] (and line) on Github /// /// ``` @@ -431,32 +498,6 @@ macro_rules! github_link_file { // ---------------------------------------------------------------------------- -/// Show debug info on hover when [`Context::set_debug_on_hover`] has been turned on. -/// -/// ``` -/// # egui::__run_test_ui(|ui| { -/// // Turn on tracing of widgets -/// ui.ctx().set_debug_on_hover(true); -/// -/// /// Show [`std::file`], [`std::line`] and argument on hover -/// egui::trace!(ui, "MyWindow"); -/// -/// /// Show [`std::file`] and [`std::line`] on hover -/// egui::trace!(ui); -/// # }); -/// ``` -#[macro_export] -macro_rules! trace { - ($ui: expr) => {{ - $ui.trace_location(format!("{}:{}", file!(), line!())) - }}; - ($ui: expr, $label: expr) => {{ - $ui.trace_location(format!("{} - {}:{}", $label, file!(), line!())) - }}; -} - -// ---------------------------------------------------------------------------- - /// An assert that is only active when `egui` is compiled with the `extra_asserts` feature /// or with the `extra_debug_asserts` feature in debug builds. #[macro_export] @@ -593,8 +634,8 @@ mod profiling_scopes { /// Profiling macro for feature "puffin" macro_rules! profile_function { ($($arg: tt)*) => { - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. #[cfg(feature = "puffin")] + #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. puffin::profile_function!($($arg)*); }; } @@ -603,12 +644,13 @@ mod profiling_scopes { /// Profiling macro for feature "puffin" macro_rules! profile_scope { ($($arg: tt)*) => { - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. #[cfg(feature = "puffin")] + #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. puffin::profile_scope!($($arg)*); }; } pub(crate) use profile_scope; } +#[allow(unused_imports)] pub(crate) use profiling_scopes::*; diff --git a/crates/egui/src/load.rs b/crates/egui/src/load.rs index 2188657111f..717383d3109 100644 --- a/crates/egui/src/load.rs +++ b/crates/egui/src/load.rs @@ -1,8 +1,12 @@ -//! Types and traits related to image loading. +//! # Image loading //! -//! If you just want to load some images, see [`egui_extras`](https://crates.io/crates/egui_extras/), -//! which contains reasonable default implementations of these traits. You can get started quickly -//! using [`egui_extras::loaders::install`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html). +//! If you just want to display some images, [`egui_extras`](https://crates.io/crates/egui_extras/) +//! will get you up and running quickly with its reasonable default implementations of the traits described below. +//! +//! 1. Add [`egui_extras`](https://crates.io/crates/egui_extras/) as a dependency with the `all_loaders` feature. +//! 2. Add a call to [`egui_extras::install_image_loaders`](https://docs.rs/egui_extras/latest/egui_extras/fn.install_image_loaders.html) +//! in your app's setup code. +//! 3. Use [`Ui::image`][`crate::ui::Ui::image`] with some [`ImageSource`][`crate::ImageSource`]. //! //! ## Loading process //! @@ -14,13 +18,13 @@ //! The different kinds of loaders represent different layers in the loading process: //! //! ```text,ignore -//! ui.image2("file://image.png") -//! └► ctx.try_load_texture("file://image.png", ...) -//! └► TextureLoader::load("file://image.png", ...) -//! └► ctx.try_load_image("file://image.png", ...) -//! └► ImageLoader::load("file://image.png", ...) -//! └► ctx.try_load_bytes("file://image.png", ...) -//! └► BytesLoader::load("file://image.png", ...) +//! ui.image("file://image.png") +//! └► Context::try_load_texture +//! └► TextureLoader::load +//! └► Context::try_load_image +//! └► ImageLoader::load +//! └► Context::try_load_bytes +//! └► BytesLoader::load //! ``` //! //! As each layer attempts to load the URI, it first asks the layer below it @@ -48,28 +52,68 @@ //! For example, a loader may determine that it doesn't support loading a specific URI //! if the protocol does not match what it expects. -use crate::Context; +mod bytes_loader; +mod texture_loader; + +use std::borrow::Cow; +use std::fmt::Debug; +use std::ops::Deref; +use std::{error::Error as StdError, fmt::Display, sync::Arc}; + use ahash::HashMap; + use epaint::mutex::Mutex; +use epaint::util::FloatOrd; +use epaint::util::OrderedFloat; use epaint::TextureHandle; use epaint::{textures::TextureOptions, ColorImage, TextureId, Vec2}; -use std::ops::Deref; -use std::{error::Error as StdError, fmt::Display, sync::Arc}; +use crate::Context; + +pub use self::bytes_loader::DefaultBytesLoader; +pub use self::texture_loader::DefaultTextureLoader; + +/// Represents a failed attempt at loading an image. #[derive(Clone, Debug)] pub enum LoadError { - /// This loader does not support this protocol or image format. + /// Programmer error: There are no image loaders installed. + NoImageLoaders, + + /// A specific loader does not support this scheme, protocol or image format. NotSupported, - /// A custom error message (e.g. "File not found: foo.png"). - Custom(String), + /// Programmer error: Failed to find the bytes for this image because + /// there was no [`BytesLoader`] supporting the scheme. + NoMatchingBytesLoader, + + /// Programmer error: Failed to parse the bytes as an image because + /// there was no [`ImageLoader`] supporting the scheme. + NoMatchingImageLoader, + + /// Programmer error: no matching [`TextureLoader`]. + /// Because of the [`DefaultTextureLoader`], this error should never happen. + NoMatchingTextureLoader, + + /// Runtime error: Loading was attempted, but failed (e.g. "File not found"). + Loading(String), } impl Display for LoadError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - LoadError::NotSupported => f.write_str("not supported"), - LoadError::Custom(message) => f.write_str(message), + Self::NoImageLoaders => f.write_str( + "No image loaders are installed. If you're trying to load some images \ + for the first time, follow the steps outlined in https://docs.rs/egui/latest/egui/load/index.html"), + + Self::NoMatchingBytesLoader => f.write_str("No matching BytesLoader. Either you need to call Context::include_bytes, or install some more bytes loaders, e.g. using egui_extras."), + + Self::NoMatchingImageLoader => f.write_str("No matching ImageLoader. Either you need to call Context::include_bytes, or install some more bytes loaders, e.g. using egui_extras."), + + Self::NoMatchingTextureLoader => f.write_str("No matching TextureLoader. Did you remove the default one?"), + + Self::NotSupported => f.write_str("Image scheme or URI not supported by this loader"), + + Self::Loading(message) => f.write_str(message), } } } @@ -85,11 +129,10 @@ pub type Result = std::result::Result; /// All variants will preserve the original aspect ratio. /// /// Similar to `usvg::FitTo`. -#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum SizeHint { - /// Keep original size. - #[default] - Original, + /// Scale original size by some factor. + Scale(OrderedFloat), /// Scale to width. Width(u32), @@ -101,22 +144,36 @@ pub enum SizeHint { Size(u32, u32), } +impl Default for SizeHint { + fn default() -> Self { + Self::Scale(1.0.ord()) + } +} + impl From for SizeHint { fn from(value: Vec2) -> Self { Self::Size(value.x.round() as u32, value.y.round() as u32) } } -// TODO: API for querying bytes caches in each loader - -pub type Size = [usize; 2]; - +/// Represents a byte buffer. +/// +/// This is essentially `Cow<'static, [u8]>` but with the `Owned` variant being an `Arc`. #[derive(Clone)] pub enum Bytes { Static(&'static [u8]), Shared(Arc<[u8]>), } +impl Debug for Bytes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Static(arg0) => f.debug_tuple("Static").field(&arg0.len()).finish(), + Self::Shared(arg0) => f.debug_tuple("Shared").field(&arg0.len()).finish(), + } + } +} + impl From<&'static [u8]> for Bytes { #[inline] fn from(value: &'static [u8]) -> Self { @@ -124,6 +181,13 @@ impl From<&'static [u8]> for Bytes { } } +impl From<&'static [u8; N]> for Bytes { + #[inline] + fn from(value: &'static [u8; N]) -> Self { + Bytes::Static(value) + } +} + impl From> for Bytes { #[inline] fn from(value: Arc<[u8]>) -> Self { @@ -131,6 +195,13 @@ impl From> for Bytes { } } +impl From> for Bytes { + #[inline] + fn from(value: Vec) -> Self { + Bytes::Shared(value.into()) + } +} + impl AsRef<[u8]> for Bytes { #[inline] fn as_ref(&self) -> &[u8] { @@ -150,27 +221,60 @@ impl Deref for Bytes { } } +/// Represents bytes which are currently being loaded. +/// +/// This is similar to [`std::task::Poll`], but the `Pending` variant +/// contains an optional `size`, which may be used during layout to +/// pre-allocate space the image. #[derive(Clone)] pub enum BytesPoll { /// Bytes are being loaded. Pending { /// Set if known (e.g. from a HTTP header, or by parsing the image file header). - size: Option, + size: Option, }, /// Bytes are loaded. Ready { /// Set if known (e.g. from a HTTP header, or by parsing the image file header). - size: Option, + size: Option, /// File contents, e.g. the contents of a `.png`. bytes: Bytes, + + /// Mime type of the content, e.g. `image/png`. + /// + /// Set if known (e.g. from `Content-Type` HTTP header). + mime: Option, }, } +/// Used to get a unique ID when implementing one of the loader traits: [`BytesLoader::id`], [`ImageLoader::id`], and [`TextureLoader::id`]. +/// +/// This just expands to `module_path!()` concatenated with the given type name. +#[macro_export] +macro_rules! generate_loader_id { + ($ty:ident) => { + concat!(module_path!(), "::", stringify!($ty)) + }; +} +pub use crate::generate_loader_id; + pub type BytesLoadResult = Result; +/// Represents a loader capable of loading raw unstructured bytes from somewhere, +/// e.g. from disk or network. +/// +/// It should also provide any subsequent loaders a hint for what the bytes may +/// represent using [`BytesPoll::Ready::mime`], if it can be inferred. +/// +/// Implementations are expected to cache at least each `URI`. pub trait BytesLoader { + /// Unique ID of this loader. + /// + /// To reduce the chance of collisions, use [`generate_loader_id`] for this. + fn id(&self) -> &str; + /// Try loading the bytes from the given uri. /// /// Implementations should call `ctx.request_repaint` to wake up the ui @@ -182,7 +286,7 @@ pub trait BytesLoader { /// # Errors /// This may fail with: /// - [`LoadError::NotSupported`] if the loader does not support loading `uri`. - /// - [`LoadError::Custom`] if the loading process failed. + /// - [`LoadError::Loading`] if the loading process failed. fn load(&self, ctx: &Context, uri: &str) -> BytesLoadResult; /// Forget the given `uri`. @@ -191,6 +295,12 @@ pub trait BytesLoader { /// so that it may be fully reloaded. fn forget(&self, uri: &str); + /// Forget all URIs ever given to this loader. + /// + /// If the loader caches any URIs, the entire cache should be cleared, + /// so that all of them may be fully reloaded. + fn forget_all(&self); + /// Implementations may use this to perform work at the end of a frame, /// such as evicting unused entries from a cache. fn end_frame(&self, frame_index: usize) { @@ -201,12 +311,17 @@ pub trait BytesLoader { fn byte_size(&self) -> usize; } +/// Represents an image which is currently being loaded. +/// +/// This is similar to [`std::task::Poll`], but the `Pending` variant +/// contains an optional `size`, which may be used during layout to +/// pre-allocate space the image. #[derive(Clone)] pub enum ImagePoll { /// Image is loading. Pending { /// Set if known (e.g. from a HTTP header, or by parsing the image file header). - size: Option, + size: Option, }, /// Image is loaded. @@ -215,7 +330,18 @@ pub enum ImagePoll { pub type ImageLoadResult = Result; +/// An `ImageLoader` decodes raw bytes into a [`ColorImage`]. +/// +/// Implementations are expected to cache at least each `URI`. pub trait ImageLoader { + /// Unique ID of this loader. + /// + /// To reduce the chance of collisions, include `module_path!()` as part of this ID. + /// + /// For example: `concat!(module_path!(), "::MyLoader")` + /// for `my_crate::my_loader::MyLoader`. + fn id(&self) -> &str; + /// Try loading the image from the given uri. /// /// Implementations should call `ctx.request_repaint` to wake up the ui @@ -227,7 +353,7 @@ pub trait ImageLoader { /// # Errors /// This may fail with: /// - [`LoadError::NotSupported`] if the loader does not support loading `uri`. - /// - [`LoadError::Custom`] if the loading process failed. + /// - [`LoadError::Loading`] if the loading process failed. fn load(&self, ctx: &Context, uri: &str, size_hint: SizeHint) -> ImageLoadResult; /// Forget the given `uri`. @@ -236,6 +362,12 @@ pub trait ImageLoader { /// so that it may be fully reloaded. fn forget(&self, uri: &str); + /// Forget all URIs ever given to this loader. + /// + /// If the loader caches any URIs, the entire cache should be cleared, + /// so that all of them may be fully reloaded. + fn forget_all(&self); + /// Implementations may use this to perform work at the end of a frame, /// such as evicting unused entries from a cache. fn end_frame(&self, frame_index: usize) { @@ -247,36 +379,91 @@ pub trait ImageLoader { } /// A texture with a known size. -#[derive(Clone)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct SizedTexture { pub id: TextureId, - pub size: Size, + pub size: Vec2, } impl SizedTexture { + /// Create a [`SizedTexture`] from a texture `id` with a specific `size`. + pub fn new(id: impl Into, size: impl Into) -> Self { + Self { + id: id.into(), + size: size.into(), + } + } + + /// Fetch the [id][`SizedTexture::id`] and [size][`SizedTexture::size`] from a [`TextureHandle`]. pub fn from_handle(handle: &TextureHandle) -> Self { + let size = handle.size(); Self { id: handle.id(), - size: handle.size(), + size: Vec2::new(size[0] as f32, size[1] as f32), } } } -#[derive(Clone)] +impl From<(TextureId, Vec2)> for SizedTexture { + #[inline] + fn from((id, size): (TextureId, Vec2)) -> Self { + Self { id, size } + } +} + +impl<'a> From<&'a TextureHandle> for SizedTexture { + fn from(handle: &'a TextureHandle) -> Self { + Self::from_handle(handle) + } +} + +/// Represents a texture is currently being loaded. +/// +/// This is similar to [`std::task::Poll`], but the `Pending` variant +/// contains an optional `size`, which may be used during layout to +/// pre-allocate space the image. +#[derive(Clone, Copy)] pub enum TexturePoll { /// Texture is loading. Pending { /// Set if known (e.g. from a HTTP header, or by parsing the image file header). - size: Option, + size: Option, }, /// Texture is loaded. Ready { texture: SizedTexture }, } +impl TexturePoll { + pub fn size(self) -> Option { + match self { + TexturePoll::Pending { size } => size, + TexturePoll::Ready { texture } => Some(texture.size), + } + } +} + pub type TextureLoadResult = Result; +/// A `TextureLoader` uploads a [`ColorImage`] to the GPU, returning a [`SizedTexture`]. +/// +/// `egui` comes with an implementation that uses [`Context::load_texture`], +/// which just asks the egui backend to upload the image to the GPU. +/// +/// You can implement this trait if you do your own uploading of images to the GPU. +/// For instance, you can use this to refer to textures in a game engine that egui +/// doesn't otherwise know about. +/// +/// Implementations are expected to cache each combination of `(URI, TextureOptions)`. pub trait TextureLoader { + /// Unique ID of this loader. + /// + /// To reduce the chance of collisions, include `module_path!()` as part of this ID. + /// + /// For example: `concat!(module_path!(), "::MyLoader")` + /// for `my_crate::my_loader::MyLoader`. + fn id(&self) -> &str; + /// Try loading the texture from the given uri. /// /// Implementations should call `ctx.request_repaint` to wake up the ui @@ -288,7 +475,7 @@ pub trait TextureLoader { /// # Errors /// This may fail with: /// - [`LoadError::NotSupported`] if the loader does not support loading `uri`. - /// - [`LoadError::Custom`] if the loading process failed. + /// - [`LoadError::Loading`] if the loading process failed. fn load( &self, ctx: &Context, @@ -303,6 +490,12 @@ pub trait TextureLoader { /// so that it may be fully reloaded. fn forget(&self, uri: &str); + /// Forget all URIs ever given to this loader. + /// + /// If the loader caches any URIs, the entire cache should be cleared, + /// so that all of them may be fully reloaded. + fn forget_all(&self); + /// Implementations may use this to perform work at the end of a frame, /// such as evicting unused entries from a cache. fn end_frame(&self, frame_index: usize) { @@ -313,105 +506,27 @@ pub trait TextureLoader { fn byte_size(&self) -> usize; } -#[derive(Default)] -pub(crate) struct DefaultBytesLoader { - cache: Mutex>, -} - -impl DefaultBytesLoader { - pub(crate) fn insert_static(&self, uri: &'static str, bytes: &'static [u8]) { - self.cache - .lock() - .entry(uri) - .or_insert_with(|| Bytes::Static(bytes)); - } - - pub(crate) fn insert_shared(&self, uri: &'static str, bytes: impl Into>) { - self.cache - .lock() - .entry(uri) - .or_insert_with(|| Bytes::Shared(bytes.into())); - } -} - -impl BytesLoader for DefaultBytesLoader { - fn load(&self, _: &Context, uri: &str) -> BytesLoadResult { - match self.cache.lock().get(uri).cloned() { - Some(bytes) => Ok(BytesPoll::Ready { size: None, bytes }), - None => Err(LoadError::NotSupported), - } - } - - fn forget(&self, uri: &str) { - let _ = self.cache.lock().remove(uri); - } - - fn byte_size(&self) -> usize { - self.cache.lock().values().map(|bytes| bytes.len()).sum() - } -} - -#[derive(Default)] -struct DefaultTextureLoader { - cache: Mutex>, -} - -impl TextureLoader for DefaultTextureLoader { - fn load( - &self, - ctx: &Context, - uri: &str, - texture_options: TextureOptions, - size_hint: SizeHint, - ) -> TextureLoadResult { - let mut cache = self.cache.lock(); - if let Some(handle) = cache.get(&(uri.into(), texture_options)) { - let texture = SizedTexture::from_handle(handle); - Ok(TexturePoll::Ready { texture }) - } else { - match ctx.try_load_image(uri, size_hint)? { - ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }), - ImagePoll::Ready { image } => { - let handle = ctx.load_texture(uri, image, texture_options); - let texture = SizedTexture::from_handle(&handle); - cache.insert((uri.into(), texture_options), handle); - Ok(TexturePoll::Ready { texture }) - } - } - } - } - - fn forget(&self, uri: &str) { - self.cache.lock().retain(|(u, _), _| u != uri); - } - - fn end_frame(&self, _: usize) {} - - fn byte_size(&self) -> usize { - self.cache - .lock() - .values() - .map(|texture| texture.byte_size()) - .sum() - } -} +type BytesLoaderImpl = Arc; +type ImageLoaderImpl = Arc; +type TextureLoaderImpl = Arc; #[derive(Clone)] -pub(crate) struct Loaders { +/// The loaders of bytes, images, and textures. +pub struct Loaders { pub include: Arc, - pub bytes: Vec>, - pub image: Vec>, - pub texture: Vec>, + pub bytes: Mutex>, + pub image: Mutex>, + pub texture: Mutex>, } impl Default for Loaders { fn default() -> Self { let include = Arc::new(DefaultBytesLoader::default()); Self { - bytes: vec![include.clone()], - image: Vec::new(), + bytes: Mutex::new(vec![include.clone()]), + image: Mutex::new(Vec::new()), // By default we only include `DefaultTextureLoader`. - texture: vec![Arc::new(DefaultTextureLoader::default())], + texture: Mutex::new(vec![Arc::new(DefaultTextureLoader::default())]), include, } } diff --git a/crates/egui/src/load/bytes_loader.rs b/crates/egui/src/load/bytes_loader.rs new file mode 100644 index 00000000000..3ab46794912 --- /dev/null +++ b/crates/egui/src/load/bytes_loader.rs @@ -0,0 +1,69 @@ +use super::*; + +/// Maps URI:s to [`Bytes`], e.g. found with `include_bytes!`. +/// +/// By convention, the URI:s should be prefixed with `bytes://`. +#[derive(Default)] +pub struct DefaultBytesLoader { + cache: Mutex, Bytes>>, +} + +impl DefaultBytesLoader { + pub fn insert(&self, uri: impl Into>, bytes: impl Into) { + self.cache + .lock() + .entry(uri.into()) + .or_insert_with_key(|_uri| { + let bytes: Bytes = bytes.into(); + + #[cfg(feature = "log")] + log::trace!("loaded {} bytes for uri {_uri:?}", bytes.len()); + + bytes + }); + } +} + +impl BytesLoader for DefaultBytesLoader { + fn id(&self) -> &str { + generate_loader_id!(DefaultBytesLoader) + } + + fn load(&self, _: &Context, uri: &str) -> BytesLoadResult { + // We accept uri:s that don't start with `bytes://` too… for now. + match self.cache.lock().get(uri).cloned() { + Some(bytes) => Ok(BytesPoll::Ready { + size: None, + bytes, + mime: None, + }), + None => { + if uri.starts_with("bytes://") { + Err(LoadError::Loading( + "Bytes not found. Did you forget to call Context::include_bytes?".into(), + )) + } else { + Err(LoadError::NotSupported) + } + } + } + } + + fn forget(&self, uri: &str) { + #[cfg(feature = "log")] + log::trace!("forget {uri:?}"); + + let _ = self.cache.lock().remove(uri); + } + + fn forget_all(&self) { + #[cfg(feature = "log")] + log::trace!("forget all"); + + self.cache.lock().clear(); + } + + fn byte_size(&self) -> usize { + self.cache.lock().values().map(|bytes| bytes.len()).sum() + } +} diff --git a/crates/egui/src/load/texture_loader.rs b/crates/egui/src/load/texture_loader.rs new file mode 100644 index 00000000000..89d616e4760 --- /dev/null +++ b/crates/egui/src/load/texture_loader.rs @@ -0,0 +1,60 @@ +use super::*; + +#[derive(Default)] +pub struct DefaultTextureLoader { + cache: Mutex>, +} + +impl TextureLoader for DefaultTextureLoader { + fn id(&self) -> &str { + crate::generate_loader_id!(DefaultTextureLoader) + } + + fn load( + &self, + ctx: &Context, + uri: &str, + texture_options: TextureOptions, + size_hint: SizeHint, + ) -> TextureLoadResult { + let mut cache = self.cache.lock(); + if let Some(handle) = cache.get(&(uri.into(), texture_options)) { + let texture = SizedTexture::from_handle(handle); + Ok(TexturePoll::Ready { texture }) + } else { + match ctx.try_load_image(uri, size_hint)? { + ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }), + ImagePoll::Ready { image } => { + let handle = ctx.load_texture(uri, image, texture_options); + let texture = SizedTexture::from_handle(&handle); + cache.insert((uri.into(), texture_options), handle); + Ok(TexturePoll::Ready { texture }) + } + } + } + } + + fn forget(&self, uri: &str) { + #[cfg(feature = "log")] + log::trace!("forget {uri:?}"); + + self.cache.lock().retain(|(u, _), _| u != uri); + } + + fn forget_all(&self) { + #[cfg(feature = "log")] + log::trace!("forget all"); + + self.cache.lock().clear(); + } + + fn end_frame(&self, _: usize) {} + + fn byte_size(&self) -> usize { + self.cache + .lock() + .values() + .map(|texture| texture.byte_size()) + .sum() + } +} diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index a997914e911..2c56d967248 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -1,6 +1,8 @@ +#![warn(missing_docs)] // Let's keep this file well-documented.` to memory.rs + use epaint::{emath::Rangef, vec2, Vec2}; -use crate::{area, window, Id, IdMap, InputState, LayerId, Pos2, Rect, Style}; +use crate::{area, window, EventFilter, Id, IdMap, InputState, LayerId, Pos2, Rect, Style}; // ---------------------------------------------------------------------------- @@ -17,6 +19,7 @@ use crate::{area, window, Id, IdMap, InputState, LayerId, Pos2, Rect, Style}; #[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "persistence", serde(default))] pub struct Memory { + /// Global egui options. pub options: Options, /// This map stores some superficial state for all widgets with custom [`Id`]s. @@ -219,7 +222,7 @@ pub(crate) struct Interaction { #[derive(Clone, Debug, Default)] pub(crate) struct Focus { /// The widget with keyboard focus (i.e. a text input field). - pub(crate) id: Option, + focused_widget: Option, /// What had keyboard focus previous frame? id_previous_frame: Option, @@ -237,9 +240,6 @@ pub(crate) struct Focus { /// The last widget interested in focus. last_interested: Option, - /// If `true`, pressing tab will NOT move focus away from the current widget. - is_focus_locked: bool, - /// Set when looking for widget with navigational keys like arrows, tab, shift+tab focus_direction: FocusDirection, @@ -247,6 +247,22 @@ pub(crate) struct Focus { focus_widgets_cache: IdMap, } +/// The widget with focus. +#[derive(Clone, Copy, Debug)] +struct FocusWidget { + pub id: Id, + pub filter: EventFilter, +} + +impl FocusWidget { + pub fn new(id: Id) -> Self { + Self { + id, + filter: Default::default(), + } + } +} + impl Interaction { /// Are we currently clicking or dragging an egui widget? pub fn is_using_pointer(&self) -> bool { @@ -278,14 +294,15 @@ impl Interaction { impl Focus { /// Which widget currently has keyboard focus? pub fn focused(&self) -> Option { - self.id + self.focused_widget.as_ref().map(|w| w.id) } fn begin_frame(&mut self, new_input: &crate::data::input::RawInput) { - self.id_previous_frame = self.id; + self.id_previous_frame = self.focused(); if let Some(id) = self.id_next_frame.take() { - self.id = Some(id); + self.focused_widget = Some(FocusWidget::new(id)); } + let event_filter = self.focused_widget.map(|w| w.filter).unwrap_or_default(); #[cfg(feature = "accesskit")] { @@ -295,37 +312,35 @@ impl Focus { self.focus_direction = FocusDirection::None; for event in &new_input.events { - if let crate::Event::Key { - key, - pressed: true, - modifiers, - .. - } = event - { - if let Some(cardinality) = match key { - crate::Key::ArrowUp => Some(FocusDirection::Up), - crate::Key::ArrowRight => Some(FocusDirection::Right), - crate::Key::ArrowDown => Some(FocusDirection::Down), - crate::Key::ArrowLeft => Some(FocusDirection::Left), - crate::Key::Tab => { - if !self.is_focus_locked { + if !event_filter.matches(event) { + if let crate::Event::Key { + key, + pressed: true, + modifiers, + .. + } = event + { + if let Some(cardinality) = match key { + crate::Key::ArrowUp => Some(FocusDirection::Up), + crate::Key::ArrowRight => Some(FocusDirection::Right), + crate::Key::ArrowDown => Some(FocusDirection::Down), + crate::Key::ArrowLeft => Some(FocusDirection::Left), + + crate::Key::Tab => { if modifiers.shift { Some(FocusDirection::Previous) } else { Some(FocusDirection::Next) } - } else { - None } + crate::Key::Escape => { + self.focused_widget = None; + Some(FocusDirection::None) + } + _ => None, + } { + self.focus_direction = cardinality; } - crate::Key::Escape => { - self.id = None; - self.is_focus_locked = false; - Some(FocusDirection::None) - } - _ => None, - } { - self.focus_direction = cardinality; } } @@ -346,17 +361,17 @@ impl Focus { pub(crate) fn end_frame(&mut self, used_ids: &IdMap) { if self.focus_direction.is_cardinal() { if let Some(found_widget) = self.find_widget_in_direction(used_ids) { - self.id = Some(found_widget); + self.focused_widget = Some(FocusWidget::new(found_widget)); } } - if let Some(id) = self.id { + if let Some(focused_widget) = self.focused_widget { // Allow calling `request_focus` one frame and not using it until next frame - let recently_gained_focus = self.id_previous_frame != Some(id); + let recently_gained_focus = self.id_previous_frame != Some(focused_widget.id); - if !recently_gained_focus && !used_ids.contains_key(&id) { + if !recently_gained_focus && !used_ids.contains_key(&focused_widget.id) { // Dead-mans-switch: the widget with focus has disappeared! - self.id = None; + self.focused_widget = None; } } } @@ -369,7 +384,7 @@ impl Focus { #[cfg(feature = "accesskit")] { if self.id_requested_by_accesskit == Some(id.accesskit_id()) { - self.id = Some(id); + self.focused_widget = Some(FocusWidget::new(id)); self.id_requested_by_accesskit = None; self.give_to_next = false; self.reset_focus(); @@ -382,23 +397,23 @@ impl Focus { .or_insert(Rect::EVERYTHING); if self.give_to_next && !self.had_focus_last_frame(id) { - self.id = Some(id); + self.focused_widget = Some(FocusWidget::new(id)); self.give_to_next = false; - } else if self.id == Some(id) { - if self.focus_direction == FocusDirection::Next && !self.is_focus_locked { - self.id = None; + } else if self.focused() == Some(id) { + if self.focus_direction == FocusDirection::Next { + self.focused_widget = None; self.give_to_next = true; self.reset_focus(); - } else if self.focus_direction == FocusDirection::Previous && !self.is_focus_locked { + } else if self.focus_direction == FocusDirection::Previous { self.id_next_frame = self.last_interested; // frame-delay so gained_focus works self.reset_focus(); } } else if self.focus_direction == FocusDirection::Next - && self.id.is_none() + && self.focused_widget.is_none() && !self.give_to_next { // nothing has focus and the user pressed tab - give focus to the first widgets that wants it: - self.id = Some(id); + self.focused_widget = Some(FocusWidget::new(id)); self.reset_focus(); } @@ -424,7 +439,7 @@ impl Focus { } } - let Some(focus_id) = self.id else { + let Some(current_focused) = self.focused_widget else { return None; }; @@ -449,7 +464,7 @@ impl Focus { } }); - let Some(current_rect) = self.focus_widgets_cache.get(&focus_id) else { + let Some(current_rect) = self.focus_widgets_cache.get(¤t_focused.id) else { return None; }; @@ -457,7 +472,7 @@ impl Focus { let mut best_id = None; for (candidate_id, candidate_rect) in &self.focus_widgets_cache { - if Some(*candidate_id) == self.id { + if *candidate_id == current_focused.id { continue; } @@ -542,46 +557,58 @@ impl Memory { /// from the window and back. #[inline(always)] pub fn has_focus(&self, id: Id) -> bool { - self.interaction.focus.id == Some(id) + self.interaction.focus.focused() == Some(id) } /// Which widget has keyboard focus? pub fn focus(&self) -> Option { - self.interaction.focus.id + self.interaction.focus.focused() } - /// Prevent keyboard focus from moving away from this widget even if users presses the tab key. + /// Set an event filter for a widget. + /// + /// This allows you to control whether the widget will loose focus + /// when the user presses tab, arrow keys, or escape. + /// /// You must first give focus to the widget before calling this. - pub fn lock_focus(&mut self, id: Id, lock_focus: bool) { + pub fn set_focus_lock_filter(&mut self, id: Id, event_filter: EventFilter) { if self.had_focus_last_frame(id) && self.has_focus(id) { - self.interaction.focus.is_focus_locked = lock_focus; + if let Some(focused) = &mut self.interaction.focus.focused_widget { + if focused.id == id { + focused.filter = event_filter; + } + } } } - /// Is the keyboard focus locked on this widget? If so the focus won't move even if the user presses the tab key. - pub fn has_lock_focus(&self, id: Id) -> bool { - if self.had_focus_last_frame(id) && self.has_focus(id) { - self.interaction.focus.is_focus_locked - } else { - false - } + /// Set an event filter for a widget. + /// + /// You must first give focus to the widget before calling this. + #[deprecated = "Use set_focus_lock_filter instead"] + pub fn lock_focus(&mut self, id: Id, lock_focus: bool) { + self.set_focus_lock_filter( + id, + EventFilter { + tab: lock_focus, + arrows: lock_focus, + escape: false, + }, + ); } /// Give keyboard focus to a specific widget. /// See also [`crate::Response::request_focus`]. #[inline(always)] pub fn request_focus(&mut self, id: Id) { - self.interaction.focus.id = Some(id); - self.interaction.focus.is_focus_locked = false; + self.interaction.focus.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) { - if self.interaction.focus.id == Some(id) { - self.interaction.focus.id = None; - self.interaction.focus.is_focus_locked = false; + if self.interaction.focus.focused() == Some(id) { + self.interaction.focus.focused_widget = None; } } @@ -600,24 +627,28 @@ impl Memory { /// Stop editing of active [`TextEdit`](crate::TextEdit) (if any). #[inline(always)] pub fn stop_text_input(&mut self) { - self.interaction.focus.id = None; + self.interaction.focus.focused_widget = None; } + /// Is any widget being dragged? #[inline(always)] pub fn is_anything_being_dragged(&self) -> bool { self.interaction.drag_id.is_some() } + /// Is this specific widget being dragged? #[inline(always)] pub fn is_being_dragged(&self, id: Id) -> bool { self.interaction.drag_id == Some(id) } + /// Set which widget is being dragged. #[inline(always)] pub fn set_dragged_id(&mut self, id: Id) { self.interaction.drag_id = Some(id); } + /// Stop dragging any widget. #[inline(always)] pub fn stop_dragging(&mut self) { self.interaction.drag_id = None; @@ -639,22 +670,29 @@ impl Memory { /// Popups are things like combo-boxes, color pickers, menus etc. /// Only one can be be open at a time. impl Memory { + /// Is the given popup open? pub fn is_popup_open(&self, popup_id: Id) -> bool { self.popup == Some(popup_id) || self.everything_is_visible() } + /// Is any popup open? pub fn any_popup_open(&self) -> bool { self.popup.is_some() || self.everything_is_visible() } + /// Open the given popup, and close all other. pub fn open_popup(&mut self, popup_id: Id) { self.popup = Some(popup_id); } + /// Close the open popup, if any. pub fn close_popup(&mut self) { self.popup = None; } + /// Toggle the given popup between closed and open. + /// + /// Note: at most one popup can be open at one time. pub fn toggle_popup(&mut self, popup_id: Id) { if self.is_popup_open(popup_id) { self.close_popup(); diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 6c8d8f9b83f..410d6b2e188 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -111,7 +111,7 @@ pub fn menu_button( /// Returns `None` if the menu is not open. pub fn menu_image_button( ui: &mut Ui, - image_button: ImageButton, + image_button: ImageButton<'_>, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse> { stationary_menu_image_impl(ui, image_button, Box::new(add_contents)) @@ -146,10 +146,9 @@ pub(crate) fn menu_ui<'c, R>( let area = Area::new(menu_id) .order(Order::Foreground) - .constrain(true) .fixed_pos(pos) - .interactable(true) - .drag_bounds(ctx.screen_rect()); + .constrain_to(ctx.screen_rect()) + .interactable(true); area.show(ctx, |ui| { set_menu_style(ui.style_mut()); @@ -201,7 +200,7 @@ fn stationary_menu_impl<'c, R>( /// Responds to primary clicks. fn stationary_menu_image_impl<'c, R>( ui: &mut Ui, - image_button: ImageButton, + image_button: ImageButton<'_>, add_contents: Box R + 'c>, ) -> InnerResponse> { let bar_id = ui.id(); @@ -348,8 +347,7 @@ impl MenuRoot { if let Some(root) = root.inner.as_mut() { if root.id == id { // pressed somewhere while this menu is open - let menu_state = root.menu_state.read(); - let in_menu = menu_state.area_contains(pos); + let in_menu = root.menu_state.read().area_contains(pos); if !in_menu { return MenuResponse::Close; } @@ -374,8 +372,7 @@ impl MenuRoot { let mut destroy = false; let mut in_old_menu = false; if let Some(root) = root { - let menu_state = root.menu_state.read(); - in_old_menu = menu_state.area_contains(pos); + in_old_menu = root.menu_state.read().area_contains(pos); destroy = root.id == response.id; } if !in_old_menu { @@ -611,7 +608,7 @@ impl MenuState { } /// Sense button interaction opening and closing submenu. - fn submenu_button_interaction(&mut self, ui: &mut Ui, sub_id: Id, button: &Response) { + fn submenu_button_interaction(&mut self, ui: &Ui, sub_id: Id, button: &Response) { let pointer = ui.input(|i| i.pointer.clone()); let open = self.is_open(sub_id); if self.moving_towards_current_submenu(&pointer) { diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index ab2b0c15349..a88862325a6 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -333,12 +333,13 @@ impl Painter { } /// Show an arrow starting at `origin` and going in the direction of `vec`, with the length `vec.length()`. - pub fn arrow(&self, origin: Pos2, vec: Vec2, stroke: Stroke) { + pub fn arrow(&self, origin: Pos2, vec: Vec2, stroke: impl Into) { use crate::emath::*; let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0); let tip_length = vec.length() / 4.0; let tip = origin + vec; let dir = vec.normalized(); + let stroke = stroke.into(); self.line_segment([origin, tip], stroke); self.line_segment([tip, tip - tip_length * (rot * dir)], stroke); self.line_segment([tip, tip - tip_length * (rot.inverse() * dir)], stroke); @@ -350,6 +351,18 @@ impl Painter { /// unless you want to crop or flip the image. /// /// `tint` is a color multiplier. Use [`Color32::WHITE`] if you don't want to tint the image. + /// + /// Usually it is easier to use [`crate::Image::paint_at`] instead: + /// + /// ``` + /// # egui::__run_test_ui(|ui| { + /// # let rect = egui::Rect::from_min_size(Default::default(), egui::Vec2::splat(100.0)); + /// egui::Image::new(egui::include_image!("../assets/ferris.png")) + /// .rounding(5.0) + /// .tint(egui::Color32::LIGHT_BLUE) + /// .paint_at(ui, rect); + /// # }); + /// ``` pub fn image(&self, texture_id: epaint::TextureId, rect: Rect, uv: Rect, tint: Color32) { self.add(Shape::image(texture_id, rect, uv, tint)); } diff --git a/crates/egui/src/placer.rs b/crates/egui/src/placer.rs index c9f9ddefbc5..81c137f4e83 100644 --- a/crates/egui/src/placer.rs +++ b/crates/egui/src/placer.rs @@ -263,6 +263,7 @@ impl Placer { } impl Placer { + #[cfg(debug_assertions)] pub(crate) fn debug_paint_cursor(&self, painter: &crate::Painter, text: impl ToString) { let stroke = Stroke::new(1.0, Color32::DEBUG_COLOR); diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index e95d07c204a..a194a666658 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -78,7 +78,7 @@ pub struct Response { #[doc(hidden)] pub interact_pointer_pos: Option, - /// What the underlying data changed? + /// Was the underlying data changed? /// /// e.g. the slider was dragged, text was entered in a [`TextEdit`](crate::TextEdit) etc. /// Always `false` for something like a [`Button`](crate::Button). @@ -339,7 +339,7 @@ impl Response { self.is_pointer_button_down_on } - /// What the underlying data changed? + /// Was the underlying data changed? /// /// e.g. the slider was dragged, text was entered in a [`TextEdit`](crate::TextEdit) etc. /// Always `false` for something like a [`Button`](crate::Button). @@ -479,7 +479,7 @@ impl Response { /// Highlight this widget, to make it look like it is hovered, even if it isn't. /// - /// The highlight takes on frame to take effect if you call this after the widget has been fully rendered. + /// The highlight takes one frame to take effect if you call this after the widget has been fully rendered. /// /// See also [`Context::highlight_widget`]. pub fn highlight(mut self) -> Self { @@ -620,20 +620,20 @@ impl Response { info: crate::WidgetInfo, ) { use crate::WidgetType; - use accesskit::{CheckedState, Role}; + use accesskit::{Checked, Role}; self.fill_accesskit_node_common(builder); builder.set_role(match info.typ { WidgetType::Label => Role::StaticText, WidgetType::Link => Role::Link, - WidgetType::TextEdit => Role::TextField, + WidgetType::TextEdit => Role::TextInput, WidgetType::Button | WidgetType::ImageButton | WidgetType::CollapsingHeader => { Role::Button } WidgetType::Checkbox => Role::CheckBox, WidgetType::RadioButton => Role::RadioButton, WidgetType::SelectableLabel => Role::ToggleButton, - WidgetType::ComboBox => Role::PopupButton, + WidgetType::ComboBox => Role::ComboBox, WidgetType::Slider => Role::Slider, WidgetType::DragValue => Role::SpinButton, WidgetType::ColorButton => Role::ColorWell, @@ -649,10 +649,10 @@ impl Response { builder.set_numeric_value(value); } if let Some(selected) = info.selected { - builder.set_checked_state(if selected { - CheckedState::True + builder.set_checked(if selected { + Checked::True } else { - CheckedState::False + Checked::False }); } } diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 3c7da1b1d13..ba0cc1b9e83 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -2,10 +2,14 @@ #![allow(clippy::if_same_then_else)] -use crate::{ecolor::*, emath::*, FontFamily, FontId, Response, RichText, WidgetText}; -use epaint::{Rounding, Shadow, Stroke}; use std::collections::BTreeMap; +use epaint::{Rounding, Shadow, Stroke}; + +use crate::{ + ecolor::*, emath::*, ComboBox, CursorIcon, FontFamily, FontId, Response, RichText, WidgetText, +}; + // ---------------------------------------------------------------------------- /// Alias for a [`FontId`] (font of a certain size). @@ -199,6 +203,9 @@ pub struct Style { pub animation_time: f32, /// Options to help debug why egui behaves strangely. + /// + /// Only available in debug builds. + #[cfg(debug_assertions)] pub debug: DebugOptions, /// Show tooltips explaining [`DragValue`]:s etc when hovered. @@ -298,16 +305,8 @@ pub struct Spacing { /// Height of a combo-box before showing scroll bars. pub combo_height: f32, - pub scroll_bar_width: f32, - - /// Make sure the scroll handle is at least this big - pub scroll_handle_min_length: f32, - - /// Margin between contents and scroll bar. - pub scroll_bar_inner_margin: f32, - - /// Margin between scroll bar and the outer container (e.g. right of a vertical scroll bar). - pub scroll_bar_outer_margin: f32, + /// Controls the spacing of a [`crate::ScrollArea`]. + pub scroll: ScrollStyle, } impl Spacing { @@ -328,6 +327,277 @@ impl Spacing { // ---------------------------------------------------------------------------- +/// Controls the spacing and visuals of a [`crate::ScrollArea`]. +/// +/// There are three presets to chose from: +/// * [`Self::solid`] +/// * [`Self::thin`] +/// * [`Self::floating`] +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +pub struct ScrollStyle { + /// If `true`, scroll bars float above the content, partially covering it. + /// + /// If `false`, the scroll bars allocate space, shrinking the area + /// available to the contents. + /// + /// This also changes the colors of the scroll-handle to make + /// it more promiment. + pub floating: bool, + + /// The width of the scroll bars at it largest. + pub bar_width: f32, + + /// Make sure the scroll handle is at least this big + pub handle_min_length: f32, + + /// Margin between contents and scroll bar. + pub bar_inner_margin: f32, + + /// Margin between scroll bar and the outer container (e.g. right of a vertical scroll bar). + /// Only makes sense for non-floating scroll bars. + pub bar_outer_margin: f32, + + /// The thin width of floating scroll bars that the user is NOT hovering. + /// + /// When the user hovers the scroll bars they expand to [`Self::bar_width`]. + pub floating_width: f32, + + /// How much space i allocated for a floating scroll bar? + /// + /// Normally this is zero, but you could set this to something small + /// like 4.0 and set [`Self::dormant_handle_opacity`] and + /// [`Self::dormant_background_opacity`] to e.g. 0.5 + /// so as to always show a thin scroll bar. + pub floating_allocated_width: f32, + + /// If true, use colors with more contrast. Good for floating scroll bars. + pub foreground_color: bool, + + /// The opaqueness of the background when the user is neither scrolling + /// nor hovering the scroll area. + /// + /// This is only for floating scroll bars. + /// Solid scroll bars are always opaque. + pub dormant_background_opacity: f32, + + /// The opaqueness of the background when the user is hovering + /// the scroll area, but not the scroll bar. + /// + /// This is only for floating scroll bars. + /// Solid scroll bars are always opaque. + pub active_background_opacity: f32, + + /// The opaqueness of the background when the user is hovering + /// over the scroll bars. + /// + /// This is only for floating scroll bars. + /// Solid scroll bars are always opaque. + pub interact_background_opacity: f32, + + /// The opaqueness of the handle when the user is neither scrolling + /// nor hovering the scroll area. + /// + /// This is only for floating scroll bars. + /// Solid scroll bars are always opaque. + pub dormant_handle_opacity: f32, + + /// The opaqueness of the handle when the user is hovering + /// the scroll area, but not the scroll bar. + /// + /// This is only for floating scroll bars. + /// Solid scroll bars are always opaque. + pub active_handle_opacity: f32, + + /// The opaqueness of the handle when the user is hovering + /// over the scroll bars. + /// + /// This is only for floating scroll bars. + /// Solid scroll bars are always opaque. + pub interact_handle_opacity: f32, +} + +impl Default for ScrollStyle { + fn default() -> Self { + Self::floating() + } +} + +impl ScrollStyle { + /// Solid scroll bars that always use up space + pub fn solid() -> Self { + Self { + floating: false, + bar_width: 6.0, + handle_min_length: 12.0, + bar_inner_margin: 4.0, + bar_outer_margin: 0.0, + floating_width: 2.0, + floating_allocated_width: 0.0, + + foreground_color: false, + + dormant_background_opacity: 0.0, + active_background_opacity: 0.4, + interact_background_opacity: 0.7, + + dormant_handle_opacity: 0.0, + active_handle_opacity: 0.6, + interact_handle_opacity: 1.0, + } + } + + /// Thin scroll bars that expand on hover + pub fn thin() -> Self { + Self { + floating: true, + bar_width: 12.0, + floating_allocated_width: 6.0, + foreground_color: false, + + dormant_background_opacity: 1.0, + dormant_handle_opacity: 1.0, + + active_background_opacity: 1.0, + active_handle_opacity: 1.0, + + // Be tranlucent when expanded so we can see the content + interact_background_opacity: 0.6, + interact_handle_opacity: 0.6, + + ..Self::solid() + } + } + + /// No scroll bars until you hover the scroll area, + /// at which time they appear faintly, and then expand + /// when you hover the scroll bars. + pub fn floating() -> Self { + Self { + floating: true, + bar_width: 12.0, + foreground_color: true, + floating_allocated_width: 0.0, + dormant_background_opacity: 0.0, + dormant_handle_opacity: 0.0, + ..Self::solid() + } + } + + /// Width of a solid vertical scrollbar, or height of a horizontal scroll bar, when it is at its widest. + pub fn allocated_width(&self) -> f32 { + if self.floating { + self.floating_allocated_width + } else { + self.bar_inner_margin + self.bar_width + self.bar_outer_margin + } + } + + pub fn ui(&mut self, ui: &mut Ui) { + ui.horizontal(|ui| { + ui.label("Presets:"); + ui.selectable_value(self, Self::solid(), "Solid"); + ui.selectable_value(self, Self::thin(), "Thin"); + ui.selectable_value(self, Self::floating(), "Floating"); + }); + + ui.collapsing("Details", |ui| { + self.details_ui(ui); + }); + } + + pub fn details_ui(&mut self, ui: &mut Ui) { + let Self { + floating, + bar_width, + handle_min_length, + bar_inner_margin, + bar_outer_margin, + floating_width, + floating_allocated_width, + + foreground_color, + + dormant_background_opacity, + active_background_opacity, + interact_background_opacity, + dormant_handle_opacity, + active_handle_opacity, + interact_handle_opacity, + } = self; + + ui.horizontal(|ui| { + ui.label("Type:"); + ui.selectable_value(floating, false, "Solid"); + ui.selectable_value(floating, true, "Floating"); + }); + + ui.horizontal(|ui| { + ui.add(DragValue::new(bar_width).clamp_range(0.0..=32.0)); + ui.label("Full bar width"); + }); + if *floating { + ui.horizontal(|ui| { + ui.add(DragValue::new(floating_width).clamp_range(0.0..=32.0)); + ui.label("Thin bar width"); + }); + ui.horizontal(|ui| { + ui.add(DragValue::new(floating_allocated_width).clamp_range(0.0..=32.0)); + ui.label("Allocated width"); + }); + } + + ui.horizontal(|ui| { + ui.add(DragValue::new(handle_min_length).clamp_range(0.0..=32.0)); + ui.label("Minimum handle length"); + }); + ui.horizontal(|ui| { + ui.add(DragValue::new(bar_outer_margin).clamp_range(0.0..=32.0)); + ui.label("Outer margin"); + }); + + ui.horizontal(|ui| { + ui.label("Color:"); + ui.selectable_value(foreground_color, false, "Background"); + ui.selectable_value(foreground_color, true, "Foreground"); + }); + + if *floating { + crate::Grid::new("opacity").show(ui, |ui| { + fn opacity_ui(ui: &mut Ui, opacity: &mut f32) { + ui.add(DragValue::new(opacity).speed(0.01).clamp_range(0.0..=1.0)); + } + + ui.label("Opacity"); + ui.label("Dormant"); + ui.label("Active"); + ui.label("Interacting"); + ui.end_row(); + + ui.label("Background:"); + opacity_ui(ui, dormant_background_opacity); + opacity_ui(ui, active_background_opacity); + opacity_ui(ui, interact_background_opacity); + ui.end_row(); + + ui.label("Handle:"); + opacity_ui(ui, dormant_handle_opacity); + opacity_ui(ui, active_handle_opacity); + opacity_ui(ui, interact_handle_opacity); + ui.end_row(); + }); + } else { + ui.horizontal(|ui| { + ui.add(DragValue::new(bar_inner_margin).clamp_range(0.0..=32.0)); + ui.label("Inner margin"); + }); + } + } +} + +// ---------------------------------------------------------------------------- + #[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Margin { @@ -544,6 +814,16 @@ pub struct Visuals { /// /// Enabling this will affect ALL sliders, and can be enabled/disabled per slider with [`Slider::trailing_fill`]. pub slider_trailing_fill: bool, + + /// Should the cursor change when the user hovers over an interactive/clickable item? + /// + /// This is consistent with a lot of browser-based applications (vscode, github + /// all turn your cursor into [`CursorIcon::PointingHand`] when a button is + /// hovered) but it is inconsistent with native UI toolkits. + pub interact_cursor: Option, + + /// Show a spinner when loading an image. + pub image_loading_spinners: bool, } impl Visuals { @@ -678,12 +958,36 @@ impl WidgetVisuals { } /// Options for help debug egui by adding extra visualization -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg(debug_assertions)] pub struct DebugOptions { - /// However over widgets to see their rectangles + /// Always show callstack to ui on hover. + /// + /// Useful for figuring out where in the code some UI is being created. + /// + /// Only works in debug builds. + /// Requires the `callstack` feature. + /// Does not work on web. + #[cfg(debug_assertions)] pub debug_on_hover: bool, + /// Show callstack for the current widget on hover if all modifier keys are pressed down. + /// + /// Useful for figuring out where in the code some UI is being created. + /// + /// Only works in debug builds. + /// Requires the `callstack` feature. + /// Does not work on web. + /// + /// Default is `true` in debug builds, on native, if the `callstack` feature is enabled. + #[cfg(debug_assertions)] + pub debug_on_hover_with_all_modifiers: bool, + + /// If we show the hover ui, include where the next widget is placed. + #[cfg(debug_assertions)] + pub hover_shows_next: bool, + /// Show which widgets make their parent wider pub show_expand_width: bool, @@ -699,6 +1003,23 @@ pub struct DebugOptions { pub show_blocking_widget: bool, } +#[cfg(debug_assertions)] +impl Default for DebugOptions { + fn default() -> Self { + Self { + debug_on_hover: false, + debug_on_hover_with_all_modifiers: cfg!(feature = "callstack") + && !cfg!(target_arch = "wasm32"), + hover_shows_next: false, + show_expand_width: false, + show_expand_height: false, + show_resize: false, + show_interactive_widgets: false, + show_blocking_widget: false, + } + } +} + // ---------------------------------------------------------------------------- /// The default text styles of the default egui theme. @@ -727,6 +1048,7 @@ impl Default for Style { interaction: Interaction::default(), visuals: Visuals::default(), animation_time: 1.0 / 12.0, + #[cfg(debug_assertions)] debug: Default::default(), explanation_tooltips: false, } @@ -750,10 +1072,7 @@ impl Default for Spacing { icon_spacing: 4.0, tooltip_width: 600.0, combo_height: 200.0, - scroll_bar_width: 8.0, - scroll_handle_min_length: 12.0, - scroll_bar_inner_margin: 4.0, - scroll_bar_outer_margin: 0.0, + scroll: Default::default(), indent_ends_with_horizontal_line: false, } } @@ -806,6 +1125,10 @@ impl Visuals { striped: false, slider_trailing_fill: false, + + interact_cursor: None, + + image_loading_spinners: true, } } @@ -977,6 +1300,7 @@ impl Style { interaction, visuals, animation_time, + #[cfg(debug_assertions)] debug, explanation_tooltips, } = self; @@ -1039,6 +1363,8 @@ impl Style { ui.collapsing("📏 Spacing", |ui| spacing.ui(ui)); ui.collapsing("☝ Interaction", |ui| interaction.ui(ui)); ui.collapsing("🎨 Visuals", |ui| visuals.ui(ui)); + + #[cfg(debug_assertions)] ui.collapsing("🐛 Debug", |ui| debug.ui(ui)); ui.checkbox(explanation_tooltips, "Explanation tooltips") @@ -1082,10 +1408,7 @@ impl Spacing { tooltip_width, indent_ends_with_horizontal_line, combo_height, - scroll_bar_width, - scroll_handle_min_length, - scroll_bar_inner_margin, - scroll_bar_outer_margin, + scroll, } = self; ui.add(slider_vec2(item_spacing, 0.0..=20.0, "Item spacing")); @@ -1112,21 +1435,9 @@ impl Spacing { ui.add(DragValue::new(text_edit_width).clamp_range(0.0..=1000.0)); ui.label("TextEdit width"); }); - ui.horizontal(|ui| { - ui.add(DragValue::new(scroll_bar_width).clamp_range(0.0..=32.0)); - ui.label("Scroll-bar width"); - }); - ui.horizontal(|ui| { - ui.add(DragValue::new(scroll_handle_min_length).clamp_range(0.0..=32.0)); - ui.label("Scroll-bar handle min length"); - }); - ui.horizontal(|ui| { - ui.add(DragValue::new(scroll_bar_inner_margin).clamp_range(0.0..=32.0)); - ui.label("Scroll-bar inner margin"); - }); - ui.horizontal(|ui| { - ui.add(DragValue::new(scroll_bar_outer_margin).clamp_range(0.0..=32.0)); - ui.label("Scroll-bar outer margin"); + + ui.collapsing("Scroll Area", |ui| { + scroll.ui(ui); }); ui.horizontal(|ui| { @@ -1376,6 +1687,9 @@ impl Visuals { striped, slider_trailing_fill, + interact_cursor, + + image_loading_spinners, } = self; ui.collapsing("Background Colors", |ui| { @@ -1441,14 +1755,30 @@ impl Visuals { ui.checkbox(slider_trailing_fill, "Add trailing color to sliders"); + ComboBox::from_label("Interact Cursor") + .selected_text(format!("{interact_cursor:?}")) + .show_ui(ui, |ui| { + ui.selectable_value(interact_cursor, None, "None"); + + for icon in CursorIcon::ALL { + ui.selectable_value(interact_cursor, Some(icon), format!("{icon:?}")); + } + }); + + 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)); } } +#[cfg(debug_assertions)] impl DebugOptions { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { debug_on_hover, + debug_on_hover_with_all_modifiers, + hover_shows_next, show_expand_width, show_expand_height, show_resize, @@ -1456,7 +1786,16 @@ impl DebugOptions { show_blocking_widget, } = self; - ui.checkbox(debug_on_hover, "Show debug info on hover"); + { + ui.checkbox(debug_on_hover, "Show widget info on hover."); + ui.checkbox( + debug_on_hover_with_all_modifiers, + "Show widget info on hover if holding all modifier keys", + ); + + ui.checkbox(hover_shows_next, "Show next widget placement on hover"); + } + ui.checkbox( show_expand_width, "Show which widgets make their parent wider", diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index c40ef5f51aa..1c0d2155585 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -738,43 +738,41 @@ impl Ui { /// # }); /// ``` pub fn allocate_space(&mut self, desired_size: Vec2) -> (Id, Rect) { - // For debug rendering + #[cfg(debug_assertions)] let original_available = self.available_size_before_wrap(); - let too_wide = desired_size.x > original_available.x; - let too_high = desired_size.y > original_available.y; let rect = self.allocate_space_impl(desired_size); - if self.style().debug.debug_on_hover && self.rect_contains_pointer(rect) { - let painter = self.ctx().debug_painter(); - painter.rect_stroke(rect, 4.0, (1.0, Color32::LIGHT_BLUE)); - self.placer.debug_paint_cursor(&painter, "next"); - } - - let debug_expand_width = self.style().debug.show_expand_width; - let debug_expand_height = self.style().debug.show_expand_height; + #[cfg(debug_assertions)] + { + let too_wide = desired_size.x > original_available.x; + let too_high = desired_size.y > original_available.y; - if (debug_expand_width && too_wide) || (debug_expand_height && too_high) { - self.painter - .rect_stroke(rect, 0.0, (1.0, Color32::LIGHT_BLUE)); + let debug_expand_width = self.style().debug.show_expand_width; + let debug_expand_height = self.style().debug.show_expand_height; - let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0)); - let paint_line_seg = |a, b| self.painter().line_segment([a, b], stroke); - - if debug_expand_width && too_wide { - paint_line_seg(rect.left_top(), rect.left_bottom()); - paint_line_seg(rect.left_center(), rect.right_center()); - paint_line_seg( - pos2(rect.left() + original_available.x, rect.top()), - pos2(rect.left() + original_available.x, rect.bottom()), - ); - paint_line_seg(rect.right_top(), rect.right_bottom()); - } - - if debug_expand_height && too_high { - paint_line_seg(rect.left_top(), rect.right_top()); - paint_line_seg(rect.center_top(), rect.center_bottom()); - paint_line_seg(rect.left_bottom(), rect.right_bottom()); + if (debug_expand_width && too_wide) || (debug_expand_height && too_high) { + self.painter + .rect_stroke(rect, 0.0, (1.0, Color32::LIGHT_BLUE)); + + let stroke = Stroke::new(2.5, Color32::from_rgb(200, 0, 0)); + let paint_line_seg = |a, b| self.painter().line_segment([a, b], stroke); + + if debug_expand_width && too_wide { + paint_line_seg(rect.left_top(), rect.left_bottom()); + paint_line_seg(rect.left_center(), rect.right_center()); + paint_line_seg( + pos2(rect.left() + original_available.x, rect.top()), + pos2(rect.left() + original_available.x, rect.bottom()), + ); + paint_line_seg(rect.right_top(), rect.right_bottom()); + } + + if debug_expand_height && too_high { + paint_line_seg(rect.left_top(), rect.right_top()); + paint_line_seg(rect.center_top(), rect.center_bottom()); + paint_line_seg(rect.left_bottom(), rect.right_bottom()); + } } } @@ -795,6 +793,8 @@ impl Ui { self.placer .advance_after_rects(frame_rect, widget_rect, item_spacing); + register_rect(self, widget_rect); + widget_rect } @@ -803,6 +803,7 @@ impl Ui { /// Ignore the layout of the [`Ui`]: just put my widget here! /// The layout cursor will advance to past this `rect`. pub fn allocate_rect(&mut self, rect: Rect, sense: Sense) -> Response { + register_rect(self, rect); let id = self.advance_cursor_after_rect(rect); self.interact(rect, id, sense) } @@ -813,12 +814,6 @@ impl Ui { let item_spacing = self.spacing().item_spacing; self.placer.advance_after_rects(rect, rect, item_spacing); - if self.style().debug.debug_on_hover && self.rect_contains_pointer(rect) { - let painter = self.ctx().debug_painter(); - painter.rect_stroke(rect, 4.0, (1.0, Color32::LIGHT_BLUE)); - self.placer.debug_paint_cursor(&painter, "next"); - } - let id = Id::new(self.next_auto_id_source); self.next_auto_id_source = self.next_auto_id_source.wrapping_add(1); id @@ -896,13 +891,6 @@ impl Ui { self.placer .advance_after_rects(final_child_rect, final_child_rect, item_spacing); - if self.style().debug.debug_on_hover && self.rect_contains_pointer(final_child_rect) { - let painter = self.ctx().debug_painter(); - painter.rect_stroke(frame_rect, 4.0, (1.0, Color32::LIGHT_BLUE)); - painter.rect_stroke(final_child_rect, 4.0, (1.0, Color32::LIGHT_BLUE)); - self.placer.debug_paint_cursor(&painter, "next"); - } - let response = self.interact(final_child_rect, child_ui.id, Sense::hover()); InnerResponse::new(ret, response) } @@ -1558,56 +1546,35 @@ impl Ui { response } - /// Show an image here with the given size. - /// - /// In order to display an image you must first acquire a [`TextureHandle`]. - /// This is best done with [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html) or [`Context::load_texture`]. - /// - /// ``` - /// struct MyImage { - /// texture: Option, - /// } - /// - /// impl MyImage { - /// fn ui(&mut self, ui: &mut egui::Ui) { - /// let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| { - /// // Load the texture only once. - /// ui.ctx().load_texture( - /// "my-image", - /// egui::ColorImage::example(), - /// Default::default() - /// ) - /// }); - /// - /// // Show the image: - /// ui.image(texture, texture.size_vec2()); - /// } - /// } - /// ``` - /// - /// See also [`crate::Image`] and [`crate::ImageButton`]. - #[inline] - pub fn image(&mut self, texture_id: impl Into, size: impl Into) -> Response { - Image::new(texture_id, size).ui(self) - } - /// Show an image available at the given `uri`. /// /// ⚠ This will do nothing unless you install some image loaders first! - /// The easiest way to do this is via [`egui_extras::loaders::install`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html). + /// The easiest way to do this is via [`egui_extras::install_image_loaders`](https://docs.rs/egui_extras/latest/egui_extras/fn.install_image_loaders.html). /// /// The loaders handle caching image data, sampled textures, etc. across frames, so calling this is immediate-mode safe. /// /// ``` /// # egui::__run_test_ui(|ui| { - /// ui.image2("file://ferris.svg"); + /// ui.image("https://picsum.photos/480"); + /// ui.image("file://assets/ferris.png"); + /// ui.image(egui::include_image!("../assets/ferris.png")); + /// ui.add( + /// egui::Image::new(egui::include_image!("../assets/ferris.png")) + /// .max_width(200.0) + /// .rounding(10.0), + /// ); /// # }); /// ``` /// - /// See also [`crate::Image2`] and [`crate::ImageSource`]. + /// Using [`include_image`] is often the most ergonomic, and the path + /// will be resolved at compile-time and embedded in the binary. + /// When using a "file://" url on the other hand, you need to make sure + /// the files can be found in the right spot at runtime! + /// + /// See also [`crate::Image`], [`crate::ImageSource`]. #[inline] - pub fn image2<'a>(&mut self, source: impl Into>) -> Response { - Image2::new(source.into()).ui(self) + pub fn image<'a>(&mut self, source: impl Into>) -> Response { + Image::new(source).ui(self) } } @@ -1710,7 +1677,7 @@ impl Ui { /// # }); /// ``` /// - /// Se also [`Self::scope`]. + /// See also [`Self::scope`]. pub fn group(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse { crate::Frame::group(self.style()).show(self, add_contents) } @@ -1815,10 +1782,7 @@ impl Ui { let mut child_rect = self.placer.available_rect_before_wrap(); child_rect.min.x += indent; - let mut child_ui = Self { - id: self.id.with(id_source), - ..self.child_ui(child_rect, *self.layout()) - }; + let mut child_ui = self.child_ui_with_id_source(child_rect, *self.layout(), id_source); let ret = add_contents(&mut child_ui); let left_vline = self.visuals().indent_has_left_vline; @@ -2046,12 +2010,6 @@ impl Ui { let item_spacing = self.spacing().item_spacing; self.placer.advance_after_rects(rect, rect, item_spacing); - if self.style().debug.debug_on_hover && self.rect_contains_pointer(rect) { - let painter = self.ctx().debug_painter(); - painter.rect_stroke(rect, 4.0, (1.0, Color32::LIGHT_BLUE)); - self.placer.debug_paint_cursor(&painter, "next"); - } - InnerResponse::new(inner, self.interact(rect, child_ui.id, Sense::hover())) } @@ -2206,15 +2164,9 @@ impl Ui { /// If called from within a menu this will instead create a button for a sub-menu. /// /// ```ignore - /// use egui_extras; + /// let img = egui::include_image!("../assets/ferris.png"); /// - /// let img = egui_extras::RetainedImage::from_svg_bytes_with_size( - /// "rss", - /// include_bytes!("rss.svg"), - /// egui_extras::image::FitTo::Size(24, 24), - /// ); - /// - /// ui.menu_image_button(img.texture_id(ctx), img.size_vec2(), |ui| { + /// ui.menu_image_button(img, |ui| { /// ui.menu_button("My sub-menu", |ui| { /// if ui.button("Close the menu").clicked() { /// ui.close_menu(); @@ -2225,16 +2177,15 @@ impl Ui { /// /// See also: [`Self::close_menu`] and [`Response::context_menu`]. #[inline] - pub fn menu_image_button( + pub fn menu_image_button<'a, R>( &mut self, - texture_id: TextureId, - image_size: impl Into, + image: impl Into>, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse> { if let Some(menu_state) = self.menu_state.clone() { menu::submenu_button(self, menu_state, String::new(), add_contents) } else { - menu::menu_image_button(self, ImageButton::new(texture_id, image_size), add_contents) + menu::menu_image_button(self, ImageButton::new(image), add_contents) } } } @@ -2244,21 +2195,121 @@ impl Ui { /// # Debug stuff impl Ui { /// Shows where the next widget is going to be placed + #[cfg(debug_assertions)] pub fn debug_paint_cursor(&self) { self.placer.debug_paint_cursor(&self.painter, "next"); } +} + +#[cfg(debug_assertions)] +impl Drop for Ui { + fn drop(&mut self) { + register_rect(self, self.min_rect()); + } +} + +/// Show this rectangle to the user if certain debug options are set. +#[cfg(debug_assertions)] +fn register_rect(ui: &Ui, rect: Rect) { + let debug = ui.style().debug; + + let show_callstacks = debug.debug_on_hover + || debug.debug_on_hover_with_all_modifiers && ui.input(|i| i.modifiers.all()); + + if !show_callstacks { + return; + } + + if ui.ctx().frame_state(|o| o.has_debug_viewed_this_frame) { + return; + } + + if !ui.rect_contains_pointer(rect) { + return; + } + + // We only show one debug rectangle, or things get confusing: + ui.ctx() + .frame_state_mut(|o| o.has_debug_viewed_this_frame = true); + + // ---------------------------------------------- + + let is_clicking = ui.input(|i| i.pointer.could_any_button_be_click()); + + // Use the debug-painter to avoid clip rect, + // otherwise the content of the widget may cover what we paint here! + let painter = ui.ctx().debug_painter(); + + // Paint rectangle around widget: + { + let rect_fg_color = if is_clicking { + Color32::WHITE + } else { + Color32::LIGHT_BLUE + }; + let rect_bg_color = Color32::BLUE.gamma_multiply(0.5); + + painter.rect(rect, 0.0, rect_bg_color, (1.0, rect_fg_color)); + } + + // ---------------------------------------------- + + if debug.hover_shows_next { + ui.placer.debug_paint_cursor(&painter, "next"); + } + + // ---------------------------------------------- + + #[cfg(feature = "callstack")] + let callstack = crate::callstack::capture(); + + #[cfg(not(feature = "callstack"))] + let callstack = String::default(); + + if !callstack.is_empty() { + let font_id = FontId::monospace(12.0); + let text = format!("{callstack}\n\n(click to copy)"); + let galley = painter.layout_no_wrap(text, font_id, Color32::WHITE); - /// Shows the given text where the next widget is to be placed - /// if when [`Context::set_debug_on_hover`] has been turned on and the mouse is hovering the Ui. - pub fn trace_location(&self, text: impl ToString) { - let rect = self.max_rect(); - if self.style().debug.debug_on_hover && self.rect_contains_pointer(rect) { - self.placer - .debug_paint_cursor(&self.ctx().debug_painter(), text); + // Position the text either under or above: + let screen_rect = ui.ctx().screen_rect(); + let y = if galley.size().y <= rect.top() { + // Above + rect.top() - galley.size().y + } else { + // Below + rect.bottom() + }; + + let y = y + .at_most(screen_rect.bottom() - galley.size().y) + .at_least(0.0); + + let x = rect + .left() + .at_most(screen_rect.right() - galley.size().x) + .at_least(0.0); + let text_pos = pos2(x, y); + + let text_bg_color = Color32::from_black_alpha(180); + let text_rect_stroke_color = if is_clicking { + Color32::WHITE + } else { + text_bg_color + }; + let text_rect = Rect::from_min_size(text_pos, galley.size()); + painter.rect(text_rect, 0.0, text_bg_color, (1.0, text_rect_stroke_color)); + painter.galley(text_pos, galley); + + if ui.input(|i| i.pointer.any_click()) { + ui.ctx().copy_text(callstack); } } } +#[cfg(not(debug_assertions))] +fn register_rect(_ui: &Ui, _rect: Rect) {} + #[test] fn ui_impl_send_sync() { fn assert_send_sync() {} diff --git a/crates/egui/src/util/id_type_map.rs b/crates/egui/src/util/id_type_map.rs index a9867eb1b1d..8cb33360bb8 100644 --- a/crates/egui/src/util/id_type_map.rs +++ b/crates/egui/src/util/id_type_map.rs @@ -588,10 +588,9 @@ impl PersistedMap { crate::profile_scope!("gather"); for (hash, element) in &map.map { if let Some(element) = element.to_serialize() { - let mut stats = types_map.entry(element.type_id).or_default(); + let stats = types_map.entry(element.type_id).or_default(); stats.num_bytes += element.ron.len(); - let mut generation_stats = - stats.generations.entry(element.generation).or_default(); + let generation_stats = stats.generations.entry(element.generation).or_default(); generation_stats.num_bytes += element.ron.len(); generation_stats.elements.push((*hash, element)); } else { diff --git a/crates/egui/src/util/undoer.rs b/crates/egui/src/util/undoer.rs index b5e272aff1e..de6d2716171 100644 --- a/crates/egui/src/util/undoer.rs +++ b/crates/egui/src/util/undoer.rs @@ -57,15 +57,22 @@ pub struct Undoer { /// The latest undo point may (often) be the current state. undos: VecDeque, + /// 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. + redos: Vec, + #[cfg_attr(feature = "serde", serde(skip))] flux: Option>, } impl std::fmt::Debug for Undoer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Self { undos, .. } = self; + let Self { undos, redos, .. } = self; f.debug_struct("Undoer") .field("undo count", &undos.len()) + .field("redo count", &redos.len()) .finish() } } @@ -91,6 +98,10 @@ where } } + pub fn has_redo(&self, current_state: &State) -> bool { + !self.redos.is_empty() && self.undos.back() == Some(current_state) + } + /// Return true if the state is currently changing pub fn is_in_flux(&self) -> bool { self.flux.is_some() @@ -101,7 +112,9 @@ where self.flux = None; if self.undos.back() == Some(current_state) { - self.undos.pop_back(); + self.redos.push(self.undos.pop_back().unwrap()); + } else { + self.redos.push(current_state.clone()); } // Note: we keep the undo point intact. @@ -111,9 +124,20 @@ where } } + pub fn redo(&mut self, current_state: &State) -> Option<&State> { + if !self.undos.is_empty() && self.undos.back() != Some(current_state) { + // state changed since the last undo, redos should be cleared. + self.redos.clear(); + None + } else if let Some(state) = self.redos.pop() { + self.undos.push_back(state); + self.undos.back() + } else { + None + } + } + /// Add an undo point if, and only if, there has been a change since the latest undo point. - /// - /// * `time`: current time in seconds. pub fn add_undo(&mut self, current_state: &State) { if self.undos.back() != Some(current_state) { self.undos.push_back(current_state.clone()); @@ -139,6 +163,8 @@ where if latest_undo == current_state { self.flux = None; } else { + self.redos.clear(); + match self.flux.as_mut() { None => { self.flux = Some(Flux { diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index 531f35820b5..fe0de370c01 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -269,12 +269,66 @@ impl RichText { fonts.row_height(&font_id) } + /// Append to an existing [`LayoutJob`] + /// + /// Note that the color of the [`RichText`] must be set, or may default to an undesirable color. + /// + /// ### Example + /// ``` + /// use egui::{Style, RichText, text::LayoutJob, Color32, FontSelection, Align}; + /// + /// let style = Style::default(); + /// let mut layout_job = LayoutJob::default(); + /// RichText::new("Normal") + /// .color(style.visuals.text_color()) + /// .append_to( + /// &mut layout_job, + /// &style, + /// FontSelection::Default, + /// Align::Center, + /// ); + /// RichText::new("Large and underlined") + /// .color(style.visuals.text_color()) + /// .size(20.0) + /// .underline() + /// .append_to( + /// &mut layout_job, + /// &style, + /// FontSelection::Default, + /// Align::Center, + /// ); + /// ``` + pub fn append_to( + self, + layout_job: &mut LayoutJob, + style: &Style, + fallback_font: FontSelection, + default_valign: Align, + ) { + let (text, format) = self.into_text_and_format(style, fallback_font, default_valign); + + layout_job.append(&text, 0.0, format); + } + fn into_text_job( self, style: &Style, fallback_font: FontSelection, default_valign: Align, ) -> WidgetTextJob { + let job_has_color = self.get_text_color(&style.visuals).is_some(); + let (text, text_format) = self.into_text_and_format(style, fallback_font, default_valign); + + let job = LayoutJob::single_section(text, text_format); + WidgetTextJob { job, job_has_color } + } + + fn into_text_and_format( + self, + style: &Style, + fallback_font: FontSelection, + default_valign: Align, + ) -> (String, crate::text::TextFormat) { let text_color = self.get_text_color(&style.visuals); let Self { @@ -295,7 +349,6 @@ impl RichText { raised, } = self; - let job_has_color = text_color.is_some(); let line_color = text_color.unwrap_or_else(|| style.visuals.text_color()); let text_color = text_color.unwrap_or(crate::Color32::TEMPORARY_COLOR); @@ -336,20 +389,20 @@ impl RichText { default_valign }; - let text_format = crate::text::TextFormat { - font_id, - extra_letter_spacing, - line_height, - color: text_color, - background: background_color, - italics, - underline, - strikethrough, - valign, - }; - - let job = LayoutJob::single_section(text, text_format); - WidgetTextJob { job, job_has_color } + ( + text, + crate::text::TextFormat { + font_id, + extra_letter_spacing, + line_height, + color: text_color, + background: background_color, + italics, + underline, + strikethrough, + valign, + }, + ) } fn get_text_color(&self, visuals: &Visuals) -> Option { diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 932918ab646..e11a33b1905 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -19,8 +19,9 @@ use crate::*; /// # }); /// ``` #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] -pub struct Button { - text: WidgetText, +pub struct Button<'a> { + image: Option>, + text: Option, shortcut_text: WidgetText, wrap: Option, @@ -32,13 +33,30 @@ pub struct Button { frame: Option, min_size: Vec2, rounding: Option, - image: Option, + selected: bool, } -impl Button { +impl<'a> Button<'a> { pub fn new(text: impl Into) -> Self { + Self::opt_image_and_text(None, Some(text.into())) + } + + /// Creates a button with an image. The size of the image as displayed is defined by the provided size. + #[allow(clippy::needless_pass_by_value)] + pub fn image(image: impl Into>) -> Self { + Self::opt_image_and_text(Some(image.into()), None) + } + + /// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the provided size. + #[allow(clippy::needless_pass_by_value)] + pub fn image_and_text(image: impl Into>, text: impl Into) -> Self { + Self::opt_image_and_text(Some(image.into()), Some(text.into())) + } + + pub fn opt_image_and_text(image: Option>, text: Option) -> Self { Self { - text: text.into(), + text, + image, shortcut_text: Default::default(), wrap: None, fill: None, @@ -48,20 +66,7 @@ impl Button { frame: None, min_size: Vec2::ZERO, rounding: None, - image: None, - } - } - - /// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the provided size. - #[allow(clippy::needless_pass_by_value)] - pub fn image_and_text( - texture_id: TextureId, - image_size: impl Into, - text: impl Into, - ) -> Self { - Self { - image: Some(widgets::Image::new(texture_id, image_size)), - ..Self::new(text) + selected: false, } } @@ -96,7 +101,9 @@ impl Button { /// Make this a small button, suitable for embedding into text. pub fn small(mut self) -> Self { - self.text = self.text.text_style(TextStyle::Body); + if let Some(text) = self.text { + self.text = Some(text.text_style(TextStyle::Body)); + } self.small = true; self } @@ -135,12 +142,19 @@ impl Button { self.shortcut_text = shortcut_text.into(); self } + + /// If `true`, mark this button as "selected". + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } } -impl Widget for Button { +impl Widget for Button<'_> { fn ui(self, ui: &mut Ui) -> Response { let Button { text, + image, shortcut_text, wrap, fill, @@ -150,32 +164,58 @@ impl Widget for Button { frame, min_size, rounding, - image, + selected, } = self; let frame = frame.unwrap_or_else(|| ui.visuals().button_frame); - let mut button_padding = ui.spacing().button_padding; + let mut button_padding = if frame { + ui.spacing().button_padding + } else { + Vec2::ZERO + }; if small { button_padding.y = 0.0; } + let space_available_for_image = if let Some(text) = &text { + let font_height = ui.fonts(|fonts| text.font_height(fonts, ui.style())); + Vec2::splat(font_height) // Reasonable? + } else { + ui.available_size() - 2.0 * button_padding + }; + + let image_size = if let Some(image) = &image { + image + .load_and_calc_size(ui, space_available_for_image) + .unwrap_or(space_available_for_image) + } else { + Vec2::ZERO + }; + let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x; - if let Some(image) = image { - text_wrap_width -= image.size().x + ui.spacing().icon_spacing; + if image.is_some() { + text_wrap_width -= image_size.x + ui.spacing().icon_spacing; } if !shortcut_text.is_empty() { text_wrap_width -= 60.0; // Some space for the shortcut text (which we never wrap). } - let text = text.into_galley(ui, wrap, text_wrap_width, TextStyle::Button); + let text = text.map(|text| text.into_galley(ui, wrap, text_wrap_width, TextStyle::Button)); let shortcut_text = (!shortcut_text.is_empty()) .then(|| shortcut_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button)); - let mut desired_size = text.size(); - if let Some(image) = image { - desired_size.x += image.size().x + ui.spacing().icon_spacing; - desired_size.y = desired_size.y.max(image.size().y); + let mut desired_size = Vec2::ZERO; + if image.is_some() { + desired_size.x += image_size.x; + desired_size.y = desired_size.y.max(image_size.y); + } + if image.is_some() && text.is_some() { + desired_size.x += ui.spacing().icon_spacing; + } + if let Some(text) = &text { + desired_size.x += text.size().x; + desired_size.y = desired_size.y.max(text.size().y); } if let Some(shortcut_text) = &shortcut_text { desired_size.x += ui.spacing().item_spacing.x + shortcut_text.size().x; @@ -187,32 +227,82 @@ impl Widget for Button { } desired_size = desired_size.at_least(min_size); - let (rect, response) = ui.allocate_at_least(desired_size, sense); - response.widget_info(|| WidgetInfo::labeled(WidgetType::Button, text.text())); + let (rect, mut response) = ui.allocate_at_least(desired_size, sense); + response.widget_info(|| { + if let Some(text) = &text { + WidgetInfo::labeled(WidgetType::Button, text.text()) + } else { + WidgetInfo::new(WidgetType::Button) + } + }); if ui.is_rect_visible(rect) { let visuals = ui.style().interact(&response); - if frame { - let fill = fill.unwrap_or(visuals.weak_bg_fill); - let stroke = stroke.unwrap_or(visuals.bg_stroke); - let rounding = rounding.unwrap_or(visuals.rounding); - ui.painter() - .rect(rect.expand(visuals.expansion), rounding, fill, stroke); - } - - let text_pos = if let Some(image) = image { - let icon_spacing = ui.spacing().icon_spacing; - pos2( - rect.min.x + button_padding.x + image.size().x + icon_spacing, - rect.center().y - 0.5 * text.size().y, + let (frame_expansion, frame_rounding, frame_fill, frame_stroke) = if selected { + let selection = ui.visuals().selection; + ( + Vec2::ZERO, + Rounding::ZERO, + selection.bg_fill, + selection.stroke, + ) + } else if frame { + let expansion = Vec2::splat(visuals.expansion); + ( + expansion, + visuals.rounding, + visuals.weak_bg_fill, + visuals.bg_stroke, ) } else { - ui.layout() - .align_size_within_rect(text.size(), rect.shrink2(button_padding)) - .min + Default::default() }; - text.paint_with_visuals(ui.painter(), text_pos, visuals); + let frame_rounding = rounding.unwrap_or(frame_rounding); + let frame_fill = fill.unwrap_or(frame_fill); + let frame_stroke = stroke.unwrap_or(frame_stroke); + ui.painter().rect( + rect.expand2(frame_expansion), + frame_rounding, + frame_fill, + frame_stroke, + ); + + let mut cursor_x = rect.min.x + button_padding.x; + + if let Some(image) = &image { + let image_rect = Rect::from_min_size( + pos2(cursor_x, rect.center().y - 0.5 - (image_size.y / 2.0)), + image_size, + ); + cursor_x += image_size.x; + let tlr = image.load_for_size(ui.ctx(), image_size); + widgets::image::paint_texture_load_result( + ui, + &tlr, + image_rect, + image.show_loading_spinner, + image.image_options(), + ); + response = + widgets::image::texture_load_result_response(image.source(), &tlr, response); + } + + if image.is_some() && text.is_some() { + cursor_x += ui.spacing().icon_spacing; + } + + if let Some(text) = text { + let text_pos = if image.is_some() || shortcut_text.is_some() { + pos2(cursor_x, rect.center().y - 0.5 * text.size().y) + } else { + // Make sure button text is centered if within a centered layout + ui.layout() + .align_size_within_rect(text.size(), rect.shrink2(button_padding)) + .min + }; + text.paint_with_visuals(ui.painter(), text_pos, visuals); + } if let Some(shortcut_text) = shortcut_text { let shortcut_text_pos = pos2( @@ -225,16 +315,11 @@ impl Widget for Button { ui.visuals().weak_text_color(), ); } + } - if let Some(image) = image { - let image_rect = Rect::from_min_size( - pos2( - rect.min.x + button_padding.x, - rect.center().y - 0.5 - (image.size().y / 2.0), - ), - image.size(), - ); - image.paint_at(ui, image_rect); + if let Some(cursor) = ui.visuals().interact_cursor { + if response.hovered { + ui.ctx().set_cursor_icon(cursor); } } @@ -462,17 +547,17 @@ impl Widget for RadioButton { /// 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 { - image: widgets::Image, +pub struct ImageButton<'a> { + image: Image<'a>, sense: Sense, frame: bool, selected: bool, } -impl ImageButton { - pub fn new(texture_id: impl Into, size: impl Into) -> Self { +impl<'a> ImageButton<'a> { + pub fn new(image: impl Into>) -> Self { Self { - image: widgets::Image::new(texture_id, size), + image: image.into(), sense: Sense::click(), frame: true, selected: false, @@ -509,42 +594,51 @@ impl ImageButton { self.sense = sense; self } + + /// Set rounding for the `ImageButton`. + /// If the underlying image already has rounding, this + /// will override that value. + pub fn rounding(mut self, rounding: impl Into) -> Self { + self.image = self.image.rounding(rounding.into()); + self + } } -impl Widget for ImageButton { +impl<'a> Widget for ImageButton<'a> { fn ui(self, ui: &mut Ui) -> Response { - let Self { - image, - sense, - frame, - selected, - } = self; - - let padding = if frame { + 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 padded_size = image.size() + 2.0 * padding; - let (rect, response) = ui.allocate_exact_size(padded_size, sense); + + 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 selected { + let (expansion, rounding, fill, stroke) = if self.selected { let selection = ui.visuals().selection; ( Vec2::ZERO, - Rounding::ZERO, + self.image.image_options().rounding, selection.bg_fill, selection.stroke, ) - } else if frame { + } else if self.frame { let visuals = ui.style().interact(&response); let expansion = Vec2::splat(visuals.expansion); ( expansion, - visuals.rounding, + self.image.image_options().rounding, visuals.weak_bg_fill, visuals.bg_stroke, ) @@ -552,23 +646,23 @@ impl Widget for ImageButton { Default::default() }; - let image = image.rounding(rounding); // apply rounding to the image - // 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)); + .align_size_within_rect(image_size, rect.shrink2(padding)); // let image_rect = image_rect.expand2(expansion); // can make it blurry, so let's not - image.paint_at(ui, image_rect); + 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); } - response + widgets::image::texture_load_result_response(self.image.source(), &tlr, response) } } diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs index fcb701f47fa..5958d0758dc 100644 --- a/crates/egui/src/widgets/color_picker.rs +++ b/crates/egui/src/widgets/color_picker.rs @@ -234,9 +234,9 @@ fn color_text_ui(ui: &mut Ui, color: impl Into, alpha: Alpha) { if ui.button("📋").on_hover_text("Click to copy").clicked() { if alpha == Alpha::Opaque { - ui.output_mut(|o| o.copied_text = format!("{r}, {g}, {b}")); + ui.ctx().copy_text(format!("{r}, {g}, {b}")); } else { - ui.output_mut(|o| o.copied_text = format!("{r}, {g}, {b}, {a}")); + ui.ctx().copy_text(format!("{r}, {g}, {b}, {a}")); } } @@ -317,7 +317,7 @@ fn color_picker_hsvag_2d(ui: &mut Ui, hsva: &mut HsvaGamma, alpha: Alpha) { color_slider_2d(ui, s, v, |s, v| HsvaGamma { s, v, ..opaque }.into()); } -//// Shows a color picker where the user can change the given [`Hsva`] color. +/// Shows a color picker where the user can change the given [`Hsva`] color. /// /// Returns `true` on change. pub fn color_picker_hsva_2d(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> bool { diff --git a/crates/egui/src/widgets/hyperlink.rs b/crates/egui/src/widgets/hyperlink.rs index 0ef0ee4887f..7017d74639f 100644 --- a/crates/egui/src/widgets/hyperlink.rs +++ b/crates/egui/src/widgets/hyperlink.rs @@ -118,21 +118,18 @@ impl Widget for Hyperlink { let Self { url, text, new_tab } = self; let response = ui.add(Link::new(text)); + if response.clicked() { let modifiers = ui.ctx().input(|i| i.modifiers); - ui.ctx().output_mut(|o| { - o.open_url = Some(crate::output::OpenUrl { - url: url.clone(), - new_tab: new_tab || modifiers.any(), - }); + ui.ctx().open_url(crate::OpenUrl { + url: url.clone(), + new_tab: new_tab || modifiers.any(), }); } if response.middle_clicked() { - ui.ctx().output_mut(|o| { - o.open_url = Some(crate::output::OpenUrl { - url: url.clone(), - new_tab: true, - }); + ui.ctx().open_url(crate::OpenUrl { + url: url.clone(), + new_tab: true, }); } response.on_hover_text(url) diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index 496ddaaf912..2bf150c9fe3 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -1,93 +1,219 @@ -use std::sync::Arc; +use std::borrow::Cow; -use crate::load::Bytes; -use crate::{load::SizeHint, load::TexturePoll, *}; +use crate::load::TextureLoadResult; +use crate::{ + load::{Bytes, SizeHint, SizedTexture, TexturePoll}, + *, +}; use emath::Rot2; +use epaint::{util::FloatOrd, RectShape}; -/// An widget to show an image of a given size. -/// -/// In order to display an image you must first acquire a [`TextureHandle`]. -/// This is best done with [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html) or [`Context::load_texture`]. +/// A widget which displays an image. /// -/// ``` -/// struct MyImage { -/// texture: Option, -/// } +/// The task of actually loading the image is deferred to when the `Image` is added to the [`Ui`], +/// and how it is loaded depends on the provided [`ImageSource`]: /// -/// impl MyImage { -/// fn ui(&mut self, ui: &mut egui::Ui) { -/// let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| { -/// // Load the texture only once. -/// ui.ctx().load_texture( -/// "my-image", -/// egui::ColorImage::example(), -/// Default::default() -/// ) -/// }); +/// - [`ImageSource::Uri`] will load the image using the [asynchronous loading process][`load`]. +/// - [`ImageSource::Bytes`] will also load the image using the [asynchronous loading process][`load`], but with lower latency. +/// - [`ImageSource::Texture`] will use the provided texture. /// -/// // Show the image: -/// ui.add(egui::Image::new(texture, texture.size_vec2())); +/// See [`load`] for more information. /// -/// // Shorter version: -/// ui.image(texture, texture.size_vec2()); -/// } -/// } +/// ### Examples +/// // Using it in a layout: +/// ``` +/// # egui::__run_test_ui(|ui| { +/// ui.add( +/// egui::Image::new(egui::include_image!("../../assets/ferris.png")) +/// .rounding(5.0) +/// ); +/// # }); /// ``` /// -/// Se also [`crate::Ui::image`] and [`crate::ImageButton`]. +/// // Using it just to paint: +/// ``` +/// # egui::__run_test_ui(|ui| { +/// # let rect = egui::Rect::from_min_size(Default::default(), egui::Vec2::splat(100.0)); +/// egui::Image::new(egui::include_image!("../../assets/ferris.png")) +/// .rounding(5.0) +/// .tint(egui::Color32::LIGHT_BLUE) +/// .paint_at(ui, rect); +/// # }); +/// ``` #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] -#[derive(Clone, Copy, Debug)] -pub struct Image { - texture_id: TextureId, - uv: Rect, - size: Vec2, - bg_fill: Color32, - tint: Color32, +#[derive(Debug, Clone)] +pub struct Image<'a> { + source: ImageSource<'a>, + texture_options: TextureOptions, + image_options: ImageOptions, sense: Sense, - rotation: Option<(Rot2, Vec2)>, - rounding: Rounding, + size: ImageSize, + pub(crate) show_loading_spinner: Option, } -impl Image { - pub fn new(texture_id: impl Into, size: impl Into) -> Self { - Self { - texture_id: texture_id.into(), - uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), - size: size.into(), - bg_fill: Default::default(), - tint: Color32::WHITE, - sense: Sense::hover(), - rotation: None, - rounding: Rounding::ZERO, +impl<'a> Image<'a> { + /// Load the image from some source. + pub fn new(source: impl Into>) -> Self { + fn new_mono(source: ImageSource<'_>) -> Image<'_> { + let size = if let ImageSource::Texture(tex) = &source { + // User is probably expecting their texture to have + // the exact size of the provided `SizedTexture`. + ImageSize { + maintain_aspect_ratio: true, + max_size: Vec2::INFINITY, + fit: ImageFit::Exact(tex.size), + } + } else { + Default::default() + }; + + Image { + source, + texture_options: Default::default(), + image_options: Default::default(), + sense: Sense::hover(), + size, + show_loading_spinner: None, + } } + + new_mono(source.into()) } - /// Select UV range. Default is (0,0) in top-left, (1,1) bottom right. - pub fn uv(mut self, uv: impl Into) -> Self { - self.uv = uv.into(); + /// Load the image from a URI. + /// + /// See [`ImageSource::Uri`]. + pub fn from_uri(uri: impl Into>) -> Self { + Self::new(ImageSource::Uri(uri.into())) + } + + /// Load the image from an existing texture. + /// + /// See [`ImageSource::Texture`]. + pub fn from_texture(texture: impl Into) -> Self { + Self::new(ImageSource::Texture(texture.into())) + } + + /// Load the image from some raw bytes. + /// + /// For better error messages, use the `bytes://` prefix for the URI. + /// + /// See [`ImageSource::Bytes`]. + pub fn from_bytes(uri: impl Into>, bytes: impl Into) -> Self { + Self::new(ImageSource::Bytes { + uri: uri.into(), + bytes: bytes.into(), + }) + } + + /// Texture options used when creating the texture. + #[inline] + pub fn texture_options(mut self, texture_options: TextureOptions) -> Self { + self.texture_options = texture_options; self } - /// A solid color to put behind the image. Useful for transparent images. - pub fn bg_fill(mut self, bg_fill: impl Into) -> Self { - self.bg_fill = bg_fill.into(); + /// Set the max width of the image. + /// + /// No matter what the image is scaled to, it will never exceed this limit. + #[inline] + pub fn max_width(mut self, width: f32) -> Self { + self.size.max_size.x = width; self } - /// Multiply image color with this. Default is WHITE (no tint). - pub fn tint(mut self, tint: impl Into) -> Self { - self.tint = tint.into(); + /// Set the max height of the image. + /// + /// No matter what the image is scaled to, it will never exceed this limit. + #[inline] + pub fn max_height(mut self, height: f32) -> Self { + self.size.max_size.y = height; self } - /// Make the image respond to clicks and/or drags. + /// Set the max size of the image. + /// + /// No matter what the image is scaled to, it will never exceed this limit. + #[inline] + pub fn max_size(mut self, size: Vec2) -> Self { + self.size.max_size = size; + self + } + + /// Whether or not the [`ImageFit`] should maintain the image's original aspect ratio. + #[inline] + pub fn maintain_aspect_ratio(mut self, value: bool) -> Self { + self.size.maintain_aspect_ratio = value; + self + } + + /// Fit the image to its original size with some scaling. + /// + /// This will cause the image to overflow if it is larger than the available space. + /// + /// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit. + #[inline] + pub fn fit_to_original_size(mut self, scale: f32) -> Self { + self.size.fit = ImageFit::Original { scale }; + self + } + + /// Fit the image to an exact size. + /// + /// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit. + #[inline] + pub fn fit_to_exact_size(mut self, size: Vec2) -> Self { + self.size.fit = ImageFit::Exact(size); + self + } + + /// Fit the image to a fraction of the available space. + /// + /// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit. + #[inline] + pub fn fit_to_fraction(mut self, fraction: Vec2) -> Self { + self.size.fit = ImageFit::Fraction(fraction); + self + } + + /// Fit the image to 100% of its available size, shrinking it if necessary. + /// + /// This is a shorthand for [`Image::fit_to_fraction`] with `1.0` for both width and height. /// - /// Consider using [`ImageButton`] instead, for an on-hover effect. + /// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit. + #[inline] + pub fn shrink_to_fit(self) -> Self { + self.fit_to_fraction(Vec2::new(1.0, 1.0)) + } + + /// Make the image respond to clicks and/or drags. + #[inline] pub fn sense(mut self, sense: Sense) -> Self { self.sense = sense; 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 { + self.image_options.uv = uv.into(); + self + } + + /// A solid color to put behind the image. Useful for transparent images. + #[inline] + pub fn bg_fill(mut self, bg_fill: impl Into) -> Self { + self.image_options.bg_fill = bg_fill.into(); + self + } + + /// Multiply image color with this. Default is WHITE (no tint). + #[inline] + pub fn tint(mut self, tint: impl Into) -> Self { + self.image_options.tint = tint.into(); + self + } + /// Rotate the image about an origin by some angle /// /// Positive angle is clockwise. @@ -97,9 +223,10 @@ impl Image { /// /// Due to limitations in the current implementation, /// this will turn off rounding of the image. + #[inline] pub fn rotate(mut self, angle: f32, origin: Vec2) -> Self { - self.rotation = Some((Rot2::from_angle(angle), origin)); - self.rounding = Rounding::ZERO; // incompatible with rotation + self.image_options.rotation = Some((Rot2::from_angle(angle), origin)); + self.image_options.rounding = Rounding::ZERO; // incompatible with rotation self } @@ -109,145 +236,266 @@ impl Image { /// /// Due to limitations in the current implementation, /// this will turn off any rotation of the image. + #[inline] pub fn rounding(mut self, rounding: impl Into) -> Self { - self.rounding = rounding.into(); - if self.rounding != Rounding::ZERO { - self.rotation = None; // incompatible with rounding + self.image_options.rounding = rounding.into(); + if self.image_options.rounding != Rounding::ZERO { + self.image_options.rotation = None; // incompatible with rounding } self } + + /// Show a spinner when the image is loading. + /// + /// By default this uses the value of [`Visuals::image_loading_spinners`]. + #[inline] + pub fn show_loading_spinner(mut self, show: bool) -> Self { + self.show_loading_spinner = Some(show); + self + } } -impl Image { - pub fn size(&self) -> Vec2 { - self.size +impl<'a, T: Into>> From for Image<'a> { + fn from(value: T) -> Self { + Image::new(value) } +} - pub fn paint_at(&self, ui: &mut Ui, rect: Rect) { - if ui.is_rect_visible(rect) { - use epaint::*; - let Self { - texture_id, - uv, - size, - bg_fill, - tint, - sense: _, - rotation, - rounding, - } = self; - - if *bg_fill != Default::default() { - let mut mesh = Mesh::default(); - mesh.add_colored_rect(rect, *bg_fill); - ui.painter().add(Shape::mesh(mesh)); - } +impl<'a> Image<'a> { + /// Returns the size the image will occupy in the final UI. + #[inline] + pub fn calc_size(&self, available_size: Vec2, original_image_size: Option) -> Vec2 { + let original_image_size = original_image_size.unwrap_or(Vec2::splat(24.0)); // Fallback for still-loading textures, or failure to load. + self.size.calc_size(available_size, original_image_size) + } - if let Some((rot, origin)) = rotation { - // TODO(emilk): implement this using `PathShape` (add texture support to it). - // This will also give us anti-aliasing of rotated images. - egui_assert!( - *rounding == Rounding::ZERO, - "Image had both rounding and rotation. Please pick only one" - ); - - let mut mesh = Mesh::with_texture(*texture_id); - mesh.add_rect_with_uv(rect, *uv, *tint); - mesh.rotate(*rot, rect.min + *origin * *size); - ui.painter().add(Shape::mesh(mesh)); - } else { - ui.painter().add(RectShape { - rect, - rounding: *rounding, - fill: *tint, - stroke: Stroke::NONE, - fill_texture_id: *texture_id, - uv: *uv, - }); - } + pub fn load_and_calc_size(&self, ui: &Ui, available_size: Vec2) -> Option { + let image_size = self.load_for_size(ui.ctx(), available_size).ok()?.size()?; + Some(self.size.calc_size(available_size, image_size)) + } + + #[inline] + pub fn size(&self) -> Option { + match &self.source { + ImageSource::Texture(texture) => Some(texture.size), + ImageSource::Uri(_) | ImageSource::Bytes { .. } => None, } } + + #[inline] + pub fn image_options(&self) -> &ImageOptions { + &self.image_options + } + + #[inline] + pub fn source(&self) -> &ImageSource<'a> { + &self.source + } + + /// Load the image from its [`Image::source`], returning the resulting [`SizedTexture`]. + /// + /// The `available_size` is used as a hint when e.g. rendering an svg. + /// + /// # Errors + /// May fail if they underlying [`Context::try_load_texture`] call fails. + pub fn load_for_size(&self, ctx: &Context, available_size: Vec2) -> TextureLoadResult { + let size_hint = self.size.hint(available_size); + self.source + .clone() + .load(ctx, self.texture_options, size_hint) + } + + /// Paint the image in the given rectangle. + /// + /// ``` + /// # egui::__run_test_ui(|ui| { + /// # let rect = egui::Rect::from_min_size(Default::default(), egui::Vec2::splat(100.0)); + /// egui::Image::new(egui::include_image!("../../assets/ferris.png")) + /// .rounding(5.0) + /// .tint(egui::Color32::LIGHT_BLUE) + /// .paint_at(ui, rect); + /// # }); + /// ``` + #[inline] + pub fn paint_at(&self, ui: &Ui, rect: Rect) { + paint_texture_load_result( + ui, + &self.load_for_size(ui.ctx(), rect.size()), + rect, + self.show_loading_spinner, + &self.image_options, + ); + } } -impl Widget for Image { +impl<'a> Widget for Image<'a> { fn ui(self, ui: &mut Ui) -> Response { - let (rect, response) = ui.allocate_exact_size(self.size, self.sense); - self.paint_at(ui, rect); - response + let tlr = self.load_for_size(ui.ctx(), ui.available_size()); + let original_image_size = tlr.as_ref().ok().and_then(|t| t.size()); + let ui_size = self.calc_size(ui.available_size(), original_image_size); + + let (rect, response) = ui.allocate_exact_size(ui_size, self.sense); + if ui.is_rect_visible(rect) { + paint_texture_load_result( + ui, + &tlr, + rect, + self.show_loading_spinner, + &self.image_options, + ); + } + texture_load_result_response(&self.source, &tlr, response) } } -/// A widget which displays an image. -/// -/// There are three ways to construct this widget: -/// - [`Image2::from_uri`] -/// - [`Image2::from_bytes`] -/// - [`Image2::from_static_bytes`] -/// -/// In both cases the task of actually loading the image -/// is deferred to when the `Image2` is added to the [`Ui`]. -/// -/// See [`crate::load`] for more information. -pub struct Image2<'a> { - source: ImageSource<'a>, - texture_options: TextureOptions, - size_hint: SizeHint, - fit: ImageFit, - sense: Sense, +/// This type determines the constraints on how +/// the size of an image should be calculated. +#[derive(Debug, Clone, Copy)] +pub struct ImageSize { + /// Whether or not the final size should maintain the original aspect ratio. + /// + /// This setting is applied last. + /// + /// This defaults to `true`. + pub maintain_aspect_ratio: bool, + + /// Determines the maximum size of the image. + /// + /// Defaults to `Vec2::INFINITY` (no limit). + pub max_size: Vec2, + + /// Determines how the image should shrink/expand/stretch/etc. to fit within its allocated space. + /// + /// This setting is applied first. + /// + /// Defaults to `ImageFit::Fraction([1, 1])` + pub fit: ImageFit, } -#[derive(Default, Clone, Copy)] -enum ImageFit { - // TODO: options for aspect ratio - // TODO: other fit strategies - // FitToWidth, - // FitToHeight, - // FitToWidthExact(f32), - // FitToHeightExact(f32), - #[default] - ShrinkToFit, +/// This type determines how the image should try to fit within the UI. +/// +/// The final fit will be clamped to [`ImageSize::max_size`]. +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum ImageFit { + /// Fit the image to its original size, scaled by some factor. + /// + /// Ignores how much space is actually available in the ui. + Original { scale: f32 }, + + /// Fit the image to a fraction of the available size. + Fraction(Vec2), + + /// Fit the image to an exact size. + /// + /// Ignores how much space is actually available in the ui. + Exact(Vec2), } impl ImageFit { - pub fn calculate_final_size(&self, available_size: Vec2, image_size: Vec2) -> Vec2 { - let aspect_ratio = image_size.x / image_size.y; - // TODO: more image sizing options + pub fn resolve(self, available_size: Vec2, image_size: Vec2) -> Vec2 { match self { - // ImageFit::FitToWidth => todo!(), - // ImageFit::FitToHeight => todo!(), - // ImageFit::FitToWidthExact(_) => todo!(), - // ImageFit::FitToHeightExact(_) => todo!(), - ImageFit::ShrinkToFit => { - let width = if available_size.x < image_size.x { - available_size.x - } else { - image_size.x - }; - let height = if available_size.y < image_size.y { - available_size.y - } else { - image_size.y - }; - if width < height { - Vec2::new(width, width / aspect_ratio) + ImageFit::Original { scale } => image_size * scale, + ImageFit::Fraction(fract) => available_size * fract, + ImageFit::Exact(size) => size, + } + } +} + +impl ImageSize { + /// Size hint for e.g. rasterizing an svg. + pub fn hint(&self, available_size: Vec2) -> SizeHint { + let size = match self.fit { + ImageFit::Original { scale } => return SizeHint::Scale(scale.ord()), + ImageFit::Fraction(fract) => available_size * fract, + ImageFit::Exact(size) => size, + }; + + let size = size.min(self.max_size); + + // TODO(emilk): take pixels_per_point into account here! + + // `inf` on an axis means "any value" + match (size.x.is_finite(), size.y.is_finite()) { + (true, true) => SizeHint::Size(size.x.round() as u32, size.y.round() as u32), + (true, false) => SizeHint::Width(size.x.round() as u32), + (false, true) => SizeHint::Height(size.y.round() as u32), + (false, false) => SizeHint::Scale(1.0.ord()), + } + } + + /// Calculate the final on-screen size in points. + pub fn calc_size(&self, available_size: Vec2, original_image_size: Vec2) -> Vec2 { + let Self { + maintain_aspect_ratio, + max_size, + fit, + } = *self; + match fit { + ImageFit::Original { scale } => { + let image_size = original_image_size * scale; + if image_size.x <= max_size.x && image_size.y <= max_size.y { + image_size } else { - Vec2::new(height * aspect_ratio, height) + scale_to_fit(image_size, max_size, maintain_aspect_ratio) } } + ImageFit::Fraction(fract) => { + let scale_to_size = (available_size * fract).min(max_size); + scale_to_fit(original_image_size, scale_to_size, maintain_aspect_ratio) + } + ImageFit::Exact(size) => { + let scale_to_size = size.min(max_size); + scale_to_fit(original_image_size, scale_to_size, maintain_aspect_ratio) + } + } + } +} + +// TODO: 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; + let ratio_y = available_size.y / image_size.y; + let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y }; + let ratio = if ratio.is_finite() { ratio } else { 1.0 }; + image_size * ratio + } else { + available_size + } +} + +impl Default for ImageSize { + #[inline] + fn default() -> Self { + Self { + max_size: Vec2::INFINITY, + fit: ImageFit::Fraction(Vec2::new(1.0, 1.0)), + maintain_aspect_ratio: true, } } } -/// This type tells the [`Ui`] how to load the image. +/// This type tells the [`Ui`] how to load an image. +/// +/// This is used by [`Image::new`] and [`Ui::image`]. +#[derive(Clone)] pub enum ImageSource<'a> { - /// Load the image from a URI. + /// Load the image from a URI, e.g. `https://example.com/image.png`. + /// + /// This could be a `file://` path, `https://` url, `bytes://` identifier, or some other scheme. /// - /// This could be a `file://` url, `http://` url, or a `bare` identifier. /// How the URI will be turned into a texture for rendering purposes is /// up to the registered loaders to handle. /// /// See [`crate::load`] for more information. - Uri(&'a str), + Uri(Cow<'a, str>), + + /// Load the image from an existing texture. + /// + /// The user is responsible for loading the texture, determining its size, + /// and allocating a [`TextureId`] for it. + Texture(SizedTexture), /// Load the image from some raw bytes. /// @@ -257,138 +505,268 @@ pub enum ImageSource<'a> { /// /// This instructs the [`Ui`] to cache the raw bytes, which are then further processed by any registered loaders. /// + /// See also [`include_image`] for an easy way to load and display static images. + /// /// See [`crate::load`] for more information. - Bytes(&'static str, Bytes), + Bytes { + /// The unique identifier for this image, e.g. `bytes://my_logo.png`. + /// + /// You should use a proper extension (`.jpg`, `.png`, `.svg`, etc) for the image to load properly. + /// + /// Use the `bytes://` scheme for the URI for better error messages. + uri: Cow<'static, str>, + + bytes: Bytes, + }, } -impl<'a> From<&'a str> for ImageSource<'a> { - #[inline] - fn from(value: &'a str) -> Self { - Self::Uri(value) +impl<'a> std::fmt::Debug for ImageSource<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ImageSource::Bytes { uri, .. } | ImageSource::Uri(uri) => uri.as_ref().fmt(f), + ImageSource::Texture(st) => st.id.fmt(f), + } } } -impl> From<(&'static str, T)> for ImageSource<'static> { +impl<'a> ImageSource<'a> { + /// Size of the texture, if known. #[inline] - fn from((uri, bytes): (&'static str, T)) -> Self { - Self::Bytes(uri, bytes.into()) + pub fn texture_size(&self) -> Option { + match self { + ImageSource::Texture(texture) => Some(texture.size), + ImageSource::Uri(_) | ImageSource::Bytes { .. } => None, + } } -} -impl<'a> Image2<'a> { - /// Load the image from some source. - pub fn new(source: ImageSource<'a>) -> Self { - Self { - source, - texture_options: Default::default(), - size_hint: Default::default(), - fit: Default::default(), - sense: Sense::hover(), + /// # Errors + /// Failure to load the texture. + pub fn load( + self, + ctx: &Context, + texture_options: TextureOptions, + size_hint: SizeHint, + ) -> TextureLoadResult { + match self { + Self::Texture(texture) => Ok(TexturePoll::Ready { texture }), + Self::Uri(uri) => ctx.try_load_texture(uri.as_ref(), texture_options, size_hint), + Self::Bytes { uri, bytes } => { + ctx.include_bytes(uri.clone(), bytes); + ctx.try_load_texture(uri.as_ref(), texture_options, size_hint) + } } } - /// Load the image from a URI. + /// Get the `uri` that this image was constructed from. /// - /// See [`ImageSource::Uri`]. - pub fn from_uri(uri: &'a str) -> Self { - Self { - source: ImageSource::Uri(uri), - texture_options: Default::default(), - size_hint: Default::default(), - fit: Default::default(), - sense: Sense::hover(), + /// This will return `None` for [`Self::Texture`]. + pub fn uri(&self) -> Option<&str> { + match self { + ImageSource::Bytes { uri, .. } | ImageSource::Uri(uri) => Some(uri), + ImageSource::Texture(_) => None, } } +} - /// Load the image from some raw `'static` bytes. - /// - /// For example, you can use this to load an image from bytes obtained via [`include_bytes`]. - /// - /// See [`ImageSource::Bytes`]. - pub fn from_static_bytes(name: &'static str, bytes: &'static [u8]) -> Self { - Self { - source: ImageSource::Bytes(name, Bytes::Static(bytes)), - texture_options: Default::default(), - size_hint: Default::default(), - fit: Default::default(), - sense: Sense::hover(), +pub fn paint_texture_load_result( + ui: &Ui, + tlr: &TextureLoadResult, + rect: Rect, + show_loading_spinner: Option, + options: &ImageOptions, +) { + match tlr { + Ok(TexturePoll::Ready { texture }) => { + paint_texture_at(ui.painter(), rect, options, texture); + } + Ok(TexturePoll::Pending { .. }) => { + let show_loading_spinner = + show_loading_spinner.unwrap_or(ui.visuals().image_loading_spinners); + if show_loading_spinner { + Spinner::new().paint_at(ui, rect); + } + } + Err(_) => { + let font_id = TextStyle::Body.resolve(ui.style()); + ui.painter().text( + rect.center(), + Align2::CENTER_CENTER, + "⚠", + font_id, + ui.visuals().error_fg_color, + ); } } +} - /// Load the image from some raw bytes. - /// - /// See [`ImageSource::Bytes`]. - pub fn from_bytes(name: &'static str, bytes: impl Into>) -> Self { - Self { - source: ImageSource::Bytes(name, Bytes::Shared(bytes.into())), - texture_options: Default::default(), - size_hint: Default::default(), - fit: Default::default(), - sense: Sense::hover(), +/// Attach tooltips like "Loading…" or "Failed loading: …". +pub fn texture_load_result_response( + source: &ImageSource<'_>, + tlr: &TextureLoadResult, + response: Response, +) -> Response { + match tlr { + Ok(TexturePoll::Ready { .. }) => response, + Ok(TexturePoll::Pending { .. }) => { + let uri = source.uri().unwrap_or("image"); + response.on_hover_text(format!("Loading {uri}…")) + } + Err(err) => { + let uri = source.uri().unwrap_or("image"); + response.on_hover_text(format!("Failed loading {uri}: {err}")) } } +} - /// Texture options used when creating the texture. +impl<'a> From<&'a str> for ImageSource<'a> { #[inline] - pub fn texture_options(mut self, texture_options: TextureOptions) -> Self { - self.texture_options = texture_options; - self + fn from(value: &'a str) -> Self { + Self::Uri(value.into()) } +} - /// Size hint used when creating the texture. +impl<'a> From<&'a String> for ImageSource<'a> { #[inline] - pub fn size_hint(mut self, size_hint: impl Into) -> Self { - self.size_hint = size_hint.into(); - self + fn from(value: &'a String) -> Self { + Self::Uri(value.as_str().into()) } +} - /// Make the image respond to clicks and/or drags. +impl From for ImageSource<'static> { + fn from(value: String) -> Self { + Self::Uri(value.into()) + } +} + +impl<'a> From<&'a Cow<'a, str>> for ImageSource<'a> { #[inline] - pub fn sense(mut self, sense: Sense) -> Self { - self.sense = sense; - self + fn from(value: &'a Cow<'a, str>) -> Self { + Self::Uri(value.clone()) } } -impl<'a> Widget for Image2<'a> { - fn ui(self, ui: &mut Ui) -> Response { - let uri = match self.source { - ImageSource::Uri(uri) => uri, - ImageSource::Bytes(uri, bytes) => { - match bytes { - Bytes::Static(bytes) => ui.ctx().include_static_bytes(uri, bytes), - Bytes::Shared(bytes) => ui.ctx().include_bytes(uri, bytes), - } - uri - } - }; +impl<'a> From> for ImageSource<'a> { + #[inline] + fn from(value: Cow<'a, str>) -> Self { + Self::Uri(value) + } +} - match ui - .ctx() - .try_load_texture(uri, self.texture_options, self.size_hint) - { - Ok(TexturePoll::Ready { texture }) => { - let final_size = self.fit.calculate_final_size( - ui.available_size(), - Vec2::new(texture.size[0] as f32, texture.size[1] as f32), - ); - - let (rect, response) = ui.allocate_exact_size(final_size, self.sense); - - let mut mesh = Mesh::with_texture(texture.id); - mesh.add_rect_with_uv( - rect, - Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), - Color32::WHITE, - ); - ui.painter().add(Shape::mesh(mesh)); - - response - } - Ok(TexturePoll::Pending { .. }) => { - ui.spinner().on_hover_text(format!("Loading {uri:?}…")) - } - Err(err) => ui.colored_label(ui.visuals().error_fg_color, err.to_string()), +impl> From<(&'static str, T)> for ImageSource<'static> { + #[inline] + fn from((uri, bytes): (&'static str, T)) -> Self { + Self::Bytes { + uri: uri.into(), + bytes: bytes.into(), + } + } +} + +impl> From<(Cow<'static, str>, T)> for ImageSource<'static> { + #[inline] + fn from((uri, bytes): (Cow<'static, str>, T)) -> Self { + Self::Bytes { + uri, + bytes: bytes.into(), + } + } +} + +impl> From<(String, T)> for ImageSource<'static> { + #[inline] + fn from((uri, bytes): (String, T)) -> Self { + Self::Bytes { + uri: uri.into(), + bytes: bytes.into(), + } + } +} + +impl> From for ImageSource<'static> { + fn from(value: T) -> Self { + Self::Texture(value.into()) + } +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct ImageOptions { + /// Select UV range. Default is (0,0) in top-left, (1,1) bottom right. + pub uv: Rect, + + /// A solid color to put behind the image. Useful for transparent images. + pub bg_fill: Color32, + + /// Multiply image color with this. Default is WHITE (no tint). + pub tint: Color32, + + /// Rotate the image about an origin by some angle + /// + /// Positive angle is clockwise. + /// Origin is a vector in normalized UV space ((0,0) in top-left, (1,1) bottom right). + /// + /// To rotate about the center you can pass `Vec2::splat(0.5)` as the origin. + /// + /// Due to limitations in the current implementation, + /// this will turn off rounding of the image. + pub rotation: Option<(Rot2, Vec2)>, + + /// Round the corners of the image. + /// + /// The default is no rounding ([`Rounding::ZERO`]). + /// + /// Due to limitations in the current implementation, + /// this will turn off any rotation of the image. + pub rounding: Rounding, +} + +impl Default for ImageOptions { + fn default() -> Self { + Self { + uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), + bg_fill: Default::default(), + tint: Color32::WHITE, + rotation: None, + rounding: Rounding::ZERO, + } + } +} + +pub fn paint_texture_at( + painter: &Painter, + rect: Rect, + options: &ImageOptions, + texture: &SizedTexture, +) { + if options.bg_fill != Default::default() { + let mut mesh = Mesh::default(); + mesh.add_colored_rect(rect, options.bg_fill); + painter.add(Shape::mesh(mesh)); + } + + match options.rotation { + 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!( + options.rounding == Rounding::ZERO, + "Image had both rounding and rotation. Please pick only one" + ); + + let mut mesh = Mesh::with_texture(texture.id); + mesh.add_rect_with_uv(rect, options.uv, options.tint); + mesh.rotate(rot, rect.min + origin * rect.size()); + painter.add(Shape::mesh(mesh)); + } + None => { + painter.add(RectShape { + rect, + rounding: options.rounding, + fill: options.tint, + stroke: Stroke::NONE, + fill_texture_id: texture.id, + uv: options.uv, + }); } } } diff --git a/crates/egui/src/widgets/mod.rs b/crates/egui/src/widgets/mod.rs index c1193dba6e4..d589e386ac7 100644 --- a/crates/egui/src/widgets/mod.rs +++ b/crates/egui/src/widgets/mod.rs @@ -22,7 +22,7 @@ pub mod text_edit; pub use button::*; pub use drag_value::DragValue; pub use hyperlink::*; -pub use image::{Image, Image2, ImageSource}; +pub use image::{paint_texture_at, Image, ImageFit, ImageOptions, ImageSize, ImageSource}; pub use label::*; pub use progress_bar::ProgressBar; pub use selected_label::SelectableLabel; diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index 4314498c5e2..07c4f410a2d 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -508,7 +508,8 @@ impl<'a> Slider<'a> { value = emath::round_to_decimals(value, max_decimals); } if let Some(step) = self.step { - value = (value / step).round() * step; + let start = *self.range.start(); + value = start + ((value - start) / step).round() * step; } set(&mut self.get_set_value, value); } @@ -548,7 +549,7 @@ impl<'a> Slider<'a> { } /// Just the slider, no text - fn slider_ui(&mut self, ui: &mut Ui, response: &Response) { + fn slider_ui(&mut self, ui: &Ui, response: &Response) { let rect = &response.rect; let position_range = self.position_range(rect); @@ -570,6 +571,16 @@ impl<'a> Slider<'a> { let mut increment = 0usize; if response.has_focus() { + ui.ctx().memory_mut(|m| { + m.set_focus_lock_filter( + response.id, + EventFilter { + arrows: true, // pressing arrows should not move focus to next widget + ..Default::default() + }, + ); + }); + let (dec_key, inc_key) = match self.orientation { SliderOrientation::Horizontal => (Key::ArrowLeft, Key::ArrowRight), // Note that this is for moving the slider position, @@ -595,13 +606,14 @@ impl<'a> Slider<'a> { let kb_step = increment as f32 - decrement as f32; if kb_step != 0.0 { + let ui_point_per_step = 1.0; // move this many ui points for each kb_step let prev_value = self.get_value(); let prev_position = self.position_from_value(prev_value, position_range); - let new_position = prev_position + kb_step; + let new_position = prev_position + ui_point_per_step * kb_step; let new_value = match self.step { Some(step) => prev_value + (kb_step as f64 * step), None if self.smart_aim => { - let aim_radius = ui.input(|i| i.aim_radius()); + let aim_radius = 0.49 * ui_point_per_step; // Chosen so we don't include `prev_value` in the search. emath::smart_aim::best_in_range_f64( self.value_from_position(new_position - aim_radius, position_range), self.value_from_position(new_position + aim_radius, position_range), @@ -692,7 +704,9 @@ impl<'a> Slider<'a> { let handle_radius = self.handle_radius(rect); match self.orientation { SliderOrientation::Horizontal => rect.x_range().shrink(handle_radius), - SliderOrientation::Vertical => rect.y_range().shrink(handle_radius), + // The vertical case has to be flipped because the largest slider value maps to the + // lowest y value (which is at the top) + SliderOrientation::Vertical => rect.y_range().shrink(handle_radius).flip(), } } diff --git a/crates/egui/src/widgets/spinner.rs b/crates/egui/src/widgets/spinner.rs index f2b253bc6a6..b6fb928de3a 100644 --- a/crates/egui/src/widgets/spinner.rs +++ b/crates/egui/src/widgets/spinner.rs @@ -1,4 +1,4 @@ -use epaint::{emath::lerp, vec2, Color32, Pos2, Shape, Stroke}; +use epaint::{emath::lerp, vec2, Color32, Pos2, Rect, Shape, Stroke}; use crate::{Response, Sense, Ui, Widget}; @@ -31,21 +31,15 @@ impl Spinner { self.color = Some(color.into()); self } -} - -impl Widget for Spinner { - fn ui(self, ui: &mut Ui) -> Response { - let size = self - .size - .unwrap_or_else(|| ui.style().spacing.interact_size.y); - let color = self - .color - .unwrap_or_else(|| ui.visuals().strong_text_color()); - let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover()); + /// Paint the spinner in the given rectangle. + pub fn paint_at(&self, ui: &Ui, rect: Rect) { if ui.is_rect_visible(rect) { - ui.ctx().request_repaint(); + ui.ctx().request_repaint(); // because it is animated + let color = self + .color + .unwrap_or_else(|| ui.visuals().strong_text_color()); let radius = (rect.height() / 2.0) - 2.0; let n_points = 20; let time = ui.input(|i| i.time); @@ -61,6 +55,16 @@ impl Widget for Spinner { ui.painter() .add(Shape::line(points, Stroke::new(3.0, color))); } + } +} + +impl Widget for Spinner { + fn ui(self, ui: &mut Ui) -> Response { + let size = self + .size + .unwrap_or_else(|| ui.style().spacing.interact_size.y); + let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover()); + self.paint_at(ui, rect); response } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 3019fa7fd92..7898c2256fe 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +#[cfg(feature = "accesskit")] +use accesskit::Role; use epaint::text::{cursor::*, Galley, LayoutJob}; use crate::{output::OutputEvent, *}; @@ -65,7 +67,7 @@ pub struct TextEdit<'t> { interactive: bool, desired_width: Option, desired_height_rows: usize, - lock_focus: bool, + event_filter: EventFilter, cursor_at_end: bool, min_size: Vec2, align: Align2, @@ -115,7 +117,11 @@ impl<'t> TextEdit<'t> { interactive: true, desired_width: None, desired_height_rows: 4, - lock_focus: false, + event_filter: EventFilter { + arrows: true, // moving the cursor is really important + tab: false, // tab is used to change focus, not to insert a tab character + ..Default::default() + }, cursor_at_end: true, min_size: Vec2::ZERO, align: Align2::LEFT_TOP, @@ -127,7 +133,7 @@ impl<'t> TextEdit<'t> { /// Build a [`TextEdit`] focused on code editing. /// By default it comes with: /// - monospaced font - /// - focus lock + /// - focus lock (tab will insert a tab character instead of moving focus) pub fn code_editor(self) -> Self { self.font(TextStyle::Monospace).lock_focus(true) } @@ -266,8 +272,8 @@ impl<'t> TextEdit<'t> { /// /// When `true`, the widget will keep the focus and pressing TAB /// will insert the `'\t'` character. - pub fn lock_focus(mut self, b: bool) -> Self { - self.lock_focus = b; + pub fn lock_focus(mut self, tab_will_indent: bool) -> Self { + self.event_filter.tab = tab_will_indent; self } @@ -352,7 +358,9 @@ impl<'t> TextEdit<'t> { let margin = self.margin; let max_rect = ui.available_rect_before_wrap().shrink2(margin); let mut content_ui = ui.child_ui(max_rect, *ui.layout()); + let mut output = self.show_content(&mut content_ui); + let id = output.response.id; let frame_rect = output.response.rect.expand2(margin); ui.allocate_space(frame_rect.size()); @@ -413,7 +421,7 @@ impl<'t> TextEdit<'t> { interactive, desired_width, desired_height_rows, - lock_focus, + event_filter, cursor_at_end, min_size, align, @@ -569,7 +577,7 @@ impl<'t> TextEdit<'t> { let mut cursor_range = None; let prev_cursor_range = state.cursor_range(&galley); if interactive && ui.memory(|mem| mem.has_focus(id)) { - ui.memory_mut(|mem| mem.lock_focus(id, lock_focus)); + ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter)); let default_cursor_range = if cursor_at_end { CursorRange::one(galley.end()) @@ -589,6 +597,7 @@ impl<'t> TextEdit<'t> { password, default_cursor_range, char_limit, + event_filter, ); if changed { @@ -744,7 +753,7 @@ impl<'t> TextEdit<'t> { builder.set_default_action_verb(accesskit::DefaultActionVerb::Focus); if self.multiline { - builder.set_multiline(); + builder.set_role(Role::MultilineTextInput); } parent_id @@ -752,7 +761,7 @@ impl<'t> TextEdit<'t> { if let Some(parent_id) = parent_id { // drop ctx lock before further processing - use accesskit::{Role, TextDirection}; + use accesskit::TextDirection; ui.ctx().with_accessibility_parent(parent_id, || { for (i, row) in galley.rows.iter().enumerate() { @@ -869,7 +878,7 @@ fn ccursor_from_accesskit_text_position( /// Check for (keyboard) events to edit the cursor and/or text. #[allow(clippy::too_many_arguments)] fn events( - ui: &mut crate::Ui, + ui: &crate::Ui, state: &mut TextEditState, text: &mut dyn TextBuffer, galley: &mut Arc, @@ -880,6 +889,7 @@ fn events( password: bool, default_cursor_range: CursorRange, char_limit: usize, + event_filter: EventFilter, ) -> (bool, CursorRange) { let mut cursor_range = state.cursor_range(galley).unwrap_or(default_cursor_range); @@ -892,13 +902,13 @@ fn events( let copy_if_not_password = |ui: &Ui, text: String| { if !password { - ui.ctx().output_mut(|o| o.copied_text = text); + ui.ctx().copy_text(text); } }; let mut any_change = false; - let events = ui.input(|i| i.events.clone()); // avoid dead-lock by cloning. TODO(emilk): optimize + let events = ui.input(|i| i.filtered_events(&event_filter)); for event in &events { let did_mutate_text = match event { Event::Copy => { @@ -946,19 +956,15 @@ fn events( pressed: true, modifiers, .. - } => { - if multiline && ui.memory(|mem| mem.has_lock_focus(id)) { - let mut ccursor = delete_selected(text, &cursor_range); - if modifiers.shift { - // TODO(emilk): support removing indentation over a selection? - decrease_indentation(&mut ccursor, text); - } else { - insert_text(&mut ccursor, text, "\t", char_limit); - } - Some(CCursorRange::one(ccursor)) + } if multiline => { + let mut ccursor = delete_selected(text, &cursor_range); + if modifiers.shift { + // TODO(emilk): support removing indentation over a selection? + decrease_indentation(&mut ccursor, text); } else { - None + insert_text(&mut ccursor, text, "\t", char_limit); } + Some(CCursorRange::one(ccursor)) } Event::Key { key: Key::Enter, @@ -980,8 +986,7 @@ fn events( pressed: true, modifiers, .. - } if modifiers.command && !modifiers.shift => { - // TODO(emilk): redo + } if modifiers.matches(Modifiers::COMMAND) => { if let Some((undo_ccursor_range, undo_txt)) = state .undoer .lock() @@ -993,6 +998,25 @@ fn events( None } } + Event::Key { + key, + pressed: true, + modifiers, + .. + } if (modifiers.matches(Modifiers::COMMAND) && *key == Key::Y) + || (modifiers.matches(Modifiers::SHIFT | Modifiers::COMMAND) && *key == Key::Z) => + { + if let Some((redo_ccursor_range, redo_txt)) = state + .undoer + .lock() + .redo(&(cursor_range.as_ccursor_range(), text.as_str().to_owned())) + { + text.replace(redo_txt); + Some(*redo_ccursor_range) + } else { + None + } + } Event::Key { key, @@ -1085,7 +1109,7 @@ fn events( // ---------------------------------------------------------------------------- fn paint_cursor_selection( - ui: &mut Ui, + ui: &Ui, painter: &Painter, pos: Pos2, galley: &Galley, @@ -1127,7 +1151,7 @@ fn paint_cursor_selection( } fn paint_cursor_end( - ui: &mut Ui, + ui: &Ui, row_height: f32, painter: &Painter, pos: Pos2, diff --git a/crates/egui/src/widgets/text_edit/text_buffer.rs b/crates/egui/src/widgets/text_edit/text_buffer.rs index e29a07a9ff5..a97d264a15c 100644 --- a/crates/egui/src/widgets/text_edit/text_buffer.rs +++ b/crates/egui/src/widgets/text_edit/text_buffer.rs @@ -1,4 +1,4 @@ -use std::ops::Range; +use std::{borrow::Cow, ops::Range}; /// Trait constraining what types [`crate::TextEdit`] may use as /// an underlying buffer. @@ -100,6 +100,36 @@ impl TextBuffer for String { } } +impl<'a> TextBuffer for Cow<'a, str> { + fn is_mutable(&self) -> bool { + true + } + + fn as_str(&self) -> &str { + self.as_ref() + } + + fn insert_text(&mut self, text: &str, char_index: usize) -> usize { + ::insert_text(self.to_mut(), text, char_index) + } + + fn delete_char_range(&mut self, char_range: Range) { + ::delete_char_range(self.to_mut(), char_range); + } + + fn clear(&mut self) { + ::clear(self.to_mut()); + } + + fn replace(&mut self, text: &str) { + *self = Cow::Owned(text.to_owned()); + } + + fn take(&mut self) -> String { + std::mem::take(self).into_owned() + } +} + /// Immutable view of a `&str`! impl<'a> TextBuffer for &'a str { fn is_mutable(&self) -> bool { diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index ef35b1ccb3d..7ea72e67f57 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "egui_demo_app" -version = "0.22.0" +version = "0.23.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" @@ -18,26 +18,31 @@ crate-type = ["cdylib", "rlib"] [features] default = ["glow", "persistence"] +# image_viewer adds about 0.9 MB of WASM +web_app = ["http", "persistence", "web_screen_reader"] + http = ["ehttp", "image", "poll-promise", "egui_extras/image"] +image_viewer = ["image", "egui_extras/all_loaders", "rfd"] persistence = ["eframe/persistence", "egui/persistence", "serde"] web_screen_reader = ["eframe/web_screen_reader"] # experimental serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"] -syntax_highlighting = ["egui_demo_lib/syntax_highlighting"] +syntect = ["egui_demo_lib/syntect"] glow = ["eframe/glow"] wgpu = ["eframe/wgpu", "bytemuck"] - [dependencies] chrono = { version = "0.4", default-features = false, features = [ "js-sys", "wasmbind", ] } -eframe = { version = "0.22.0", path = "../eframe", default-features = false } -egui = { version = "0.22.0", path = "../egui", features = [ +eframe = { version = "0.23.0", path = "../eframe", default-features = false } +egui = { version = "0.23.0", path = "../egui", features = [ + "callstack", "extra_debug_asserts", + "log", ] } -egui_demo_lib = { version = "0.22.0", path = "../egui_demo_lib", features = [ +egui_demo_lib = { version = "0.23.0", path = "../egui_demo_lib", features = [ "chrono", ] } log = { version = "0.4", features = ["std"] } @@ -45,17 +50,17 @@ log = { version = "0.4", features = ["std"] } # Optional dependencies: bytemuck = { version = "1.7.1", optional = true } -egui_extras = { version = "0.22.0", optional = true, path = "../egui_extras", features = [ - "log", +egui_extras = { version = "0.23.0", path = "../egui_extras", features = [ + "image", ] } # feature "http": -ehttp = { version = "0.3.0", optional = true } +ehttp = { version = "0.3.1", optional = true } image = { version = "0.24", optional = true, default-features = false, features = [ "jpeg", "png", ] } -poll-promise = { version = "0.2", optional = true, default-features = false } +poll-promise = { version = "0.3", optional = true, default-features = false } # feature "persistence": serde = { version = "1", optional = true, features = ["derive"] } @@ -64,6 +69,7 @@ serde = { version = "1", optional = true, features = ["derive"] } # native: [target.'cfg(not(target_arch = "wasm32"))'.dependencies] env_logger = "0.10" +rfd = { version = "0.11", optional = true } # web: [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/crates/egui_demo_app/src/apps/http_app.rs b/crates/egui_demo_app/src/apps/http_app.rs index e550c1dbc19..90be24dd280 100644 --- a/crates/egui_demo_app/src/apps/http_app.rs +++ b/crates/egui_demo_app/src/apps/http_app.rs @@ -1,4 +1,4 @@ -use egui_extras::RetainedImage; +use egui::Image; use poll_promise::Promise; struct Resource { @@ -8,7 +8,7 @@ struct Resource { text: Option, /// If set, the response was an image. - image: Option, + image: Option>, /// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md"). colored_text: Option, @@ -17,21 +17,27 @@ struct Resource { impl Resource { fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self { let content_type = response.content_type().unwrap_or_default(); - let image = if content_type.starts_with("image/") { - RetainedImage::from_image_bytes(&response.url, &response.bytes).ok() + if content_type.starts_with("image/") { + ctx.include_bytes(response.url.clone(), response.bytes.clone()); + let image = Image::from_uri(response.url.clone()); + + Self { + response, + text: None, + colored_text: None, + image: Some(image), + } } else { - None - }; - - let text = response.text(); - let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text)); - let text = text.map(|text| text.to_owned()); - - Self { - response, - text, - image, - colored_text, + let text = response.text(); + let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text)); + let text = text.map(|text| text.to_owned()); + + Self { + response, + text, + colored_text, + image: None, + } } } } @@ -63,6 +69,7 @@ impl eframe::App for HttpApp { }); egui::CentralPanel::default().show(ctx, |ui| { + let prev_url = self.url.clone(); let trigger_fetch = ui_url(ui, frame, &mut self.url); ui.horizontal_wrapped(|ui| { @@ -77,6 +84,7 @@ impl eframe::App for HttpApp { let (sender, promise) = Promise::new(); let request = ehttp::Request::get(&self.url); ehttp::fetch(request, move |response| { + ctx.forget_image(&prev_url); ctx.request_repaint(); // wake up UI thread let resource = response.map(|response| Resource::from_response(&ctx, response)); sender.send(resource); @@ -187,15 +195,13 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) { if let Some(text) = &text { let tooltip = "Click to copy the response body"; if ui.button("📋").on_hover_text(tooltip).clicked() { - ui.output_mut(|o| o.copied_text = text.clone()); + ui.ctx().copy_text(text.clone()); } ui.separator(); } if let Some(image) = image { - let mut size = image.size_vec2(); - size *= (ui.available_width() / size.x).min(1.0); - image.show_size(ui, size); + ui.add(image.clone()); } else if let Some(colored_text) = colored_text { colored_text.ui(ui); } else if let Some(text) = &text { @@ -217,25 +223,19 @@ fn selectable_text(ui: &mut egui::Ui, mut text: &str) { // ---------------------------------------------------------------------------- // Syntax highlighting: -#[cfg(feature = "syntect")] fn syntax_highlighting( ctx: &egui::Context, response: &ehttp::Response, text: &str, ) -> Option { let extension_and_rest: Vec<&str> = response.url.rsplitn(2, '.').collect(); - let extension = extension_and_rest.get(0)?; - let theme = crate::syntax_highlighting::CodeTheme::from_style(&ctx.style()); - Some(ColoredText(crate::syntax_highlighting::highlight( + let extension = extension_and_rest.first()?; + let theme = egui_extras::syntax_highlighting::CodeTheme::from_style(&ctx.style()); + Some(ColoredText(egui_extras::syntax_highlighting::highlight( ctx, &theme, text, extension, ))) } -#[cfg(not(feature = "syntect"))] -fn syntax_highlighting(_ctx: &egui::Context, _: &ehttp::Response, _: &str) -> Option { - None -} - struct ColoredText(egui::text::LayoutJob); impl ColoredText { diff --git a/crates/egui_demo_app/src/apps/image_viewer.rs b/crates/egui_demo_app/src/apps/image_viewer.rs new file mode 100644 index 00000000000..6bc6cc05bea --- /dev/null +++ b/crates/egui_demo_app/src/apps/image_viewer.rs @@ -0,0 +1,214 @@ +use egui::emath::Rot2; +use egui::panel::Side; +use egui::panel::TopBottomSide; +use egui::ImageFit; +use egui::Slider; +use egui::Vec2; + +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct ImageViewer { + current_uri: String, + uri_edit_text: String, + image_options: egui::ImageOptions, + chosen_fit: ChosenFit, + fit: ImageFit, + maintain_aspect_ratio: bool, + max_size: Vec2, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +enum ChosenFit { + ExactSize, + Fraction, + OriginalSize, +} + +impl ChosenFit { + fn as_str(&self) -> &'static str { + match self { + ChosenFit::ExactSize => "exact size", + ChosenFit::Fraction => "fraction", + ChosenFit::OriginalSize => "original size", + } + } +} + +impl Default for ImageViewer { + fn default() -> Self { + Self { + current_uri: "https://picsum.photos/seed/1.759706314/1024".to_owned(), + uri_edit_text: "https://picsum.photos/seed/1.759706314/1024".to_owned(), + image_options: egui::ImageOptions::default(), + chosen_fit: ChosenFit::Fraction, + fit: ImageFit::Fraction(Vec2::splat(1.0)), + maintain_aspect_ratio: true, + max_size: Vec2::splat(2048.0), + } + } +} + +impl eframe::App for ImageViewer { + fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) { + egui::TopBottomPanel::new(TopBottomSide::Top, "url bar").show(ctx, |ui| { + ui.horizontal_centered(|ui| { + ui.label("URI:"); + ui.text_edit_singleline(&mut self.uri_edit_text); + if ui.small_button("✔").clicked() { + ctx.forget_image(&self.current_uri); + self.uri_edit_text = self.uri_edit_text.trim().to_owned(); + self.current_uri = self.uri_edit_text.clone(); + }; + + #[cfg(not(target_arch = "wasm32"))] + if ui.button("file…").clicked() { + if let Some(path) = rfd::FileDialog::new().pick_file() { + self.uri_edit_text = format!("file://{}", path.display()); + self.current_uri = self.uri_edit_text.clone(); + } + } + }); + }); + + egui::SidePanel::new(Side::Left, "controls").show(ctx, |ui| { + // uv + ui.label("UV"); + ui.add(Slider::new(&mut self.image_options.uv.min.x, 0.0..=1.0).text("min x")); + ui.add(Slider::new(&mut self.image_options.uv.min.y, 0.0..=1.0).text("min y")); + ui.add(Slider::new(&mut self.image_options.uv.max.x, 0.0..=1.0).text("max x")); + ui.add(Slider::new(&mut self.image_options.uv.max.y, 0.0..=1.0).text("max y")); + + // rotation + ui.add_space(2.0); + let had_rotation = self.image_options.rotation.is_some(); + let mut has_rotation = had_rotation; + ui.checkbox(&mut has_rotation, "Rotation"); + match (had_rotation, has_rotation) { + (true, false) => self.image_options.rotation = None, + (false, true) => { + self.image_options.rotation = + Some((Rot2::from_angle(0.0), Vec2::new(0.5, 0.5))); + } + (true, true) | (false, false) => {} + } + + if let Some((rot, origin)) = self.image_options.rotation.as_mut() { + let mut angle = rot.angle(); + + ui.label("angle"); + ui.drag_angle(&mut angle); + *rot = Rot2::from_angle(angle); + + ui.add(Slider::new(&mut origin.x, 0.0..=1.0).text("origin x")); + ui.add(Slider::new(&mut origin.y, 0.0..=1.0).text("origin y")); + } + + // bg_fill + ui.add_space(2.0); + ui.horizontal(|ui| { + ui.color_edit_button_srgba(&mut self.image_options.bg_fill); + ui.label("Background color"); + }); + + // tint + ui.add_space(2.0); + ui.horizontal(|ui| { + ui.color_edit_button_srgba(&mut self.image_options.tint); + ui.label("Tint"); + }); + + // fit + ui.add_space(10.0); + ui.label( + "The chosen fit will determine how the image tries to fill the available space", + ); + egui::ComboBox::from_label("Fit") + .selected_text(self.chosen_fit.as_str()) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut self.chosen_fit, + ChosenFit::ExactSize, + ChosenFit::ExactSize.as_str(), + ); + ui.selectable_value( + &mut self.chosen_fit, + ChosenFit::Fraction, + ChosenFit::Fraction.as_str(), + ); + ui.selectable_value( + &mut self.chosen_fit, + ChosenFit::OriginalSize, + ChosenFit::OriginalSize.as_str(), + ); + }); + + match self.chosen_fit { + ChosenFit::ExactSize => { + if !matches!(self.fit, ImageFit::Exact(_)) { + self.fit = ImageFit::Exact(Vec2::splat(128.0)); + } + let ImageFit::Exact(size) = &mut self.fit else { + unreachable!() + }; + ui.add(Slider::new(&mut size.x, 0.0..=2048.0).text("width")); + ui.add(Slider::new(&mut size.y, 0.0..=2048.0).text("height")); + } + ChosenFit::Fraction => { + if !matches!(self.fit, ImageFit::Fraction(_)) { + self.fit = ImageFit::Fraction(Vec2::splat(1.0)); + } + let ImageFit::Fraction(fract) = &mut self.fit else { + unreachable!() + }; + ui.add(Slider::new(&mut fract.x, 0.0..=1.0).text("width")); + ui.add(Slider::new(&mut fract.y, 0.0..=1.0).text("height")); + } + ChosenFit::OriginalSize => { + if !matches!(self.fit, ImageFit::Original { .. }) { + self.fit = ImageFit::Original { scale: 1.0 }; + } + let ImageFit::Original { scale } = &mut self.fit else { + unreachable!() + }; + ui.add(Slider::new(scale, 0.1..=4.0).text("scale")); + } + } + + // max size + ui.add_space(5.0); + ui.label("The calculated size will not exceed the maximum size"); + ui.add(Slider::new(&mut self.max_size.x, 0.0..=2048.0).text("width")); + ui.add(Slider::new(&mut self.max_size.y, 0.0..=2048.0).text("height")); + + // aspect ratio + ui.add_space(5.0); + ui.label("Aspect ratio is maintained by scaling both sides as necessary"); + ui.checkbox(&mut self.maintain_aspect_ratio, "Maintain aspect ratio"); + }); + + egui::CentralPanel::default().show(ctx, |ui| { + egui::ScrollArea::new([true, true]).show(ui, |ui| { + let mut image = egui::Image::from_uri(&self.current_uri); + image = image.uv(self.image_options.uv); + image = image.bg_fill(self.image_options.bg_fill); + image = image.tint(self.image_options.tint); + let (angle, origin) = self + .image_options + .rotation + .map_or((0.0, Vec2::splat(0.5)), |(rot, origin)| { + (rot.angle(), origin) + }); + image = image.rotate(angle, origin); + match self.fit { + ImageFit::Original { scale } => image = image.fit_to_original_size(scale), + ImageFit::Fraction(fract) => image = image.fit_to_fraction(fract), + ImageFit::Exact(size) => image = image.fit_to_exact_size(size), + } + image = image.maintain_aspect_ratio(self.maintain_aspect_ratio); + image = image.max_size(self.max_size); + + ui.add_sized(ui.available_size(), image); + }); + }); + } +} diff --git a/crates/egui_demo_app/src/apps/mod.rs b/crates/egui_demo_app/src/apps/mod.rs index 1e28bbd6b4e..33368bac662 100644 --- a/crates/egui_demo_app/src/apps/mod.rs +++ b/crates/egui_demo_app/src/apps/mod.rs @@ -9,6 +9,12 @@ mod fractal_clock; #[cfg(feature = "http")] mod http_app; +#[cfg(feature = "image_viewer")] +mod image_viewer; + +#[cfg(feature = "image_viewer")] +pub use image_viewer::ImageViewer; + #[cfg(all(feature = "glow", not(feature = "wgpu")))] pub use custom3d_glow::Custom3d; diff --git a/crates/egui_demo_app/src/backend_panel.rs b/crates/egui_demo_app/src/backend_panel.rs index ebef75f4321..fb0d9ae4979 100644 --- a/crates/egui_demo_app/src/backend_panel.rs +++ b/crates/egui_demo_app/src/backend_panel.rs @@ -62,7 +62,7 @@ pub struct BackendPanel { } impl BackendPanel { - pub fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + pub fn update(&mut self, ctx: &egui::Context, frame: &eframe::Frame) { self.frame_history .on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage); @@ -82,8 +82,6 @@ impl BackendPanel { } pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { - egui::trace!(ui); - self.integration_ui(ui, frame); ui.separator(); @@ -101,11 +99,9 @@ impl BackendPanel { ui.separator(); - { - let mut debug_on_hover = ui.ctx().debug_on_hover(); - ui.checkbox(&mut debug_on_hover, "🐛 Debug on hover") - .on_hover_text("Show structure of the ui when you hover with the mouse"); - ui.ctx().set_debug_on_hover(debug_on_hover); + #[cfg(debug_assertions)] + if ui.ctx().style().debug.debug_on_hover_with_all_modifiers { + ui.label("Press down all modifiers and hover a widget to see a callstack for it"); } #[cfg(target_arch = "wasm32")] diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index 84581d5635e..e5f45887439 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -82,6 +82,8 @@ enum Anchor { EasyMarkEditor, #[cfg(feature = "http")] Http, + #[cfg(feature = "image_viewer")] + ImageViewer, Clock, #[cfg(any(feature = "glow", feature = "wgpu"))] Custom3d, @@ -142,6 +144,8 @@ pub struct State { easy_mark_editor: EasyMarkApp, #[cfg(feature = "http")] http: crate::apps::HttpApp, + #[cfg(feature = "image_viewer")] + image_viewer: crate::apps::ImageViewer, clock: FractalClockApp, color_test: ColorTestApp, @@ -160,19 +164,22 @@ pub struct WrapApp { } impl WrapApp { - pub fn new(_cc: &eframe::CreationContext<'_>) -> Self { + pub fn new(cc: &eframe::CreationContext<'_>) -> Self { + // This gives us image support: + egui_extras::install_image_loaders(&cc.egui_ctx); + #[allow(unused_mut)] let mut slf = Self { state: State::default(), #[cfg(any(feature = "glow", feature = "wgpu"))] - custom3d: crate::apps::Custom3d::new(_cc), + custom3d: crate::apps::Custom3d::new(cc), dropped_files: Default::default(), }; #[cfg(feature = "persistence")] - if let Some(storage) = _cc.storage { + if let Some(storage) = cc.storage { if let Some(state) = eframe::get_value(storage, eframe::APP_KEY) { slf.state = state; } @@ -204,6 +211,12 @@ impl WrapApp { Anchor::Clock, &mut self.state.clock as &mut dyn eframe::App, ), + #[cfg(feature = "image_viewer")] + ( + "🖼 Image Viewer", + Anchor::ImageViewer, + &mut self.state.image_viewer as &mut dyn eframe::App, + ), ]; #[cfg(any(feature = "glow", feature = "wgpu"))] @@ -251,7 +264,6 @@ impl eframe::App for WrapApp { let mut cmd = Command::Nothing; egui::TopBottomPanel::top("wrap_app_top_bar").show(ctx, |ui| { - egui::trace!(ui); ui.horizontal_wrapped(|ui| { ui.visuals_mut().button_frame = false; self.bar_contents(ui, frame, &mut cmd); @@ -384,7 +396,8 @@ impl WrapApp { { selected_anchor = anchor; if frame.is_web() { - ui.output_mut(|o| o.open_url(format!("#{anchor}"))); + ui.ctx() + .open_url(egui::OpenUrl::same_tab(format!("#{anchor}"))); } } } @@ -396,7 +409,7 @@ impl WrapApp { if clock_button(ui, crate::seconds_since_midnight()).clicked() { self.state.selected_anchor = Anchor::Clock; if frame.is_web() { - ui.output_mut(|o| o.open_url("#clock")); + ui.ctx().open_url(egui::OpenUrl::same_tab("#clock")); } } } diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index 3fd6a6939cd..10fac0d113c 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "egui_demo_lib" -version = "0.22.0" +version = "0.23.0" authors = ["Emil Ernerfeldt "] description = "Example library for egui" edition = "2021" @@ -11,7 +11,13 @@ readme = "README.md" repository = "https://github.com/emilk/egui/tree/master/crates/egui_demo_lib" categories = ["gui", "graphics"] keywords = ["glium", "egui", "gui", "gamedev"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +include = [ + "../LICENSE-APACHE", + "../LICENSE-MIT", + "**/*.rs", + "Cargo.toml", + "data/icon.png", +] [package.metadata.docs.rs] all-features = true @@ -28,16 +34,13 @@ chrono = ["egui_extras/datepicker", "dep:chrono"] serde = ["egui/serde", "egui_plot/serde", "dep:serde"] ## Enable better syntax highlighting using [`syntect`](https://docs.rs/syntect). -syntax_highlighting = ["syntect"] +syntect = ["egui_extras/syntect"] [dependencies] -egui = { version = "0.22.0", path = "../egui", default-features = false } -egui_extras = { version = "0.22.0", path = "../egui_extras", features = [ - "log", -] } -egui_plot = { version = "0.22.0", path = "../egui_plot" } -enum-map = { version = "2", features = ["serde"] } +egui = { version = "0.23.0", path = "../egui", default-features = false } +egui_extras = { version = "0.23.0", path = "../egui_extras" } +egui_plot = { version = "0.23.0", path = "../egui_plot" } log = { version = "0.4", features = ["std"] } unicode_names2 = { version = "0.6.0", default-features = false } @@ -46,9 +49,6 @@ chrono = { version = "0.4", optional = true, features = ["js-sys", "wasmbind"] } ## Enable this when generating docs. document-features = { version = "0.2", optional = true } serde = { version = "1", optional = true, features = ["derive"] } -syntect = { version = "5", optional = true, default-features = false, features = [ - "default-fancy", -] } [dev-dependencies] diff --git a/crates/egui_demo_lib/data/icon.png b/crates/egui_demo_lib/data/icon.png new file mode 100644 index 00000000000..87f15e746e4 Binary files /dev/null and b/crates/egui_demo_lib/data/icon.png differ diff --git a/crates/egui_demo_lib/src/color_test.rs b/crates/egui_demo_lib/src/color_test.rs index 9f209f9d5c8..ebbb3076ffc 100644 --- a/crates/egui_demo_lib/src/color_test.rs +++ b/crates/egui_demo_lib/src/color_test.rs @@ -87,8 +87,12 @@ impl ColorTest { let tex = self.tex_mngr.get(ui.ctx(), &g); let texel_offset = 0.5 / (g.0.len() as f32); let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0)); - ui.add(Image::new(tex, GRADIENT_SIZE).tint(vertex_color).uv(uv)) - .on_hover_text(format!("A texture that is {} texels wide", g.0.len())); + ui.add( + Image::from_texture((tex.id(), GRADIENT_SIZE)) + .tint(vertex_color) + .uv(uv), + ) + .on_hover_text(format!("A texture that is {} texels wide", g.0.len())); ui.label("GPU result"); }); }); @@ -225,11 +229,15 @@ impl ColorTest { let tex = self.tex_mngr.get(ui.ctx(), gradient); let texel_offset = 0.5 / (gradient.0.len() as f32); let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0)); - ui.add(Image::new(tex, GRADIENT_SIZE).bg_fill(bg_fill).uv(uv)) - .on_hover_text(format!( - "A texture that is {} texels wide", - gradient.0.len() - )); + ui.add( + Image::from_texture((tex.id(), GRADIENT_SIZE)) + .bg_fill(bg_fill) + .uv(uv), + ) + .on_hover_text(format!( + "A texture that is {} texels wide", + gradient.0.len() + )); ui.label(label); }); } diff --git a/crates/egui_demo_lib/src/demo/about.rs b/crates/egui_demo_lib/src/demo/about.rs index 4972b224b1d..9ee3dee7d8e 100644 --- a/crates/egui_demo_lib/src/demo/about.rs +++ b/crates/egui_demo_lib/src/demo/about.rs @@ -11,6 +11,7 @@ impl super::Demo for About { fn show(&mut self, ctx: &egui::Context, open: &mut bool) { egui::Window::new(self.name()) .default_width(320.0) + .default_height(480.0) .open(open) .show(ctx, |ui| { use super::View as _; @@ -41,11 +42,19 @@ impl super::View for About { ui.add_space(12.0); // ui.separator(); ui.heading("Links"); links(ui); + + ui.add_space(12.0); + + ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("egui development is sponsored by "); + ui.hyperlink_to("Rerun.io", "https://www.rerun.io/"); + ui.label(", a startup building an SDK for visualizing streams of multimodal data"); + }); } } fn about_immediate_mode(ui: &mut egui::Ui) { - use crate::syntax_highlighting::code_view_ui; ui.style_mut().spacing.interact_size.y = 0.0; // hack to make `horizontal_wrapped` work better with text. ui.horizontal_wrapped(|ui| { @@ -56,7 +65,7 @@ fn about_immediate_mode(ui: &mut egui::Ui) { }); ui.add_space(8.0); - code_view_ui( + crate::rust_view_ui( ui, r#" if ui.button("Save").clicked() { @@ -66,15 +75,10 @@ fn about_immediate_mode(ui: &mut egui::Ui) { ); ui.add_space(8.0); - ui.label("Note how there are no callbacks or messages, and no button state to store."); - - ui.label("Immediate mode has its roots in gaming, where everything on the screen is painted at the display refresh rate, i.e. at 60+ frames per second. \ - In immediate mode GUIs, the entire interface is laid out and painted at the same high rate. \ - This makes immediate mode GUIs especially well suited for highly interactive applications."); - ui.horizontal_wrapped(|ui| { ui.spacing_mut().item_spacing.x = 0.0; - ui.label("More about immediate mode "); + ui.label("There are no callbacks or messages, and no button state to store. "); + ui.label("Read more about immediate mode "); ui.hyperlink_to("here", "https://github.com/emilk/egui#why-immediate-mode"); ui.label("."); }); diff --git a/crates/egui_demo_lib/src/demo/code_editor.rs b/crates/egui_demo_lib/src/demo/code_editor.rs index e9632a208ec..2a8f6a6c97e 100644 --- a/crates/egui_demo_lib/src/demo/code_editor.rs +++ b/crates/egui_demo_lib/src/demo/code_editor.rs @@ -67,7 +67,7 @@ impl super::View for CodeEditor { }); } - let mut theme = crate::syntax_highlighting::CodeTheme::from_memory(ui.ctx()); + let mut theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx()); ui.collapsing("Theme", |ui| { ui.group(|ui| { theme.ui(ui); @@ -77,7 +77,7 @@ impl super::View for CodeEditor { let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| { let mut layout_job = - crate::syntax_highlighting::highlight(ui.ctx(), &theme, string, language); + egui_extras::syntax_highlighting::highlight(ui.ctx(), &theme, string, language); layout_job.wrap.max_width = wrap_width; ui.fonts(|f| f.layout_job(layout_job)) }; diff --git a/crates/egui_demo_lib/src/demo/code_example.rs b/crates/egui_demo_lib/src/demo/code_example.rs index 7b13514762f..7c97dacccba 100644 --- a/crates/egui_demo_lib/src/demo/code_example.rs +++ b/crates/egui_demo_lib/src/demo/code_example.rs @@ -81,13 +81,11 @@ impl super::Demo for CodeExample { impl super::View for CodeExample { fn ui(&mut self, ui: &mut egui::Ui) { - use crate::syntax_highlighting::code_view_ui; - ui.vertical_centered(|ui| { ui.add(crate::egui_github_link_file!()); }); - code_view_ui( + crate::rust_view_ui( ui, r" pub struct CodeExample { @@ -117,15 +115,15 @@ impl CodeExample { }); }); - code_view_ui(ui, " }\n}"); + crate::rust_view_ui(ui, " }\n}"); ui.separator(); - code_view_ui(ui, &format!("{self:#?}")); + crate::rust_view_ui(ui, &format!("{self:#?}")); ui.separator(); - let mut theme = crate::syntax_highlighting::CodeTheme::from_memory(ui.ctx()); + let mut theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx()); ui.collapsing("Theme", |ui| { theme.ui(ui); theme.store_in_memory(ui.ctx()); @@ -135,7 +133,7 @@ impl CodeExample { fn show_code(ui: &mut egui::Ui, code: &str) { let code = remove_leading_indentation(code.trim_start_matches('\n')); - crate::syntax_highlighting::code_view_ui(ui, &code); + crate::rust_view_ui(ui, &code); } fn remove_leading_indentation(code: &str) -> String { 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 02c88357c93..0e82d03c23d 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -1,6 +1,7 @@ -use egui::{Context, Modifiers, ScrollArea, Ui}; use std::collections::BTreeSet; +use egui::{Context, Modifiers, NumExt as _, ScrollArea, Ui}; + use super::About; use super::Demo; use super::View; @@ -180,7 +181,7 @@ impl DemoWindows { fn mobile_ui(&mut self, ctx: &Context) { if self.about_is_open { let screen_size = ctx.input(|i| i.screen_rect.size()); - let default_width = (screen_size.x - 20.0).min(400.0); + let default_width = (screen_size.x - 32.0).at_most(400.0); let mut close = false; egui::Window::new(self.about.name()) @@ -243,7 +244,6 @@ impl DemoWindows { .resizable(false) .default_width(150.0) .show(ctx, |ui| { - egui::trace!(ui); ui.vertical_centered(|ui| { ui.heading("✒ egui demos"); }); diff --git a/crates/egui_demo_lib/src/demo/font_book.rs b/crates/egui_demo_lib/src/demo/font_book.rs index 2b4eebae019..4dda2d87b48 100644 --- a/crates/egui_demo_lib/src/demo/font_book.rs +++ b/crates/egui_demo_lib/src/demo/font_book.rs @@ -93,7 +93,7 @@ impl super::View for FontBook { }; if ui.add(button).on_hover_ui(tooltip_ui).clicked() { - ui.output_mut(|o| o.copied_text = chr.to_string()); + ui.ctx().copy_text(chr.to_string()); } } } 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 195b79e223a..a90465cc19d 100644 --- a/crates/egui_demo_lib/src/demo/misc_demo_window.rs +++ b/crates/egui_demo_lib/src/demo/misc_demo_window.rs @@ -371,7 +371,7 @@ impl BoxPainting { ui.painter().rect( rect, self.rounding, - Color32::from_gray(64), + ui.visuals().text_color().gamma_multiply(0.5), Stroke::new(self.stroke_width, Color32::WHITE), ); } diff --git a/crates/egui_demo_lib/src/demo/scrolling.rs b/crates/egui_demo_lib/src/demo/scrolling.rs index f5833521b5b..ddcf95550a0 100644 --- a/crates/egui_demo_lib/src/demo/scrolling.rs +++ b/crates/egui_demo_lib/src/demo/scrolling.rs @@ -1,8 +1,9 @@ -use egui::*; +use egui::{scroll_area::ScrollBarVisibility, *}; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[derive(Clone, Copy, Debug, PartialEq)] enum ScrollDemo { + ScrollAppearance, ScrollTo, ManyLines, LargeCanvas, @@ -12,7 +13,7 @@ enum ScrollDemo { impl Default for ScrollDemo { fn default() -> Self { - Self::ScrollTo + Self::ScrollAppearance } } @@ -20,6 +21,7 @@ impl Default for ScrollDemo { #[cfg_attr(feature = "serde", serde(default))] #[derive(Default, PartialEq)] pub struct Scrolling { + appearance: ScrollAppearance, demo: ScrollDemo, scroll_to: ScrollTo, scroll_stick_to: ScrollStickTo, @@ -33,7 +35,9 @@ impl super::Demo for Scrolling { fn show(&mut self, ctx: &egui::Context, open: &mut bool) { egui::Window::new(self.name()) .open(open) - .resizable(false) + .resizable(true) + .hscroll(false) + .vscroll(false) .show(ctx, |ui| { use super::View as _; self.ui(ui); @@ -44,6 +48,7 @@ impl super::Demo for Scrolling { impl super::View for Scrolling { fn ui(&mut self, ui: &mut Ui) { ui.horizontal(|ui| { + ui.selectable_value(&mut self.demo, ScrollDemo::ScrollAppearance, "Appearance"); ui.selectable_value(&mut self.demo, ScrollDemo::ScrollTo, "Scroll to"); ui.selectable_value( &mut self.demo, @@ -60,6 +65,9 @@ impl super::View for Scrolling { }); ui.separator(); match self.demo { + ScrollDemo::ScrollAppearance => { + self.appearance.ui(ui); + } ScrollDemo::ScrollTo => { self.scroll_to.ui(ui); } @@ -84,6 +92,75 @@ impl super::View for Scrolling { } } +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +#[derive(PartialEq)] +struct ScrollAppearance { + num_lorem_ipsums: usize, + visibility: ScrollBarVisibility, +} + +impl Default for ScrollAppearance { + fn default() -> Self { + Self { + num_lorem_ipsums: 2, + visibility: ScrollBarVisibility::default(), + } + } +} + +impl ScrollAppearance { + fn ui(&mut self, ui: &mut egui::Ui) { + let Self { + num_lorem_ipsums, + visibility, + } = self; + + let mut style: Style = (*ui.ctx().style()).clone(); + + style.spacing.scroll.ui(ui); + + ui.add_space(8.0); + + ui.horizontal(|ui| { + ui.label("ScrollBarVisibility:"); + for option in ScrollBarVisibility::ALL { + ui.selectable_value(visibility, option, format!("{option:?}")); + } + }); + ui.weak("When to show scroll bars; resize the window to see the effect."); + + ui.add_space(8.0); + + ui.ctx().set_style(style.clone()); + ui.set_style(style); + + ui.separator(); + + ui.add( + egui::Slider::new(num_lorem_ipsums, 1..=100) + .text("Content length") + .logarithmic(true), + ); + + ui.separator(); + + ScrollArea::vertical() + .auto_shrink([false; 2]) + .scroll_bar_visibility(*visibility) + .show(ui, |ui| { + ui.with_layout( + egui::Layout::top_down(egui::Align::LEFT).with_cross_justify(true), + |ui| { + for _ in 0..*num_lorem_ipsums { + ui.label(crate::LOREM_IPSUM_LONG); + } + }, + ); + }); + } +} + fn huge_content_lines(ui: &mut egui::Ui) { ui.label( "A lot of rows, but only the visible ones are laid out, so performance is still good:", diff --git a/crates/egui_demo_lib/src/demo/text_layout.rs b/crates/egui_demo_lib/src/demo/text_layout.rs index 01a5c7787a3..f2512ae8c18 100644 --- a/crates/egui_demo_lib/src/demo/text_layout.rs +++ b/crates/egui_demo_lib/src/demo/text_layout.rs @@ -7,16 +7,18 @@ pub struct TextLayoutDemo { overflow_character: Option, extra_letter_spacing_pixels: i32, line_height_pixels: u32, + lorem_ipsum: bool, } impl Default for TextLayoutDemo { fn default() -> Self { Self { - max_rows: 3, + max_rows: 6, break_anywhere: true, overflow_character: Some('…'), extra_letter_spacing_pixels: 0, line_height_pixels: 0, + lorem_ipsum: true, } } } @@ -45,6 +47,7 @@ impl super::View for TextLayoutDemo { overflow_character, extra_letter_spacing_pixels, line_height_pixels, + lorem_ipsum, } = self; use egui::text::LayoutJob; @@ -104,32 +107,55 @@ impl super::View for TextLayoutDemo { } }); ui.end_row(); + + ui.label("Text:"); + ui.horizontal(|ui| { + ui.selectable_value(lorem_ipsum, true, "Lorem Ipsum"); + ui.selectable_value(lorem_ipsum, false, "La Pasionaria"); + }); }); ui.add_space(12.0); - egui::ScrollArea::vertical().show(ui, |ui| { - let extra_letter_spacing = points_per_pixel * *extra_letter_spacing_pixels as f32; - let line_height = - (*line_height_pixels != 0).then_some(points_per_pixel * *line_height_pixels as f32); + let text = if *lorem_ipsum { + crate::LOREM_IPSUM_LONG + } else { + TO_BE_OR_NOT_TO_BE + }; - let mut job = LayoutJob::single_section( - crate::LOREM_IPSUM_LONG.to_owned(), - egui::TextFormat { - extra_letter_spacing, - line_height, + egui::ScrollArea::vertical() + .auto_shrink([false; 2]) + .show(ui, |ui| { + let extra_letter_spacing = points_per_pixel * *extra_letter_spacing_pixels as f32; + let line_height = (*line_height_pixels != 0) + .then_some(points_per_pixel * *line_height_pixels as f32); + + let mut job = LayoutJob::single_section( + text.to_owned(), + egui::TextFormat { + extra_letter_spacing, + line_height, + ..Default::default() + }, + ); + job.wrap = egui::text::TextWrapping { + max_rows: *max_rows, + break_anywhere: *break_anywhere, + overflow_character: *overflow_character, ..Default::default() - }, - ); - job.wrap = egui::text::TextWrapping { - max_rows: *max_rows, - break_anywhere: *break_anywhere, - overflow_character: *overflow_character, - ..Default::default() - }; - - // NOTE: `Label` overrides some of the wrapping settings, e.g. wrap width - ui.label(job); - }); + }; + + // NOTE: `Label` overrides some of the wrapping settings, e.g. wrap width + ui.label(job); + }); } } + +/// Excerpt from Dolores Ibárruri's farwel speech to the International Brigades: +const TO_BE_OR_NOT_TO_BE: &str = "Mothers! Women!\n +When the years pass by and the wounds of war are stanched; when the memory of the sad and bloody days dissipates in a present of liberty, of peace and of wellbeing; when the rancor have died out and pride in a free country is felt equally by all Spaniards, speak to your children. Tell them of these men of the International Brigades.\n\ +\n\ +Recount for them how, coming over seas and mountains, crossing frontiers bristling with bayonets, sought by raving dogs thirsting to tear their flesh, these men reached our country as crusaders for freedom, to fight and die for Spain’s liberty and independence threatened by German and Italian fascism. \ +They gave up everything — their loves, their countries, home and fortune, fathers, mothers, wives, brothers, sisters and children — and they came and said to us: “We are here. Your cause, Spain’s cause, is ours. It is the cause of all advanced and progressive mankind.”\n\ +\n\ +- Dolores Ibárruri, 1938"; diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index 54c083504df..4189e61c9ff 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -21,9 +21,6 @@ pub struct WidgetGallery { #[cfg(feature = "chrono")] #[cfg_attr(feature = "serde", serde(skip))] date: Option, - - #[cfg_attr(feature = "serde", serde(skip))] - texture: Option, } impl Default for WidgetGallery { @@ -39,7 +36,6 @@ impl Default for WidgetGallery { animate_progress_bar: false, #[cfg(feature = "chrono")] date: None, - texture: None, } } } @@ -111,15 +107,9 @@ impl WidgetGallery { animate_progress_bar, #[cfg(feature = "chrono")] date, - texture, } = self; - let texture: &egui::TextureHandle = texture.get_or_insert_with(|| { - ui.ctx() - .load_texture("example", egui::ColorImage::example(), Default::default()) - }); - - ui.add(doc_link_label("Label", "label,heading")); + ui.add(doc_link_label("Label", "label")); ui.label("Welcome to the widget gallery!"); ui.end_row(); @@ -131,7 +121,7 @@ impl WidgetGallery { ); ui.end_row(); - ui.add(doc_link_label("TextEdit", "TextEdit,text_edit")); + ui.add(doc_link_label("TextEdit", "TextEdit")); ui.add(egui::TextEdit::singleline(string).hint_text("Write something here")); ui.end_row(); @@ -159,10 +149,7 @@ impl WidgetGallery { }); ui.end_row(); - ui.add(doc_link_label( - "SelectableLabel", - "selectable_value,SelectableLabel", - )); + ui.add(doc_link_label("SelectableLabel", "SelectableLabel")); ui.horizontal(|ui| { ui.selectable_value(radio, Enum::First, "First"); ui.selectable_value(radio, Enum::Second, "Second"); @@ -206,14 +193,19 @@ impl WidgetGallery { ui.color_edit_button_srgba(color); ui.end_row(); - let img_size = 16.0 * texture.size_vec2() / texture.size_vec2().y; - ui.add(doc_link_label("Image", "Image")); - ui.image(texture, img_size); + let egui_icon = egui::include_image!("../../data/icon.png"); + ui.add(egui::Image::new(egui_icon.clone())); ui.end_row(); - ui.add(doc_link_label("ImageButton", "ImageButton")); - if ui.add(egui::ImageButton::new(texture, img_size)).clicked() { + ui.add(doc_link_label( + "Button with image", + "Button::image_and_text", + )); + if ui + .add(egui::Button::image_and_text(egui_icon, "Click me!")) + .clicked() + { *boolean = !*boolean; } ui.end_row(); @@ -221,7 +213,11 @@ impl WidgetGallery { #[cfg(feature = "chrono")] { let date = date.get_or_insert_with(|| chrono::offset::Utc::now().date_naive()); - ui.add(doc_link_label("DatePickerButton", "DatePickerButton")); + ui.add(doc_link_label_with_crate( + "egui_extras", + "DatePickerButton", + "DatePickerButton", + )); ui.add(egui_extras::DatePickerButton::new(date)); ui.end_row(); } @@ -242,7 +238,7 @@ impl WidgetGallery { }); ui.end_row(); - ui.add(doc_link_label("Plot", "plot")); + ui.add(doc_link_label_with_crate("egui_plot", "Plot", "plot")); example_plot(ui); ui.end_row(); @@ -278,8 +274,16 @@ fn example_plot(ui: &mut egui::Ui) -> egui::Response { } fn doc_link_label<'a>(title: &'a str, search_term: &'a str) -> impl egui::Widget + 'a { + doc_link_label_with_crate("egui", title, search_term) +} + +fn doc_link_label_with_crate<'a>( + crate_name: &'a str, + title: &'a str, + search_term: &'a str, +) -> impl egui::Widget + 'a { let label = format!("{title}:"); - let url = format!("https://docs.rs/egui?search={search_term}"); + let url = format!("https://docs.rs/{crate_name}?search={search_term}"); move |ui: &mut egui::Ui| { ui.hyperlink_to(label, url).on_hover_ui(|ui| { ui.horizontal_wrapped(|ui| { diff --git a/crates/egui_demo_lib/src/demo/window_options.rs b/crates/egui_demo_lib/src/demo/window_options.rs index ed142d08997..a69f6cf7929 100644 --- a/crates/egui_demo_lib/src/demo/window_options.rs +++ b/crates/egui_demo_lib/src/demo/window_options.rs @@ -6,6 +6,7 @@ pub struct WindowOptions { closable: bool, collapsible: bool, resizable: bool, + constrain: bool, scroll2: [bool; 2], disabled_time: f64, @@ -22,6 +23,7 @@ impl Default for WindowOptions { closable: true, collapsible: true, resizable: true, + constrain: true, scroll2: [true; 2], disabled_time: f64::NEG_INFINITY, anchored: false, @@ -43,6 +45,7 @@ impl super::Demo for WindowOptions { closable, collapsible, resizable, + constrain, scroll2, disabled_time, anchored, @@ -59,6 +62,7 @@ impl super::Demo for WindowOptions { let mut window = egui::Window::new(title) .id(egui::Id::new("demo_window_options")) // required since we change the title .resizable(resizable) + .constrain(constrain) .collapsible(collapsible) .title_bar(title_bar) .scroll2(scroll2) @@ -81,6 +85,7 @@ impl super::View for WindowOptions { closable, collapsible, resizable, + constrain, scroll2, disabled_time: _, anchored, @@ -99,6 +104,8 @@ impl super::View for WindowOptions { ui.checkbox(closable, "closable"); ui.checkbox(collapsible, "collapsible"); ui.checkbox(resizable, "resizable"); + ui.checkbox(constrain, "constrain") + .on_hover_text("Constrain window to the screen"); ui.checkbox(&mut scroll2[0], "hscroll"); ui.checkbox(&mut scroll2[1], "vscroll"); }); diff --git a/crates/egui_demo_lib/src/lib.rs b/crates/egui_demo_lib/src/lib.rs index 43db89f2d9d..9d7eaa53622 100644 --- a/crates/egui_demo_lib/src/lib.rs +++ b/crates/egui_demo_lib/src/lib.rs @@ -15,11 +15,17 @@ mod color_test; mod demo; pub mod easy_mark; -pub mod syntax_highlighting; pub use color_test::ColorTest; pub use demo::DemoWindows; +/// View some Rust code with syntax highlighting and selection. +pub(crate) fn rust_view_ui(ui: &mut egui::Ui, code: &str) { + let language = "rs"; + let theme = egui_extras::syntax_highlighting::CodeTheme::from_memory(ui.ctx()); + egui_extras::syntax_highlighting::code_view_ui(ui, &theme, code, language); +} + // ---------------------------------------------------------------------------- /// Create a [`Hyperlink`](egui::Hyperlink) to this egui source code file on github. diff --git a/crates/egui_extras/CHANGELOG.md b/crates/egui_extras/CHANGELOG.md index 3a968179ba6..937a896779f 100644 --- a/crates/egui_extras/CHANGELOG.md +++ b/crates/egui_extras/CHANGELOG.md @@ -5,6 +5,14 @@ This file is updated upon each release. Changes since the last release can be found by running the `scripts/generate_changelog.py` script. +## 0.23.0 - 2023-09-27 +* `egui_extras::install_image_loaders` [#3297](https://github.com/emilk/egui/pull/3297) [#3315](https://github.com/emilk/egui/pull/3315) [#3328](https://github.com/emilk/egui/pull/3328) (thanks [@jprochazk](https://github.com/jprochazk)!) +* Add syntax highlighting feature to `egui_extras` [#3333](https://github.com/emilk/egui/pull/3333) [#3388](https://github.com/emilk/egui/pull/3388) +* Add `TableBuilder::drag_to_scroll` [#3100](https://github.com/emilk/egui/pull/3100) (thanks [@KYovchevski](https://github.com/KYovchevski)!) +* Add opt-in `puffin` feature to `egui-extras` [#3307](https://github.com/emilk/egui/pull/3307) +* Always depend on `log` crate [#3336](https://github.com/emilk/egui/pull/3336) +* Fix not taking clipping into account when calculating column remainder [#3357](https://github.com/emilk/egui/pull/3357) (thanks [@daxpedda](https://github.com/daxpedda)!) + ## 0.22.0 - 2023-05-23 - Add option to hide datepicker button calendar icon [#2910](https://github.com/emilk/egui/pull/2910) (thanks [@Barugon](https://github.com/Barugon)!) diff --git a/crates/egui_extras/Cargo.toml b/crates/egui_extras/Cargo.toml index 25ec2acda45..5712a16b21a 100644 --- a/crates/egui_extras/Cargo.toml +++ b/crates/egui_extras/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "egui_extras" -version = "0.22.0" +version = "0.23.0" authors = [ "Dominik Rössler ", "Emil Ernerfeldt ", @@ -24,20 +24,29 @@ all-features = true [features] -default = [] +default = ["dep:mime_guess2"] -## Shorthand for enabling `svg`, `image`, and `ehttp`. -all-loaders = ["svg", "image", "http"] +## Shorthand for enabling the different types of image loaders (`file`, `http`, `image`, `svg`). +all_loaders = ["file", "http", "image", "svg"] ## Enable [`DatePickerButton`] widget. datepicker = ["chrono"] -## Log warnings using [`log`](https://docs.rs/log) crate. -log = ["dep:log", "egui/log"] +## Add support for loading images from `file://` URIs. +file = ["dep:mime_guess2"] ## Add support for loading images via HTTP. http = ["dep:ehttp"] +## Add support for loading images with the [`image`](https://docs.rs/image) crate. +## +## You also need to ALSO opt-in to the image formats you want to support, like so: +## ```toml +## image = { version = "0.24", features = ["jpeg", "png"] } # Add the types you want support for + +## ``` +image = ["dep:image"] + ## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. ## ## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. @@ -46,9 +55,16 @@ puffin = ["dep:puffin", "egui/puffin"] ## Support loading svg images. svg = ["resvg", "tiny-skia", "usvg"] -[dependencies] -egui = { version = "0.22.0", path = "../egui", default-features = false } +## Enable better syntax highlighting using [`syntect`](https://docs.rs/syntect). +syntect = ["dep:syntect"] + +[dependencies] +egui = { version = "0.23.0", path = "../egui", default-features = false, features = [ + "serde", +] } +enum-map = { version = "2", features = ["serde"] } +log = { version = "0.4", features = ["std"] } serde = { version = "1", features = ["derive"] } #! ### Optional dependencies @@ -64,23 +80,21 @@ chrono = { version = "0.4", optional = true, default-features = false, features ## Enable this when generating docs. document-features = { version = "0.2", optional = true } -## Add support for loading images with the [`image`](https://docs.rs/image) crate. -## -## You also need to ALSO opt-in to the image formats you want to support, like so: -## ```toml -## image = { version = "0.24", features = ["jpeg", "png"] } -## ``` image = { version = "0.24", optional = true, default-features = false } -# feature "log" -log = { version = "0.4", optional = true, features = ["std"] } +# file feature +mime_guess2 = { version = "2", optional = true, default-features = false } puffin = { version = "0.16", optional = true } +syntect = { version = "5", optional = true, default-features = false, features = [ + "default-fancy", +] } + # svg feature resvg = { version = "0.28", optional = true, default-features = false } tiny-skia = { version = "0.8", optional = true, default-features = false } # must be updated in lock-step with resvg usvg = { version = "0.28", optional = true, default-features = false } # http feature -ehttp = { version = "0.3.0", optional = true, default-features = false } +ehttp = { version = "0.3.1", optional = true, default-features = false } diff --git a/crates/egui_extras/README.md b/crates/egui_extras/README.md index d49a594ea2d..0ccb1de2b1e 100644 --- a/crates/egui_extras/README.md +++ b/crates/egui_extras/README.md @@ -7,3 +7,15 @@ ![Apache](https://img.shields.io/badge/license-Apache-blue.svg) This is a crate that adds some features on top top of [`egui`](https://github.com/emilk/egui). This crate is for experimental features, and features that require big dependencies that do not belong in `egui`. + +## Images +One thing `egui_extras` is commonly used for is to install image loaders for `egui`: + +```toml +egui_extras = { version = "*", features = ["all_loaders"] } +image = { version = "0.24", features = ["jpeg", "png"] } # Add the types you want support for +``` + +```rs +egui_extras::install_image_loaders(egui_ctx); +``` diff --git a/crates/egui_extras/src/datepicker/button.rs b/crates/egui_extras/src/datepicker/button.rs index b63e5bc014d..55d9bd023ce 100644 --- a/crates/egui_extras/src/datepicker/button.rs +++ b/crates/egui_extras/src/datepicker/button.rs @@ -7,6 +7,7 @@ pub(crate) struct DatePickerButtonState { pub picker_visible: bool, } +/// Shows a date, and will open a date picker popup when clicked. pub struct DatePickerButton<'a> { selection: &'a mut NaiveDate, id_source: Option<&'a str>, @@ -116,6 +117,7 @@ impl<'a> Widget for DatePickerButton<'a> { } = Area::new(ui.make_persistent_id(self.id_source)) .order(Order::Foreground) .fixed_pos(pos) + .constrain_to(ui.ctx().screen_rect()) .show(ui.ctx(), |ui| { let frame = Frame::popup(ui.style()); frame diff --git a/crates/egui_extras/src/image.rs b/crates/egui_extras/src/image.rs index 9304f2b803b..de55732596e 100644 --- a/crates/egui_extras/src/image.rs +++ b/crates/egui_extras/src/image.rs @@ -1,3 +1,5 @@ +#![allow(deprecated)] + use egui::{mutex::Mutex, TextureFilter, TextureOptions}; #[cfg(feature = "svg")] @@ -8,6 +10,9 @@ pub use usvg::FitTo; /// Load once, and save somewhere in your app state. /// /// Use the `svg` and `image` features to enable more constructors. +/// +/// ⚠ This type is deprecated: Consider using [`egui::Image`] instead. +#[deprecated = "consider using `egui::Image` instead"] pub struct RetainedImage { debug_name: String, @@ -151,7 +156,7 @@ impl RetainedImage { &self.debug_name } - /// The texture if for this image. + /// The texture id for this image. pub fn texture_id(&self, ctx: &egui::Context) -> egui::TextureId { self.texture .lock() @@ -186,7 +191,7 @@ impl RetainedImage { // We need to convert the SVG to a texture to display it: // Future improvement: tell backend to do mip-mapping of the image to // make it look smoother when downsized. - ui.image(self.texture_id(ui.ctx()), desired_size) + ui.image((self.texture_id(ui.ctx()), desired_size)) } } diff --git a/crates/egui_extras/src/lib.rs b/crates/egui_extras/src/lib.rs index 6ae107452a2..e9206dd1396 100644 --- a/crates/egui_extras/src/lib.rs +++ b/crates/egui_extras/src/lib.rs @@ -13,9 +13,12 @@ #[cfg(feature = "chrono")] mod datepicker; +pub mod syntax_highlighting; + +#[doc(hidden)] pub mod image; mod layout; -pub mod loaders; +mod loaders; mod sizing; mod strip; mod table; @@ -23,12 +26,16 @@ mod table; #[cfg(feature = "chrono")] pub use crate::datepicker::DatePickerButton; +#[doc(hidden)] +#[allow(deprecated)] pub use crate::image::RetainedImage; pub(crate) use crate::layout::StripLayout; pub use crate::sizing::Size; pub use crate::strip::*; pub use crate::table::*; +pub use loaders::install_image_loaders; + // --------------------------------------------------------------------------- mod profiling_scopes { @@ -38,8 +45,8 @@ mod profiling_scopes { /// Profiling macro for feature "puffin" macro_rules! profile_function { ($($arg: tt)*) => { - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. #[cfg(feature = "puffin")] + #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. puffin::profile_function!($($arg)*); }; } @@ -48,8 +55,8 @@ mod profiling_scopes { /// Profiling macro for feature "puffin" macro_rules! profile_scope { ($($arg: tt)*) => { - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. #[cfg(feature = "puffin")] + #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. puffin::profile_scope!($($arg)*); }; } @@ -61,56 +68,15 @@ pub(crate) use profiling_scopes::*; // --------------------------------------------------------------------------- -/// Log an error with either `log` or `eprintln` -macro_rules! log_err { - ($fmt: literal, $($arg: tt)*) => {{ - #[cfg(feature = "log")] - log::error!($fmt, $($arg)*); - - #[cfg(not(feature = "log"))] - eprintln!( - concat!("egui_extras: ", $fmt), $($arg)* - ); - }}; -} -pub(crate) use log_err; - /// Panic in debug builds, log otherwise. macro_rules! log_or_panic { + ($fmt: literal) => {$crate::log_or_panic!($fmt,)}; ($fmt: literal, $($arg: tt)*) => {{ if cfg!(debug_assertions) { panic!($fmt, $($arg)*); } else { - $crate::log_err!($fmt, $($arg)*); + log::error!($fmt, $($arg)*); } }}; } pub(crate) use log_or_panic; - -#[allow(unused_macros)] -macro_rules! log_warn { - ($fmt: literal) => {$crate::log_warn!($fmt,)}; - ($fmt: literal, $($arg: tt)*) => {{ - #[cfg(feature = "log")] - log::warn!($fmt, $($arg)*); - - #[cfg(not(feature = "log"))] - println!( - concat!("egui_extras: warning: ", $fmt), $($arg)* - ) - }}; -} - -#[allow(unused_imports)] -pub(crate) use log_warn; - -#[allow(unused_macros)] -macro_rules! log_trace { - ($fmt: literal) => {$crate::log_trace!($fmt,)}; - ($fmt: literal, $($arg: tt)*) => {{ - #[cfg(feature = "log")] - log::trace!($fmt, $($arg)*); - }}; -} -#[allow(unused_imports)] -pub(crate) use log_trace; diff --git a/crates/egui_extras/src/loaders.rs b/crates/egui_extras/src/loaders.rs index 717d4ddc700..cc775a2ca20 100644 --- a/crates/egui_extras/src/loaders.rs +++ b/crates/egui_extras/src/loaders.rs @@ -1,46 +1,96 @@ // TODO: automatic cache eviction -/// Installs the default set of loaders: -/// - `file` loader on non-Wasm targets -/// - `http` loader (with the `ehttp` feature) -/// - `image` loader (with the `image` feature) -/// - `svg` loader with the `svg` feature +/// Installs a set of image loaders. /// -/// ⚠ This will do nothing and you won't see any images unless you enable some features! -/// If you just want to be able to load `file://` and `http://` images, enable the `all-loaders` feature. +/// Calling this enables the use of [`egui::Image`] and [`egui::Ui::image`]. /// -/// ⚠ The supported set of image formats is configured by adding the [`image`](https://crates.io/crates/image) +/// ⚠ This will do nothing and you won't see any images unless you also enable some feature flags on `egui_extras`: +/// +/// - `file` feature: `file://` loader on non-Wasm targets +/// - `http` feature: `http(s)://` loader +/// - `image` feature: Loader of png, jpeg etc using the [`image`] crate +/// - `svg` feature: `.svg` loader +/// +/// Calling this multiple times on the same [`egui::Context`] is safe. +/// It will never install duplicate loaders. +/// +/// - If you just want to be able to load `file://` and `http://` URIs, enable the `all_loaders` feature. +/// - The supported set of image formats is configured by adding the [`image`](https://crates.io/crates/image) /// crate as your direct dependency, and enabling features on it: /// /// ```toml,ignore -/// image = { version = "0.24", features = ["jpeg", "png"] } +/// egui_extras = { version = "*", features = ["all_loaders"] } +/// image = { version = "0.24", features = ["jpeg", "png"] } # Add the types you want support for /// ``` /// +/// ⚠ You have to configure both the supported loaders in `egui_extras` _and_ the supported image formats +/// in `image` to get any output! +/// +/// ## Loader-specific information +/// +/// ⚠ The exact way bytes, images, and textures are loaded is subject to change, +/// but the supported protocols and file extensions are not. +/// +/// The `file` loader is a [`BytesLoader`][`egui::load::BytesLoader`]. +/// It will attempt to load `file://` URIs, and infer the content type from the extension. +/// The path will be passed to [`std::fs::read`] after trimming the `file://` prefix, +/// and is resolved the same way as with `std::fs::read(path)`: +/// - Relative paths are relative to the current working directory +/// - Absolute paths are left as is. +/// +/// The `http` loader is a [`BytesLoader`][`egui::load::BytesLoader`]. +/// It will attempt to load `http://` and `https://` URIs, and infer the content type from the `Content-Type` header. +/// +/// The `image` loader is an [`ImageLoader`][`egui::load::ImageLoader`]. +/// It will attempt to load any URI with any extension other than `svg`. +/// It will also try to load any URI without an extension. +/// The content type specified by [`BytesPoll::Ready::mime`][`egui::load::BytesPoll::Ready::mime`] always takes precedence. +/// This means that even if the URI has a `png` extension, and the `png` image format is enabled, if the content type is +/// not one of the supported and enabled image formats, the loader will return [`LoadError::NotSupported`][`egui::load::LoadError::NotSupported`], +/// allowing a different loader to attempt to load the image. +/// +/// The `svg` loader is an [`ImageLoader`][`egui::load::ImageLoader`]. +/// It will attempt to load any URI with an `svg` extension. It will _not_ attempt to load a URI without an extension. +/// The content type specified by [`BytesPoll::Ready::mime`][`egui::load::BytesPoll::Ready::mime`] always takes precedence, +/// and must include `svg` for it to be considered supported. For example, `image/svg+xml` would be loaded by the `svg` loader. +/// /// See [`egui::load`] for more information about how loaders work. -pub fn install(ctx: &egui::Context) { - #[cfg(not(target_arch = "wasm32"))] - ctx.add_bytes_loader(std::sync::Arc::new(self::file_loader::FileLoader::default())); +pub fn install_image_loaders(ctx: &egui::Context) { + #[cfg(all(not(target_arch = "wasm32"), feature = "file"))] + if !ctx.is_loader_installed(self::file_loader::FileLoader::ID) { + ctx.add_bytes_loader(std::sync::Arc::new(self::file_loader::FileLoader::default())); + log::trace!("installed FileLoader"); + } #[cfg(feature = "http")] - ctx.add_bytes_loader(std::sync::Arc::new( - self::ehttp_loader::EhttpLoader::default(), - )); + if !ctx.is_loader_installed(self::ehttp_loader::EhttpLoader::ID) { + ctx.add_bytes_loader(std::sync::Arc::new( + self::ehttp_loader::EhttpLoader::default(), + )); + log::trace!("installed EhttpLoader"); + } #[cfg(feature = "image")] - ctx.add_image_loader(std::sync::Arc::new( - self::image_loader::ImageCrateLoader::default(), - )); + if !ctx.is_loader_installed(self::image_loader::ImageCrateLoader::ID) { + ctx.add_image_loader(std::sync::Arc::new( + self::image_loader::ImageCrateLoader::default(), + )); + log::trace!("installed ImageCrateLoader"); + } #[cfg(feature = "svg")] - ctx.add_image_loader(std::sync::Arc::new(self::svg_loader::SvgLoader::default())); + if !ctx.is_loader_installed(self::svg_loader::SvgLoader::ID) { + ctx.add_image_loader(std::sync::Arc::new(self::svg_loader::SvgLoader::default())); + log::trace!("installed SvgLoader"); + } #[cfg(all( - target_arch = "wasm32", + any(target_arch = "wasm32", not(feature = "file")), not(feature = "http"), not(feature = "image"), not(feature = "svg") ))] - crate::log_warn!("`loaders::install` was called, but no loaders are enabled"); + log::warn!("`install_image_loaders` was called, but no loaders are enabled"); let _ = ctx; } diff --git a/crates/egui_extras/src/loaders/ehttp_loader.rs b/crates/egui_extras/src/loaders/ehttp_loader.rs index 9467eafd5d8..8cc9a6e714e 100644 --- a/crates/egui_extras/src/loaders/ehttp_loader.rs +++ b/crates/egui_extras/src/loaders/ehttp_loader.rs @@ -5,52 +5,60 @@ use egui::{ }; use std::{sync::Arc, task::Poll}; -type Entry = Poll, String>>; +#[derive(Clone)] +struct File { + bytes: Arc<[u8]>, + mime: Option, +} + +impl File { + fn from_response(uri: &str, response: ehttp::Response) -> Result { + if !response.ok { + match response.text() { + Some(response_text) => { + return Err(format!( + "failed to load {uri:?}: {} {} {response_text}", + response.status, response.status_text + )) + } + None => { + return Err(format!( + "failed to load {uri:?}: {} {}", + response.status, response.status_text + )) + } + } + } + + let mime = response.content_type().map(|v| v.to_owned()); + let bytes = response.bytes.into(); + + Ok(File { bytes, mime }) + } +} + +type Entry = Poll>; #[derive(Default)] pub struct EhttpLoader { cache: Arc>>, } +impl EhttpLoader { + pub const ID: &str = egui::generate_loader_id!(EhttpLoader); +} + const PROTOCOLS: &[&str] = &["http://", "https://"]; fn starts_with_one_of(s: &str, prefixes: &[&str]) -> bool { prefixes.iter().any(|prefix| s.starts_with(prefix)) } -fn get_image_bytes( - uri: &str, - response: Result, -) -> Result, String> { - let response = response?; - if !response.ok { - match response.text() { - Some(response_text) => { - return Err(format!( - "failed to load {uri:?}: {} {} {response_text}", - response.status, response.status_text - )) - } - None => { - return Err(format!( - "failed to load {uri:?}: {} {}", - response.status, response.status_text - )) - } - } - } - - let Some(content_type) = response.content_type() else { - return Err(format!("failed to load {uri:?}: no content-type header found")); - }; - if !content_type.starts_with("image/") { - return Err(format!("failed to load {uri:?}: expected content-type starting with \"image/\", found {content_type:?}")); +impl BytesLoader for EhttpLoader { + fn id(&self) -> &str { + Self::ID } - Ok(response.bytes.into()) -} - -impl BytesLoader for EhttpLoader { fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult { if !starts_with_one_of(uri, PROTOCOLS) { return Err(LoadError::NotSupported); @@ -59,15 +67,16 @@ impl BytesLoader for EhttpLoader { let mut cache = self.cache.lock(); if let Some(entry) = cache.get(uri).cloned() { match entry { - Poll::Ready(Ok(bytes)) => Ok(BytesPoll::Ready { + Poll::Ready(Ok(file)) => Ok(BytesPoll::Ready { size: None, - bytes: Bytes::Shared(bytes), + bytes: Bytes::Shared(file.bytes), + mime: file.mime, }), - Poll::Ready(Err(err)) => Err(LoadError::Custom(err)), + Poll::Ready(Err(err)) => Err(LoadError::Loading(err)), Poll::Pending => Ok(BytesPoll::Pending { size: None }), } } else { - crate::log_trace!("started loading {uri:?}"); + log::trace!("started loading {uri:?}"); let uri = uri.to_owned(); cache.insert(uri.clone(), Poll::Pending); @@ -77,8 +86,15 @@ impl BytesLoader for EhttpLoader { let ctx = ctx.clone(); let cache = self.cache.clone(); move |response| { - let result = get_image_bytes(&uri, response); - crate::log_trace!("finished loading {uri:?}"); + let result = match response { + Ok(response) => File::from_response(&uri, response), + Err(err) => { + // Log details; return summary + log::error!("Failed to load {uri:?}: {err}"); + Err(format!("Failed to load {uri:?}")) + } + }; + log::trace!("finished loading {uri:?}"); let prev = cache.lock().insert(uri, Poll::Ready(result)); assert!(matches!(prev, Some(Poll::Pending))); ctx.request_repaint(); @@ -93,12 +109,18 @@ impl BytesLoader for EhttpLoader { let _ = self.cache.lock().remove(uri); } + fn forget_all(&self) { + self.cache.lock().clear(); + } + fn byte_size(&self) -> usize { self.cache .lock() .values() .map(|entry| match entry { - Poll::Ready(Ok(bytes)) => bytes.len(), + Poll::Ready(Ok(file)) => { + file.bytes.len() + file.mime.as_ref().map_or(0, |m| m.len()) + } Poll::Ready(Err(err)) => err.len(), _ => 0, }) diff --git a/crates/egui_extras/src/loaders/file_loader.rs b/crates/egui_extras/src/loaders/file_loader.rs index 533c3dc95ad..31e6cf2ee0e 100644 --- a/crates/egui_extras/src/loaders/file_loader.rs +++ b/crates/egui_extras/src/loaders/file_loader.rs @@ -5,7 +5,13 @@ use egui::{ }; use std::{sync::Arc, task::Poll, thread}; -type Entry = Poll, String>>; +#[derive(Clone)] +struct File { + bytes: Arc<[u8]>, + mime: Option, +} + +type Entry = Poll>; #[derive(Default)] pub struct FileLoader { @@ -13,9 +19,17 @@ pub struct FileLoader { cache: Arc>>, } +impl FileLoader { + pub const ID: &str = egui::generate_loader_id!(FileLoader); +} + const PROTOCOL: &str = "file://"; impl BytesLoader for FileLoader { + fn id(&self) -> &str { + Self::ID + } + fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult { // File loader only supports the `file` protocol. let Some(path) = uri.strip_prefix(PROTOCOL) else { @@ -26,15 +40,16 @@ impl BytesLoader for FileLoader { if let Some(entry) = cache.get(path).cloned() { // `path` has either begun loading, is loaded, or has failed to load. match entry { - Poll::Ready(Ok(bytes)) => Ok(BytesPoll::Ready { + Poll::Ready(Ok(file)) => Ok(BytesPoll::Ready { size: None, - bytes: Bytes::Shared(bytes), + bytes: Bytes::Shared(file.bytes), + mime: file.mime, }), - Poll::Ready(Err(err)) => Err(LoadError::Custom(err)), + Poll::Ready(Err(err)) => Err(LoadError::Loading(err)), Poll::Pending => Ok(BytesPoll::Pending { size: None }), } } else { - crate::log_trace!("started loading {uri:?}"); + log::trace!("started loading {uri:?}"); // We need to load the file at `path`. // Set the file to `pending` until we finish loading it. @@ -48,16 +63,29 @@ impl BytesLoader for FileLoader { .spawn({ let ctx = ctx.clone(); let cache = self.cache.clone(); - let uri = uri.to_owned(); + let _uri = uri.to_owned(); move || { let result = match std::fs::read(&path) { - Ok(bytes) => Ok(bytes.into()), + Ok(bytes) => { + #[cfg(feature = "mime_guess")] + let mime = mime_guess2::from_path(&path) + .first_raw() + .map(|v| v.to_owned()); + + #[cfg(not(feature = "mime_guess"))] + let mime = None; + + Ok(File { + bytes: bytes.into(), + mime, + }) + } Err(err) => Err(err.to_string()), }; let prev = cache.lock().insert(path, Poll::Ready(result)); assert!(matches!(prev, Some(Poll::Pending))); ctx.request_repaint(); - crate::log_trace!("finished loading {uri:?}"); + log::trace!("finished loading {_uri:?}"); } }) .expect("failed to spawn thread"); @@ -70,12 +98,18 @@ impl BytesLoader for FileLoader { let _ = self.cache.lock().remove(uri); } + fn forget_all(&self) { + self.cache.lock().clear(); + } + fn byte_size(&self) -> usize { self.cache .lock() .values() .map(|entry| match entry { - Poll::Ready(Ok(bytes)) => bytes.len(), + Poll::Ready(Ok(file)) => { + file.bytes.len() + file.mime.as_ref().map_or(0, |m| m.len()) + } Poll::Ready(Err(err)) => err.len(), _ => 0, }) diff --git a/crates/egui_extras/src/loaders/image_loader.rs b/crates/egui_extras/src/loaders/image_loader.rs index 2dbc1f8bdb7..c566e65499d 100644 --- a/crates/egui_extras/src/loaders/image_loader.rs +++ b/crates/egui_extras/src/loaders/image_loader.rs @@ -13,18 +13,36 @@ pub struct ImageCrateLoader { cache: Mutex>, } -fn is_supported(uri: &str) -> bool { +impl ImageCrateLoader { + pub const ID: &str = egui::generate_loader_id!(ImageCrateLoader); +} + +fn is_supported_uri(uri: &str) -> bool { let Some(ext) = Path::new(uri).extension().and_then(|ext| ext.to_str()) else { // `true` because if there's no extension, assume that we support it - return true + return true; }; ext != "svg" } +fn is_unsupported_mime(mime: &str) -> bool { + mime.contains("svg") +} + impl ImageLoader for ImageCrateLoader { + fn id(&self) -> &str { + Self::ID + } + fn load(&self, ctx: &egui::Context, uri: &str, _: SizeHint) -> ImageLoadResult { - if !is_supported(uri) { + // three stages of guessing if we support loading the image: + // 1. URI extension + // 2. Mime from `BytesPoll::Ready` + // 3. image::guess_format + + // (1) + if !is_supported_uri(uri) { return Err(LoadError::NotSupported); } @@ -32,18 +50,25 @@ impl ImageLoader for ImageCrateLoader { if let Some(entry) = cache.get(uri).cloned() { match entry { Ok(image) => Ok(ImagePoll::Ready { image }), - Err(err) => Err(LoadError::Custom(err)), + Err(err) => Err(LoadError::Loading(err)), } } else { match ctx.try_load_bytes(uri) { - Ok(BytesPoll::Ready { bytes, .. }) => { - crate::log_trace!("started loading {uri:?}"); + Ok(BytesPoll::Ready { bytes, mime, .. }) => { + // (2 and 3) + if mime.as_deref().is_some_and(is_unsupported_mime) + || image::guess_format(&bytes).is_err() + { + return Err(LoadError::NotSupported); + } + + log::trace!("started loading {uri:?}"); let result = crate::image::load_image_bytes(&bytes).map(Arc::new); - crate::log_trace!("finished loading {uri:?}"); + log::trace!("finished loading {uri:?}"); cache.insert(uri.into(), result.clone()); match result { Ok(image) => Ok(ImagePoll::Ready { image }), - Err(err) => Err(LoadError::Custom(err)), + Err(err) => Err(LoadError::Loading(err)), } } Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }), @@ -56,6 +81,10 @@ impl ImageLoader for ImageCrateLoader { let _ = self.cache.lock().remove(uri); } + fn forget_all(&self) { + self.cache.lock().clear(); + } + fn byte_size(&self) -> usize { self.cache .lock() @@ -74,11 +103,11 @@ mod tests { #[test] fn check_support() { - assert!(is_supported("https://test.png")); - assert!(is_supported("test.jpeg")); - assert!(is_supported("http://test.gif")); - assert!(is_supported("test.webp")); - assert!(is_supported("file://test")); - assert!(!is_supported("test.svg")); + assert!(is_supported_uri("https://test.png")); + assert!(is_supported_uri("test.jpeg")); + assert!(is_supported_uri("http://test.gif")); + assert!(is_supported_uri("test.webp")); + assert!(is_supported_uri("file://test")); + assert!(!is_supported_uri("test.svg")); } } diff --git a/crates/egui_extras/src/loaders/svg_loader.rs b/crates/egui_extras/src/loaders/svg_loader.rs index 71f23380353..c5ac37b40e9 100644 --- a/crates/egui_extras/src/loaders/svg_loader.rs +++ b/crates/egui_extras/src/loaders/svg_loader.rs @@ -13,13 +13,23 @@ pub struct SvgLoader { cache: Mutex>, } +impl SvgLoader { + pub const ID: &str = egui::generate_loader_id!(SvgLoader); +} + fn is_supported(uri: &str) -> bool { - let Some(ext) = Path::new(uri).extension().and_then(|ext| ext.to_str()) else { return false }; + let Some(ext) = Path::new(uri).extension().and_then(|ext| ext.to_str()) else { + return false; + }; ext == "svg" } impl ImageLoader for SvgLoader { + fn id(&self) -> &str { + Self::ID + } + fn load(&self, ctx: &egui::Context, uri: &str, size_hint: SizeHint) -> ImageLoadResult { if !is_supported(uri) { return Err(LoadError::NotSupported); @@ -32,25 +42,25 @@ impl ImageLoader for SvgLoader { if let Some(entry) = cache.get(&(uri.clone(), size_hint)).cloned() { match entry { Ok(image) => Ok(ImagePoll::Ready { image }), - Err(err) => Err(LoadError::Custom(err)), + Err(err) => Err(LoadError::Loading(err)), } } else { match ctx.try_load_bytes(&uri) { Ok(BytesPoll::Ready { bytes, .. }) => { - crate::log_trace!("started loading {uri:?}"); + log::trace!("started loading {uri:?}"); let fit_to = match size_hint { - SizeHint::Original => usvg::FitTo::Original, + SizeHint::Scale(factor) => usvg::FitTo::Zoom(factor.into_inner()), SizeHint::Width(w) => usvg::FitTo::Width(w), SizeHint::Height(h) => usvg::FitTo::Height(h), SizeHint::Size(w, h) => usvg::FitTo::Size(w, h), }; let result = crate::image::load_svg_bytes_with_size(&bytes, fit_to).map(Arc::new); - crate::log_trace!("finished loading {uri:?}"); + log::trace!("finished loading {uri:?}"); cache.insert((uri, size_hint), result.clone()); match result { Ok(image) => Ok(ImagePoll::Ready { image }), - Err(err) => Err(LoadError::Custom(err)), + Err(err) => Err(LoadError::Loading(err)), } } Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }), @@ -63,6 +73,10 @@ impl ImageLoader for SvgLoader { self.cache.lock().retain(|(u, _), _| u != uri); } + fn forget_all(&self) { + self.cache.lock().clear(); + } + fn byte_size(&self) -> usize { self.cache .lock() diff --git a/crates/egui_extras/src/sizing.rs b/crates/egui_extras/src/sizing.rs index c6ae9d6daae..70a2ca4e693 100644 --- a/crates/egui_extras/src/sizing.rs +++ b/crates/egui_extras/src/sizing.rs @@ -153,7 +153,7 @@ impl From> for Sizing { #[test] fn test_sizing() { let sizing: Sizing = vec![].into(); - assert_eq!(sizing.to_lengths(50.0, 0.0), vec![]); + assert_eq!(sizing.to_lengths(50.0, 0.0), Vec::::new()); let sizing: Sizing = vec![Size::remainder().at_least(20.0), Size::remainder()].into(); assert_eq!(sizing.to_lengths(50.0, 0.0), vec![25.0, 25.0]); diff --git a/crates/egui_demo_lib/src/syntax_highlighting.rs b/crates/egui_extras/src/syntax_highlighting.rs similarity index 69% rename from crates/egui_demo_lib/src/syntax_highlighting.rs rename to crates/egui_extras/src/syntax_highlighting.rs index 50f1ff93933..775d7207bbb 100644 --- a/crates/egui_demo_lib/src/syntax_highlighting.rs +++ b/crates/egui_extras/src/syntax_highlighting.rs @@ -1,12 +1,19 @@ +//! Syntax highlighting for code. +//! +//! Turn on the `syntect` feature for great syntax highlighting of any language. +//! Otherwise, a very simple fallback will be used, that works okish for C, C++, Rust, and Python. + use egui::text::LayoutJob; /// View some code with syntax highlighting and selection. -pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) { - let language = "rs"; - let theme = CodeTheme::from_memory(ui.ctx()); - +pub fn code_view_ui( + ui: &mut egui::Ui, + theme: &CodeTheme, + mut code: &str, + language: &str, +) -> egui::Response { let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| { - let layout_job = highlight(ui.ctx(), &theme, string, language); + let layout_job = highlight(ui.ctx(), theme, string, language); // layout_job.wrap.max_width = wrap_width; // no wrapping ui.fonts(|f| f.layout_job(layout_job)) }; @@ -18,10 +25,12 @@ pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) { .desired_rows(1) .lock_focus(true) .layouter(&mut layouter), - ); + ) } -/// Memoized Code highlighting +/// Add syntax highlighting to a code string. +/// +/// The results are memoized, so you can call this every frame without performance penalty. pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str, language: &str) -> LayoutJob { impl egui::util::cache::ComputerMut<(&CodeTheme, &str, &str), LayoutJob> for Highlighter { fn compute(&mut self, (theme, code, lang): (&CodeTheme, &str, &str)) -> LayoutJob { @@ -41,9 +50,7 @@ pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str, language: & // ---------------------------------------------------------------------------- #[cfg(not(feature = "syntect"))] -#[derive(Clone, Copy, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[derive(enum_map::Enum)] +#[derive(Clone, Copy, PartialEq, serde::Deserialize, serde::Serialize, enum_map::Enum)] enum TokenType { Comment, Keyword, @@ -54,8 +61,7 @@ enum TokenType { } #[cfg(feature = "syntect")] -#[derive(Clone, Copy, Hash, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[derive(Clone, Copy, Hash, PartialEq, serde::Deserialize, serde::Serialize)] enum SyntectTheme { Base16EightiesDark, Base16MochaDark, @@ -118,9 +124,9 @@ impl SyntectTheme { } } -#[derive(Clone, Hash, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "serde", serde(default))] +/// A selected color theme. +#[derive(Clone, Hash, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(default)] pub struct CodeTheme { dark_mode: bool, @@ -138,6 +144,7 @@ impl Default for CodeTheme { } impl CodeTheme { + /// Selects either dark or light theme based on the given style. pub fn from_style(style: &egui::Style) -> Self { if style.visuals.dark_mode { Self::dark() @@ -146,6 +153,9 @@ impl CodeTheme { } } + /// Load code theme from egui memory. + /// + /// There is one dark and one light theme stored at any one time. pub fn from_memory(ctx: &egui::Context) -> Self { if ctx.style().visuals.dark_mode { ctx.data_mut(|d| { @@ -160,6 +170,9 @@ impl CodeTheme { } } + /// Store theme to egui memory. + /// + /// There is one dark and one light theme stored at any one time. pub fn store_in_memory(self, ctx: &egui::Context) { if self.dark_mode { ctx.data_mut(|d| d.insert_persisted(egui::Id::new("dark"), self)); @@ -185,6 +198,7 @@ impl CodeTheme { } } + /// Show UI for changing the color theme. pub fn ui(&mut self, ui: &mut egui::Ui) { egui::widgets::global_dark_light_mode_buttons(ui); @@ -231,6 +245,7 @@ impl CodeTheme { } } + /// Show UI for changing the color theme. pub fn ui(&mut self, ui: &mut egui::Ui) { ui.horizontal_top(|ui| { let selected_id = egui::Id::null(); @@ -306,6 +321,7 @@ struct Highlighter { #[cfg(feature = "syntect")] impl Default for Highlighter { fn default() -> Self { + crate::profile_function!(); Self { ps: syntect::parsing::SyntaxSet::load_defaults_newlines(), ts: syntect::highlighting::ThemeSet::load_defaults(), @@ -313,7 +329,6 @@ impl Default for Highlighter { } } -#[cfg(feature = "syntect")] impl Highlighter { #[allow(clippy::unused_self, clippy::unnecessary_wraps)] fn highlight(&self, theme: &CodeTheme, code: &str, lang: &str) -> LayoutJob { @@ -332,7 +347,10 @@ impl Highlighter { }) } + #[cfg(feature = "syntect")] fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option { + crate::profile_function!(); + use syntect::easy::HighlightLines; use syntect::highlighting::FontStyle; use syntect::util::LinesWithEndings; @@ -400,13 +418,24 @@ struct Highlighter {} #[cfg(not(feature = "syntect"))] impl Highlighter { #[allow(clippy::unused_self, clippy::unnecessary_wraps)] - fn highlight(&self, theme: &CodeTheme, mut text: &str, _language: &str) -> LayoutJob { + fn highlight_impl( + &self, + theme: &CodeTheme, + mut text: &str, + language: &str, + ) -> Option { + crate::profile_function!(); + + let language = Language::new(language)?; + // Extremely simple syntax highlighter for when we compile without syntect let mut job = LayoutJob::default(); while !text.is_empty() { - if text.starts_with("//") { + if language.double_slash_comments && text.starts_with("//") + || language.hash_comments && text.starts_with('#') + { let end = text.find('\n').unwrap_or(text.len()); job.append(&text[..end], 0.0, theme.formats[TokenType::Comment].clone()); text = &text[end..]; @@ -427,7 +456,7 @@ impl Highlighter { .find(|c: char| !c.is_ascii_alphanumeric()) .map_or_else(|| text.len(), |i| i + 1); let word = &text[..end]; - let tt = if is_keyword(word) { + let tt = if language.is_keyword(word) { TokenType::Keyword } else { TokenType::Literal @@ -457,50 +486,182 @@ impl Highlighter { } } - job + Some(job) } } #[cfg(not(feature = "syntect"))] -fn is_keyword(word: &str) -> bool { - matches!( - word, - "as" | "async" - | "await" - | "break" - | "const" - | "continue" - | "crate" - | "dyn" - | "else" - | "enum" - | "extern" - | "false" - | "fn" - | "for" - | "if" - | "impl" - | "in" - | "let" - | "loop" - | "match" - | "mod" - | "move" - | "mut" - | "pub" - | "ref" - | "return" - | "self" - | "Self" - | "static" - | "struct" - | "super" - | "trait" - | "true" - | "type" - | "unsafe" - | "use" - | "where" - | "while" - ) +struct Language { + /// `// comment` + double_slash_comments: bool, + + /// `# comment` + hash_comments: bool, + + keywords: std::collections::BTreeSet<&'static str>, +} + +#[cfg(not(feature = "syntect"))] +impl Language { + fn new(language: &str) -> Option { + match language.to_lowercase().as_str() { + "c" | "h" | "hpp" | "cpp" | "c++" => Some(Self::cpp()), + "py" | "python" => Some(Self::python()), + "rs" | "rust" => Some(Self::rust()), + "toml" => Some(Self::toml()), + _ => { + None // unsupported language + } + } + } + + fn is_keyword(&self, word: &str) -> bool { + self.keywords.contains(word) + } + + fn cpp() -> Self { + Self { + double_slash_comments: true, + hash_comments: false, + keywords: [ + "alignas", + "alignof", + "and_eq", + "and", + "asm", + "atomic_cancel", + "atomic_commit", + "atomic_noexcept", + "auto", + "bitand", + "bitor", + "bool", + "break", + "case", + "catch", + "char", + "char16_t", + "char32_t", + "char8_t", + "class", + "co_await", + "co_return", + "co_yield", + "compl", + "concept", + "const_cast", + "const", + "consteval", + "constexpr", + "constinit", + "continue", + "decltype", + "default", + "delete", + "do", + "double", + "dynamic_cast", + "else", + "enum", + "explicit", + "export", + "extern", + "false", + "float", + "for", + "friend", + "goto", + "if", + "inline", + "int", + "long", + "mutable", + "namespace", + "new", + "noexcept", + "not_eq", + "not", + "nullptr", + "operator", + "or_eq", + "or", + "private", + "protected", + "public", + "reflexpr", + "register", + "reinterpret_cast", + "requires", + "return", + "short", + "signed", + "sizeof", + "static_assert", + "static_cast", + "static", + "struct", + "switch", + "synchronized", + "template", + "this", + "thread_local", + "throw", + "true", + "try", + "typedef", + "typeid", + "typename", + "union", + "unsigned", + "using", + "virtual", + "void", + "volatile", + "wchar_t", + "while", + "xor_eq", + "xor", + ] + .into_iter() + .collect(), + } + } + + fn python() -> Self { + Self { + double_slash_comments: false, + hash_comments: true, + keywords: [ + "and", "as", "assert", "break", "class", "continue", "def", "del", "elif", "else", + "except", "False", "finally", "for", "from", "global", "if", "import", "in", "is", + "lambda", "None", "nonlocal", "not", "or", "pass", "raise", "return", "True", + "try", "while", "with", "yield", + ] + .into_iter() + .collect(), + } + } + + fn rust() -> Self { + Self { + double_slash_comments: true, + hash_comments: false, + keywords: [ + "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", + "enum", "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", + "mod", "move", "mut", "pub", "ref", "return", "self", "Self", "static", "struct", + "super", "trait", "true", "type", "unsafe", "use", "where", "while", + ] + .into_iter() + .collect(), + } + } + + fn toml() -> Self { + Self { + double_slash_comments: false, + hash_comments: true, + keywords: Default::default(), + } + } } diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index e30367db446..ca09b385931 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -366,9 +366,9 @@ impl<'a> TableBuilder<'a> { fn available_width(&self) -> f32 { self.ui.available_rect_before_wrap().width() - if self.scroll_options.vscroll { - self.ui.spacing().scroll_bar_inner_margin - + self.ui.spacing().scroll_bar_width - + self.ui.spacing().scroll_bar_outer_margin + self.ui.spacing().scroll.bar_inner_margin + + self.ui.spacing().scroll.bar_width + + self.ui.spacing().scroll.bar_outer_margin } else { 0.0 } @@ -648,7 +648,9 @@ impl<'a> Table<'a> { // If the last column is 'remainder', then let it fill the remainder! let eps = 0.1; // just to avoid some rounding errors. *column_width = available_width - eps; - *column_width = column_width.at_least(max_used_widths[i]); + if !column.clip { + *column_width = column_width.at_least(max_used_widths[i]); + } *column_width = width_range.clamp(*column_width); break; } diff --git a/crates/egui_glium/Cargo.toml b/crates/egui_glium/Cargo.toml index 7a8f758704e..a76cde4e3eb 100644 --- a/crates/egui_glium/Cargo.toml +++ b/crates/egui_glium/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "egui_glium" -version = "0.22.0" +version = "0.23.0" authors = ["Emil Ernerfeldt "] description = "Bindings for using egui natively using the glium library" edition = "2021" @@ -36,10 +36,10 @@ links = ["egui-winit/links"] [dependencies] -egui = { version = "0.22.0", path = "../egui", default-features = false, features = [ +egui = { version = "0.23.0", path = "../egui", default-features = false, features = [ "bytemuck", ] } -egui-winit = { version = "0.22.0", path = "../egui-winit", default-features = false } +egui-winit = { version = "0.23.0", path = "../egui-winit", default-features = false } ahash = { version = "0.8.1", default-features = false, features = [ "no-rng", # we don't need DOS-protection, so we let users opt-in to it instead @@ -54,5 +54,5 @@ document-features = { version = "0.2", optional = true } [dev-dependencies] -egui_demo_lib = { version = "0.22.0", path = "../egui_demo_lib", default-features = false } +egui_demo_lib = { version = "0.23.0", path = "../egui_demo_lib", default-features = false } image = { version = "0.24", default-features = false, features = ["png"] } diff --git a/crates/egui_glium/examples/native_texture.rs b/crates/egui_glium/examples/native_texture.rs index 94977cd6176..eb5a956f464 100644 --- a/crates/egui_glium/examples/native_texture.rs +++ b/crates/egui_glium/examples/native_texture.rs @@ -30,8 +30,7 @@ fn main() { egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| { if ui .add(egui::Button::image_and_text( - texture_id, - button_image_size, + (texture_id, button_image_size), "Quit", )) .clicked() diff --git a/crates/egui_glow/CHANGELOG.md b/crates/egui_glow/CHANGELOG.md index 3710edab442..4b82b58d22c 100644 --- a/crates/egui_glow/CHANGELOG.md +++ b/crates/egui_glow/CHANGELOG.md @@ -5,7 +5,12 @@ This file is updated upon each release. Changes since the last release can be found by running the `scripts/generate_changelog.py` script. +## 0.23.0 - 2023-09-27 +* Update `egui` + + ## 0.22.0 - 2023-05-23 +* Update `egui` ## 0.21.0 - 2023-02-08 diff --git a/crates/egui_glow/Cargo.toml b/crates/egui_glow/Cargo.toml index d1e0d5afec6..1f52b9b1db7 100644 --- a/crates/egui_glow/Cargo.toml +++ b/crates/egui_glow/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "egui_glow" -version = "0.22.0" +version = "0.23.0" authors = ["Emil Ernerfeldt "] description = "Bindings for using egui natively using the glow library" edition = "2021" @@ -44,7 +44,7 @@ winit = ["egui-winit"] [dependencies] -egui = { version = "0.22.0", path = "../egui", default-features = false, features = [ +egui = { version = "0.23.0", path = "../egui", default-features = false, features = [ "bytemuck", ] } @@ -59,7 +59,7 @@ document-features = { version = "0.2", optional = true } # Native: [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -egui-winit = { version = "0.22.0", path = "../egui-winit", optional = true, default-features = false } +egui-winit = { version = "0.23.0", path = "../egui-winit", optional = true, default-features = false } puffin = { version = "0.16", optional = true } # Web: diff --git a/crates/egui_glow/src/lib.rs b/crates/egui_glow/src/lib.rs index 18d8ff870b9..e5381952390 100644 --- a/crates/egui_glow/src/lib.rs +++ b/crates/egui_glow/src/lib.rs @@ -1,6 +1,6 @@ //! [`egui`] bindings for [`glow`](https://github.com/grovesNL/glow). //! -//! The main types you want to look are are [`Painter`]. +//! The main type you want to look at is [`Painter`]. //! //! If you are writing an app, you may want to look at [`eframe`](https://docs.rs/eframe) instead. //! @@ -112,22 +112,30 @@ pub fn check_for_gl_error_impl(gl: &glow::Context, file: &str, line: u32, contex // --------------------------------------------------------------------------- -/// Profiling macro for feature "puffin" -macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] - puffin::profile_function!($($arg)*); - }; -} -pub(crate) use profile_function; +mod profiling_scopes { + #![allow(unused_macros)] + #![allow(unused_imports)] + + /// Profiling macro for feature "puffin" + macro_rules! profile_function { + ($($arg: tt)*) => { + #[cfg(feature = "puffin")] + #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. + puffin::profile_function!($($arg)*); + }; + } + pub(crate) use profile_function; -/// Profiling macro for feature "puffin" -macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] - puffin::profile_scope!($($arg)*); - }; + /// Profiling macro for feature "puffin" + macro_rules! profile_scope { + ($($arg: tt)*) => { + #[cfg(feature = "puffin")] + #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. + puffin::profile_scope!($($arg)*); + }; + } + pub(crate) use profile_scope; } -pub(crate) use profile_scope; + +#[allow(unused_imports)] +pub(crate) use profiling_scopes::*; diff --git a/crates/egui_glow/src/painter.rs b/crates/egui_glow/src/painter.rs index e6ac79ecaf2..c04dd927e9b 100644 --- a/crates/egui_glow/src/painter.rs +++ b/crates/egui_glow/src/painter.rs @@ -370,25 +370,6 @@ impl Painter { Primitive::Callback(callback) => { if callback.rect.is_positive() { crate::profile_scope!("callback"); - // Transform callback rect to physical pixels: - let rect_min_x = pixels_per_point * callback.rect.min.x; - let rect_min_y = pixels_per_point * callback.rect.min.y; - let rect_max_x = pixels_per_point * callback.rect.max.x; - let rect_max_y = pixels_per_point * callback.rect.max.y; - - let rect_min_x = rect_min_x.round() as i32; - let rect_min_y = rect_min_y.round() as i32; - let rect_max_x = rect_max_x.round() as i32; - let rect_max_y = rect_max_y.round() as i32; - - unsafe { - self.gl.viewport( - rect_min_x, - size_in_pixels.1 as i32 - rect_max_y, - rect_max_x - rect_min_x, - rect_max_y - rect_min_y, - ); - } let info = egui::PaintCallbackInfo { viewport: callback.rect, @@ -397,6 +378,16 @@ impl Painter { screen_size_px, }; + let viewport_px = info.viewport_in_pixels(); + unsafe { + self.gl.viewport( + viewport_px.left_px.round() as _, + viewport_px.from_bottom_px.round() as _, + viewport_px.width_px.round() as _, + viewport_px.height_px.round() as _, + ); + } + if let Some(callback) = callback.callback.downcast_ref::() { (callback.f)(info, self); } else { @@ -494,10 +485,13 @@ impl Painter { "Mismatch between texture size and texel count" ); - let data: Vec = image - .srgba_pixels(None) - .flat_map(|a| a.to_array()) - .collect(); + let data: Vec = { + crate::profile_scope!("font -> sRGBA"); + image + .srgba_pixels(None) + .flat_map(|a| a.to_array()) + .collect() + }; self.upload_texture_srgb(delta.pos, image.size, delta.options, &data); } @@ -511,6 +505,7 @@ impl Painter { options: egui::TextureOptions, data: &[u8], ) { + crate::profile_function!(); assert_eq!(data.len(), w * h * 4); assert!( w <= self.max_texture_side && h <= self.max_texture_side, @@ -561,6 +556,7 @@ impl Painter { let level = 0; if let Some([x, y]) = pos { + crate::profile_scope!("gl.tex_sub_image_2d"); self.gl.tex_sub_image_2d( glow::TEXTURE_2D, level, @@ -575,6 +571,7 @@ impl Painter { check_for_gl_error!(&self.gl, "tex_sub_image_2d"); } else { let border = 0; + crate::profile_scope!("gl.tex_image_2d"); self.gl.tex_image_2d( glow::TEXTURE_2D, level, diff --git a/crates/egui_plot/CHANGELOG.md b/crates/egui_plot/CHANGELOG.md index 5f3b7cbc9a3..e0e093b52c2 100644 --- a/crates/egui_plot/CHANGELOG.md +++ b/crates/egui_plot/CHANGELOG.md @@ -3,3 +3,14 @@ All notable changes to the `egui_plot` integration will be noted in this file. This file is updated upon each release. Changes since the last release can be found by running the `scripts/generate_changelog.py` script. + + +## 0.23.0 - 2023-09-27 - Initial release, after being forked out from `egui` +* Draw axis labels and ticks outside of plotting window [#2284](https://github.com/emilk/egui/pull/2284) (thanks [@JohannesProgrammiert](https://github.com/JohannesProgrammiert)!) +* Add `PlotUi::response()` to replace `plot_clicked()` etc [#3223](https://github.com/emilk/egui/pull/3223) +* Add rotation feature to plot images [#3121](https://github.com/emilk/egui/pull/3121) (thanks [@ThundR67](https://github.com/ThundR67)!) +* Plot items: Image rotation and size in plot coordinates, polygon fill color [#3182](https://github.com/emilk/egui/pull/3182) (thanks [@s-nie](https://github.com/s-nie)!) +* Add method to specify `tip_size` of plot arrows [#3138](https://github.com/emilk/egui/pull/3138) (thanks [@nagua](https://github.com/nagua)!) +* Better handle additive colors in plots [#3387](https://github.com/emilk/egui/pull/3387) +* Fix auto_bounds when only one axis has restricted navigation [#3171](https://github.com/emilk/egui/pull/3171) (thanks [@KoffeinFlummi](https://github.com/KoffeinFlummi)!) +* Fix plot formatter not taking closures [#3260](https://github.com/emilk/egui/pull/3260) (thanks [@Wumpf](https://github.com/Wumpf)!) diff --git a/crates/egui_plot/Cargo.toml b/crates/egui_plot/Cargo.toml index 53f35c42020..d61a2c63d18 100644 --- a/crates/egui_plot/Cargo.toml +++ b/crates/egui_plot/Cargo.toml @@ -1,11 +1,7 @@ [package] name = "egui_plot" -version = "0.22.0" -authors = [ - "Dominik Rössler ", - "Emil Ernerfeldt ", - "René Rössler ", -] +version = "0.23.0" +authors = ["Emil Ernerfeldt "] description = "Immediate mode plotting for the egui GUI library" edition = "2021" rust-version = "1.70" @@ -32,7 +28,7 @@ serde = ["dep:serde", "egui/serde"] [dependencies] -egui = { version = "0.22.0", path = "../egui", default-features = false } +egui = { version = "0.23.0", path = "../egui", default-features = false } #! ### Optional dependencies diff --git a/crates/egui_plot/src/items/mod.rs b/crates/egui_plot/src/items/mod.rs index 820850f4296..644a2c14e79 100644 --- a/crates/egui_plot/src/items/mod.rs +++ b/crates/egui_plot/src/items/mod.rs @@ -1234,12 +1234,19 @@ impl PlotItem for PlotImage { Rect::from_two_pos(left_top_screen, right_bottom_screen) }; let screen_rotation = -*rotation as f32; - Image::new(*texture_id, image_screen_rect.size()) - .bg_fill(*bg_fill) - .tint(*tint) - .uv(*uv) - .rotate(screen_rotation, Vec2::splat(0.5)) - .paint_at(ui, image_screen_rect); + + egui::paint_texture_at( + ui.painter(), + image_screen_rect, + &ImageOptions { + uv: *uv, + bg_fill: *bg_fill, + tint: *tint, + rotation: Some((Rot2::from_angle(screen_rotation), Vec2::splat(0.5))), + rounding: Rounding::ZERO, + }, + &(*texture_id, image_screen_rect.size()).into(), + ); if *highlight { let center = image_screen_rect.center(); let rotation = Rot2::from_angle(screen_rotation); diff --git a/crates/egui_plot/src/items/rect_elem.rs b/crates/egui_plot/src/items/rect_elem.rs index 1ac470c77d1..b83da7c3dbb 100644 --- a/crates/egui_plot/src/items/rect_elem.rs +++ b/crates/egui_plot/src/items/rect_elem.rs @@ -58,8 +58,16 @@ pub(super) trait RectElement { pub(super) fn highlighted_color(mut stroke: Stroke, fill: Color32) -> (Stroke, Color32) { stroke.width *= 2.0; - let fill = Rgba::from(fill); - let fill_alpha = (2.0 * fill.a()).at_most(1.0); - let fill = fill.to_opaque().multiply(fill_alpha); + + let mut fill = Rgba::from(fill); + if fill.is_additive() { + // Make slightly brighter + fill = 1.3 * fill; + } else { + // Make more opaque: + let fill_alpha = (2.0 * fill.a()).at_most(1.0); + fill = fill.to_opaque().multiply(fill_alpha); + } + (stroke, fill.into()) } diff --git a/crates/egui_plot/src/items/values.rs b/crates/egui_plot/src/items/values.rs index 55e0e464b4f..0527fb26e75 100644 --- a/crates/egui_plot/src/items/values.rs +++ b/crates/egui_plot/src/items/values.rs @@ -47,6 +47,7 @@ impl PlotPoint { // ---------------------------------------------------------------------------- +/// Solid, dotted, dashed, etc. #[derive(Debug, PartialEq, Clone, Copy)] pub enum LineStyle { Solid, @@ -319,6 +320,7 @@ impl PlotPoints { // ---------------------------------------------------------------------------- +/// Circle, Diamond, Square, Cross, … #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum MarkerShape { Circle, diff --git a/crates/egui_plot/src/lib.rs b/crates/egui_plot/src/lib.rs index 835a75d1941..7e8507a622b 100644 --- a/crates/egui_plot/src/lib.rs +++ b/crates/egui_plot/src/lib.rs @@ -1,4 +1,4 @@ -//! Simple plotting library. +//! Simple plotting library for [`egui`](https://github.com/emilk/egui). //! //! ## Feature flags #![cfg_attr(feature = "document-features", doc = document_features::document_features!())] @@ -77,6 +77,7 @@ impl Default for CoordinatesFormatter { const MIN_LINE_SPACING_IN_POINTS: f64 = 6.0; // TODO(emilk): large enough for a wide label +/// Two bools, one for each axis (X and Y). #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct AxisBools { @@ -1404,7 +1405,7 @@ impl PlotUi { Vec2::new(delta.x / dp_dv[0] as f32, delta.y / dp_dv[1] as f32) } - /// Read the transform netween plot coordinates and screen coordinates. + /// Read the transform between plot coordinates and screen coordinates. pub fn transform(&self) -> &PlotTransform { &self.last_plot_transform } diff --git a/crates/emath/Cargo.toml b/crates/emath/Cargo.toml index 065b475a7dd..0e40ea274f9 100644 --- a/crates/emath/Cargo.toml +++ b/crates/emath/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "emath" -version = "0.22.0" +version = "0.23.0" authors = ["Emil Ernerfeldt "] description = "Minimal 2D math library for GUI work" edition = "2021" diff --git a/crates/emath/src/range.rs b/crates/emath/src/range.rs index b7b975c4586..6de7b2a099f 100644 --- a/crates/emath/src/range.rs +++ b/crates/emath/src/range.rs @@ -97,6 +97,16 @@ impl Rangef { } } + /// Flip the min and the max + #[inline] + #[must_use] + pub fn flip(self) -> Self { + Self { + min: self.max, + max: self.min, + } + } + /// The overlap of two ranges, i.e. the range that is contained by both. /// /// If the ranges do not overlap, returns a range with `span() < 0.0`. diff --git a/crates/emath/src/rect.rs b/crates/emath/src/rect.rs index 1007c219712..8a5670fdf02 100644 --- a/crates/emath/src/rect.rs +++ b/crates/emath/src/rect.rs @@ -13,6 +13,10 @@ use crate::*; /// of `min` and `max` are swapped. These are usually a sign of an error. /// /// Normally the unit is points (logical pixels) in screen space coordinates. +/// +/// `Rect` does NOT implement `Default`, because there is no obvious default value. +/// [`Rect::ZERO`] may seem reasonable, but when used as a bounding box, [`Rect::NOTHING`] +/// is a better default - so be explicit instead! #[repr(C)] #[derive(Clone, Copy, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -146,6 +150,34 @@ impl Rect { rect } + #[inline] + #[must_use] + pub fn with_min_x(mut self, min_x: f32) -> Self { + self.min.x = min_x; + self + } + + #[inline] + #[must_use] + pub fn with_min_y(mut self, min_y: f32) -> Self { + self.min.y = min_y; + self + } + + #[inline] + #[must_use] + pub fn with_max_x(mut self, max_x: f32) -> Self { + self.max.x = max_x; + self + } + + #[inline] + #[must_use] + pub fn with_max_y(mut self, max_y: f32) -> Self { + self.max.y = max_y; + self + } + /// Expand by this much in each direction, keeping the center #[must_use] pub fn expand(self, amnt: f32) -> Self { diff --git a/crates/emath/src/vec2.rs b/crates/emath/src/vec2.rs index 133c3fd3c20..d4a2c33008f 100644 --- a/crates/emath/src/vec2.rs +++ b/crates/emath/src/vec2.rs @@ -1,4 +1,4 @@ -use std::ops::{Add, AddAssign, Div, Mul, MulAssign, Neg, Sub, SubAssign}; +use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; /// A vector has a direction and length. /// A [`Vec2`] is often used to represent a size. @@ -203,7 +203,8 @@ impl Vec2 { /// ``` #[inline(always)] pub fn angled(angle: f32) -> Self { - vec2(angle.cos(), angle.sin()) + let (sin, cos) = angle.sin_cos(); + vec2(cos, sin) } #[must_use] @@ -274,6 +275,16 @@ impl Vec2 { self.x.max(self.y) } + /// Swizzle the axes. + #[inline] + #[must_use] + pub fn yx(self) -> Vec2 { + Vec2 { + x: self.y, + y: self.x, + } + } + #[must_use] #[inline] pub fn clamp(self, min: Self, max: Self) -> Self { @@ -397,6 +408,14 @@ impl MulAssign for Vec2 { } } +impl DivAssign for Vec2 { + #[inline(always)] + fn div_assign(&mut self, rhs: f32) { + self.x /= rhs; + self.y /= rhs; + } +} + impl Mul for Vec2 { type Output = Vec2; @@ -460,4 +479,20 @@ fn test_vec2() { assert_eq!(Vec2::DOWN.angle(), 0.25 * TAU); almost_eq!(Vec2::LEFT.angle(), 0.50 * TAU); assert_eq!(Vec2::UP.angle(), -0.25 * TAU); + + let mut assignment = vec2(1.0, 2.0); + assignment += vec2(3.0, 4.0); + assert_eq!(assignment, vec2(4.0, 6.0)); + + let mut assignment = vec2(4.0, 6.0); + assignment -= vec2(1.0, 2.0); + assert_eq!(assignment, vec2(3.0, 4.0)); + + let mut assignment = vec2(1.0, 2.0); + assignment *= 2.0; + assert_eq!(assignment, vec2(2.0, 4.0)); + + let mut assignment = vec2(2.0, 4.0); + assignment /= 2.0; + assert_eq!(assignment, vec2(1.0, 2.0)); } diff --git a/crates/epaint/CHANGELOG.md b/crates/epaint/CHANGELOG.md index e0f4e05699f..36fcfc1b58c 100644 --- a/crates/epaint/CHANGELOG.md +++ b/crates/epaint/CHANGELOG.md @@ -5,6 +5,17 @@ This file is updated upon each release. Changes since the last release can be found by running the `scripts/generate_changelog.py` script. +## 0.23.0 - 2023-09-27 +* Update MSRV to Rust 1.70.0 [#3310](https://github.com/emilk/egui/pull/3310) +* Add option to truncate text at wrap width [#3244](https://github.com/emilk/egui/pull/3244) [#3366](https://github.com/emilk/egui/pull/3366) +* Add control of line height and letter spacing [#3302](https://github.com/emilk/egui/pull/3302) +* Support images with rounded corners [#3257](https://github.com/emilk/egui/pull/3257) +* Add `ColorImage::from_gray` [#3166](https://github.com/emilk/egui/pull/3166) (thanks [@thomaseliot](https://github.com/thomaseliot)!) +* Provide `into_inner()` for `egui::mutex::{Mutex, RwLock}` [#3110](https://github.com/emilk/egui/pull/3110) (thanks [@KmolYuan](https://github.com/KmolYuan)!) +* Fix problems with tabs in text [#3355](https://github.com/emilk/egui/pull/3355) +* Refactor: change `ClippedShape` from struct-enum to a normal struct [#3225](https://github.com/emilk/egui/pull/3225) +* Document when `Galley`s get invalidated [#3024](https://github.com/emilk/egui/pull/3024) (thanks [@e00E](https://github.com/e00E)!) + ## 0.22.0 - 2023-05-23 * Fix compiling `epaint` without `bytemuck` dependency [#2913](https://github.com/emilk/egui/pull/2913) (thanks [@lunixbochs](https://github.com/lunixbochs)!) diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml index 4da6195cc80..16eda3dfa2d 100644 --- a/crates/epaint/Cargo.toml +++ b/crates/epaint/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "epaint" -version = "0.22.0" +version = "0.23.0" authors = ["Emil Ernerfeldt "] description = "Minimal 2D graphics library for GUI work" edition = "2021" @@ -70,8 +70,8 @@ serde = ["dep:serde", "ahash/serde", "emath/serde", "ecolor/serde"] unity = [] [dependencies] -emath = { version = "0.22.0", path = "../emath" } -ecolor = { version = "0.22.0", path = "../ecolor" } +emath = { version = "0.23.0", path = "../emath" } +ecolor = { version = "0.23.0", path = "../ecolor" } ab_glyph = "0.2.11" ahash = { version = "0.8.1", default-features = false, features = [ diff --git a/crates/epaint/src/image.rs b/crates/epaint/src/image.rs index dca86f38c47..0a40fe4a0c3 100644 --- a/crates/epaint/src/image.rs +++ b/crates/epaint/src/image.rs @@ -280,6 +280,7 @@ impl FontImage { /// `gamma` should normally be set to `None`. /// /// If you are having problems with text looking skinny and pixelated, try using a low gamma, e.g. `0.4`. + #[inline] pub fn srgba_pixels( &'_ self, gamma: Option, @@ -338,6 +339,7 @@ impl From for ImageData { } } +#[inline] fn fast_round(r: f32) -> u8 { (r + 0.5).floor() as _ // rust does a saturating cast since 1.45 } diff --git a/crates/epaint/src/mutex.rs b/crates/epaint/src/mutex.rs index 73b54ca898f..2c61f038f9c 100644 --- a/crates/epaint/src/mutex.rs +++ b/crates/epaint/src/mutex.rs @@ -387,14 +387,6 @@ mod tests { let _b = two.lock(); } - #[test] - #[should_panic] - fn lock_reentry_single_thread() { - let one = Mutex::new(()); - let _a = one.lock(); - let _a2 = one.lock(); // panics - } - #[test] fn lock_multiple_threads() { use std::sync::Arc; diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 8854a2e860e..cc4294f6cc1 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -498,6 +498,8 @@ pub struct RectShape { /// /// To display a texture, set [`Self::fill_texture_id`], /// and set this to `Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0))`. + /// + /// Use [`Rect::ZERO`] to turn off texturing. pub uv: Rect, } @@ -787,6 +789,8 @@ pub struct PaintCallbackInfo { /// Rect is the [-1, +1] of the Normalized Device Coordinates. /// /// Note than only a portion of this may be visible due to [`Self::clip_rect`]. + /// + /// This comes from [`PaintCallback::rect`]. pub viewport: Rect, /// Clip rectangle in points. @@ -819,7 +823,7 @@ pub struct ViewportInPixels { } impl PaintCallbackInfo { - fn points_to_pixels(&self, rect: &Rect) -> ViewportInPixels { + fn pixels_from_points(&self, rect: &Rect) -> ViewportInPixels { ViewportInPixels { left_px: rect.min.x * self.pixels_per_point, top_px: rect.min.y * self.pixels_per_point, @@ -831,12 +835,12 @@ impl PaintCallbackInfo { /// The viewport rectangle. This is what you would use in e.g. `glViewport`. pub fn viewport_in_pixels(&self) -> ViewportInPixels { - self.points_to_pixels(&self.viewport) + self.pixels_from_points(&self.viewport) } /// The "scissor" or "clip" rectangle. This is what you would use in e.g. `glScissor`. pub fn clip_rect_in_pixels(&self) -> ViewportInPixels { - self.points_to_pixels(&self.clip_rect) + self.pixels_from_points(&self.clip_rect) } } @@ -846,6 +850,8 @@ impl PaintCallbackInfo { #[derive(Clone)] pub struct PaintCallback { /// Where to paint. + /// + /// This will become [`PaintCallbackInfo::viewport`]. pub rect: Rect, /// Paint something custom (e.g. 3D stuff). diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 44300cab286..9771362ba37 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -535,6 +535,7 @@ pub mod path { 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 } } @@ -1063,7 +1064,7 @@ fn mul_color(color: Color32, factor: f32) -> Color32 { /// /// For performance reasons it is smart to reuse the same [`Tessellator`]. /// -/// Se also [`tessellate_shapes`], a convenient wrapper around [`Tessellator`]. +/// See also [`tessellate_shapes`], a convenient wrapper around [`Tessellator`]. pub struct Tessellator { pixels_per_point: f32, options: TessellationOptions, diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index e6b495c23a2..f33ff1cee1c 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -43,12 +43,6 @@ pub struct GlyphInfo { /// Unit: points. pub advance_width: f32, - /// `ascent` value from the font metrics. - /// this is the distance from the top to the baseline. - /// - /// Unit: points. - pub ascent: f32, - /// Texture coordinates. pub uv_rect: UvRect, } @@ -59,7 +53,6 @@ impl Default for GlyphInfo { Self { id: ab_glyph::GlyphId(0), advance_width: 0.0, - ascent: 0.0, uv_rect: Default::default(), } } @@ -79,7 +72,7 @@ pub struct FontImpl { height_in_points: f32, // move each character by this much (hack) - y_offset: f32, + y_offset_in_points: f32, ascent: f32, pixels_per_point: f32, @@ -118,22 +111,23 @@ impl FontImpl { scale_in_points * tweak.y_offset_factor } + tweak.y_offset; - // center scaled glyphs properly - let y_offset_points = y_offset_points + (tweak.scale - 1.0) * 0.5 * (ascent + descent); + // Center scaled glyphs properly: + let height = ascent + descent; + let y_offset_points = y_offset_points - (1.0 - tweak.scale) * 0.5 * height; // Round to an even number of physical pixels to get even kerning. // See https://github.com/emilk/egui/issues/382 let scale_in_pixels = scale_in_pixels.round() as u32; // Round to closest pixel: - let y_offset = (y_offset_points * pixels_per_point).round() / pixels_per_point; + let y_offset_in_points = (y_offset_points * pixels_per_point).round() / pixels_per_point; Self { name, ab_glyph_font, scale_in_pixels, height_in_points: ascent - descent + line_gap, - y_offset, + y_offset_in_points, ascent: ascent + baseline_offset, pixels_per_point, glyph_info_cache: Default::default(), @@ -188,7 +182,7 @@ impl FontImpl { if let Some(space) = self.glyph_info(' ') { let glyph_info = GlyphInfo { advance_width: crate::text::TAB_SIZE as f32 * space.advance_width, - ..GlyphInfo::default() + ..space }; self.glyph_info_cache.write().insert(c, glyph_info); return Some(glyph_info); @@ -205,7 +199,7 @@ impl FontImpl { let advance_width = f32::min(em / 6.0, space.advance_width * 0.5); let glyph_info = GlyphInfo { advance_width, - ..GlyphInfo::default() + ..space }; self.glyph_info_cache.write().insert(c, glyph_info); return Some(glyph_info); @@ -255,6 +249,14 @@ impl FontImpl { self.pixels_per_point } + /// This is the distance from the top to the baseline. + /// + /// Unit: points. + #[inline(always)] + pub fn ascent(&self) -> f32 { + self.ascent + } + fn allocate_glyph(&self, glyph_id: ab_glyph::GlyphId) -> GlyphInfo { assert!(glyph_id.0 != 0); use ab_glyph::{Font as _, ScaleFont}; @@ -282,7 +284,8 @@ impl FontImpl { }); let offset_in_pixels = vec2(bb.min.x, bb.min.y); - let offset = offset_in_pixels / self.pixels_per_point + self.y_offset * Vec2::Y; + let offset = + offset_in_pixels / self.pixels_per_point + self.y_offset_in_points * Vec2::Y; UvRect { offset, size: vec2(glyph_width as f32, glyph_height as f32) / self.pixels_per_point, @@ -305,7 +308,6 @@ impl FontImpl { GlyphInfo { id: glyph_id, advance_width: advance_width_in_points, - ascent: self.ascent, uv_rect, } } @@ -442,7 +444,7 @@ impl Font { } #[inline] - pub(crate) fn glyph_info_and_font_impl(&mut self, c: char) -> (Option<&FontImpl>, GlyphInfo) { + pub(crate) fn font_impl_and_glyph_info(&mut self, c: char) -> (Option<&FontImpl>, GlyphInfo) { if self.fonts.is_empty() { return (None, self.replacement_glyph.1); } diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 4bd101bf06f..9b055edc577 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::{Color32, Mesh, Stroke, Vertex}; +use crate::{text::font::Font, Color32, Mesh, Stroke, Vertex}; use super::{FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, Row, RowVisuals}; @@ -40,17 +40,31 @@ impl PointScale { // ---------------------------------------------------------------------------- /// Temporary storage before line-wrapping. -#[derive(Default, Clone)] +#[derive(Clone)] struct Paragraph { /// Start of the next glyph to be added. pub cursor_x: f32, + /// This is included in case there are no glyphs + pub section_index_at_start: u32, + pub glyphs: Vec, /// In case of an empty paragraph ("\n"), use this as height. pub empty_paragraph_height: f32, } +impl Paragraph { + pub fn from_section_index(section_index_at_start: u32) -> Self { + Self { + cursor_x: 0.0, + section_index_at_start, + glyphs: vec![], + empty_paragraph_height: 0.0, + } + } +} + /// Layout text into a [`Galley`]. /// /// In most cases you should use [`crate::Fonts::layout_job`] instead @@ -70,7 +84,9 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { }; } - let mut paragraphs = vec![Paragraph::default()]; + // For most of this we ignore the y coordinate: + + let mut paragraphs = vec![Paragraph::from_section_index(0)]; for (section_index, section) in job.sections.iter().enumerate() { layout_section(fonts, &job, section_index as u32, section, &mut paragraphs); } @@ -78,7 +94,12 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { let point_scale = PointScale::new(fonts.pixels_per_point()); let mut elided = false; - let mut rows = rows_from_paragraphs(fonts, paragraphs, &job, &mut elided); + let mut rows = rows_from_paragraphs(paragraphs, &job, &mut elided); + if elided { + if let Some(last_row) = rows.last_mut() { + replace_last_glyph_with_overflow_character(fonts, &job, last_row); + } + } let justify = job.justify && job.wrap.max_width.is_finite(); @@ -97,9 +118,11 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { } } + // Calculate the Y positions and tessellate the text: galley_from_rows(point_scale, job, rows, elided) } +// Ignores the Y coordinate. fn layout_section( fonts: &mut FontsImpl, job: &LayoutJob, @@ -130,11 +153,11 @@ fn layout_section( for chr in job.text[byte_range.clone()].chars() { if job.break_on_newline && chr == '\n' { - out_paragraphs.push(Paragraph::default()); + out_paragraphs.push(Paragraph::from_section_index(section_index)); paragraph = out_paragraphs.last_mut().unwrap(); paragraph.empty_paragraph_height = line_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs? } else { - let (font_impl, glyph_info) = font.glyph_info_and_font_impl(chr); + let (font_impl, glyph_info) = font.font_impl_and_glyph_info(chr); if let Some(font_impl) = font_impl { if let Some(last_glyph_id) = last_glyph_id { paragraph.cursor_x += font_impl.pair_kerning(last_glyph_id, glyph_info.id); @@ -146,7 +169,7 @@ fn layout_section( chr, pos: pos2(paragraph.cursor_x, f32::NAN), size: vec2(glyph_info.advance_width, line_height), - ascent: glyph_info.ascent, + ascent: font_impl.map_or(0.0, |font| font.ascent()), // Failure to find the font here would be weird uv_rect: glyph_info.uv_rect, section_index, }); @@ -163,8 +186,8 @@ fn rect_from_x_range(x_range: RangeInclusive) -> Rect { Rect::from_x_y_ranges(x_range, 0.0..=0.0) } +// Ignores the Y coordinate. fn rows_from_paragraphs( - fonts: &mut FontsImpl, paragraphs: Vec, job: &LayoutJob, elided: &mut bool, @@ -183,6 +206,7 @@ fn rows_from_paragraphs( if paragraph.glyphs.is_empty() { rows.push(Row { + section_index_at_start: paragraph.section_index_at_start, glyphs: vec![], visuals: Default::default(), rect: Rect::from_min_size( @@ -197,13 +221,14 @@ fn rows_from_paragraphs( // Early-out optimization: the whole paragraph fits on one row. let paragraph_min_x = paragraph.glyphs[0].pos.x; rows.push(Row { + section_index_at_start: paragraph.section_index_at_start, glyphs: paragraph.glyphs, visuals: Default::default(), rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), ends_with_newline: !is_last_paragraph, }); } else { - line_break(fonts, ¶graph, job, &mut rows, elided); + line_break(¶graph, job, &mut rows, elided); rows.last_mut().unwrap().ends_with_newline = !is_last_paragraph; } } @@ -212,13 +237,7 @@ fn rows_from_paragraphs( rows } -fn line_break( - fonts: &mut FontsImpl, - paragraph: &Paragraph, - job: &LayoutJob, - out_rows: &mut Vec, - elided: &mut bool, -) { +fn line_break(paragraph: &Paragraph, job: &LayoutJob, out_rows: &mut Vec, elided: &mut bool) { // Keeps track of good places to insert row break if we exceed `wrap_width`. let mut row_break_candidates = RowBreakCandidates::default(); @@ -227,13 +246,13 @@ fn line_break( let mut row_start_idx = 0; for i in 0..paragraph.glyphs.len() { - let potential_row_width = paragraph.glyphs[i].max_x() - row_start_x; - if job.wrap.max_rows <= out_rows.len() { *elided = true; break; } + let potential_row_width = paragraph.glyphs[i].max_x() - row_start_x; + if job.wrap.max_width < potential_row_width { // Row break: @@ -243,6 +262,7 @@ fn line_break( // Allow the first row to be completely empty, because we know there will be more space on the next row: // TODO(emilk): this records the height of this first row as zero, though that is probably fine since first_row_indentation usually comes with a first_row_min_height. out_rows.push(Row { + section_index_at_start: paragraph.section_index_at_start, glyphs: vec![], visuals: Default::default(), rect: rect_from_x_range(first_row_indentation..=first_row_indentation), @@ -261,10 +281,12 @@ fn line_break( }) .collect(); + let section_index_at_start = glyphs[0].section_index; let paragraph_min_x = glyphs[0].pos.x; let paragraph_max_x = glyphs.last().unwrap().max_x(); out_rows.push(Row { + section_index_at_start, glyphs, visuals: Default::default(), rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), @@ -287,10 +309,7 @@ fn line_break( // Final row of text: if job.wrap.max_rows <= out_rows.len() { - if let Some(last_row) = out_rows.last_mut() { - replace_last_glyph_with_overflow_character(fonts, job, last_row); - *elided = true; - } + *elided = true; // can't fit another row } else { let glyphs: Vec = paragraph.glyphs[row_start_idx..] .iter() @@ -301,10 +320,12 @@ fn line_break( }) .collect(); + let section_index_at_start = glyphs[0].section_index; let paragraph_min_x = glyphs[0].pos.x; let paragraph_max_x = glyphs.last().unwrap().max_x(); out_rows.push(Row { + section_index_at_start, glyphs, visuals: Default::default(), rect: rect_from_x_range(paragraph_min_x..=paragraph_max_x), @@ -315,77 +336,148 @@ fn line_break( } /// Trims the last glyphs in the row and replaces it with an overflow character (e.g. `…`). +/// +/// Called before we have any Y coordinates. fn replace_last_glyph_with_overflow_character( fonts: &mut FontsImpl, job: &LayoutJob, row: &mut Row, ) { + fn row_width(row: &Row) -> f32 { + if let (Some(first), Some(last)) = (row.glyphs.first(), row.glyphs.last()) { + last.max_x() - first.pos.x + } else { + 0.0 + } + } + + fn row_height(section: &LayoutSection, font: &Font) -> f32 { + section + .format + .line_height + .unwrap_or_else(|| font.row_height()) + } + let Some(overflow_character) = job.wrap.overflow_character else { return; }; + // We always try to just append the character first: + if let Some(last_glyph) = row.glyphs.last() { + let section_index = last_glyph.section_index; + let section = &job.sections[section_index as usize]; + let font = fonts.font(§ion.format.font_id); + let line_height = row_height(section, font); + + let (_, last_glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr); + + let mut x = last_glyph.pos.x + last_glyph.size.x; + + let (font_impl, replacement_glyph_info) = font.font_impl_and_glyph_info(overflow_character); + + { + // Kerning: + x += section.format.extra_letter_spacing; + if let Some(font_impl) = font_impl { + x += font_impl.pair_kerning(last_glyph_info.id, replacement_glyph_info.id); + } + } + + row.glyphs.push(Glyph { + chr: overflow_character, + pos: pos2(x, f32::NAN), + size: vec2(replacement_glyph_info.advance_width, line_height), + ascent: font_impl.map_or(0.0, |font| font.ascent()), // Failure to find the font here would be weird + uv_rect: replacement_glyph_info.uv_rect, + section_index, + }); + } else { + let section_index = row.section_index_at_start; + let section = &job.sections[section_index as usize]; + let font = fonts.font(§ion.format.font_id); + let line_height = row_height(section, font); + + let x = 0.0; // TODO(emilk): heed paragraph leading_space 😬 + + let (font_impl, replacement_glyph_info) = font.font_impl_and_glyph_info(overflow_character); + + row.glyphs.push(Glyph { + chr: overflow_character, + pos: pos2(x, f32::NAN), + size: vec2(replacement_glyph_info.advance_width, line_height), + ascent: font_impl.map_or(0.0, |font| font.ascent()), // Failure to find the font here would be weird + uv_rect: replacement_glyph_info.uv_rect, + section_index, + }); + } + + if row_width(row) <= job.wrap.max_width || row.glyphs.len() == 1 { + return; // we are done + } + + // We didn't fit it. Remove it again… + row.glyphs.pop(); + + // …then go into a loop where we replace the last character with the overflow character + // until we fit within the max_width: + loop { let (prev_glyph, last_glyph) = match row.glyphs.as_mut_slice() { [.., prev, last] => (Some(prev), last), [.., last] => (None, last), - _ => break, + _ => { + unreachable!("We've already explicitly handled the empty row"); + } }; let section = &job.sections[last_glyph.section_index as usize]; let extra_letter_spacing = section.format.extra_letter_spacing; let font = fonts.font(§ion.format.font_id); - let line_height = section - .format - .line_height - .unwrap_or_else(|| font.row_height()); + let line_height = row_height(section, font); - let prev_glyph_id = prev_glyph.map(|prev_glyph| { - let (_, prev_glyph_info) = font.glyph_info_and_font_impl(prev_glyph.chr); - prev_glyph_info.id - }); + if let Some(prev_glyph) = prev_glyph { + let prev_glyph_id = font.font_impl_and_glyph_info(prev_glyph.chr).1.id; - // undo kerning with previous glyph - let (font_impl, glyph_info) = font.glyph_info_and_font_impl(last_glyph.chr); - last_glyph.pos.x -= extra_letter_spacing - + font_impl - .zip(prev_glyph_id) - .map(|(font_impl, prev_glyph_id)| { - font_impl.pair_kerning(prev_glyph_id, glyph_info.id) - }) - .unwrap_or_default(); - - // replace the glyph - last_glyph.chr = overflow_character; - let (font_impl, glyph_info) = font.glyph_info_and_font_impl(last_glyph.chr); - last_glyph.size = vec2(glyph_info.advance_width, line_height); - last_glyph.uv_rect = glyph_info.uv_rect; - last_glyph.ascent = glyph_info.ascent; - - // reapply kerning - last_glyph.pos.x += extra_letter_spacing - + font_impl - .zip(prev_glyph_id) - .map(|(font_impl, prev_glyph_id)| { - font_impl.pair_kerning(prev_glyph_id, glyph_info.id) - }) - .unwrap_or_default(); + // Undo kerning with previous glyph: + let (font_impl, glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr); + last_glyph.pos.x -= extra_letter_spacing; + if let Some(font_impl) = font_impl { + last_glyph.pos.x -= font_impl.pair_kerning(prev_glyph_id, glyph_info.id); + } - row.rect.max.x = last_glyph.max_x(); + // Replace the glyph: + last_glyph.chr = overflow_character; + let (font_impl, glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr); + last_glyph.size = vec2(glyph_info.advance_width, line_height); + last_glyph.uv_rect = glyph_info.uv_rect; - // check if we're within width budget - let row_end_x = last_glyph.max_x(); - let row_start_x = row.glyphs.first().unwrap().pos.x; // if `last_mut()` returned `Some`, then so will `first()` - let row_width = row_end_x - row_start_x; - if row_width <= job.wrap.max_width { - return; // we are done - } + // Reapply kerning: + last_glyph.pos.x += extra_letter_spacing; + if let Some(font_impl) = font_impl { + last_glyph.pos.x += font_impl.pair_kerning(prev_glyph_id, glyph_info.id); + } - row.glyphs.pop(); - } + // Check if we're within width budget: + if row_width(row) <= job.wrap.max_width || row.glyphs.len() == 1 { + return; // We are done + } - // We failed to insert `overflow_character` without exceeding `wrap_width`. + // We didn't fit - pop the last glyph and try again. + row.glyphs.pop(); + } else { + // Just replace and be done with it. + last_glyph.chr = overflow_character; + let (_, glyph_info) = font.font_impl_and_glyph_info(last_glyph.chr); + last_glyph.size = vec2(glyph_info.advance_width, line_height); + last_glyph.uv_rect = glyph_info.uv_rect; + return; + } + } } +/// Horizontally aligned the text on a row. +/// +/// /// Ignores the Y coordinate. fn halign_and_justify_row( point_scale: PointScale, row: &mut Row, @@ -573,7 +665,7 @@ fn tessellate_row( point_scale: PointScale, job: &LayoutJob, format_summary: &FormatSummary, - row: &mut Row, + row: &Row, ) -> RowVisuals { if row.glyphs.is_empty() { return Default::default(); @@ -880,49 +972,93 @@ fn is_cjk_break_allowed(c: char) -> bool { // ---------------------------------------------------------------------------- -#[test] -fn test_zero_max_width() { - let mut fonts = FontsImpl::new(1.0, 1024, super::FontDefinitions::default()); - let mut layout_job = LayoutJob::single_section("W".into(), super::TextFormat::default()); - layout_job.wrap.max_width = 0.0; - let galley = super::layout(&mut fonts, layout_job.into()); - assert_eq!(galley.rows.len(), 1); -} +#[cfg(test)] +mod tests { + use super::{super::*, *}; + + #[test] + fn test_zero_max_width() { + let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default()); + let mut layout_job = LayoutJob::single_section("W".into(), TextFormat::default()); + layout_job.wrap.max_width = 0.0; + let galley = layout(&mut fonts, layout_job.into()); + assert_eq!(galley.rows.len(), 1); + } -#[test] -fn test_cjk() { - let mut fonts = FontsImpl::new(1.0, 1024, super::FontDefinitions::default()); - let mut layout_job = LayoutJob::single_section( - "日本語とEnglishの混在した文章".into(), - super::TextFormat::default(), - ); - layout_job.wrap.max_width = 90.0; - let galley = super::layout(&mut fonts, layout_job.into()); - assert_eq!( - galley - .rows - .iter() - .map(|row| row.glyphs.iter().map(|g| g.chr).collect::()) - .collect::>(), - vec!["日本語と", "Englishの混在", "した文章"] - ); -} + #[test] + fn test_truncate_with_newline() { + // No matter where we wrap, we should be appending the newline character. -#[test] -fn test_pre_cjk() { - let mut fonts = FontsImpl::new(1.0, 1024, super::FontDefinitions::default()); - let mut layout_job = LayoutJob::single_section( - "日本語とEnglishの混在した文章".into(), - super::TextFormat::default(), - ); - layout_job.wrap.max_width = 110.0; - let galley = super::layout(&mut fonts, layout_job.into()); - assert_eq!( - galley - .rows - .iter() - .map(|row| row.glyphs.iter().map(|g| g.chr).collect::()) - .collect::>(), - vec!["日本語とEnglish", "の混在した文章"] - ); + let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default()); + let text_format = TextFormat { + font_id: FontId::monospace(12.0), + ..Default::default() + }; + + for text in ["Hello\nworld", "\nfoo"] { + for break_anywhere in [false, true] { + for max_width in [0.0, 5.0, 10.0, 20.0, f32::INFINITY] { + let mut layout_job = + LayoutJob::single_section(text.into(), text_format.clone()); + layout_job.wrap.max_width = max_width; + layout_job.wrap.max_rows = 1; + layout_job.wrap.break_anywhere = break_anywhere; + + let galley = layout(&mut fonts, layout_job.into()); + + assert!(galley.elided); + assert_eq!(galley.rows.len(), 1); + let row_text = galley.rows[0].text(); + assert!( + row_text.ends_with('…'), + "Expected row to end with `…`, got {row_text:?} when line-breaking the text {text:?} with max_width {max_width} and break_anywhere {break_anywhere}.", + ); + } + } + } + + { + let mut layout_job = LayoutJob::single_section("Hello\nworld".into(), text_format); + layout_job.wrap.max_width = 50.0; + layout_job.wrap.max_rows = 1; + layout_job.wrap.break_anywhere = false; + + let galley = layout(&mut fonts, layout_job.into()); + + assert!(galley.elided); + assert_eq!(galley.rows.len(), 1); + let row_text = galley.rows[0].text(); + assert_eq!(row_text, "Hello…"); + } + } + + #[test] + fn test_cjk() { + let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default()); + let mut layout_job = LayoutJob::single_section( + "日本語とEnglishの混在した文章".into(), + TextFormat::default(), + ); + layout_job.wrap.max_width = 90.0; + let galley = layout(&mut fonts, layout_job.into()); + assert_eq!( + galley.rows.iter().map(|row| row.text()).collect::>(), + vec!["日本語と", "Englishの混在", "した文章"] + ); + } + + #[test] + fn test_pre_cjk() { + let mut fonts = FontsImpl::new(1.0, 1024, FontDefinitions::default()); + let mut layout_job = LayoutJob::single_section( + "日本語とEnglishの混在した文章".into(), + TextFormat::default(), + ); + layout_job.wrap.max_width = 110.0; + let galley = layout(&mut fonts, layout_job.into()); + assert_eq!( + galley.rows.iter().map(|row| row.text()).collect::>(), + vec!["日本語とEnglish", "の混在した文章"] + ); + } } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 12c625ccd34..7b81cd82440 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -394,7 +394,7 @@ impl Default for TextWrapping { } impl TextWrapping { - /// A row can be as long as it need to be + /// A row can be as long as it need to be. pub fn no_max_width() -> Self { Self { max_width: f32::INFINITY, @@ -402,8 +402,8 @@ impl TextWrapping { } } - /// Elide text that doesn't fit within the given width. - pub fn elide_at_width(max_width: f32) -> Self { + /// Elide text that doesn't fit within the given width, replaced with `…`. + pub fn truncate_at_width(max_width: f32) -> Self { Self { max_width, max_rows: 1, @@ -475,6 +475,9 @@ pub struct Galley { #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Row { + /// This is included in case there are no glyphs + pub section_index_at_start: u32, + /// One for each `char`. pub glyphs: Vec, @@ -561,6 +564,11 @@ impl Glyph { // ---------------------------------------------------------------------------- impl Row { + /// The text on this row, excluding the implicit `\n` if any. + pub fn text(&self) -> String { + self.glyphs.iter().map(|g| g.chr).collect() + } + /// Excludes the implicit `\n` after the [`Row`], if any. #[inline] pub fn char_count_excluding_newline(&self) -> usize { diff --git a/crates/epaint/src/texture_handle.rs b/crates/epaint/src/texture_handle.rs index 24d7179b9ae..f4142d91510 100644 --- a/crates/epaint/src/texture_handle.rs +++ b/crates/epaint/src/texture_handle.rs @@ -86,7 +86,10 @@ impl TextureHandle { /// width x height pub fn size(&self) -> [usize; 2] { - self.tex_mngr.read().meta(self.id).unwrap().size + self.tex_mngr + .read() + .meta(self.id) + .map_or([0, 0], |tex| tex.size) } /// width x height @@ -97,7 +100,10 @@ impl TextureHandle { /// `width x height x bytes_per_pixel` pub fn byte_size(&self) -> usize { - self.tex_mngr.read().meta(self.id).unwrap().bytes_used() + self.tex_mngr + .read() + .meta(self.id) + .map_or(0, |tex| tex.bytes_used()) } /// width / height @@ -108,7 +114,10 @@ impl TextureHandle { /// Debug-name. pub fn name(&self) -> String { - self.tex_mngr.read().meta(self.id).unwrap().name.clone() + self.tex_mngr + .read() + .meta(self.id) + .map_or_else(|| "".to_owned(), |tex| tex.name.clone()) } } diff --git a/crates/epaint/src/util/ordered_float.rs b/crates/epaint/src/util/ordered_float.rs index 1bef308cd1c..0b305ca1293 100644 --- a/crates/epaint/src/util/ordered_float.rs +++ b/crates/epaint/src/util/ordered_float.rs @@ -8,8 +8,16 @@ use std::hash::{Hash, Hasher}; /// Possible types for `T` are `f32` and `f64`. /// /// See also [`FloatOrd`]. +#[derive(Clone, Copy)] pub struct OrderedFloat(T); +impl OrderedFloat { + #[inline] + pub fn into_inner(self) -> T { + self.0 + } +} + impl Eq for OrderedFloat {} impl PartialEq for OrderedFloat { diff --git a/deny.toml b/deny.toml index 3c1f21d8cdb..5bca6c829d0 100644 --- a/deny.toml +++ b/deny.toml @@ -35,7 +35,9 @@ deny = [ skip = [ { name = "arrayvec" }, # old version via tiny-skiaz + { name = "base64" }, # small crate, old version from usvg { 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 = "nix" }, # old version via winit { name = "redox_syscall" }, # old version via winit { name = "time" }, # old version pulled in by unmaintianed crate 'chrono' diff --git a/docs/egui_demo_app.js b/docs/egui_demo_app.js index 1ff9caa9cc4..30d8de5dcbf 100644 --- a/docs/egui_demo_app.js +++ b/docs/egui_demo_app.js @@ -75,14 +75,14 @@ function passStringToWasm0(arg, malloc, realloc) { if (realloc === undefined) { const buf = cachedTextEncoder.encode(arg); - const ptr = malloc(buf.length) >>> 0; + const ptr = malloc(buf.length, 1) >>> 0; getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); WASM_VECTOR_LEN = buf.length; return ptr; } let len = arg.length; - let ptr = malloc(len) >>> 0; + let ptr = malloc(len, 1) >>> 0; const mem = getUint8Memory0(); @@ -98,7 +98,7 @@ function passStringToWasm0(arg, malloc, realloc) { if (offset !== 0) { arg = arg.slice(offset); } - ptr = realloc(ptr, len, len = offset + arg.length * 3) >>> 0; + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; const view = getUint8Memory0().subarray(ptr + offset, ptr + len); const ret = encodeString(arg, view); @@ -220,10 +220,10 @@ function makeMutClosure(arg0, arg1, dtor, f) { return real; } -function __wbg_adapter_28(arg0, arg1) { +function __wbg_adapter_30(arg0, arg1) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb0203c3310c02e85(retptr, arg0, arg1); + wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hd926101060f0783e(retptr, arg0, arg1); var r0 = getInt32Memory0()[retptr / 4 + 0]; var r1 = getInt32Memory0()[retptr / 4 + 1]; if (r1) { @@ -234,12 +234,12 @@ function __wbg_adapter_28(arg0, arg1) { } } -function __wbg_adapter_31(arg0, arg1) { - wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h2a3101d1095ab00a(arg0, arg1); +function __wbg_adapter_33(arg0, arg1) { + wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h5e12b61ce76cf0a6(arg0, arg1); } -function __wbg_adapter_34(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h4d1e0c1db4f0e0bb(arg0, arg1, addHeapObject(arg2)); +function __wbg_adapter_36(arg0, arg1, arg2) { + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h2000f7e9add97f30(arg0, arg1, addHeapObject(arg2)); } function makeClosure(arg0, arg1, dtor, f) { @@ -263,12 +263,12 @@ function makeClosure(arg0, arg1, dtor, f) { return real; } -function __wbg_adapter_37(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hbc77240210741db8(arg0, arg1, addHeapObject(arg2)); +function __wbg_adapter_39(arg0, arg1, arg2) { + wasm._dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h5304a2544517cb21(arg0, arg1, addHeapObject(arg2)); } -function __wbg_adapter_42(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h19a82ca01a9b327d(arg0, arg1, addHeapObject(arg2)); +function __wbg_adapter_44(arg0, arg1, arg2) { + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h033aeb86989a3bc1(arg0, arg1, addHeapObject(arg2)); } function handleError(f, args) { @@ -278,8 +278,8 @@ function handleError(f, args) { wasm.__wbindgen_exn_store(addHeapObject(e)); } } -function __wbg_adapter_580(arg0, arg1, arg2, arg3) { - wasm.wasm_bindgen__convert__closures__invoke2_mut__h4169691c0456ed25(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); +function __wbg_adapter_590(arg0, arg1, arg2, arg3) { + wasm.wasm_bindgen__convert__closures__invoke2_mut__h5948535385382d86(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); } /** @@ -418,9 +418,6 @@ async function __wbg_load(module, imports) { function __wbg_get_imports() { const imports = {}; imports.wbg = {}; - imports.wbg.__wbindgen_object_drop_ref = function(arg0) { - takeObject(arg0); - }; imports.wbg.__wbindgen_cb_drop = function(arg0) { const obj = takeObject(arg0).original; if (obj.cnt-- == 1) { @@ -430,6 +427,9 @@ function __wbg_get_imports() { const ret = false; return ret; }; + imports.wbg.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); + }; imports.wbg.__wbindgen_string_new = function(arg0, arg1) { const ret = getStringFromWasm0(arg0, arg1); return addHeapObject(ret); @@ -442,13 +442,11 @@ function __wbg_get_imports() { getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; }; - imports.wbg.__wbindgen_number_get = function(arg0, arg1) { - const obj = getObject(arg1); - const ret = typeof(obj) === 'number' ? obj : undefined; - getFloat64Memory0()[arg0 / 8 + 1] = isLikeNone(ret) ? 0 : ret; - getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret); + imports.wbg.__wbindgen_number_new = function(arg0) { + const ret = arg0; + return addHeapObject(ret); }; - imports.wbg.__wbg_error_e62b64b85c2bc545 = function(arg0, arg1) { + imports.wbg.__wbg_error_e38422e56bbd072c = function(arg0, arg1) { let deferred0_0; let deferred0_1; try { @@ -456,42 +454,48 @@ function __wbg_get_imports() { deferred0_1 = arg1; console.error(getStringFromWasm0(arg0, arg1)); } finally { - wasm.__wbindgen_free(deferred0_0, deferred0_1); + wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); } }; - imports.wbg.__wbg_new_40620131643ca1cf = function() { + imports.wbg.__wbg_new_e7fbaa407e13d590 = function() { const ret = new Error(); return addHeapObject(ret); }; - imports.wbg.__wbg_stack_e3e173f66e044a69 = function(arg0, arg1) { + imports.wbg.__wbg_stack_21698d2a5852e13e = function(arg0, arg1) { const ret = getObject(arg1).stack; const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len1 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; }; - imports.wbg.__wbg_trace_8d8f961bba04f90a = function(arg0, arg1) { - console.trace(getStringFromWasm0(arg0, arg1)); + imports.wbg.__wbindgen_object_clone_ref = function(arg0) { + const ret = getObject(arg0); + return addHeapObject(ret); }; - imports.wbg.__wbg_debug_7fc527041707e16e = function(arg0, arg1) { - console.debug(getStringFromWasm0(arg0, arg1)); + imports.wbg.__wbg_warn_a95766347500bf17 = function(arg0, arg1) { + console.warn(getStringFromWasm0(arg0, arg1)); }; - imports.wbg.__wbg_info_e495fa41540a3eaa = function(arg0, arg1) { + imports.wbg.__wbg_info_7589c99c14cdc5ef = function(arg0, arg1) { console.info(getStringFromWasm0(arg0, arg1)); }; - imports.wbg.__wbg_warn_8b4e19d4032139f0 = function(arg0, arg1) { - console.warn(getStringFromWasm0(arg0, arg1)); + imports.wbg.__wbg_debug_97244d3ec765cb44 = function(arg0, arg1) { + console.debug(getStringFromWasm0(arg0, arg1)); }; - imports.wbg.__wbindgen_object_clone_ref = function(arg0) { - const ret = getObject(arg0); - return addHeapObject(ret); + imports.wbg.__wbg_trace_71659133a99ab3e3 = function(arg0, arg1) { + console.trace(getStringFromWasm0(arg0, arg1)); + }; + imports.wbg.__wbindgen_number_get = function(arg0, arg1) { + const obj = getObject(arg1); + const ret = typeof(obj) === 'number' ? obj : undefined; + getFloat64Memory0()[arg0 / 8 + 1] = isLikeNone(ret) ? 0 : ret; + getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret); }; imports.wbg.__wbindgen_boolean_get = function(arg0) { const v = getObject(arg0); const ret = typeof(v) === 'boolean' ? (v ? 1 : 0) : 2; return ret; }; - imports.wbg.__wbg_instanceof_WebGl2RenderingContext_a9b8e563e17071fe = function(arg0) { + imports.wbg.__wbg_instanceof_WebGl2RenderingContext_f921526c513bf717 = function(arg0) { let result; try { result = getObject(arg0) instanceof WebGL2RenderingContext; @@ -501,187 +505,187 @@ function __wbg_get_imports() { const ret = result; return ret; }; - imports.wbg.__wbg_bindVertexArray_ff82138c68ab11e2 = function(arg0, arg1) { + imports.wbg.__wbg_bindVertexArray_8863a216d7b0a339 = function(arg0, arg1) { getObject(arg0).bindVertexArray(getObject(arg1)); }; - imports.wbg.__wbg_bufferData_bc2f1a27f7162655 = function(arg0, arg1, arg2, arg3) { + imports.wbg.__wbg_bufferData_21334671c4ba6004 = function(arg0, arg1, arg2, arg3) { getObject(arg0).bufferData(arg1 >>> 0, getObject(arg2), arg3 >>> 0); }; - imports.wbg.__wbg_createVertexArray_c3ea33e844216f0c = function(arg0) { + imports.wbg.__wbg_createVertexArray_51d51e1e1e13e9f6 = function(arg0) { const ret = getObject(arg0).createVertexArray(); return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_deleteVertexArray_6cac5e6d920ec62c = function(arg0, arg1) { + imports.wbg.__wbg_deleteVertexArray_3e4f2e2ff7f05a19 = function(arg0, arg1) { getObject(arg0).deleteVertexArray(getObject(arg1)); }; - imports.wbg.__wbg_texImage2D_dd4aced57a17360f = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { + imports.wbg.__wbg_texImage2D_07240affd06971e9 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { getObject(arg0).texImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7 >>> 0, arg8 >>> 0, getObject(arg9)); }, arguments) }; - imports.wbg.__wbg_texSubImage2D_c3c012dc814eb4de = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { + imports.wbg.__wbg_texSubImage2D_d2841ded12a8aa66 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { getObject(arg0).texSubImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7 >>> 0, arg8 >>> 0, getObject(arg9)); }, arguments) }; - imports.wbg.__wbg_texSubImage2D_6cc58218d4d2218e = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { + imports.wbg.__wbg_texSubImage2D_bccf4e250f1ce1b8 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { getObject(arg0).texSubImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7 >>> 0, arg8 >>> 0, arg9); }, arguments) }; - imports.wbg.__wbg_activeTexture_0c3957272c193058 = function(arg0, arg1) { + imports.wbg.__wbg_activeTexture_799bf1387e911c27 = function(arg0, arg1) { getObject(arg0).activeTexture(arg1 >>> 0); }; - imports.wbg.__wbg_attachShader_cda29f0482c65440 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_attachShader_47256b6b3d42a22e = function(arg0, arg1, arg2) { getObject(arg0).attachShader(getObject(arg1), getObject(arg2)); }; - imports.wbg.__wbg_bindBuffer_6f9a2fa9ebc65b01 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_bindBuffer_24f6010e273fa400 = function(arg0, arg1, arg2) { getObject(arg0).bindBuffer(arg1 >>> 0, getObject(arg2)); }; - imports.wbg.__wbg_bindTexture_10219c0f804bff90 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_bindTexture_92d6d7f8bff9531e = function(arg0, arg1, arg2) { getObject(arg0).bindTexture(arg1 >>> 0, getObject(arg2)); }; - imports.wbg.__wbg_blendEquationSeparate_02dd5ec6a2c24f28 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_blendEquationSeparate_205526dad772d160 = function(arg0, arg1, arg2) { getObject(arg0).blendEquationSeparate(arg1 >>> 0, arg2 >>> 0); }; - imports.wbg.__wbg_blendFuncSeparate_f9dccffef5a98f44 = function(arg0, arg1, arg2, arg3, arg4) { + imports.wbg.__wbg_blendFuncSeparate_fbf93dee3e5ce456 = function(arg0, arg1, arg2, arg3, arg4) { getObject(arg0).blendFuncSeparate(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0, arg4 >>> 0); }; - imports.wbg.__wbg_clear_15c6565459b2686d = function(arg0, arg1) { + imports.wbg.__wbg_clear_2db2efe323bfdf68 = function(arg0, arg1) { getObject(arg0).clear(arg1 >>> 0); }; - imports.wbg.__wbg_clearColor_116005afbb6df00f = function(arg0, arg1, arg2, arg3, arg4) { + imports.wbg.__wbg_clearColor_7a7d04702f7e38e5 = function(arg0, arg1, arg2, arg3, arg4) { getObject(arg0).clearColor(arg1, arg2, arg3, arg4); }; - imports.wbg.__wbg_colorMask_1dfb9a91ae2b9e71 = function(arg0, arg1, arg2, arg3, arg4) { + imports.wbg.__wbg_colorMask_fba1e2efd891e2ac = function(arg0, arg1, arg2, arg3, arg4) { getObject(arg0).colorMask(arg1 !== 0, arg2 !== 0, arg3 !== 0, arg4 !== 0); }; - imports.wbg.__wbg_compileShader_6f505d659e2795e6 = function(arg0, arg1) { + imports.wbg.__wbg_compileShader_6bf78b425d5c98e1 = function(arg0, arg1) { getObject(arg0).compileShader(getObject(arg1)); }; - imports.wbg.__wbg_createBuffer_0da7eb27184081a8 = function(arg0) { + imports.wbg.__wbg_createBuffer_323425af422748ac = function(arg0) { const ret = getObject(arg0).createBuffer(); return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_createProgram_535e1a7a84baa7ff = function(arg0) { + imports.wbg.__wbg_createProgram_4eaf3b97b5747a62 = function(arg0) { const ret = getObject(arg0).createProgram(); return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_createShader_b1a69c91a9abbcf9 = function(arg0, arg1) { + imports.wbg.__wbg_createShader_429776c9dd6fb87b = function(arg0, arg1) { const ret = getObject(arg0).createShader(arg1 >>> 0); return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_createTexture_f999863e1ff4544e = function(arg0) { + imports.wbg.__wbg_createTexture_1bf4d6fec570124b = function(arg0) { const ret = getObject(arg0).createTexture(); return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_deleteBuffer_d6466e666563f36a = function(arg0, arg1) { + imports.wbg.__wbg_deleteBuffer_2c09d03fa4b0bd08 = function(arg0, arg1) { getObject(arg0).deleteBuffer(getObject(arg1)); }; - imports.wbg.__wbg_deleteProgram_c6b502ab111ca429 = function(arg0, arg1) { + imports.wbg.__wbg_deleteProgram_53a32852f245b839 = function(arg0, arg1) { getObject(arg0).deleteProgram(getObject(arg1)); }; - imports.wbg.__wbg_deleteShader_cbb128151b8b2b6b = function(arg0, arg1) { + imports.wbg.__wbg_deleteShader_7c1222349324b5e2 = function(arg0, arg1) { getObject(arg0).deleteShader(getObject(arg1)); }; - imports.wbg.__wbg_deleteTexture_43dcaa158a7a967b = function(arg0, arg1) { + imports.wbg.__wbg_deleteTexture_4fcfea73cd8f6214 = function(arg0, arg1) { getObject(arg0).deleteTexture(getObject(arg1)); }; - imports.wbg.__wbg_detachShader_10a74e43565974e2 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_detachShader_04abccd441871232 = function(arg0, arg1, arg2) { getObject(arg0).detachShader(getObject(arg1), getObject(arg2)); }; - imports.wbg.__wbg_disable_94431bed03515efb = function(arg0, arg1) { + imports.wbg.__wbg_disable_e02106ca6c7002d6 = function(arg0, arg1) { getObject(arg0).disable(arg1 >>> 0); }; - imports.wbg.__wbg_disableVertexAttribArray_6f95b891552695e8 = function(arg0, arg1) { + imports.wbg.__wbg_disableVertexAttribArray_6d57776c8f642f44 = function(arg0, arg1) { getObject(arg0).disableVertexAttribArray(arg1 >>> 0); }; - imports.wbg.__wbg_drawArrays_77814548b9e573f2 = function(arg0, arg1, arg2, arg3) { + imports.wbg.__wbg_drawArrays_c91ce3f736bf1f2a = function(arg0, arg1, arg2, arg3) { getObject(arg0).drawArrays(arg1 >>> 0, arg2, arg3); }; - imports.wbg.__wbg_drawElements_9eb2a38d09666d1f = function(arg0, arg1, arg2, arg3, arg4) { + imports.wbg.__wbg_drawElements_a9529eefaf2008bd = function(arg0, arg1, arg2, arg3, arg4) { getObject(arg0).drawElements(arg1 >>> 0, arg2, arg3 >>> 0, arg4); }; - imports.wbg.__wbg_enable_b36ff5159bc88f3d = function(arg0, arg1) { + imports.wbg.__wbg_enable_195891416c520019 = function(arg0, arg1) { getObject(arg0).enable(arg1 >>> 0); }; - imports.wbg.__wbg_enableVertexAttribArray_7e45a67bd47ec1bc = function(arg0, arg1) { + imports.wbg.__wbg_enableVertexAttribArray_8804480c2ea0bb72 = function(arg0, arg1) { getObject(arg0).enableVertexAttribArray(arg1 >>> 0); }; - imports.wbg.__wbg_getAttribLocation_9fb8d1fcf1e79c7d = function(arg0, arg1, arg2, arg3) { + imports.wbg.__wbg_getAttribLocation_7dbdbad935433494 = function(arg0, arg1, arg2, arg3) { const ret = getObject(arg0).getAttribLocation(getObject(arg1), getStringFromWasm0(arg2, arg3)); return ret; }; - imports.wbg.__wbg_getError_1a05a504dbd2417e = function(arg0) { + imports.wbg.__wbg_getError_7191ad6ea53607fe = function(arg0) { const ret = getObject(arg0).getError(); return ret; }; - imports.wbg.__wbg_getExtension_6b00e2c6c766b6cb = function() { return handleError(function (arg0, arg1, arg2) { + imports.wbg.__wbg_getExtension_77909f6d51d49d4d = function() { return handleError(function (arg0, arg1, arg2) { const ret = getObject(arg0).getExtension(getStringFromWasm0(arg1, arg2)); return isLikeNone(ret) ? 0 : addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_getParameter_9e1070be2e213377 = function() { return handleError(function (arg0, arg1) { + imports.wbg.__wbg_getParameter_55b36a787dbbfb74 = function() { return handleError(function (arg0, arg1) { const ret = getObject(arg0).getParameter(arg1 >>> 0); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_getProgramInfoLog_03d7941c48fa9179 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_getProgramInfoLog_b81bc53188e286fa = function(arg0, arg1, arg2) { const ret = getObject(arg1).getProgramInfoLog(getObject(arg2)); var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); var len1 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; }; - imports.wbg.__wbg_getProgramParameter_dd171792e4ba3184 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_getProgramParameter_35522a0bfdfaad27 = function(arg0, arg1, arg2) { const ret = getObject(arg0).getProgramParameter(getObject(arg1), arg2 >>> 0); return addHeapObject(ret); }; - imports.wbg.__wbg_getShaderInfoLog_c1cca646bf94aa17 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_getShaderInfoLog_968b93e75477d725 = function(arg0, arg1, arg2) { const ret = getObject(arg1).getShaderInfoLog(getObject(arg2)); var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); var len1 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; }; - imports.wbg.__wbg_getShaderParameter_c1d89b570b67be37 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_getShaderParameter_ac2727ae4fe7648e = function(arg0, arg1, arg2) { const ret = getObject(arg0).getShaderParameter(getObject(arg1), arg2 >>> 0); return addHeapObject(ret); }; - imports.wbg.__wbg_getSupportedExtensions_675e4373f0fe08ca = function(arg0) { + imports.wbg.__wbg_getSupportedExtensions_fafc31aab913037d = function(arg0) { const ret = getObject(arg0).getSupportedExtensions(); return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_getUniformLocation_984bcb57f0539335 = function(arg0, arg1, arg2, arg3) { + imports.wbg.__wbg_getUniformLocation_9f6eb60c560a347b = function(arg0, arg1, arg2, arg3) { const ret = getObject(arg0).getUniformLocation(getObject(arg1), getStringFromWasm0(arg2, arg3)); return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_linkProgram_0a25df9d4086c8c9 = function(arg0, arg1) { + imports.wbg.__wbg_linkProgram_33998194075d71fb = function(arg0, arg1) { getObject(arg0).linkProgram(getObject(arg1)); }; - imports.wbg.__wbg_pixelStorei_2a2698776f2da87a = function(arg0, arg1, arg2) { + imports.wbg.__wbg_pixelStorei_f3a24990aa352fc7 = function(arg0, arg1, arg2) { getObject(arg0).pixelStorei(arg1 >>> 0, arg2); }; - imports.wbg.__wbg_scissor_5bbf5da585fcd6cd = function(arg0, arg1, arg2, arg3, arg4) { + imports.wbg.__wbg_scissor_e8e41e1c0a9817c8 = function(arg0, arg1, arg2, arg3, arg4) { getObject(arg0).scissor(arg1, arg2, arg3, arg4); }; - imports.wbg.__wbg_shaderSource_5c55ce208ee2dc38 = function(arg0, arg1, arg2, arg3) { + imports.wbg.__wbg_shaderSource_1cb7c64dc7d1a500 = function(arg0, arg1, arg2, arg3) { getObject(arg0).shaderSource(getObject(arg1), getStringFromWasm0(arg2, arg3)); }; - imports.wbg.__wbg_texParameteri_05700ca575d5f41d = function(arg0, arg1, arg2, arg3) { + imports.wbg.__wbg_texParameteri_85dad939f62a15aa = function(arg0, arg1, arg2, arg3) { getObject(arg0).texParameteri(arg1 >>> 0, arg2 >>> 0, arg3); }; - imports.wbg.__wbg_uniform1f_51ae9c9d19ab2a9e = function(arg0, arg1, arg2) { + imports.wbg.__wbg_uniform1f_88379f4e2630bc66 = function(arg0, arg1, arg2) { getObject(arg0).uniform1f(getObject(arg1), arg2); }; - imports.wbg.__wbg_uniform1i_ef0ff3d67b59f4de = function(arg0, arg1, arg2) { + imports.wbg.__wbg_uniform1i_d2e61a6a43889648 = function(arg0, arg1, arg2) { getObject(arg0).uniform1i(getObject(arg1), arg2); }; - imports.wbg.__wbg_uniform2f_7a195f76fb8bc260 = function(arg0, arg1, arg2, arg3) { + imports.wbg.__wbg_uniform2f_b6e484a1302ea599 = function(arg0, arg1, arg2, arg3) { getObject(arg0).uniform2f(getObject(arg1), arg2, arg3); }; - imports.wbg.__wbg_useProgram_f16b06e2ecdf168f = function(arg0, arg1) { + imports.wbg.__wbg_useProgram_3683cf6f60939dcd = function(arg0, arg1) { getObject(arg0).useProgram(getObject(arg1)); }; - imports.wbg.__wbg_vertexAttribPointer_c16280a7c840a534 = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6) { + imports.wbg.__wbg_vertexAttribPointer_316ffe2f0458fde7 = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6) { getObject(arg0).vertexAttribPointer(arg1 >>> 0, arg2, arg3 >>> 0, arg4 !== 0, arg5, arg6); }; - imports.wbg.__wbg_viewport_a79678835091995e = function(arg0, arg1, arg2, arg3, arg4) { + imports.wbg.__wbg_viewport_fad1ce9e18f741c0 = function(arg0, arg1, arg2, arg3, arg4) { getObject(arg0).viewport(arg1, arg2, arg3, arg4); }; - imports.wbg.__wbg_instanceof_Window_c5579e140698a9dc = function(arg0) { + imports.wbg.__wbg_instanceof_Window_9029196b662bc42a = function(arg0) { let result; try { result = getObject(arg0) instanceof Window; @@ -691,306 +695,210 @@ function __wbg_get_imports() { const ret = result; return ret; }; - imports.wbg.__wbg_document_508774c021174a52 = function(arg0) { + imports.wbg.__wbg_document_f7ace2b956f30a4f = function(arg0) { const ret = getObject(arg0).document; return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_location_f6c62a50e72200c8 = function(arg0) { + imports.wbg.__wbg_location_56243dba507f472d = function(arg0) { const ret = getObject(arg0).location; return addHeapObject(ret); }; - imports.wbg.__wbg_navigator_957c9b40d49df205 = function(arg0) { + imports.wbg.__wbg_navigator_7c9103698acde322 = function(arg0) { const ret = getObject(arg0).navigator; return addHeapObject(ret); }; - imports.wbg.__wbg_innerHeight_972bafa16334ae25 = function() { return handleError(function (arg0) { + imports.wbg.__wbg_innerHeight_2dd06d8cf68f1d7d = function() { return handleError(function (arg0) { const ret = getObject(arg0).innerHeight; return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_devicePixelRatio_027b47127fcabea6 = function(arg0) { + imports.wbg.__wbg_devicePixelRatio_f9de7bddca0eaf20 = function(arg0) { const ret = getObject(arg0).devicePixelRatio; return ret; }; - imports.wbg.__wbg_performance_01a75a1b70b2c191 = function(arg0) { + imports.wbg.__wbg_performance_2c295061c8b01e0b = function(arg0) { const ret = getObject(arg0).performance; return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_speechSynthesis_c411499eede06735 = function() { return handleError(function (arg0) { + imports.wbg.__wbg_speechSynthesis_58ae269ad6d93928 = function() { return handleError(function (arg0) { const ret = getObject(arg0).speechSynthesis; return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_localStorage_b5b6d3c826dbfeda = function() { return handleError(function (arg0) { + imports.wbg.__wbg_localStorage_dbac11bd189e9fa0 = function() { return handleError(function (arg0) { const ret = getObject(arg0).localStorage; return isLikeNone(ret) ? 0 : addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_matchMedia_0dd51eaa41e54a4a = function() { return handleError(function (arg0, arg1, arg2) { + imports.wbg.__wbg_matchMedia_12ef69056e32d0b3 = function() { return handleError(function (arg0, arg1, arg2) { const ret = getObject(arg0).matchMedia(getStringFromWasm0(arg1, arg2)); return isLikeNone(ret) ? 0 : addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_open_7403f38d13c728d5 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { + imports.wbg.__wbg_open_7a2a86bf6285507d = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { const ret = getObject(arg0).open(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); return isLikeNone(ret) ? 0 : addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_requestAnimationFrame_d28701d8e57998d1 = function() { return handleError(function (arg0, arg1) { + imports.wbg.__wbg_requestAnimationFrame_d082200514b6674d = function() { return handleError(function (arg0, arg1) { const ret = getObject(arg0).requestAnimationFrame(getObject(arg1)); return ret; }, arguments) }; - imports.wbg.__wbg_clearInterval_c4c01544d0ea3996 = function(arg0, arg1) { + imports.wbg.__wbg_clearInterval_080a47b47538d08c = function(arg0, arg1) { getObject(arg0).clearInterval(arg1); }; - imports.wbg.__wbg_fetch_bb49ae9f1d79408b = function(arg0, arg1) { + imports.wbg.__wbg_fetch_336b6f0cb426b46e = function(arg0, arg1) { const ret = getObject(arg0).fetch(getObject(arg1)); return addHeapObject(ret); }; - imports.wbg.__wbg_setTimeout_a71432ae24261750 = function() { return handleError(function (arg0, arg1, arg2) { + imports.wbg.__wbg_setTimeout_eb1a0d116c26d9f6 = function() { return handleError(function (arg0, arg1, arg2) { const ret = getObject(arg0).setTimeout(getObject(arg1), arg2); return ret; }, arguments) }; - imports.wbg.__wbg_setProperty_0a5af0fd1a9e8e25 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { - getObject(arg0).setProperty(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); - }, arguments) }; - imports.wbg.__wbg_width_3f3962bb2721e365 = function(arg0) { - const ret = getObject(arg0).width; + imports.wbg.__wbg_instanceof_HtmlInputElement_31b50e0cf542c524 = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof HTMLInputElement; + } catch { + result = false; + } + const ret = result; return ret; }; - imports.wbg.__wbg_height_32cf02a714d68bd4 = function(arg0) { - const ret = getObject(arg0).height; - return ret; + imports.wbg.__wbg_setautofocus_61b6a31b4866ad1f = function(arg0, arg1) { + getObject(arg0).autofocus = arg1 !== 0; }; - imports.wbg.__wbg_length_804062329ac4ddbb = function(arg0) { - const ret = getObject(arg0).length; + imports.wbg.__wbg_setsize_7532844e2c9f5e10 = function(arg0, arg1) { + getObject(arg0).size = arg1 >>> 0; + }; + imports.wbg.__wbg_value_9423da9d988ee8cf = function(arg0, arg1) { + const ret = getObject(arg1).value; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; + }; + imports.wbg.__wbg_setvalue_1f95e61cbc382f7f = function(arg0, arg1, arg2) { + getObject(arg0).value = getStringFromWasm0(arg1, arg2); + }; + imports.wbg.__wbg_top_98ff0408c018d25e = function(arg0) { + const ret = getObject(arg0).top; return ret; }; - imports.wbg.__wbg_get_43359180ff298dda = function(arg0, arg1) { - const ret = getObject(arg0)[arg1 >>> 0]; - return isLikeNone(ret) ? 0 : addHeapObject(ret); + imports.wbg.__wbg_left_23a613d619fb4206 = function(arg0) { + const ret = getObject(arg0).left; + return ret; }; - imports.wbg.__wbg_set_76353df4722f4954 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { - getObject(arg0).set(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); - }, arguments) }; - imports.wbg.__wbg_error_49c7bf770cd75883 = function(arg0) { + imports.wbg.__wbg_error_bd8f2b9a1955400c = function(arg0) { const ret = getObject(arg0).error; return addHeapObject(ret); }; - imports.wbg.__wbg_setvoice_b89f15aafa327ee8 = function(arg0, arg1) { + imports.wbg.__wbg_setvoice_1cdf4f6a91b1cc6d = function(arg0, arg1) { getObject(arg0).voice = getObject(arg1); }; - imports.wbg.__wbg_setvolume_b175e9ad36c878ee = function(arg0, arg1) { + imports.wbg.__wbg_setvolume_9ce6fa0115fb0261 = function(arg0, arg1) { getObject(arg0).volume = arg1; }; - imports.wbg.__wbg_setrate_ca2167dc6b346b26 = function(arg0, arg1) { + imports.wbg.__wbg_setrate_3cb16d28a89a549c = function(arg0, arg1) { getObject(arg0).rate = arg1; }; - imports.wbg.__wbg_setpitch_3a330092e7e9b96f = function(arg0, arg1) { + imports.wbg.__wbg_setpitch_8bf2e2409e1707df = function(arg0, arg1) { getObject(arg0).pitch = arg1; }; - imports.wbg.__wbg_setonstart_963d173442cf886e = function(arg0, arg1) { + imports.wbg.__wbg_setonstart_d67a423413afcc19 = function(arg0, arg1) { getObject(arg0).onstart = getObject(arg1); }; - imports.wbg.__wbg_setonend_1fea8ae30b203c54 = function(arg0, arg1) { + imports.wbg.__wbg_setonend_d777c00af811a8d0 = function(arg0, arg1) { getObject(arg0).onend = getObject(arg1); }; - imports.wbg.__wbg_setonerror_c117a28570d46684 = function(arg0, arg1) { + imports.wbg.__wbg_setonerror_3419fd51cfc38793 = function(arg0, arg1) { getObject(arg0).onerror = getObject(arg1); }; - imports.wbg.__wbg_newwithtext_8130d0159088eaf8 = function() { return handleError(function (arg0, arg1) { + imports.wbg.__wbg_newwithtext_7319093976235888 = function() { return handleError(function (arg0, arg1) { const ret = new SpeechSynthesisUtterance(getStringFromWasm0(arg0, arg1)); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_length_e9385289cac41215 = function(arg0) { + imports.wbg.__wbg_length_25c4aaeba8cfcc81 = function(arg0) { const ret = getObject(arg0).length; return ret; }; - imports.wbg.__wbg_item_3ce7f6824dd8c355 = function(arg0, arg1) { + imports.wbg.__wbg_item_59a092aa0f27eab6 = function(arg0, arg1) { const ret = getObject(arg0).item(arg1 >>> 0); return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_get_969f46cb0fad92dc = function(arg0, arg1) { + imports.wbg.__wbg_get_d6c4e69528650af4 = function(arg0, arg1) { const ret = getObject(arg0)[arg1 >>> 0]; return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_instanceof_HtmlInputElement_a15913e00980dd9c = function(arg0) { - let result; - try { - result = getObject(arg0) instanceof HTMLInputElement; - } catch { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_setautofocus_ae682319cc846b47 = function(arg0, arg1) { - getObject(arg0).autofocus = arg1 !== 0; - }; - imports.wbg.__wbg_setsize_ae008b7511e35484 = function(arg0, arg1) { - getObject(arg0).size = arg1 >>> 0; - }; - imports.wbg.__wbg_value_09d384cba1c51c6f = function(arg0, arg1) { - const ret = getObject(arg1).value; - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len1; - getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }; - imports.wbg.__wbg_setvalue_7605619324f70225 = function(arg0, arg1, arg2) { - getObject(arg0).value = getStringFromWasm0(arg1, arg2); - }; - imports.wbg.__wbg_type_a7d16081cac0a25b = function(arg0, arg1) { - const ret = getObject(arg1).type; - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len1; - getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }; - imports.wbg.__wbg_clientX_7ffcce9d4cf5ed8f = function(arg0) { - const ret = getObject(arg0).clientX; - return ret; - }; - imports.wbg.__wbg_clientY_2340b057451d96fb = function(arg0) { - const ret = getObject(arg0).clientY; + imports.wbg.__wbg_width_e0c6b79d8cdd8897 = function(arg0) { + const ret = getObject(arg0).width; return ret; }; - imports.wbg.__wbg_ctrlKey_1c15f65d527fd45e = function(arg0) { - const ret = getObject(arg0).ctrlKey; + imports.wbg.__wbg_height_bed51746e072a118 = function(arg0) { + const ret = getObject(arg0).height; return ret; }; - imports.wbg.__wbg_shiftKey_1a7bf1612681d447 = function(arg0) { - const ret = getObject(arg0).shiftKey; + imports.wbg.__wbg_length_b941879633a63ad8 = function(arg0) { + const ret = getObject(arg0).length; return ret; }; - imports.wbg.__wbg_metaKey_3c7419a9d32c95d1 = function(arg0) { - const ret = getObject(arg0).metaKey; - return ret; + imports.wbg.__wbg_get_b383d7f8253c2d61 = function(arg0, arg1) { + const ret = getObject(arg0)[arg1 >>> 0]; + return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_button_88e86c8fe3039068 = function(arg0) { - const ret = getObject(arg0).button; + imports.wbg.__wbg_matches_07c564b5b4101cf2 = function(arg0) { + const ret = getObject(arg0).matches; return ret; }; - imports.wbg.__wbg_instanceof_Response_7ade9a5a066d1a55 = function(arg0) { - let result; - try { - result = getObject(arg0) instanceof Response; - } catch { - result = false; - } - const ret = result; + imports.wbg.__wbg_matches_0f7e350783b542c2 = function(arg0) { + const ret = getObject(arg0).matches; return ret; }; - imports.wbg.__wbg_url_59cb32ef6a837521 = function(arg0, arg1) { - const ret = getObject(arg1).url; - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len1; - getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }; - imports.wbg.__wbg_status_d2b2d0889f7e970f = function(arg0) { - const ret = getObject(arg0).status; - return ret; + imports.wbg.__wbg_bindVertexArrayOES_b7d9da7e073aa6b5 = function(arg0, arg1) { + getObject(arg0).bindVertexArrayOES(getObject(arg1)); }; - imports.wbg.__wbg_ok_0a0ed9a1863d8af5 = function(arg0) { - const ret = getObject(arg0).ok; - return ret; + imports.wbg.__wbg_createVertexArrayOES_6a3c3a5a68201f8f = function(arg0) { + const ret = getObject(arg0).createVertexArrayOES(); + return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_statusText_3600c56ee3873605 = function(arg0, arg1) { - const ret = getObject(arg1).statusText; - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len1; - getInt32Memory0()[arg0 / 4 + 0] = ptr1; + imports.wbg.__wbg_deleteVertexArrayOES_7bf4589e63d84df6 = function(arg0, arg1) { + getObject(arg0).deleteVertexArrayOES(getObject(arg1)); }; - imports.wbg.__wbg_headers_2de03c88f895093b = function(arg0) { + imports.wbg.__wbg_headers_b439dcff02e808e5 = function(arg0) { const ret = getObject(arg0).headers; return addHeapObject(ret); }; - imports.wbg.__wbg_arrayBuffer_2693673868da65b7 = function() { return handleError(function (arg0) { - const ret = getObject(arg0).arrayBuffer(); + imports.wbg.__wbg_newwithstrandinit_cad5cd6038c7ff5d = function() { return handleError(function (arg0, arg1, arg2) { + const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2)); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_deltaX_0e9fe84a3998df5e = function(arg0) { + imports.wbg.__wbg_deltaX_84508d00a1050e70 = function(arg0) { const ret = getObject(arg0).deltaX; return ret; }; - imports.wbg.__wbg_deltaY_c24e1c19542b4ba4 = function(arg0) { + imports.wbg.__wbg_deltaY_64823169afb0335d = function(arg0) { const ret = getObject(arg0).deltaY; return ret; }; - imports.wbg.__wbg_deltaMode_998c8ea939f3998a = function(arg0) { + imports.wbg.__wbg_deltaMode_1c680147cfdba8a5 = function(arg0) { const ret = getObject(arg0).deltaMode; return ret; }; - imports.wbg.__wbg_matches_de64b7bec89b21e4 = function(arg0) { - const ret = getObject(arg0).matches; - return ret; - }; - imports.wbg.__wbg_bindVertexArrayOES_edb665af84add641 = function(arg0, arg1) { - getObject(arg0).bindVertexArrayOES(getObject(arg1)); - }; - imports.wbg.__wbg_createVertexArrayOES_72dc110fc4561db9 = function(arg0) { - const ret = getObject(arg0).createVertexArrayOES(); - return isLikeNone(ret) ? 0 : addHeapObject(ret); - }; - imports.wbg.__wbg_deleteVertexArrayOES_bb2f05cfcd49a758 = function(arg0, arg1) { - getObject(arg0).deleteVertexArrayOES(getObject(arg1)); - }; - imports.wbg.__wbg_voiceURI_1c374d1801636c26 = function(arg0, arg1) { - const ret = getObject(arg1).voiceURI; - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len1; - getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }; - imports.wbg.__wbg_name_b85b3a3d45d797bf = function(arg0, arg1) { - const ret = getObject(arg1).name; - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len1; - getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }; - imports.wbg.__wbg_lang_760dcdec80a6c745 = function(arg0, arg1) { - const ret = getObject(arg1).lang; - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len1; - getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }; - imports.wbg.__wbg_default_e1aa2046d76cda57 = function(arg0) { - const ret = getObject(arg0).default; - return ret; - }; - imports.wbg.__wbg_addEventListener_d25d1ffe6c69ae96 = function() { return handleError(function (arg0, arg1, arg2, arg3) { - getObject(arg0).addEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3)); - }, arguments) }; - imports.wbg.__wbg_removeEventListener_7a381df5fdb6037f = function() { return handleError(function (arg0, arg1, arg2, arg3) { - getObject(arg0).removeEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3)); - }, arguments) }; - imports.wbg.__wbg_now_c97f243e7947c4ac = function(arg0) { + imports.wbg.__wbg_now_0cfdc90c97d0c24b = function(arg0) { const ret = getObject(arg0).now(); return ret; }; - imports.wbg.__wbg_writeText_f3c15480e7666d78 = function(arg0, arg1, arg2) { - const ret = getObject(arg0).writeText(getStringFromWasm0(arg1, arg2)); - return addHeapObject(ret); - }; - imports.wbg.__wbg_items_577e171b04c9aaa9 = function(arg0) { + imports.wbg.__wbg_items_0076326dc6f1b7eb = function(arg0) { const ret = getObject(arg0).items; return addHeapObject(ret); }; - imports.wbg.__wbg_files_7ce5634e8bc25a85 = function(arg0) { + imports.wbg.__wbg_files_e5c28ff6ab126f7b = function(arg0) { const ret = getObject(arg0).files; return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_getData_f586c0811a7ec1c4 = function() { return handleError(function (arg0, arg1, arg2, arg3) { + imports.wbg.__wbg_getData_8aeca5994420c599 = function() { return handleError(function (arg0, arg1, arg2, arg3) { const ret = getObject(arg1).getData(getStringFromWasm0(arg2, arg3)); const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len1 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; }, arguments) }; - imports.wbg.__wbg_dataTransfer_87da4b5e2c2e3bc4 = function(arg0) { - const ret = getObject(arg0).dataTransfer; - return isLikeNone(ret) ? 0 : addHeapObject(ret); - }; - imports.wbg.__wbg_instanceof_HtmlCanvasElement_b2dfeaf97587c9fa = function(arg0) { + imports.wbg.__wbg_instanceof_HtmlCanvasElement_da5f9efa0688cf6d = function(arg0) { let result; try { result = getObject(arg0) instanceof HTMLCanvasElement; @@ -1000,101 +908,137 @@ function __wbg_get_imports() { const ret = result; return ret; }; - imports.wbg.__wbg_width_b1f2559ce447b1d9 = function(arg0) { + imports.wbg.__wbg_width_2931aaedd21f1fff = function(arg0) { const ret = getObject(arg0).width; return ret; }; - imports.wbg.__wbg_setwidth_196f4382488fd119 = function(arg0, arg1) { + imports.wbg.__wbg_setwidth_a667a942dba6656e = function(arg0, arg1) { getObject(arg0).width = arg1 >>> 0; }; - imports.wbg.__wbg_height_0d9fffc5de313208 = function(arg0) { + imports.wbg.__wbg_height_0d36fbbeb60b0661 = function(arg0) { const ret = getObject(arg0).height; return ret; }; - imports.wbg.__wbg_setheight_6d295d03e1783969 = function(arg0, arg1) { + imports.wbg.__wbg_setheight_a747d440760fe5aa = function(arg0, arg1) { getObject(arg0).height = arg1 >>> 0; }; - imports.wbg.__wbg_getContext_24464d6344024525 = function() { return handleError(function (arg0, arg1, arg2) { + imports.wbg.__wbg_getContext_7c5944ea807bf5d3 = function() { return handleError(function (arg0, arg1, arg2) { const ret = getObject(arg0).getContext(getStringFromWasm0(arg1, arg2)); return isLikeNone(ret) ? 0 : addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_matches_03fa716e6d77d76f = function(arg0) { - const ret = getObject(arg0).matches; - return ret; + imports.wbg.__wbg_clipboard_47d5c6d7496034ae = function(arg0) { + const ret = getObject(arg0).clipboard; + return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_headers_1eff4f53324496e6 = function(arg0) { - const ret = getObject(arg0).headers; + imports.wbg.__wbg_userAgent_2053026e2b1e0c7e = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg1).userAgent; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; + }, arguments) }; + imports.wbg.__wbg_writeText_9c0cc5145d005509 = function(arg0, arg1, arg2) { + const ret = getObject(arg0).writeText(getStringFromWasm0(arg1, arg2)); return addHeapObject(ret); }; - imports.wbg.__wbg_newwithstrandinit_a4cd16dfaafcf625 = function() { return handleError(function (arg0, arg1, arg2) { - const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2)); - return addHeapObject(ret); + imports.wbg.__wbg_addEventListener_5651108fc3ffeb6e = function() { return handleError(function (arg0, arg1, arg2, arg3) { + getObject(arg0).addEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3)); }, arguments) }; - imports.wbg.__wbg_body_db30cc67afcfce41 = function(arg0) { + imports.wbg.__wbg_removeEventListener_5de660c02ed784e4 = function() { return handleError(function (arg0, arg1, arg2, arg3) { + getObject(arg0).removeEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3)); + }, arguments) }; + imports.wbg.__wbg_clientX_1a480606ab0cabaa = function(arg0) { + const ret = getObject(arg0).clientX; + return ret; + }; + imports.wbg.__wbg_clientY_9c7878f7faf3900f = function(arg0) { + const ret = getObject(arg0).clientY; + return ret; + }; + imports.wbg.__wbg_ctrlKey_0a805df688b5bf42 = function(arg0) { + const ret = getObject(arg0).ctrlKey; + return ret; + }; + imports.wbg.__wbg_shiftKey_8a070ab6169b5fa4 = function(arg0) { + const ret = getObject(arg0).shiftKey; + return ret; + }; + imports.wbg.__wbg_metaKey_d89287be4389a3c1 = function(arg0) { + const ret = getObject(arg0).metaKey; + return ret; + }; + imports.wbg.__wbg_button_7a095234b69de930 = function(arg0) { + const ret = getObject(arg0).button; + return ret; + }; + imports.wbg.__wbg_error_788ae33f81d3b84b = function(arg0) { + console.error(getObject(arg0)); + }; + imports.wbg.__wbg_body_674aec4c1c0910cd = function(arg0) { const ret = getObject(arg0).body; return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_createElement_d975e66d06bc88da = function() { return handleError(function (arg0, arg1, arg2) { + imports.wbg.__wbg_createElement_4891554b28d3388b = function() { return handleError(function (arg0, arg1, arg2) { const ret = getObject(arg0).createElement(getStringFromWasm0(arg1, arg2)); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_getElementById_2d1ad15c49298068 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_getElementById_cc0e0d931b0d9a28 = function(arg0, arg1, arg2) { const ret = getObject(arg0).getElementById(getStringFromWasm0(arg1, arg2)); return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_setid_7b804eee26ac1f4c = function(arg0, arg1, arg2) { + imports.wbg.__wbg_setid_1984ee27e5075311 = function(arg0, arg1, arg2) { getObject(arg0).id = getStringFromWasm0(arg1, arg2); }; - imports.wbg.__wbg_scrollLeft_d414f3347b1709bf = function(arg0) { + imports.wbg.__wbg_scrollLeft_ea915614eac6bbeb = function(arg0) { const ret = getObject(arg0).scrollLeft; return ret; }; - imports.wbg.__wbg_clientWidth_28c68ca0ee754d86 = function(arg0) { + imports.wbg.__wbg_clientWidth_51ec21e3189f5656 = function(arg0) { const ret = getObject(arg0).clientWidth; return ret; }; - imports.wbg.__wbg_clientHeight_fdbd966f05f13573 = function(arg0) { + imports.wbg.__wbg_clientHeight_09ec0b524d59c367 = function(arg0) { const ret = getObject(arg0).clientHeight; return ret; }; - imports.wbg.__wbg_getBoundingClientRect_89e65d65040347e7 = function(arg0) { + imports.wbg.__wbg_getBoundingClientRect_ac9db8cf97ca8083 = function(arg0) { const ret = getObject(arg0).getBoundingClientRect(); return addHeapObject(ret); }; - imports.wbg.__wbg_scrollTop_073ecc1ed0e30723 = function(arg0) { + imports.wbg.__wbg_scrollTop_9e5ce77431551404 = function(arg0) { const ret = getObject(arg0).scrollTop; return ret; }; - imports.wbg.__wbg_hidden_6e9683fc743723b7 = function(arg0) { + imports.wbg.__wbg_hidden_736e60e4fd2d186b = function(arg0) { const ret = getObject(arg0).hidden; return ret; }; - imports.wbg.__wbg_sethidden_253d3f6b07efe62c = function(arg0, arg1) { + imports.wbg.__wbg_sethidden_0cbfa2481b57c377 = function(arg0, arg1) { getObject(arg0).hidden = arg1 !== 0; }; - imports.wbg.__wbg_style_6bc91a563e84d432 = function(arg0) { + imports.wbg.__wbg_style_3801009b2339aa94 = function(arg0) { const ret = getObject(arg0).style; return addHeapObject(ret); }; - imports.wbg.__wbg_offsetTop_19bb0e1c142f9499 = function(arg0) { + imports.wbg.__wbg_offsetTop_815aa9ab53b3cf18 = function(arg0) { const ret = getObject(arg0).offsetTop; return ret; }; - imports.wbg.__wbg_offsetLeft_677c5033ce585ad4 = function(arg0) { + imports.wbg.__wbg_offsetLeft_3b7ae7e9baa5358a = function(arg0) { const ret = getObject(arg0).offsetLeft; return ret; }; - imports.wbg.__wbg_offsetWidth_20880536c835f478 = function(arg0) { + imports.wbg.__wbg_offsetWidth_4e9930121c69297f = function(arg0) { const ret = getObject(arg0).offsetWidth; return ret; }; - imports.wbg.__wbg_blur_c2d062c40054c68d = function() { return handleError(function (arg0) { + imports.wbg.__wbg_blur_53431c003c82bf53 = function() { return handleError(function (arg0) { getObject(arg0).blur(); }, arguments) }; - imports.wbg.__wbg_focus_6baebc9f44af9925 = function() { return handleError(function (arg0) { + imports.wbg.__wbg_focus_dbcbbbb2a04c0e1f = function() { return handleError(function (arg0) { getObject(arg0).focus(); }, arguments) }; - imports.wbg.__wbg_instanceof_WebGlRenderingContext_e1419556cd2b2d2d = function(arg0) { + imports.wbg.__wbg_instanceof_WebGlRenderingContext_ea632546035eecb1 = function(arg0) { let result; try { result = getObject(arg0) instanceof WebGLRenderingContext; @@ -1104,384 +1048,454 @@ function __wbg_get_imports() { const ret = result; return ret; }; - imports.wbg.__wbg_bufferData_bf07fdd88f2e61a0 = function(arg0, arg1, arg2, arg3) { + imports.wbg.__wbg_bufferData_a11a9f65f31e7256 = function(arg0, arg1, arg2, arg3) { getObject(arg0).bufferData(arg1 >>> 0, getObject(arg2), arg3 >>> 0); }; - imports.wbg.__wbg_texImage2D_b23cc3496d4c8934 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { + imports.wbg.__wbg_texImage2D_6175916e58c59bc7 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { getObject(arg0).texImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7 >>> 0, arg8 >>> 0, getObject(arg9)); }, arguments) }; - imports.wbg.__wbg_texSubImage2D_3393b2faf4bdeda7 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { + imports.wbg.__wbg_texSubImage2D_f1a31f8045b7f831 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { getObject(arg0).texSubImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7 >>> 0, arg8 >>> 0, getObject(arg9)); }, arguments) }; - imports.wbg.__wbg_activeTexture_b191924c363f77ce = function(arg0, arg1) { + imports.wbg.__wbg_activeTexture_93b4de60af07da9c = function(arg0, arg1) { getObject(arg0).activeTexture(arg1 >>> 0); }; - imports.wbg.__wbg_attachShader_c6ba0e94024fcfd3 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_attachShader_b65b695055670cb5 = function(arg0, arg1, arg2) { getObject(arg0).attachShader(getObject(arg1), getObject(arg2)); }; - imports.wbg.__wbg_bindBuffer_bc746e5757cfd27a = function(arg0, arg1, arg2) { + imports.wbg.__wbg_bindBuffer_313561e5bc0e533f = function(arg0, arg1, arg2) { getObject(arg0).bindBuffer(arg1 >>> 0, getObject(arg2)); }; - imports.wbg.__wbg_bindTexture_003e92d5bd3d1cc7 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_bindTexture_9cb5c770d1ba2cca = function(arg0, arg1, arg2) { getObject(arg0).bindTexture(arg1 >>> 0, getObject(arg2)); }; - imports.wbg.__wbg_blendEquationSeparate_c22d96f84170b893 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_blendEquationSeparate_7ec5e34f066e44f8 = function(arg0, arg1, arg2) { getObject(arg0).blendEquationSeparate(arg1 >>> 0, arg2 >>> 0); }; - imports.wbg.__wbg_blendFuncSeparate_060a21922823f86f = function(arg0, arg1, arg2, arg3, arg4) { + imports.wbg.__wbg_blendFuncSeparate_7547ade0a7dfade2 = function(arg0, arg1, arg2, arg3, arg4) { getObject(arg0).blendFuncSeparate(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0, arg4 >>> 0); }; - imports.wbg.__wbg_clear_0fbd94f2b3007978 = function(arg0, arg1) { + imports.wbg.__wbg_clear_2ccea1f65b510c97 = function(arg0, arg1) { getObject(arg0).clear(arg1 >>> 0); }; - imports.wbg.__wbg_clearColor_a72825467905e9d1 = function(arg0, arg1, arg2, arg3, arg4) { + imports.wbg.__wbg_clearColor_de587608b28bc7ed = function(arg0, arg1, arg2, arg3, arg4) { getObject(arg0).clearColor(arg1, arg2, arg3, arg4); }; - imports.wbg.__wbg_colorMask_8154d2f1a5eb697a = function(arg0, arg1, arg2, arg3, arg4) { + imports.wbg.__wbg_colorMask_7cbd7a102954ede9 = function(arg0, arg1, arg2, arg3, arg4) { getObject(arg0).colorMask(arg1 !== 0, arg2 !== 0, arg3 !== 0, arg4 !== 0); }; - imports.wbg.__wbg_compileShader_11deea085b781c35 = function(arg0, arg1) { + imports.wbg.__wbg_compileShader_d88d0a8cd9b72b4d = function(arg0, arg1) { getObject(arg0).compileShader(getObject(arg1)); }; - imports.wbg.__wbg_createBuffer_d90b963a9701a002 = function(arg0) { + imports.wbg.__wbg_createBuffer_59051f4461e7c5e2 = function(arg0) { const ret = getObject(arg0).createBuffer(); return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_createProgram_d78c1ab5ce988d0a = function(arg0) { + imports.wbg.__wbg_createProgram_88dbe21c0b682e1a = function(arg0) { const ret = getObject(arg0).createProgram(); return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_createShader_94cce4c7315d3927 = function(arg0, arg1) { + imports.wbg.__wbg_createShader_9d7d388633caad18 = function(arg0, arg1) { const ret = getObject(arg0).createShader(arg1 >>> 0); return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_createTexture_ae9908092d6ebd13 = function(arg0) { + imports.wbg.__wbg_createTexture_9d0bb4d741b8ad76 = function(arg0) { const ret = getObject(arg0).createTexture(); return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_deleteBuffer_10e438f4de8bbdd0 = function(arg0, arg1) { + imports.wbg.__wbg_deleteBuffer_cdc6b9c73f54aff7 = function(arg0, arg1) { getObject(arg0).deleteBuffer(getObject(arg1)); }; - imports.wbg.__wbg_deleteProgram_71b202b504e08521 = function(arg0, arg1) { + imports.wbg.__wbg_deleteProgram_d8d7fc79ba83b256 = function(arg0, arg1) { getObject(arg0).deleteProgram(getObject(arg1)); }; - imports.wbg.__wbg_deleteShader_91b6949c2cb57fa5 = function(arg0, arg1) { + imports.wbg.__wbg_deleteShader_9a2f85efe5cb3706 = function(arg0, arg1) { getObject(arg0).deleteShader(getObject(arg1)); }; - imports.wbg.__wbg_deleteTexture_89b7cefee5715eaf = function(arg0, arg1) { + imports.wbg.__wbg_deleteTexture_a883356c5034d482 = function(arg0, arg1) { getObject(arg0).deleteTexture(getObject(arg1)); }; - imports.wbg.__wbg_detachShader_619bf2d58c955ff2 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_detachShader_a047ade0450ff0bf = function(arg0, arg1, arg2) { getObject(arg0).detachShader(getObject(arg1), getObject(arg2)); }; - imports.wbg.__wbg_disable_18e601ffe079514a = function(arg0, arg1) { + imports.wbg.__wbg_disable_5cf2070641fa2ed7 = function(arg0, arg1) { getObject(arg0).disable(arg1 >>> 0); }; - imports.wbg.__wbg_disableVertexAttribArray_eebd7bbc31fe8477 = function(arg0, arg1) { + imports.wbg.__wbg_disableVertexAttribArray_8dacd44e21adcaa2 = function(arg0, arg1) { getObject(arg0).disableVertexAttribArray(arg1 >>> 0); }; - imports.wbg.__wbg_drawArrays_fa797947fad0d5a1 = function(arg0, arg1, arg2, arg3) { + imports.wbg.__wbg_drawArrays_d5c7dc2b2376c85a = function(arg0, arg1, arg2, arg3) { getObject(arg0).drawArrays(arg1 >>> 0, arg2, arg3); }; - imports.wbg.__wbg_drawElements_f61f3169f51e95a8 = function(arg0, arg1, arg2, arg3, arg4) { + imports.wbg.__wbg_drawElements_3316ee0cd1117c2a = function(arg0, arg1, arg2, arg3, arg4) { getObject(arg0).drawElements(arg1 >>> 0, arg2, arg3 >>> 0, arg4); }; - imports.wbg.__wbg_enable_933052fc623337dd = function(arg0, arg1) { + imports.wbg.__wbg_enable_8965e69c596f0a94 = function(arg0, arg1) { getObject(arg0).enable(arg1 >>> 0); }; - imports.wbg.__wbg_enableVertexAttribArray_085d700612de8362 = function(arg0, arg1) { + imports.wbg.__wbg_enableVertexAttribArray_2b0475db43533cf2 = function(arg0, arg1) { getObject(arg0).enableVertexAttribArray(arg1 >>> 0); }; - imports.wbg.__wbg_getAttribLocation_019759a318602415 = function(arg0, arg1, arg2, arg3) { + imports.wbg.__wbg_getAttribLocation_a5a98d5272b01c0d = function(arg0, arg1, arg2, arg3) { const ret = getObject(arg0).getAttribLocation(getObject(arg1), getStringFromWasm0(arg2, arg3)); return ret; }; - imports.wbg.__wbg_getError_ffde7e40a0075b63 = function(arg0) { + imports.wbg.__wbg_getError_1e5ec1ec9e58b323 = function(arg0) { const ret = getObject(arg0).getError(); return ret; }; - imports.wbg.__wbg_getExtension_ffc1bd6e864d6abe = function() { return handleError(function (arg0, arg1, arg2) { + imports.wbg.__wbg_getExtension_088d115a16ecbd7d = function() { return handleError(function (arg0, arg1, arg2) { const ret = getObject(arg0).getExtension(getStringFromWasm0(arg1, arg2)); return isLikeNone(ret) ? 0 : addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_getParameter_211f7166414ae1d2 = function() { return handleError(function (arg0, arg1) { + imports.wbg.__wbg_getParameter_bfab7f0b00c9d7fb = function() { return handleError(function (arg0, arg1) { const ret = getObject(arg0).getParameter(arg1 >>> 0); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_getProgramInfoLog_e9101bb2902f723d = function(arg0, arg1, arg2) { + imports.wbg.__wbg_getProgramInfoLog_0b7af4ad85fa52a4 = function(arg0, arg1, arg2) { const ret = getObject(arg1).getProgramInfoLog(getObject(arg2)); var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); var len1 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; }; - imports.wbg.__wbg_getProgramParameter_2b03f89296b12b4b = function(arg0, arg1, arg2) { + imports.wbg.__wbg_getProgramParameter_2a3735278367f8bc = function(arg0, arg1, arg2) { const ret = getObject(arg0).getProgramParameter(getObject(arg1), arg2 >>> 0); return addHeapObject(ret); }; - imports.wbg.__wbg_getShaderInfoLog_311c033a58aeed83 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_getShaderInfoLog_979aafa403ffb252 = function(arg0, arg1, arg2) { const ret = getObject(arg1).getShaderInfoLog(getObject(arg2)); var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); var len1 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; }; - imports.wbg.__wbg_getShaderParameter_0003e598f392b3f6 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_getShaderParameter_e8054f1d9026fb70 = function(arg0, arg1, arg2) { const ret = getObject(arg0).getShaderParameter(getObject(arg1), arg2 >>> 0); return addHeapObject(ret); }; - imports.wbg.__wbg_getSupportedExtensions_642c7d68066b573c = function(arg0) { + imports.wbg.__wbg_getSupportedExtensions_4eb3a5f14f552ce5 = function(arg0) { const ret = getObject(arg0).getSupportedExtensions(); return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_getUniformLocation_90a9c5e63646d30f = function(arg0, arg1, arg2, arg3) { + imports.wbg.__wbg_getUniformLocation_688976233799a45a = function(arg0, arg1, arg2, arg3) { const ret = getObject(arg0).getUniformLocation(getObject(arg1), getStringFromWasm0(arg2, arg3)); return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_linkProgram_12cc552bbaf3fcd8 = function(arg0, arg1) { + imports.wbg.__wbg_linkProgram_9a2d12d120d99917 = function(arg0, arg1) { getObject(arg0).linkProgram(getObject(arg1)); }; - imports.wbg.__wbg_pixelStorei_673b59ca3de9bf71 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_pixelStorei_5ec932ebefd00149 = function(arg0, arg1, arg2) { getObject(arg0).pixelStorei(arg1 >>> 0, arg2); }; - imports.wbg.__wbg_scissor_d6dd0b6d33070a27 = function(arg0, arg1, arg2, arg3, arg4) { + imports.wbg.__wbg_scissor_c8ec3b1e053f3756 = function(arg0, arg1, arg2, arg3, arg4) { getObject(arg0).scissor(arg1, arg2, arg3, arg4); }; - imports.wbg.__wbg_shaderSource_be3c868a31ce0a63 = function(arg0, arg1, arg2, arg3) { + imports.wbg.__wbg_shaderSource_f435f9b74440bb54 = function(arg0, arg1, arg2, arg3) { getObject(arg0).shaderSource(getObject(arg1), getStringFromWasm0(arg2, arg3)); }; - imports.wbg.__wbg_texParameteri_8a613af30322a029 = function(arg0, arg1, arg2, arg3) { + imports.wbg.__wbg_texParameteri_1f17358e51eb8069 = function(arg0, arg1, arg2, arg3) { getObject(arg0).texParameteri(arg1 >>> 0, arg2 >>> 0, arg3); }; - imports.wbg.__wbg_uniform1f_47b794fe461d672a = function(arg0, arg1, arg2) { + imports.wbg.__wbg_uniform1f_7586c5e17ad254c9 = function(arg0, arg1, arg2) { getObject(arg0).uniform1f(getObject(arg1), arg2); }; - imports.wbg.__wbg_uniform1i_eb9e0e57747e2b87 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_uniform1i_9f94ef0ba6b3cc66 = function(arg0, arg1, arg2) { getObject(arg0).uniform1i(getObject(arg1), arg2); }; - imports.wbg.__wbg_uniform2f_75d4cb678b6f0e13 = function(arg0, arg1, arg2, arg3) { + imports.wbg.__wbg_uniform2f_69ee217590f07278 = function(arg0, arg1, arg2, arg3) { getObject(arg0).uniform2f(getObject(arg1), arg2, arg3); }; - imports.wbg.__wbg_useProgram_72ab2082025590d6 = function(arg0, arg1) { + imports.wbg.__wbg_useProgram_019eb6df066fabf5 = function(arg0, arg1) { getObject(arg0).useProgram(getObject(arg1)); }; - imports.wbg.__wbg_vertexAttribPointer_39284763b38d2a03 = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6) { + imports.wbg.__wbg_vertexAttribPointer_ca11984ee8843c0a = function(arg0, arg1, arg2, arg3, arg4, arg5, arg6) { getObject(arg0).vertexAttribPointer(arg1 >>> 0, arg2, arg3 >>> 0, arg4 !== 0, arg5, arg6); }; - imports.wbg.__wbg_viewport_d7c73a71f08f3aa1 = function(arg0, arg1, arg2, arg3, arg4) { + imports.wbg.__wbg_viewport_6ebef187c89e2616 = function(arg0, arg1, arg2, arg3, arg4) { getObject(arg0).viewport(arg1, arg2, arg3, arg4); }; - imports.wbg.__wbg_length_bff46949c967d66a = function(arg0) { + imports.wbg.__wbg_length_dd2eb44022569c32 = function(arg0) { const ret = getObject(arg0).length; return ret; }; - imports.wbg.__wbg_get_277c616d81e49f73 = function(arg0, arg1) { + imports.wbg.__wbg_get_135f0a95f49ed3ff = function(arg0, arg1) { const ret = getObject(arg0)[arg1 >>> 0]; return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_name_ae233a503e60a8f9 = function(arg0, arg1) { + imports.wbg.__wbg_name_a46b2d975591a0b3 = function(arg0, arg1) { const ret = getObject(arg1).name; const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len1 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; }; - imports.wbg.__wbg_lastModified_75062097476be87c = function(arg0) { + imports.wbg.__wbg_lastModified_711034410dfc02ad = function(arg0) { const ret = getObject(arg0).lastModified; return ret; }; - imports.wbg.__wbg_keyCode_48193538ac21d5a4 = function(arg0) { + imports.wbg.__wbg_keyCode_dfa86be31f5ef90c = function(arg0) { const ret = getObject(arg0).keyCode; return ret; }; - imports.wbg.__wbg_altKey_1796184c5e96a92b = function(arg0) { + imports.wbg.__wbg_altKey_612289acf855835c = function(arg0) { const ret = getObject(arg0).altKey; return ret; }; - imports.wbg.__wbg_ctrlKey_a6ae383772af67d4 = function(arg0) { + imports.wbg.__wbg_ctrlKey_582686fb2263dd3c = function(arg0) { const ret = getObject(arg0).ctrlKey; return ret; }; - imports.wbg.__wbg_shiftKey_0b1fd10d0674f847 = function(arg0) { + imports.wbg.__wbg_shiftKey_48e8701355d8e2d4 = function(arg0) { const ret = getObject(arg0).shiftKey; return ret; }; - imports.wbg.__wbg_metaKey_e6e67f783888f56d = function(arg0) { + imports.wbg.__wbg_metaKey_43193b7cc99f8914 = function(arg0) { const ret = getObject(arg0).metaKey; return ret; }; - imports.wbg.__wbg_isComposing_11821d1699a0901e = function(arg0) { + imports.wbg.__wbg_isComposing_f41d219def91d438 = function(arg0) { const ret = getObject(arg0).isComposing; return ret; }; - imports.wbg.__wbg_key_2e1ec0c70a342064 = function(arg0, arg1) { + imports.wbg.__wbg_key_8aeaa079126a9cc7 = function(arg0, arg1) { const ret = getObject(arg1).key; const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len1 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; }; - imports.wbg.__wbg_clipboard_4685056afb1bd02b = function(arg0) { - const ret = getObject(arg0).clipboard; - return isLikeNone(ret) ? 0 : addHeapObject(ret); - }; - imports.wbg.__wbg_userAgent_cda809ba30048ef3 = function() { return handleError(function (arg0, arg1) { - const ret = getObject(arg1).userAgent; + imports.wbg.__wbg_href_d62a28e4fc1ab948 = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg1).href; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; + }, arguments) }; + imports.wbg.__wbg_origin_50aa482fa6784a0a = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg1).origin; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; + }, arguments) }; + imports.wbg.__wbg_protocol_91948f5885595359 = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg1).protocol; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; + }, arguments) }; + imports.wbg.__wbg_host_15090f3de0544fea = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg1).host; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; + }, arguments) }; + imports.wbg.__wbg_hostname_b77e5e70d6ff6236 = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg1).hostname; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; + }, arguments) }; + imports.wbg.__wbg_port_1b2b1249cacfca76 = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg1).port; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; + }, arguments) }; + imports.wbg.__wbg_search_6c3c472e076ee010 = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg1).search; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; + }, arguments) }; + imports.wbg.__wbg_hash_a1a795b89dda8e3d = function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg1).hash; const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len1 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; }, arguments) }; - imports.wbg.__wbg_speaking_e166a6e6cd16926d = function(arg0) { + imports.wbg.__wbg_speaking_f518bada6d9d9beb = function(arg0) { const ret = getObject(arg0).speaking; return ret; }; - imports.wbg.__wbg_cancel_2fd875e685342a41 = function(arg0) { + imports.wbg.__wbg_cancel_5e5d3ec8238c4c1b = function(arg0) { getObject(arg0).cancel(); }; - imports.wbg.__wbg_getVoices_769c317b57ab4d62 = function(arg0) { + imports.wbg.__wbg_getVoices_c24bab8ed59e417e = function(arg0) { const ret = getObject(arg0).getVoices(); return addHeapObject(ret); }; - imports.wbg.__wbg_speak_79ff59113028b505 = function(arg0, arg1) { + imports.wbg.__wbg_speak_c5dda9a5a88d036d = function(arg0, arg1) { getObject(arg0).speak(getObject(arg1)); }; - imports.wbg.__wbg_identifier_bcd0d7d8565303c9 = function(arg0) { - const ret = getObject(arg0).identifier; - return ret; - }; - imports.wbg.__wbg_pageX_da46b41c74531c31 = function(arg0) { - const ret = getObject(arg0).pageX; - return ret; - }; - imports.wbg.__wbg_pageY_1a5658948b63f9ed = function(arg0) { - const ret = getObject(arg0).pageY; - return ret; - }; - imports.wbg.__wbg_force_77bad74f81971385 = function(arg0) { - const ret = getObject(arg0).force; - return ret; - }; - imports.wbg.__wbg_clipboardData_84b041aaf1dd9a2c = function(arg0) { + imports.wbg.__wbg_clipboardData_c480a3b34e3e7b1d = function(arg0) { const ret = getObject(arg0).clipboardData; return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_preventDefault_2f38e1471796356f = function(arg0) { + imports.wbg.__wbg_setProperty_b95ef63ab852879e = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).setProperty(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); + }, arguments) }; + imports.wbg.__wbg_preventDefault_24104f3f0a54546a = function(arg0) { getObject(arg0).preventDefault(); }; - imports.wbg.__wbg_stopPropagation_5df9f972a70ef515 = function(arg0) { + imports.wbg.__wbg_stopPropagation_55539cfa2506c867 = function(arg0) { getObject(arg0).stopPropagation(); }; - imports.wbg.__wbg_parentElement_065722829508e41a = function(arg0) { + imports.wbg.__wbg_parentElement_c75962bc9997ea5f = function(arg0) { const ret = getObject(arg0).parentElement; return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_appendChild_1139b53a65d69bed = function() { return handleError(function (arg0, arg1) { + imports.wbg.__wbg_appendChild_51339d4cde00ee22 = function() { return handleError(function (arg0, arg1) { const ret = getObject(arg0).appendChild(getObject(arg1)); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_touches_08fba6286bed8021 = function(arg0) { - const ret = getObject(arg0).touches; - return addHeapObject(ret); + imports.wbg.__wbg_identifier_da93d3d09ccdc54c = function(arg0) { + const ret = getObject(arg0).identifier; + return ret; }; - imports.wbg.__wbg_changedTouches_0e21b77cd9200e74 = function(arg0) { - const ret = getObject(arg0).changedTouches; - return addHeapObject(ret); + imports.wbg.__wbg_pageX_8e76f76ea9375a85 = function(arg0) { + const ret = getObject(arg0).pageX; + return ret; }; - imports.wbg.__wbg_size_a058ca48cf388fd6 = function(arg0) { - const ret = getObject(arg0).size; + imports.wbg.__wbg_pageY_a5a407b52fe202e7 = function(arg0) { + const ret = getObject(arg0).pageY; return ret; }; - imports.wbg.__wbg_arrayBuffer_932c610fd9598bef = function(arg0) { - const ret = getObject(arg0).arrayBuffer(); - return addHeapObject(ret); + imports.wbg.__wbg_force_4dd0ab6e9ef993ec = function(arg0) { + const ret = getObject(arg0).force; + return ret; }; - imports.wbg.__wbg_data_64c5449b2adfdff6 = function(arg0, arg1) { + imports.wbg.__wbg_data_03708a776af7d2f6 = function(arg0, arg1) { const ret = getObject(arg1).data; var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); var len1 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; }; - imports.wbg.__wbg_top_7f927afdb53442c3 = function(arg0) { - const ret = getObject(arg0).top; - return ret; - }; - imports.wbg.__wbg_left_d363f2200ca7bca7 = function(arg0) { - const ret = getObject(arg0).left; - return ret; + imports.wbg.__wbg_dataTransfer_bac494821ce31837 = function(arg0) { + const ret = getObject(arg0).dataTransfer; + return isLikeNone(ret) ? 0 : addHeapObject(ret); }; - imports.wbg.__wbg_href_68df54cac0a34be4 = function() { return handleError(function (arg0, arg1) { - const ret = getObject(arg1).href; - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len1; - getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }, arguments) }; - imports.wbg.__wbg_origin_ab6836ac8535fbb2 = function() { return handleError(function (arg0, arg1) { - const ret = getObject(arg1).origin; + imports.wbg.__wbg_voiceURI_64621a744591aba3 = function(arg0, arg1) { + const ret = getObject(arg1).voiceURI; const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len1 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }, arguments) }; - imports.wbg.__wbg_protocol_68c51e1c422d4ab9 = function() { return handleError(function (arg0, arg1) { - const ret = getObject(arg1).protocol; + }; + imports.wbg.__wbg_name_0429be7b00828d24 = function(arg0, arg1) { + const ret = getObject(arg1).name; const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len1 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }, arguments) }; - imports.wbg.__wbg_host_e72469b40dce950f = function() { return handleError(function (arg0, arg1) { - const ret = getObject(arg1).host; + }; + imports.wbg.__wbg_lang_3a66cb1ce82d3781 = function(arg0, arg1) { + const ret = getObject(arg1).lang; const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len1 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }, arguments) }; - imports.wbg.__wbg_hostname_d253c57e7fa779ea = function() { return handleError(function (arg0, arg1) { - const ret = getObject(arg1).hostname; + }; + imports.wbg.__wbg_default_d943dbf9175dc6cd = function(arg0) { + const ret = getObject(arg0).default; + return ret; + }; + imports.wbg.__wbg_touches_8338f31b10bd7975 = function(arg0) { + const ret = getObject(arg0).touches; + return addHeapObject(ret); + }; + imports.wbg.__wbg_changedTouches_60ab7fa55837664f = function(arg0) { + const ret = getObject(arg0).changedTouches; + return addHeapObject(ret); + }; + imports.wbg.__wbg_size_b9bc39a333bd5d88 = function(arg0) { + const ret = getObject(arg0).size; + return ret; + }; + imports.wbg.__wbg_type_8b3fde044d705ef3 = function(arg0, arg1) { + const ret = getObject(arg1).type; const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len1 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }, arguments) }; - imports.wbg.__wbg_port_33d51963fa5f980d = function() { return handleError(function (arg0, arg1) { - const ret = getObject(arg1).port; + }; + imports.wbg.__wbg_arrayBuffer_27cefaea55cbf063 = function(arg0) { + const ret = getObject(arg0).arrayBuffer(); + return addHeapObject(ret); + }; + imports.wbg.__wbg_type_9f716e985ca0633c = function(arg0, arg1) { + const ret = getObject(arg1).type; const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len1 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; + }; + imports.wbg.__wbg_set_b34caba58723c454 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).set(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); }, arguments) }; - imports.wbg.__wbg_search_41ecfaf18d054732 = function() { return handleError(function (arg0, arg1) { - const ret = getObject(arg1).search; + imports.wbg.__wbg_instanceof_Response_fc4327dbfcdf5ced = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof Response; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_url_8503de97f69da463 = function(arg0, arg1) { + const ret = getObject(arg1).url; const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len1 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; - }, arguments) }; - imports.wbg.__wbg_hash_afd040db1cf05017 = function() { return handleError(function (arg0, arg1) { - const ret = getObject(arg1).hash; + }; + imports.wbg.__wbg_status_ac85a3142a84caa2 = function(arg0) { + const ret = getObject(arg0).status; + return ret; + }; + imports.wbg.__wbg_ok_e3d8d84e630fd064 = function(arg0) { + const ret = getObject(arg0).ok; + return ret; + }; + imports.wbg.__wbg_statusText_1dd32f6c94d79ef0 = function(arg0, arg1) { + const ret = getObject(arg1).statusText; const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len1 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; + }; + imports.wbg.__wbg_headers_b70de86b8e989bc0 = function(arg0) { + const ret = getObject(arg0).headers; + return addHeapObject(ret); + }; + imports.wbg.__wbg_arrayBuffer_288fb3538806e85c = function() { return handleError(function (arg0) { + const ret = getObject(arg0).arrayBuffer(); + return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_getItem_84095995ffbc84fc = function() { return handleError(function (arg0, arg1, arg2, arg3) { + imports.wbg.__wbg_getItem_ed8e218e51f1efeb = function() { return handleError(function (arg0, arg1, arg2, arg3) { const ret = getObject(arg1).getItem(getStringFromWasm0(arg2, arg3)); var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); var len1 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len1; getInt32Memory0()[arg0 / 4 + 0] = ptr1; }, arguments) }; - imports.wbg.__wbg_setItem_e9a65f0e6892d9c9 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { + imports.wbg.__wbg_setItem_d002ee486462bfff = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { getObject(arg0).setItem(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); }, arguments) }; - imports.wbg.__wbg_get_7303ed2ef026b2f5 = function(arg0, arg1) { + imports.wbg.__wbg_get_44be0491f933a435 = function(arg0, arg1) { const ret = getObject(arg0)[arg1 >>> 0]; return addHeapObject(ret); }; - imports.wbg.__wbg_length_820c786973abdd8a = function(arg0) { + imports.wbg.__wbg_length_fff51ee6522a1a18 = function(arg0) { const ret = getObject(arg0).length; return ret; }; @@ -1489,7 +1503,7 @@ function __wbg_get_imports() { const ret = typeof(getObject(arg0)) === 'function'; return ret; }; - imports.wbg.__wbg_newnoargs_c9e6043b8ad84109 = function(arg0, arg1) { + imports.wbg.__wbg_newnoargs_581967eacc0e2604 = function(arg0, arg1) { const ret = new Function(getStringFromWasm0(arg0, arg1)); return addHeapObject(ret); }; @@ -1498,51 +1512,51 @@ function __wbg_get_imports() { const ret = typeof(val) === 'object' && val !== null; return ret; }; - imports.wbg.__wbg_next_f4bc0e96ea67da68 = function(arg0) { + imports.wbg.__wbg_next_526fc47e980da008 = function(arg0) { const ret = getObject(arg0).next; return addHeapObject(ret); }; - imports.wbg.__wbg_next_ec061e48a0e72a96 = function() { return handleError(function (arg0) { + imports.wbg.__wbg_next_ddb3312ca1c4e32a = function() { return handleError(function (arg0) { const ret = getObject(arg0).next(); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_done_b6abb27d42b63867 = function(arg0) { + imports.wbg.__wbg_done_5c1f01fb660d73b5 = function(arg0) { const ret = getObject(arg0).done; return ret; }; - imports.wbg.__wbg_value_2f4ef2036bfad28e = function(arg0) { + imports.wbg.__wbg_value_1695675138684bd5 = function(arg0) { const ret = getObject(arg0).value; return addHeapObject(ret); }; - imports.wbg.__wbg_iterator_7c7e58f62eb84700 = function() { + imports.wbg.__wbg_iterator_97f0c81209c6c35a = function() { const ret = Symbol.iterator; return addHeapObject(ret); }; - imports.wbg.__wbg_get_f53c921291c381bd = function() { return handleError(function (arg0, arg1) { + imports.wbg.__wbg_get_97b561fb56f034b5 = function() { return handleError(function (arg0, arg1) { const ret = Reflect.get(getObject(arg0), getObject(arg1)); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_call_557a2f2deacc4912 = function() { return handleError(function (arg0, arg1) { + imports.wbg.__wbg_call_cb65541d95d71282 = function() { return handleError(function (arg0, arg1) { const ret = getObject(arg0).call(getObject(arg1)); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_new_2b6fea4ea03b1b95 = function() { + imports.wbg.__wbg_new_b51585de1b234aff = function() { const ret = new Object(); return addHeapObject(ret); }; - imports.wbg.__wbg_self_742dd6eab3e9211e = function() { return handleError(function () { + imports.wbg.__wbg_self_1ff1d729e9aae938 = function() { return handleError(function () { const ret = self.self; return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_window_c409e731db53a0e2 = function() { return handleError(function () { + imports.wbg.__wbg_window_5f4faef6c12b79ec = function() { return handleError(function () { const ret = window.window; return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_globalThis_b70c095388441f2d = function() { return handleError(function () { + imports.wbg.__wbg_globalThis_1d39714405582d3c = function() { return handleError(function () { const ret = globalThis.globalThis; return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_global_1c72617491ed7194 = function() { return handleError(function () { + imports.wbg.__wbg_global_651f05c6a0944d1c = function() { return handleError(function () { const ret = global.global; return addHeapObject(ret); }, arguments) }; @@ -1550,30 +1564,44 @@ function __wbg_get_imports() { const ret = getObject(arg0) === undefined; return ret; }; - imports.wbg.__wbg_call_587b30eea3e09332 = function() { return handleError(function (arg0, arg1, arg2) { + imports.wbg.__wbg_call_01734de55d61e11d = function() { return handleError(function (arg0, arg1, arg2) { const ret = getObject(arg0).call(getObject(arg1), getObject(arg2)); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbg_getTime_40bd09e020e8bc8c = function(arg0) { + imports.wbg.__wbg_getTime_5e2054f832d82ec9 = function(arg0) { const ret = getObject(arg0).getTime(); return ret; }; - imports.wbg.__wbg_getTimezoneOffset_884011df4eb8cabc = function(arg0) { + imports.wbg.__wbg_getTimezoneOffset_8aee3445f323973e = function(arg0) { const ret = getObject(arg0).getTimezoneOffset(); return ret; }; - imports.wbg.__wbg_new0_494c19a27871d56f = function() { + imports.wbg.__wbg_new_cd59bfc8881f487b = function(arg0) { + const ret = new Date(getObject(arg0)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_new0_c0be7df4b6bd481f = function() { const ret = new Date(); return addHeapObject(ret); }; - imports.wbg.__wbg_new_2b55e405e4af4986 = function(arg0, arg1) { + imports.wbg.__wbg_instanceof_TypeError_6ad728101212de55 = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof TypeError; + } catch { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_new_43f1b47c28813cbd = function(arg0, arg1) { try { var state0 = {a: arg0, b: arg1}; var cb0 = (arg0, arg1) => { const a = state0.a; state0.a = 0; try { - return __wbg_adapter_580(a, state0.b, arg0, arg1); + return __wbg_adapter_590(a, state0.b, arg0, arg1); } finally { state0.a = a; } @@ -1584,62 +1612,62 @@ function __wbg_get_imports() { state0.a = state0.b = 0; } }; - imports.wbg.__wbg_resolve_ae38ad63c43ff98b = function(arg0) { + imports.wbg.__wbg_resolve_53698b95aaf7fcf8 = function(arg0) { const ret = Promise.resolve(getObject(arg0)); return addHeapObject(ret); }; - imports.wbg.__wbg_then_8df675b8bb5d5e3c = function(arg0, arg1) { + imports.wbg.__wbg_then_f7e06ee3c11698eb = function(arg0, arg1) { const ret = getObject(arg0).then(getObject(arg1)); return addHeapObject(ret); }; - imports.wbg.__wbg_then_835b073a479138e5 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_then_b2267541e2a73865 = function(arg0, arg1, arg2) { const ret = getObject(arg0).then(getObject(arg1), getObject(arg2)); return addHeapObject(ret); }; - imports.wbg.__wbg_buffer_55ba7a6b1b92e2ac = function(arg0) { + imports.wbg.__wbg_buffer_085ec1f694018c4f = function(arg0) { const ret = getObject(arg0).buffer; return addHeapObject(ret); }; - imports.wbg.__wbg_newwithbyteoffsetandlength_abbb764f85f16b6b = function(arg0, arg1, arg2) { + imports.wbg.__wbg_newwithbyteoffsetandlength_828b952f0e692245 = function(arg0, arg1, arg2) { const ret = new Int8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); return addHeapObject(ret); }; - imports.wbg.__wbg_newwithbyteoffsetandlength_6889cd5c48bfda26 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_newwithbyteoffsetandlength_735ed5ea2ae07fe9 = function(arg0, arg1, arg2) { const ret = new Int16Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); return addHeapObject(ret); }; - imports.wbg.__wbg_newwithbyteoffsetandlength_23602c96f86e952a = function(arg0, arg1, arg2) { + imports.wbg.__wbg_newwithbyteoffsetandlength_9f43b22ab631d1d6 = function(arg0, arg1, arg2) { const ret = new Int32Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); return addHeapObject(ret); }; - imports.wbg.__wbg_newwithbyteoffsetandlength_88d1d8be5df94b9b = function(arg0, arg1, arg2) { + imports.wbg.__wbg_newwithbyteoffsetandlength_6da8e527659b86aa = function(arg0, arg1, arg2) { const ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); return addHeapObject(ret); }; - imports.wbg.__wbg_new_09938a7d020f049b = function(arg0) { + imports.wbg.__wbg_new_8125e318e6245eed = function(arg0) { const ret = new Uint8Array(getObject(arg0)); return addHeapObject(ret); }; - imports.wbg.__wbg_set_3698e3ca519b3c3c = function(arg0, arg1, arg2) { + imports.wbg.__wbg_set_5cf90238115182c3 = function(arg0, arg1, arg2) { getObject(arg0).set(getObject(arg1), arg2 >>> 0); }; - imports.wbg.__wbg_length_0aab7ffd65ad19ed = function(arg0) { + imports.wbg.__wbg_length_72e2208bbc0efc61 = function(arg0) { const ret = getObject(arg0).length; return ret; }; - imports.wbg.__wbg_newwithbyteoffsetandlength_051cff28f608b6cb = function(arg0, arg1, arg2) { + imports.wbg.__wbg_newwithbyteoffsetandlength_31ff1024ef0c63c7 = function(arg0, arg1, arg2) { const ret = new Uint16Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); return addHeapObject(ret); }; - imports.wbg.__wbg_newwithbyteoffsetandlength_fae0a69639559788 = function(arg0, arg1, arg2) { + imports.wbg.__wbg_newwithbyteoffsetandlength_6df0e8c3efd2a5d3 = function(arg0, arg1, arg2) { const ret = new Uint32Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); return addHeapObject(ret); }; - imports.wbg.__wbg_newwithbyteoffsetandlength_ab5b524f83702d8d = function(arg0, arg1, arg2) { + imports.wbg.__wbg_newwithbyteoffsetandlength_69193e31c844b792 = function(arg0, arg1, arg2) { const ret = new Float32Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); return addHeapObject(ret); }; - imports.wbg.__wbg_set_07da13cc24b69217 = function() { return handleError(function (arg0, arg1, arg2) { + imports.wbg.__wbg_set_092e06b0f9d71865 = function() { return handleError(function (arg0, arg1, arg2) { const ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2)); return ret; }, arguments) }; @@ -1657,28 +1685,28 @@ function __wbg_get_imports() { const ret = wasm.memory; return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper2826 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 996, __wbg_adapter_28); + imports.wbg.__wbindgen_closure_wrapper3452 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 1232, __wbg_adapter_30); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper2827 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 996, __wbg_adapter_31); + imports.wbg.__wbindgen_closure_wrapper3453 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 1232, __wbg_adapter_33); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper2828 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 996, __wbg_adapter_34); + imports.wbg.__wbindgen_closure_wrapper3456 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 1232, __wbg_adapter_36); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper3214 = function(arg0, arg1, arg2) { - const ret = makeClosure(arg0, arg1, 1153, __wbg_adapter_37); + imports.wbg.__wbindgen_closure_wrapper3605 = function(arg0, arg1, arg2) { + const ret = makeClosure(arg0, arg1, 1300, __wbg_adapter_39); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper3216 = function(arg0, arg1, arg2) { - const ret = makeClosure(arg0, arg1, 1153, __wbg_adapter_37); + imports.wbg.__wbindgen_closure_wrapper3607 = function(arg0, arg1, arg2) { + const ret = makeClosure(arg0, arg1, 1300, __wbg_adapter_39); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper3259 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 1176, __wbg_adapter_42); + imports.wbg.__wbindgen_closure_wrapper3654 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 1328, __wbg_adapter_44); return addHeapObject(ret); }; diff --git a/docs/egui_demo_app_bg.wasm b/docs/egui_demo_app_bg.wasm index ea97afe333d..57943236525 100644 Binary files a/docs/egui_demo_app_bg.wasm and b/docs/egui_demo_app_bg.wasm differ diff --git a/examples/download_image/Cargo.toml b/examples/download_image/Cargo.toml deleted file mode 100644 index 630b7ef6855..00000000000 --- a/examples/download_image/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "download_image" -version = "0.1.0" -authors = ["Emil Ernerfeldt "] -license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.70" -publish = false - - -[dependencies] -eframe = { path = "../../crates/eframe", features = [ - "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO -] } -egui_extras = { path = "../../crates/egui_extras", features = [ - "http", - "image", - "log", -] } -env_logger = "0.10" -image = { version = "0.24", default-features = false, features = ["jpeg"] } diff --git a/examples/download_image/README.md b/examples/download_image/README.md deleted file mode 100644 index 936e1e058c4..00000000000 --- a/examples/download_image/README.md +++ /dev/null @@ -1,7 +0,0 @@ -Example how to download and show an image with eframe/egui. - -```sh -cargo run -p download_image -``` - -![](screenshot.png) diff --git a/examples/download_image/screenshot.png b/examples/download_image/screenshot.png deleted file mode 100644 index 919c4544f55..00000000000 Binary files a/examples/download_image/screenshot.png and /dev/null differ diff --git a/examples/download_image/src/main.rs b/examples/download_image/src/main.rs deleted file mode 100644 index 610c39d1f8f..00000000000 --- a/examples/download_image/src/main.rs +++ /dev/null @@ -1,41 +0,0 @@ -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release - -use eframe::egui; - -fn main() -> Result<(), eframe::Error> { - env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). - let options = eframe::NativeOptions::default(); - eframe::run_native( - "Download and show an image with eframe/egui", - options, - Box::new(|cc| { - // Without the following call, the `Image2` created below - // will simply output `not supported` error messages. - egui_extras::loaders::install(&cc.egui_ctx); - Box::new(MyApp) - }), - ) -} - -#[derive(Default)] -struct MyApp; - -impl eframe::App for MyApp { - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - egui::CentralPanel::default().show(ctx, |ui| { - let width = ui.available_width(); - let half_height = ui.available_height() / 2.0; - - ui.allocate_ui(egui::Vec2::new(width, half_height), |ui| { - ui.add(egui::Image2::from_uri( - "https://picsum.photos/seed/1.759706314/1024", - )) - }); - ui.allocate_ui(egui::Vec2::new(width, half_height), |ui| { - ui.add(egui::Image2::from_uri( - "https://this-is-hopefully-not-a-real-website.rs/image.png", - )) - }); - }); - } -} diff --git a/examples/hello_world/Cargo.toml b/examples/hello_world/Cargo.toml index b960dcfb775..a3ae0ea7d7e 100644 --- a/examples/hello_world/Cargo.toml +++ b/examples/hello_world/Cargo.toml @@ -12,4 +12,8 @@ publish = false eframe = { path = "../../crates/eframe", features = [ "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO ] } + +# For image support: +egui_extras = { path = "../../crates/egui_extras", features = ["image"] } + env_logger = "0.10" diff --git a/examples/hello_world/screenshot.png b/examples/hello_world/screenshot.png index 450f6eead24..f47e2c589a9 100644 Binary files a/examples/hello_world/screenshot.png and b/examples/hello_world/screenshot.png differ diff --git a/examples/hello_world/src/main.rs b/examples/hello_world/src/main.rs index 47c045db51b..4a64a3bec0a 100644 --- a/examples/hello_world/src/main.rs +++ b/examples/hello_world/src/main.rs @@ -11,7 +11,12 @@ fn main() -> Result<(), eframe::Error> { eframe::run_native( "My egui App", options, - Box::new(|_cc| Box::::default()), + Box::new(|cc| { + // This gives us image support: + egui_extras::install_image_loaders(&cc.egui_ctx); + + Box::::default() + }), ) } @@ -43,6 +48,10 @@ impl eframe::App for MyApp { self.age += 1; } ui.label(format!("Hello '{}', age {}", self.name, self.age)); + + ui.image(egui::include_image!( + "../../../crates/egui/assets/ferris.png" + )); }); } } diff --git a/examples/retained_image/Cargo.toml b/examples/images/Cargo.toml similarity index 62% rename from examples/retained_image/Cargo.toml rename to examples/images/Cargo.toml index 50cf0e5bb1f..851fa8f79ff 100644 --- a/examples/retained_image/Cargo.toml +++ b/examples/images/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "retained_image" +name = "images" version = "0.1.0" -authors = ["Emil Ernerfeldt "] +authors = ["Jan Procházka "] license = "MIT OR Apache-2.0" edition = "2021" rust-version = "1.70" @@ -10,8 +10,11 @@ publish = false [dependencies] eframe = { path = "../../crates/eframe", features = [ - "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO + "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO ] } -egui_extras = { path = "../../crates/egui_extras", features = ["image", "log"] } +egui_extras = { path = "../../crates/egui_extras", features = ["all_loaders"] } env_logger = "0.10" -image = { version = "0.24", default-features = false, features = ["png"] } +image = { version = "0.24", default-features = false, features = [ + "jpeg", + "png", +] } diff --git a/examples/images/README.md b/examples/images/README.md new file mode 100644 index 00000000000..02f5007a60c --- /dev/null +++ b/examples/images/README.md @@ -0,0 +1,7 @@ +Example showing how to show images with eframe/egui. + +```sh +cargo run -p images +``` + +![](screenshot.png) diff --git a/examples/images/screenshot.png b/examples/images/screenshot.png new file mode 100644 index 00000000000..97066fa7909 Binary files /dev/null and b/examples/images/screenshot.png differ diff --git a/examples/svg/src/rustacean-flat-happy.svg b/examples/images/src/ferris.svg similarity index 100% rename from examples/svg/src/rustacean-flat-happy.svg rename to examples/images/src/ferris.svg diff --git a/examples/images/src/main.rs b/examples/images/src/main.rs new file mode 100644 index 00000000000..d942a21c98f --- /dev/null +++ b/examples/images/src/main.rs @@ -0,0 +1,37 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release + +use eframe::egui; + +fn main() -> Result<(), eframe::Error> { + env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + let options = eframe::NativeOptions { + initial_window_size: Some(egui::vec2(600.0, 800.0)), + ..Default::default() + }; + eframe::run_native( + "Image Viewer", + options, + Box::new(|cc| { + // This gives us image support: + egui_extras::install_image_loaders(&cc.egui_ctx); + Box::::default() + }), + ) +} + +#[derive(Default)] +struct MyApp {} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + egui::ScrollArea::new([true, true]).show(ui, |ui| { + ui.image(egui::include_image!("ferris.svg")); + + ui.add( + egui::Image::new("https://picsum.photos/seed/1.759706314/1024").rounding(10.0), + ); + }); + }); + } +} diff --git a/examples/puffin_profiler/src/main.rs b/examples/puffin_profiler/src/main.rs index f8d52e91c1e..18b7f6e16f2 100644 --- a/examples/puffin_profiler/src/main.rs +++ b/examples/puffin_profiler/src/main.rs @@ -28,7 +28,7 @@ impl eframe::App for MyApp { ui.horizontal(|ui| { ui.monospace(cmd); if ui.small_button("📋").clicked() { - ui.output_mut(|o| o.copied_text = cmd.into()); + ui.ctx().copy_text(cmd.into()); } }); @@ -56,6 +56,12 @@ fn start_puffin_server() { Ok(puffin_server) => { eprintln!("Run: cargo install puffin_viewer && puffin_viewer --url 127.0.0.1:8585"); + std::process::Command::new("puffin_viewer") + .arg("--url") + .arg("127.0.0.1:8585") + .spawn() + .ok(); + // We can store the server if we want, but in this case we just want // it to keep running. Dropping it closes the server, so let's not drop it! #[allow(clippy::mem_forget)] diff --git a/examples/retained_image/README.md b/examples/retained_image/README.md deleted file mode 100644 index 122a7481a8a..00000000000 --- a/examples/retained_image/README.md +++ /dev/null @@ -1,7 +0,0 @@ -Example how to show an image with eframe/egui. - -```sh -cargo run -p retained_image -``` - -![](screenshot.png) diff --git a/examples/retained_image/screenshot.png b/examples/retained_image/screenshot.png deleted file mode 100644 index 9e2234e4860..00000000000 Binary files a/examples/retained_image/screenshot.png and /dev/null differ diff --git a/examples/retained_image/src/crab.png b/examples/retained_image/src/crab.png deleted file mode 100644 index 781b219937e..00000000000 Binary files a/examples/retained_image/src/crab.png and /dev/null differ diff --git a/examples/retained_image/src/main.rs b/examples/retained_image/src/main.rs deleted file mode 100644 index f96fb964232..00000000000 --- a/examples/retained_image/src/main.rs +++ /dev/null @@ -1,84 +0,0 @@ -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release - -use eframe::egui; -use egui_extras::RetainedImage; - -fn main() -> Result<(), eframe::Error> { - env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). - let options = eframe::NativeOptions { - initial_window_size: Some(egui::vec2(400.0, 1000.0)), - ..Default::default() - }; - eframe::run_native( - "Show an image with eframe/egui", - options, - Box::new(|_cc| Box::::default()), - ) -} - -struct MyApp { - image: RetainedImage, - rounding: f32, - tint: egui::Color32, -} - -impl Default for MyApp { - fn default() -> Self { - Self { - // crab image is CC0, found on https://stocksnap.io/search/crab - image: RetainedImage::from_image_bytes("crab.png", include_bytes!("crab.png")).unwrap(), - rounding: 32.0, - tint: egui::Color32::from_rgb(100, 200, 200), - } - } -} - -impl eframe::App for MyApp { - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - let Self { - image, - rounding, - tint, - } = self; - - egui::CentralPanel::default().show(ctx, |ui| { - ui.heading("This is an image:"); - image.show(ui); - - ui.add_space(32.0); - - ui.heading("This is a tinted image with rounded corners:"); - ui.add( - egui::Image::new(image.texture_id(ctx), image.size_vec2()) - .tint(*tint) - .rounding(*rounding), - ); - - ui.horizontal(|ui| { - ui.label("Tint:"); - egui::color_picker::color_edit_button_srgba( - ui, - tint, - egui::color_picker::Alpha::BlendOrAdditive, - ); - - ui.add_space(16.0); - - ui.label("Rounding:"); - ui.add( - egui::DragValue::new(rounding) - .speed(1.0) - .clamp_range(0.0..=0.5 * image.size_vec2().min_elem()), - ); - }); - - ui.add_space(32.0); - - ui.heading("This is an image you can click:"); - ui.add(egui::ImageButton::new( - image.texture_id(ctx), - image.size_vec2(), - )); - }); - } -} diff --git a/examples/save_plot/README.md b/examples/save_plot/README.md index 523af1d6b8f..4a6c2e0380e 100644 --- a/examples/save_plot/README.md +++ b/examples/save_plot/README.md @@ -3,3 +3,5 @@ This example shows that you can save a plot in egui as a png. ```sh cargo run -p save_plot ``` + +![](screenshot.png) diff --git a/examples/save_plot/screenshot.png b/examples/save_plot/screenshot.png new file mode 100644 index 00000000000..072810ef80e Binary files /dev/null and b/examples/save_plot/screenshot.png differ diff --git a/examples/save_plot/src/main.rs b/examples/save_plot/src/main.rs index c9adf1ec983..a13e071aa29 100644 --- a/examples/save_plot/src/main.rs +++ b/examples/save_plot/src/main.rs @@ -8,7 +8,7 @@ fn main() -> Result<(), eframe::Error> { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). let options = eframe::NativeOptions { - initial_window_size: Some(egui::vec2(350.0, 400.0)), + initial_window_size: Some(egui::vec2(350.0, 200.0)), ..Default::default() }; eframe::run_native( @@ -27,45 +27,19 @@ impl eframe::App for MyApp { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { let mut plot_rect = None; egui::CentralPanel::default().show(ctx, |ui| { - // these are just some dummy variables for the example, - // such that the plot is not at position (0,0) - let height = 200.0; - let border_x = 11.0; - let border_y = 18.0; - let width = 300.0; - - ui.heading("My egui Application"); - - // add some whitespace in y direction - ui.add_space(border_y); - if ui.button("Save Plot").clicked() { frame.request_screenshot(); } - // add some whitespace in y direction - ui.add_space(border_y); + let my_plot = Plot::new("My Plot").legend(Legend::default()); - ui.horizontal(|ui| { - // add some whitespace in x direction - ui.add_space(border_x); - - let my_plot = Plot::new("My Plot") - .height(height) - .width(width) - .legend(Legend::default()); - - // let's create a dummy line in the plot - let graph: Vec<[f64; 2]> = vec![[0.0, 1.0], [2.0, 3.0], [3.0, 2.0]]; - let inner = my_plot.show(ui, |plot_ui| { - plot_ui.line(Line::new(PlotPoints::from(graph)).name("curve")); - }); - // Remember the position of the plot - plot_rect = Some(inner.response.rect); + // let's create a dummy line in the plot + let graph: Vec<[f64; 2]> = vec![[0.0, 1.0], [2.0, 3.0], [3.0, 2.0]]; + let inner = my_plot.show(ui, |plot_ui| { + plot_ui.line(Line::new(PlotPoints::from(graph)).name("curve")); }); - - // add some whitespace in y direction - ui.add_space(border_y); + // Remember the position of the plot + plot_rect = Some(inner.response.rect); }); if let (Some(screenshot), Some(plot_location)) = (self.screenshot.take(), plot_rect) { diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 4dff5326df9..498d5013e0d 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -63,7 +63,7 @@ impl eframe::App for MyApp { }); if let Some(texture) = self.texture.as_ref() { - ui.image(texture, ui.available_size()); + ui.image((texture.id(), ui.available_size())); } else { ui.spinner(); } diff --git a/examples/svg/Cargo.toml b/examples/svg/Cargo.toml deleted file mode 100644 index 62333a09a3f..00000000000 --- a/examples/svg/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "svg" -version = "0.1.0" -authors = ["Emil Ernerfeldt "] -license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.70" -publish = false - - -[dependencies] -eframe = { path = "../../crates/eframe", features = [ - "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO -] } -egui_extras = { path = "../../crates/egui_extras", features = ["log", "svg"] } -env_logger = "0.10" diff --git a/examples/svg/README.md b/examples/svg/README.md deleted file mode 100644 index c171c172484..00000000000 --- a/examples/svg/README.md +++ /dev/null @@ -1,7 +0,0 @@ -Example how to show an SVG image. - -```sh -cargo run -p svg -``` - -![](screenshot.png) diff --git a/examples/svg/screenshot.png b/examples/svg/screenshot.png deleted file mode 100644 index 1a9664a82c8..00000000000 Binary files a/examples/svg/screenshot.png and /dev/null differ diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs deleted file mode 100644 index bba91d05980..00000000000 --- a/examples/svg/src/main.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! A good way of displaying an SVG image in egui. -//! -//! Requires the dependency `egui_extras` with the `svg` feature. - -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release - -use eframe::egui; - -fn main() -> Result<(), eframe::Error> { - env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). - let options = eframe::NativeOptions { - initial_window_size: Some(egui::vec2(1000.0, 700.0)), - ..Default::default() - }; - eframe::run_native( - "svg example", - options, - Box::new(|cc| { - // Without the following call, the `Image2` created below - // will simply output a `not supported` error message. - egui_extras::loaders::install(&cc.egui_ctx); - Box::new(MyApp) - }), - ) -} - -struct MyApp; - -impl eframe::App for MyApp { - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - egui::CentralPanel::default().show(ctx, |ui| { - ui.heading("SVG example"); - ui.label("The SVG is rasterized and displayed as a texture."); - - ui.separator(); - - let max_size = ui.available_size(); - ui.add( - egui::Image2::from_static_bytes( - "ferris.svg", - include_bytes!("rustacean-flat-happy.svg"), - ) - .size_hint(max_size), - ); - }); - } -} diff --git a/examples/svg/src/rust-logo-license.txt b/examples/svg/src/rust-logo-license.txt deleted file mode 100644 index 7efaf759388..00000000000 --- a/examples/svg/src/rust-logo-license.txt +++ /dev/null @@ -1 +0,0 @@ -Rust logo by Mozilla, from https://github.com/rust-lang/rust-artwork diff --git a/media/demo.gif b/media/demo.gif index 2cff5878afe..385c8228131 100644 Binary files a/media/demo.gif and b/media/demo.gif differ diff --git a/media/rerun_io_logo.png b/media/rerun_io_logo.png index 6af3db83490..cda0426627d 100644 Binary files a/media/rerun_io_logo.png and b/media/rerun_io_logo.png differ diff --git a/media/widget_gallery_0.23.gif b/media/widget_gallery_0.23.gif new file mode 100644 index 00000000000..666631079ec Binary files /dev/null and b/media/widget_gallery_0.23.gif differ diff --git a/media/widget_gallery_0.23_light.png b/media/widget_gallery_0.23_light.png new file mode 100644 index 00000000000..a975f97cdde Binary files /dev/null and b/media/widget_gallery_0.23_light.png differ diff --git a/scripts/build_demo_web.sh b/scripts/build_demo_web.sh index 37abcffc60d..0e776656802 100755 --- a/scripts/build_demo_web.sh +++ b/scripts/build_demo_web.sh @@ -12,8 +12,7 @@ export RUSTFLAGS=--cfg=web_sys_unstable_apis CRATE_NAME="egui_demo_app" - # NOTE: persistence use up about 400kB (10%) of the WASM! -FEATURES="http,persistence,web_screen_reader" +FEATURES="web_app" OPEN=false OPTIMIZE=false diff --git a/scripts/cargo_deny.sh b/scripts/cargo_deny.sh index 41b65fcda88..fa0ccc3c344 100755 --- a/scripts/cargo_deny.sh +++ b/scripts/cargo_deny.sh @@ -7,6 +7,7 @@ set -x # cargo install 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 diff --git a/scripts/generate_changelog.py b/scripts/generate_changelog.py index aed02dd0704..b9bb86e579e 100755 --- a/scripts/generate_changelog.py +++ b/scripts/generate_changelog.py @@ -3,7 +3,8 @@ """ Summarizes recent PRs based on their GitHub labels. -The result can be copy-pasted into CHANGELOG.md, though it often needs some manual editing too. +The result can be copy-pasted into CHANGELOG.md, +though it often needs some manual editing too. """ import multiprocessing @@ -89,7 +90,9 @@ def fetch_pr_info(pr_number: int) -> Optional[PrInfo]: def get_commit_info(commit: Any) -> CommitInfo: match = re.match(r"(.*) \(#(\d+)\)", commit.summary) if match: - return CommitInfo(hexsha=commit.hexsha, title=str(match.group(1)), pr_number=int(match.group(2))) + title = str(match.group(1)) + pr_number = int(match.group(2)) + return CommitInfo(hexsha=commit.hexsha, title=title, pr_number=pr_number) else: return CommitInfo(hexsha=commit.hexsha, title=commit.summary, pr_number=None) @@ -104,8 +107,9 @@ def print_section(crate: str, items: List[str]) -> None: if 0 < len(items): print(f"#### {crate}") for line in items: - line = remove_prefix(line, f"{crate}: ") line = remove_prefix(line, f"[{crate}] ") + line = remove_prefix(line, f"{crate}: ") + line = remove_prefix(line, f"`{crate}`: ") print(f"* {line}") print() @@ -152,9 +156,16 @@ def main() -> None: summary = f"{title} [{hexsha[:7]}](https://github.com/{OWNER}/{REPO}/commit/{hexsha})" unsorted_commits.append(summary) else: - title = pr_info.pr_title if pr_info else title # We prefer the PR title if available + # 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 [] + if 'exclude from changelog' in labels: + continue + if 'typo' in labels: + # We get so many typo PRs. Let's not flood the changelog with them. + continue + summary = f"{title} [#{pr_number}](https://github.com/{OWNER}/{REPO}/pull/{pr_number})" if INCLUDE_LABELS and 0 < len(labels): @@ -165,9 +176,6 @@ def main() -> None: if gh_user_name not in OFFICIAL_DEVS: summary += f" (thanks [@{gh_user_name}](https://github.com/{gh_user_name})!)" - if 'typo' in labels: - continue # We get so many typo PRs. Let's not flood the changelog with them. - added = False for crate in crate_names: