mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 14:49:06 -04:00
Merge branch 'main' into lucas/add-sense-long-click
This commit is contained in:
2
.github/workflows/preview_deploy.yml
vendored
2
.github/workflows/preview_deploy.yml
vendored
@@ -62,6 +62,6 @@ jobs:
|
||||
Preview available at https://egui-pr-preview.github.io/pr/${{ env.URL_SLUG }}
|
||||
Note that it might take a couple seconds for the update to show up after the preview_build workflow has completed.
|
||||
|
||||
View snapshot changes at [kitdiff](https://rerun-io.github.io/kitdiff/?url=${{ github.event.pull_request.html_url }})
|
||||
View snapshot changes at [kitdiff](https://rerun-io.github.io/kitdiff/?url=https://github.com/emilk/egui/pull/${{ env.PR_NUMBER }})
|
||||
pr_number: ${{ env.PR_NUMBER }}
|
||||
comment_tag: 'egui-preview'
|
||||
|
||||
6
.github/workflows/rust.yml
vendored
6
.github/workflows/rust.yml
vendored
@@ -46,7 +46,9 @@ jobs:
|
||||
|
||||
- run: cargo clippy --locked --no-default-features --lib --all-targets
|
||||
|
||||
- run: cargo clippy --locked --no-default-features --features x11 --lib -p eframe
|
||||
- run: cargo clippy --locked --no-default-features --lib -p eframe --features x11
|
||||
|
||||
- run: cargo clippy --locked --no-default-features --lib -p eframe --features x11,wgpu_no_default_features
|
||||
|
||||
- run: cargo clippy --locked --no-default-features --lib -p egui_extras
|
||||
|
||||
@@ -87,7 +89,7 @@ jobs:
|
||||
run: cargo clippy -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features
|
||||
|
||||
- name: clippy wasm32 eframe
|
||||
run: cargo clippy -p eframe --lib --no-default-features --features glow,persistence --target wasm32-unknown-unknown
|
||||
run: cargo clippy -p eframe --lib --no-default-features --features wgpu,persistence --target wasm32-unknown-unknown
|
||||
|
||||
- name: wasm-bindgen
|
||||
uses: jetli/wasm-bindgen-action@v0.1.0
|
||||
|
||||
@@ -5,7 +5,7 @@ Also see [`CONTRIBUTING.md`](CONTRIBUTING.md) for what to do before opening a PR
|
||||
|
||||
|
||||
## Crate overview
|
||||
The crates in this repository are: `egui, emath, epaint, epaint_default_fonts, egui_extras, egui-winit, egui_glow, egui_demo_lib, egui_demo_app`.
|
||||
The crates in this repository are: `egui, emath, epaint, epaint_default_fonts, egui_extras, egui-winit, egui_glow, egui-wgpu, egui_demo_lib, egui_demo_app`.
|
||||
|
||||
### `egui`: The main GUI library.
|
||||
Example code: `if ui.button("Click me").clicked() { … }`
|
||||
@@ -37,6 +37,9 @@ The library translates winit events to egui, handled copy/paste, updates the cur
|
||||
### `egui_glow`
|
||||
Puts an egui app inside a native window on your laptop. Paints the triangles that egui outputs using [glow](https://github.com/grovesNL/glow).
|
||||
|
||||
### `egui-wgpu`
|
||||
Paints the triangles that egui outputs using [wgpu](https://github.com/grovesNL/wgpu).
|
||||
|
||||
### `eframe`
|
||||
`eframe` is the official `egui` framework, built so you can compile the same app for either web or native.
|
||||
|
||||
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -14,6 +14,22 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 0.33.2 - 2025-11-13
|
||||
### ⭐ Added
|
||||
* Add `Plugin::on_widget_under_pointer` to support widget inspector [#7652](https://github.com/emilk/egui/pull/7652) by [@juancampa](https://github.com/juancampa)
|
||||
* Add `Response::total_drag_delta` and `PointerState::total_drag_delta` [#7708](https://github.com/emilk/egui/pull/7708) by [@emilk](https://github.com/emilk)
|
||||
|
||||
### 🔧 Changed
|
||||
* Improve accessibility and testability of `ComboBox` [#7658](https://github.com/emilk/egui/pull/7658) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
|
||||
### 🐛 Fixed
|
||||
* Fix `profiling::scope` compile error when profiling using `tracing` backend [#7646](https://github.com/emilk/egui/pull/7646) by [@PPakalns](https://github.com/PPakalns)
|
||||
* Fix edge cases in "smart aiming" in sliders [#7680](https://github.com/emilk/egui/pull/7680) by [@emilk](https://github.com/emilk)
|
||||
* Hide scroll bars when dragging other things [#7689](https://github.com/emilk/egui/pull/7689) by [@emilk](https://github.com/emilk)
|
||||
* Prevent widgets sometimes appearing to move relative to each other [#7710](https://github.com/emilk/egui/pull/7710) by [@emilk](https://github.com/emilk)
|
||||
* Fix `ui.response().interact(Sense::click())` being flakey [#7713](https://github.com/emilk/egui/pull/7713) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||
|
||||
|
||||
## 0.33.0 - 2025-10-09 - `egui::Plugin`, better kerning, kitdiff viewer
|
||||
Highlights from this release:
|
||||
- `egui::Plugin` a improved way to create and access egui plugins
|
||||
|
||||
64
Cargo.lock
64
Cargo.lock
@@ -1248,7 +1248,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53"
|
||||
|
||||
[[package]]
|
||||
name = "ecolor"
|
||||
version = "0.33.0"
|
||||
version = "0.33.2"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"cint",
|
||||
@@ -1260,7 +1260,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "eframe"
|
||||
version = "0.33.0"
|
||||
version = "0.33.2"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
@@ -1299,7 +1299,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui"
|
||||
version = "0.33.0"
|
||||
version = "0.33.2"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"ahash",
|
||||
@@ -1319,7 +1319,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui-wgpu"
|
||||
version = "0.33.0"
|
||||
version = "0.33.2"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bytemuck",
|
||||
@@ -1337,7 +1337,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui-winit"
|
||||
version = "0.33.0"
|
||||
version = "0.33.2"
|
||||
dependencies = [
|
||||
"accesskit_winit",
|
||||
"arboard",
|
||||
@@ -1360,7 +1360,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui_demo_app"
|
||||
version = "0.33.0"
|
||||
version = "0.33.2"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_consumer",
|
||||
@@ -1390,7 +1390,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui_demo_lib"
|
||||
version = "0.33.0"
|
||||
version = "0.33.2"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"criterion",
|
||||
@@ -1407,7 +1407,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui_extras"
|
||||
version = "0.33.0"
|
||||
version = "0.33.2"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"chrono",
|
||||
@@ -1426,7 +1426,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui_glow"
|
||||
version = "0.33.0"
|
||||
version = "0.33.2"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"document-features",
|
||||
@@ -1445,7 +1445,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "egui_kittest"
|
||||
version = "0.33.0"
|
||||
version = "0.33.2"
|
||||
dependencies = [
|
||||
"dify",
|
||||
"document-features",
|
||||
@@ -1457,13 +1457,15 @@ dependencies = [
|
||||
"kittest",
|
||||
"open",
|
||||
"pollster",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"toml",
|
||||
"wgpu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "egui_tests"
|
||||
version = "0.33.0"
|
||||
version = "0.33.2"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"egui_extras",
|
||||
@@ -1493,7 +1495,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||
|
||||
[[package]]
|
||||
name = "emath"
|
||||
version = "0.33.0"
|
||||
version = "0.33.2"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"document-features",
|
||||
@@ -1591,7 +1593,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "epaint"
|
||||
version = "0.33.0"
|
||||
version = "0.33.2"
|
||||
dependencies = [
|
||||
"ab_glyph",
|
||||
"ahash",
|
||||
@@ -1613,7 +1615,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "epaint_default_fonts"
|
||||
version = "0.33.0"
|
||||
version = "0.33.2"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
@@ -3425,7 +3427,7 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
||||
|
||||
[[package]]
|
||||
name = "popups"
|
||||
version = "0.33.0"
|
||||
version = "0.33.2"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"env_logger",
|
||||
@@ -4036,6 +4038,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial_windows"
|
||||
version = "0.1.0"
|
||||
@@ -4491,11 +4502,26 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
@@ -4504,6 +4530,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
@@ -5645,9 +5673,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.3"
|
||||
version = "0.7.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
|
||||
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -5748,7 +5776,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.33.0"
|
||||
version = "0.33.2"
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust"
|
||||
|
||||
30
Cargo.toml
30
Cargo.toml
@@ -24,7 +24,7 @@ members = [
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.88"
|
||||
version = "0.33.0"
|
||||
version = "0.33.2"
|
||||
|
||||
|
||||
[profile.release]
|
||||
@@ -55,18 +55,18 @@ opt-level = 2
|
||||
|
||||
|
||||
[workspace.dependencies]
|
||||
emath = { version = "0.33.0", path = "crates/emath", default-features = false }
|
||||
ecolor = { version = "0.33.0", path = "crates/ecolor", default-features = false }
|
||||
epaint = { version = "0.33.0", path = "crates/epaint", default-features = false }
|
||||
epaint_default_fonts = { version = "0.33.0", path = "crates/epaint_default_fonts" }
|
||||
egui = { version = "0.33.0", path = "crates/egui", default-features = false }
|
||||
egui-winit = { version = "0.33.0", path = "crates/egui-winit", default-features = false }
|
||||
egui_extras = { version = "0.33.0", path = "crates/egui_extras", default-features = false }
|
||||
egui-wgpu = { version = "0.33.0", path = "crates/egui-wgpu", default-features = false }
|
||||
egui_demo_lib = { version = "0.33.0", path = "crates/egui_demo_lib", default-features = false }
|
||||
egui_glow = { version = "0.33.0", path = "crates/egui_glow", default-features = false }
|
||||
egui_kittest = { version = "0.33.0", path = "crates/egui_kittest", default-features = false }
|
||||
eframe = { version = "0.33.0", path = "crates/eframe", default-features = false }
|
||||
emath = { version = "0.33.2", path = "crates/emath", default-features = false }
|
||||
ecolor = { version = "0.33.2", path = "crates/ecolor", default-features = false }
|
||||
epaint = { version = "0.33.2", path = "crates/epaint", default-features = false }
|
||||
epaint_default_fonts = { version = "0.33.2", path = "crates/epaint_default_fonts" }
|
||||
egui = { version = "0.33.2", path = "crates/egui", default-features = false }
|
||||
egui-winit = { version = "0.33.2", path = "crates/egui-winit", default-features = false }
|
||||
egui_extras = { version = "0.33.2", path = "crates/egui_extras", default-features = false }
|
||||
egui-wgpu = { version = "0.33.2", path = "crates/egui-wgpu", default-features = false }
|
||||
egui_demo_lib = { version = "0.33.2", path = "crates/egui_demo_lib", default-features = false }
|
||||
egui_glow = { version = "0.33.2", path = "crates/egui_glow", default-features = false }
|
||||
egui_kittest = { version = "0.33.2", path = "crates/egui_kittest", default-features = false }
|
||||
eframe = { version = "0.33.2", path = "crates/eframe", default-features = false }
|
||||
|
||||
accesskit = "0.21.1"
|
||||
accesskit_consumer = "0.30.1"
|
||||
@@ -131,6 +131,7 @@ syntect = { version = "5.3.0", default-features = false }
|
||||
tempfile = "3.23.0"
|
||||
thiserror = "2.0.17"
|
||||
tokio = "1.47.1"
|
||||
toml = "0.8"
|
||||
type-map = "0.5.1"
|
||||
unicode_names2 = { version = "2.0.0", default-features = false }
|
||||
unicode-segmentation = "1.12.0"
|
||||
@@ -232,6 +233,7 @@ iter_on_single_items = "warn"
|
||||
iter_over_hash_type = "warn"
|
||||
iter_without_into_iter = "warn"
|
||||
large_digit_groups = "warn"
|
||||
large_futures = "warn"
|
||||
large_include_file = "warn"
|
||||
large_stack_arrays = "warn"
|
||||
large_stack_frames = "warn"
|
||||
@@ -276,6 +278,7 @@ non_zero_suggestions = "warn"
|
||||
nonstandard_macro_braces = "warn"
|
||||
option_as_ref_cloned = "warn"
|
||||
option_option = "warn"
|
||||
or_fun_call = "warn"
|
||||
path_buf_push_overwrite = "warn"
|
||||
pathbuf_init_then_push = "warn"
|
||||
precedence_bits = "warn"
|
||||
@@ -329,6 +332,7 @@ unnecessary_semicolon = "warn"
|
||||
unnecessary_struct_initialization = "warn"
|
||||
unnecessary_wraps = "warn"
|
||||
unnested_or_patterns = "warn"
|
||||
unused_async = "warn"
|
||||
unused_peekable = "warn"
|
||||
unused_rounding = "warn"
|
||||
unused_self = "warn"
|
||||
|
||||
@@ -80,7 +80,7 @@ If you have questions, use [GitHub Discussions](https://github.com/emilk/egui/di
|
||||
|
||||
To test the demo app locally, run `cargo run --release -p egui_demo_app`.
|
||||
|
||||
The native backend is [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow) (using [`glow`](https://crates.io/crates/glow)) and should work out-of-the-box on Mac and Windows, but on Linux you need to first run:
|
||||
The native backend is [`egui-wgpu`](https://github.com/emilk/egui/tree/main/crates/egui-wgpu) (using [`wgpu`](https://crates.io/crates/wgpu)) and should work out-of-the-box on Mac and Windows, but on Linux you need to first run:
|
||||
|
||||
`sudo apt-get install -y libclang-dev libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev`
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 0.33.2 - 2025-11-13
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.33.0 - 2025-10-09
|
||||
* Align `Color32` to 4 bytes [#7318](https://github.com/emilk/egui/pull/7318) by [@anti-social](https://github.com/anti-social)
|
||||
* Make the `hex_color` macro `const` [#7444](https://github.com/emilk/egui/pull/7444) by [@YgorSouza](https://github.com/YgorSouza)
|
||||
|
||||
@@ -7,6 +7,11 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 0.33.2 - 2025-11-13
|
||||
* Fix jittering during window resize on MacOS for WGPU/Metal [#7641](https://github.com/emilk/egui/pull/7641) by [@aspcartman](https://github.com/aspcartman)
|
||||
* Make sure `native_pixels_per_point` is set during app creation [#7683](https://github.com/emilk/egui/pull/7683) by [@emilk](https://github.com/emilk)
|
||||
|
||||
|
||||
## 0.33.0 - 2025-10-09
|
||||
### ⭐ Added
|
||||
* Add an option to limit the repaint rate in the web runner [#7482](https://github.com/emilk/egui/pull/7482) by [@s-nie](https://github.com/s-nie)
|
||||
|
||||
@@ -28,19 +28,15 @@ workspace = true
|
||||
default = [
|
||||
"accesskit",
|
||||
"default_fonts",
|
||||
"glow",
|
||||
"wayland", # Required for Linux support (including CI!)
|
||||
"web_screen_reader",
|
||||
"wgpu",
|
||||
"winit/default",
|
||||
"x11",
|
||||
"egui-wgpu?/fragile-send-sync-non-atomic-wasm",
|
||||
# Let's enable some backends so that users can use `eframe` out-of-the-box
|
||||
# without having to explicitly opt-in to backends
|
||||
"egui-wgpu?/default",
|
||||
]
|
||||
|
||||
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
|
||||
accesskit = ["egui/accesskit", "egui-winit/accesskit"]
|
||||
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
|
||||
@@ -56,7 +52,11 @@ android-native-activity = ["egui-winit/android-native-activity"]
|
||||
## If you plan on specifying your own fonts you may disable this feature.
|
||||
default_fonts = ["egui/default_fonts"]
|
||||
|
||||
## Use [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow).
|
||||
## Enable [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow).
|
||||
##
|
||||
## There is generally no need to enable both the `wgpu` and `glow` features,
|
||||
## but if you do you can pick the renderer to use with [`NativeOptions::renderer`]
|
||||
## and `WebOptions::renderer`.
|
||||
glow = ["dep:egui_glow", "dep:glow", "dep:glutin-winit", "dep:glutin"]
|
||||
|
||||
## Enable saving app state to disk.
|
||||
@@ -78,20 +78,28 @@ wayland = [
|
||||
## For other platforms, use the `accesskit` feature instead.
|
||||
web_screen_reader = ["web-sys/SpeechSynthesis", "web-sys/SpeechSynthesisUtterance"]
|
||||
|
||||
## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/main/crates/egui-wgpu)).
|
||||
## Enable [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/main/crates/egui-wgpu)).
|
||||
##
|
||||
## This overrides the `glow` feature.
|
||||
## There is generally no need to enable both the `wgpu` and `glow` features,
|
||||
## but if you do you can pick the renderer to use with [`NativeOptions::renderer`]
|
||||
## and `WebOptions::renderer`.
|
||||
##
|
||||
## By default, only WebGPU is enabled on web.
|
||||
## If you want to enable WebGL, you need to turn on the `webgl` feature of crate `wgpu`:
|
||||
##
|
||||
## ```toml
|
||||
## wgpu = { version = "*", features = ["webgpu", "webgl"] }
|
||||
## ```
|
||||
## Switching from `wgpu (the default)` to `glow` can significantly reduce your binary size
|
||||
## (including the .wasm of a web app).
|
||||
## See <https://github.com/emilk/egui/issues/5889> for more details.
|
||||
##
|
||||
## By default, eframe will prefer WebGPU over WebGL, but
|
||||
## you can configure this at run-time with [`NativeOptions::wgpu_options`].
|
||||
wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"]
|
||||
wgpu = ["wgpu_no_default_features", "egui-wgpu/default"]
|
||||
|
||||
## This is exactly like the `wgpu` feature, but does NOT enable the default features of `wgpu` and `egui-wgpu`.
|
||||
##
|
||||
## This means that no `wgpu` backends are enabled. You will need to enable them yourself, e.g. like this:
|
||||
##
|
||||
## ```toml
|
||||
## wgpu = { version = "*", features = ["dx12", "metal", "webgl"] }
|
||||
## ```
|
||||
wgpu_no_default_features = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"]
|
||||
|
||||
## Enables compiling for x11.
|
||||
x11 = [
|
||||
@@ -137,6 +145,7 @@ winit = { workspace = true, default-features = false, features = ["rwh_06"] }
|
||||
# optional native:
|
||||
egui-wgpu = { workspace = true, optional = true, features = [
|
||||
"winit",
|
||||
"capture",
|
||||
] } # if wgpu is used, use it with winit
|
||||
pollster = { workspace = true, optional = true } # needed for wgpu
|
||||
|
||||
@@ -242,7 +251,7 @@ web-sys = { workspace = true, features = [
|
||||
] }
|
||||
|
||||
# optional web:
|
||||
egui-wgpu = { workspace = true, optional = true } # if wgpu is used, use it without (!) winit
|
||||
egui-wgpu = { workspace = true, optional = true, features = ["capture"] } # if wgpu is used, use it without (!) winit
|
||||
wgpu = { workspace = true, optional = true }
|
||||
|
||||
# Native dev dependencies for testing
|
||||
|
||||
@@ -16,7 +16,7 @@ For how to use `egui`, see [the egui docs](https://docs.rs/egui).
|
||||
|
||||
---
|
||||
|
||||
`eframe` uses [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow) for rendering, and on native it uses [`egui-winit`](https://github.com/emilk/egui/tree/main/crates/egui-winit).
|
||||
`eframe` defaults to using [wgpu](https://crates.io/crates/wgpu) for rendering (with an option to change to [glow](https://crates.io/crates/glow)), and on native it uses [`egui-winit`](https://github.com/emilk/egui/tree/main/crates/egui-winit).
|
||||
|
||||
To use on Linux, first run:
|
||||
|
||||
@@ -26,7 +26,7 @@ sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev lib
|
||||
|
||||
You need to either use `edition = "2024"`, or set `resolver = "2"` in the `[workspace]` section of your to-level `Cargo.toml`. See [this link](https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html) for more info.
|
||||
|
||||
You can opt-in to the using [`egui-wgpu`](https://github.com/emilk/egui/tree/main/crates/egui-wgpu) for rendering by enabling the `wgpu` feature and setting `NativeOptions::renderer` to `Renderer::Wgpu`.
|
||||
You can opt-in to the using [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow) for rendering by enabling the `glow` feature and setting `NativeOptions::renderer` to `Renderer::Glow`.
|
||||
|
||||
## Alternatives
|
||||
`eframe` is not the only way to write an app using `egui`! You can also try [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad), [`bevy_egui`](https://github.com/mvlabat/bevy_egui), [`egui_sdl2_gl`](https://github.com/ArjunNair/egui_sdl2_gl), and others.
|
||||
@@ -35,7 +35,7 @@ You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/win
|
||||
|
||||
|
||||
## Limitations when running egui on the web
|
||||
`eframe` uses WebGL (via [`glow`](https://crates.io/crates/glow)) and Wasm, and almost nothing else from the web tech stack. This has some benefits, but also produces some challenges and serious downsides.
|
||||
`eframe` and egui compiles to Wasm using either WebGPU (when available) or WebGL2 for rendering, and almost nothing else from the web tech stack. This has some benefits, but also produces some challenges and serious downsides.
|
||||
|
||||
* Rendering: Getting pixel-perfect rendering right on the web is very difficult.
|
||||
* Search: you cannot search an egui web page like you would a normal web page.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
use std::any::Any;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub use crate::native::winit_integration::UserEvent;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -22,7 +22,7 @@ use raw_window_handle::{
|
||||
use static_assertions::assert_not_impl_any;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub use winit::{event_loop::EventLoopBuilder, window::WindowAttributes};
|
||||
|
||||
/// Hook into the building of an event loop before it is run
|
||||
@@ -30,7 +30,7 @@ pub use winit::{event_loop::EventLoopBuilder, window::WindowAttributes};
|
||||
/// 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"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)>;
|
||||
|
||||
/// Hook into the building of a the native window.
|
||||
@@ -38,7 +38,7 @@ pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)
|
||||
/// 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"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub type WindowBuilderHook = Box<dyn FnOnce(egui::ViewportBuilder) -> egui::ViewportBuilder>;
|
||||
|
||||
type DynError = Box<dyn std::error::Error + Send + Sync>;
|
||||
@@ -79,7 +79,7 @@ pub struct CreationContext<'s> {
|
||||
/// Only available when compiling with the `wgpu` feature and using [`Renderer::Wgpu`].
|
||||
///
|
||||
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub wgpu_render_state: Option<egui_wgpu::RenderState>,
|
||||
|
||||
/// Raw platform window handle
|
||||
@@ -121,7 +121,7 @@ impl CreationContext<'_> {
|
||||
gl: None,
|
||||
#[cfg(feature = "glow")]
|
||||
get_proc_address: None,
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_render_state: None,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
raw_window_handle: Err(HandleError::NotSupported),
|
||||
@@ -137,7 +137,7 @@ impl CreationContext<'_> {
|
||||
pub trait App {
|
||||
/// Called each time the UI needs repainting, which may be many times per second.
|
||||
///
|
||||
/// Put your widgets into a [`egui::SidePanel`], [`egui::TopBottomPanel`], [`egui::CentralPanel`], [`egui::Window`] or [`egui::Area`].
|
||||
/// Put your widgets into a [`egui::Panel`], [`egui::CentralPanel`], [`egui::Window`] or [`egui::Area`].
|
||||
///
|
||||
/// The [`egui::Context`] can be cloned and saved if you like.
|
||||
///
|
||||
@@ -317,7 +317,7 @@ pub struct NativeOptions {
|
||||
pub hardware_acceleration: HardwareAcceleration,
|
||||
|
||||
/// What rendering backend to use.
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub renderer: Renderer,
|
||||
|
||||
/// This controls what happens when you close the main eframe window.
|
||||
@@ -340,7 +340,7 @@ pub struct NativeOptions {
|
||||
/// event loop before it is run.
|
||||
///
|
||||
/// Note: A [`NativeOptions`] clone will not include any `event_loop_builder` hook.
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub event_loop_builder: Option<EventLoopBuilderHook>,
|
||||
|
||||
/// Hook into the building of a window.
|
||||
@@ -349,7 +349,7 @@ pub struct NativeOptions {
|
||||
/// window appearance.
|
||||
///
|
||||
/// Note: A [`NativeOptions`] clone will not include any `window_builder` hook.
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub window_builder: Option<WindowBuilderHook>,
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
@@ -367,7 +367,7 @@ pub struct NativeOptions {
|
||||
pub centered: bool,
|
||||
|
||||
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub wgpu_options: egui_wgpu::WgpuConfiguration,
|
||||
|
||||
/// Controls whether or not the native window position and size will be
|
||||
@@ -404,13 +404,13 @@ impl Clone for NativeOptions {
|
||||
Self {
|
||||
viewport: self.viewport.clone(),
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
event_loop_builder: None, // Skip any builder callbacks if cloning
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
window_builder: None, // Skip any builder callbacks if cloning
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_options: self.wgpu_options.clone(),
|
||||
|
||||
persistence_path: self.persistence_path.clone(),
|
||||
@@ -435,15 +435,15 @@ impl Default for NativeOptions {
|
||||
stencil_buffer: 0,
|
||||
hardware_acceleration: HardwareAcceleration::Preferred,
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
renderer: Renderer::default(),
|
||||
|
||||
run_and_return: true,
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
event_loop_builder: None,
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
window_builder: None,
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
@@ -451,7 +451,7 @@ impl Default for NativeOptions {
|
||||
|
||||
centered: false,
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
|
||||
|
||||
persist_window: true,
|
||||
@@ -471,6 +471,10 @@ impl Default for NativeOptions {
|
||||
/// Options when using `eframe` in a web page.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub struct WebOptions {
|
||||
/// What rendering backend to use.
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub renderer: Renderer,
|
||||
|
||||
/// Sets the number of bits in the depth buffer.
|
||||
///
|
||||
/// `egui` doesn't need the depth buffer, so the default value is 0.
|
||||
@@ -484,7 +488,7 @@ pub struct WebOptions {
|
||||
pub webgl_context_option: WebGlContextOption,
|
||||
|
||||
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub wgpu_options: egui_wgpu::WgpuConfiguration,
|
||||
|
||||
/// Controls whether to apply dithering to minimize banding artifacts.
|
||||
@@ -519,12 +523,15 @@ pub struct WebOptions {
|
||||
impl Default for WebOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
renderer: Renderer::default(),
|
||||
|
||||
depth_buffer: 0,
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
webgl_context_option: WebGlContextOption::BestFirst,
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
|
||||
|
||||
dithering: true,
|
||||
@@ -561,7 +568,7 @@ pub enum WebGlContextOption {
|
||||
/// What rendering backend to use.
|
||||
///
|
||||
/// You need to enable the "glow" and "wgpu" features to have a choice.
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
|
||||
@@ -571,49 +578,49 @@ pub enum Renderer {
|
||||
Glow,
|
||||
|
||||
/// Use [`egui_wgpu`] renderer for [`wgpu`](https://github.com/gfx-rs/wgpu).
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
Wgpu,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
impl Default for Renderer {
|
||||
fn default() -> Self {
|
||||
#[cfg(not(feature = "glow"))]
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
#[cfg(not(feature = "wgpu_no_default_features"))]
|
||||
compile_error!(
|
||||
"eframe: you must enable at least one of the rendering backend features: 'glow' or 'wgpu'"
|
||||
);
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
#[cfg(not(feature = "wgpu_no_default_features"))]
|
||||
return Self::Glow;
|
||||
|
||||
#[cfg(not(feature = "glow"))]
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
return Self::Wgpu;
|
||||
|
||||
// By default, only the `glow` feature is enabled, so if the user added `wgpu` to the feature list
|
||||
// they probably wanted to use wgpu:
|
||||
// It's weird that the user has enabled both glow and wgpu,
|
||||
// but let's pick the better of the two (wgpu):
|
||||
#[cfg(feature = "glow")]
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
return Self::Wgpu;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
impl std::fmt::Display for Renderer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
#[cfg(feature = "glow")]
|
||||
Self::Glow => "glow".fmt(f),
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
Self::Wgpu => "wgpu".fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
impl std::str::FromStr for Renderer {
|
||||
type Err = String;
|
||||
|
||||
@@ -622,7 +629,7 @@ impl std::str::FromStr for Renderer {
|
||||
#[cfg(feature = "glow")]
|
||||
"glow" => Ok(Self::Glow),
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
"wgpu" => Ok(Self::Wgpu),
|
||||
|
||||
_ => Err(format!(
|
||||
@@ -655,7 +662,7 @@ pub struct Frame {
|
||||
Option<Box<dyn FnMut(glow::Texture) -> egui::TextureId>>,
|
||||
|
||||
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
#[doc(hidden)]
|
||||
pub wgpu_render_state: Option<egui_wgpu::RenderState>,
|
||||
|
||||
@@ -705,7 +712,7 @@ impl Frame {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
raw_window_handle: Err(HandleError::NotSupported),
|
||||
storage: None,
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_render_state: None,
|
||||
}
|
||||
}
|
||||
@@ -764,7 +771,7 @@ impl Frame {
|
||||
/// Only available when compiling with the `wgpu` feature and using [`Renderer::Wgpu`].
|
||||
///
|
||||
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub fn wgpu_render_state(&self) -> Option<&egui_wgpu::RenderState> {
|
||||
self.wgpu_render_state.as_ref()
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ pub use {egui, egui::emath, egui::epaint};
|
||||
#[cfg(feature = "glow")]
|
||||
pub use {egui_glow, glow};
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub use {egui_wgpu, wgpu};
|
||||
|
||||
mod epi;
|
||||
@@ -188,19 +188,19 @@ pub use web::{WebLogger, WebRunner};
|
||||
// When compiling natively
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
mod native;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub use native::run::EframeWinitApplication;
|
||||
|
||||
#[cfg(not(any(target_arch = "wasm32", target_os = "ios")))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub use native::run::EframePumpStatus;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
#[cfg(feature = "persistence")]
|
||||
pub use native::file_storage::storage_dir;
|
||||
|
||||
@@ -252,7 +252,7 @@ pub mod icon_data;
|
||||
/// # Errors
|
||||
/// This function can fail if we fail to set up a graphics context.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
#[allow(clippy::needless_pass_by_value, clippy::allow_attributes)]
|
||||
pub fn run_native(
|
||||
app_name: &str,
|
||||
@@ -268,7 +268,7 @@ pub fn run_native(
|
||||
native::run::run_glow(app_name, native_options, app_creator)
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
Renderer::Wgpu => {
|
||||
log::debug!("Using the wgpu renderer");
|
||||
native::run::run_wgpu(app_name, native_options, app_creator)
|
||||
@@ -322,7 +322,7 @@ pub fn run_native(
|
||||
///
|
||||
/// See the `external_eventloop` example for a more complete example.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub fn create_native<'a>(
|
||||
app_name: &str,
|
||||
mut native_options: NativeOptions,
|
||||
@@ -343,7 +343,7 @@ pub fn create_native<'a>(
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
Renderer::Wgpu => {
|
||||
log::debug!("Using the wgpu renderer");
|
||||
EframeWinitApplication::new(native::run::create_wgpu(
|
||||
@@ -357,7 +357,7 @@ pub fn create_native<'a>(
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
fn init_native(app_name: &str, native_options: &mut NativeOptions) -> Renderer {
|
||||
#[cfg(not(feature = "__screenshot"))]
|
||||
assert!(
|
||||
@@ -371,7 +371,7 @@ fn init_native(app_name: &str, native_options: &mut NativeOptions) -> Renderer {
|
||||
|
||||
let renderer = native_options.renderer;
|
||||
|
||||
#[cfg(all(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(all(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
{
|
||||
match native_options.renderer {
|
||||
Renderer::Glow => "glow",
|
||||
@@ -420,7 +420,7 @@ fn init_native(app_name: &str, native_options: &mut NativeOptions) -> Renderer {
|
||||
/// # Errors
|
||||
/// This function can fail if we fail to set up a graphics context.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
pub fn run_simple_native(
|
||||
app_name: &str,
|
||||
native_options: NativeOptions,
|
||||
@@ -472,7 +472,7 @@ pub enum Error {
|
||||
OpenGL(egui_glow::PainterError),
|
||||
|
||||
/// An error from [`wgpu`].
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
Wgpu(egui_wgpu::WgpuError),
|
||||
}
|
||||
|
||||
@@ -510,7 +510,7 @@ impl From<egui_glow::PainterError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
impl From<egui_wgpu::WgpuError> for Error {
|
||||
#[inline]
|
||||
fn from(err: egui_wgpu::WgpuError) -> Self {
|
||||
@@ -551,7 +551,7 @@ impl std::fmt::Display for Error {
|
||||
write!(f, "egui_glow: {err}")
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
Self::Wgpu(err) => {
|
||||
write!(f, "WGPU error: {err}")
|
||||
}
|
||||
|
||||
@@ -179,7 +179,9 @@ impl EpiIntegration {
|
||||
#[cfg(feature = "glow")] glow_register_native_texture: Option<
|
||||
Box<dyn FnMut(glow::Texture) -> egui::TextureId>,
|
||||
>,
|
||||
#[cfg(feature = "wgpu")] wgpu_render_state: Option<egui_wgpu::RenderState>,
|
||||
#[cfg(feature = "wgpu_no_default_features")] wgpu_render_state: Option<
|
||||
egui_wgpu::RenderState,
|
||||
>,
|
||||
) -> Self {
|
||||
let frame = epi::Frame {
|
||||
info: epi::IntegrationInfo { cpu_usage: None },
|
||||
@@ -188,7 +190,7 @@ impl EpiIntegration {
|
||||
gl,
|
||||
#[cfg(feature = "glow")]
|
||||
glow_register_native_texture,
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_render_state,
|
||||
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
|
||||
raw_window_handle: window.window_handle().map(|h| h.as_raw()),
|
||||
|
||||
@@ -239,7 +239,7 @@ impl<'app> GlowWinitApp<'app> {
|
||||
let painter = painter.clone();
|
||||
move |native| painter.borrow_mut().register_native_texture(native)
|
||||
})),
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -301,7 +301,7 @@ impl<'app> GlowWinitApp<'app> {
|
||||
storage: integration.frame.storage(),
|
||||
gl: Some(gl),
|
||||
get_proc_address: Some(&get_proc_address),
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_render_state: None,
|
||||
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
|
||||
raw_window_handle: window.window_handle().map(|h| h.as_raw()),
|
||||
@@ -719,11 +719,11 @@ impl GlowWinitRunning<'_> {
|
||||
// vsync - don't count as frame-time:
|
||||
frame_timer.pause();
|
||||
profiling::scope!("swap_buffers");
|
||||
let context = current_gl_context
|
||||
.as_ref()
|
||||
.ok_or(egui_glow::PainterError::from(
|
||||
let context = current_gl_context.as_ref().ok_or_else(|| {
|
||||
egui_glow::PainterError::from(
|
||||
"failed to get current context to swap buffers".to_owned(),
|
||||
))?;
|
||||
)
|
||||
})?;
|
||||
|
||||
gl_surface.swap_buffers(context)?;
|
||||
frame_timer.resume();
|
||||
@@ -1041,11 +1041,23 @@ impl GlutinWindowContext {
|
||||
|
||||
let mut viewport_from_window = HashMap::default();
|
||||
let mut window_from_viewport = OrderedViewportIdMap::default();
|
||||
let mut info = ViewportInfo::default();
|
||||
let mut viewport_info = ViewportInfo::default();
|
||||
if let Some(window) = &window {
|
||||
viewport_from_window.insert(window.id(), ViewportId::ROOT);
|
||||
window_from_viewport.insert(ViewportId::ROOT, window.id());
|
||||
egui_winit::update_viewport_info(&mut info, egui_ctx, window, true);
|
||||
egui_winit::update_viewport_info(&mut viewport_info, egui_ctx, window, true);
|
||||
|
||||
// Tell egui right away about native_pixels_per_point etc,
|
||||
// so that the app knows about it during app creation:
|
||||
let pixels_per_point = egui_winit::pixels_per_point(egui_ctx, window);
|
||||
|
||||
egui_ctx.input_mut(|i| {
|
||||
i.raw
|
||||
.viewports
|
||||
.insert(ViewportId::ROOT, viewport_info.clone());
|
||||
|
||||
i.pixels_per_point = pixels_per_point;
|
||||
});
|
||||
}
|
||||
|
||||
let mut viewports = OrderedViewportIdMap::default();
|
||||
@@ -1056,7 +1068,7 @@ impl GlutinWindowContext {
|
||||
class: ViewportClass::Root,
|
||||
builder: viewport_builder,
|
||||
deferred_commands: vec![],
|
||||
info,
|
||||
info: viewport_info,
|
||||
actions_requested: Default::default(),
|
||||
viewport_ui_cb: None,
|
||||
gl_surface: None,
|
||||
|
||||
@@ -12,5 +12,5 @@ pub(crate) mod winit_integration;
|
||||
#[cfg(feature = "glow")]
|
||||
mod glow_integration;
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
mod wgpu_integration;
|
||||
|
||||
@@ -381,7 +381,7 @@ pub fn create_glow<'a>(
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub fn run_wgpu(
|
||||
app_name: &str,
|
||||
mut native_options: epi::NativeOptions,
|
||||
@@ -404,7 +404,7 @@ pub fn run_wgpu(
|
||||
run_and_exit(event_loop, wgpu_eframe)
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
pub fn create_wgpu<'a>(
|
||||
app_name: &str,
|
||||
native_options: epi::NativeOptions,
|
||||
|
||||
@@ -71,6 +71,7 @@ pub struct SharedState {
|
||||
painter: egui_wgpu::winit::Painter,
|
||||
viewport_from_window: HashMap<WindowId, ViewportId>,
|
||||
focused_viewport: Option<ViewportId>,
|
||||
resized_viewport: Option<ViewportId>,
|
||||
}
|
||||
|
||||
pub type Viewports = egui::OrderedViewportIdMap<Viewport>;
|
||||
@@ -198,6 +199,22 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
},
|
||||
));
|
||||
|
||||
let mut viewport_info = ViewportInfo::default();
|
||||
egui_winit::update_viewport_info(&mut viewport_info, &egui_ctx, &window, true);
|
||||
|
||||
{
|
||||
// Tell egui right away about native_pixels_per_point etc,
|
||||
// so that the app knows about it during app creation:
|
||||
let pixels_per_point = egui_winit::pixels_per_point(&egui_ctx, &window);
|
||||
|
||||
egui_ctx.input_mut(|i| {
|
||||
i.raw
|
||||
.viewports
|
||||
.insert(ViewportId::ROOT, viewport_info.clone());
|
||||
i.pixels_per_point = pixels_per_point;
|
||||
});
|
||||
}
|
||||
|
||||
let window = Arc::new(window);
|
||||
|
||||
{
|
||||
@@ -277,9 +294,6 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
let mut viewport_from_window = HashMap::default();
|
||||
viewport_from_window.insert(window.id(), ViewportId::ROOT);
|
||||
|
||||
let mut info = ViewportInfo::default();
|
||||
egui_winit::update_viewport_info(&mut info, &egui_ctx, &window, true);
|
||||
|
||||
let mut viewports = Viewports::default();
|
||||
viewports.insert(
|
||||
ViewportId::ROOT,
|
||||
@@ -288,7 +302,7 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
class: ViewportClass::Root,
|
||||
builder,
|
||||
deferred_commands: vec![],
|
||||
info,
|
||||
info: viewport_info,
|
||||
actions_requested: Default::default(),
|
||||
viewport_ui_cb: None,
|
||||
window: Some(window),
|
||||
@@ -302,6 +316,7 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
viewports,
|
||||
painter,
|
||||
focused_viewport: Some(ViewportId::ROOT),
|
||||
resized_viewport: None,
|
||||
}));
|
||||
|
||||
{
|
||||
@@ -763,20 +778,34 @@ impl WgpuWinitRunning<'_> {
|
||||
let viewport_id = shared.viewport_from_window.get(&window_id).copied();
|
||||
|
||||
// On Windows, if a window is resized by the user, it should repaint synchronously, inside the
|
||||
// event handler.
|
||||
//
|
||||
// If this is not done, the compositor will assume that the window does not want to redraw,
|
||||
// and continue ahead.
|
||||
// event handler. If this is not done, the compositor will assume that the window does not want
|
||||
// to redraw and continue ahead.
|
||||
//
|
||||
// In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver
|
||||
// new frames to the compositor in time.
|
||||
//
|
||||
// The flickering is technically glutin or glow's fault, but we should be responding properly
|
||||
// new frames to the compositor in time. The flickering is technically glutin or glow's fault, but we should be responding properly
|
||||
// to resizes anyway, as doing so avoids dropping frames.
|
||||
//
|
||||
// See: https://github.com/emilk/egui/issues/903
|
||||
let mut repaint_asap = false;
|
||||
|
||||
// On MacOS the asap repaint is not enough. The drawn frames must be synchronized with
|
||||
// the CoreAnimation transactions driving the window resize process.
|
||||
//
|
||||
// Thus, Painter, responsible for wgpu surfaces and their resize, has to be notified of the
|
||||
// resize lifecycle, yet winit does not provide any events for that. To work around,
|
||||
// the last resized viewport is tracked until any next non-resize event is received.
|
||||
//
|
||||
// Accidental state change during the resize process due to an unexpected event fire
|
||||
// is ok, state will switch back upon next resize event.
|
||||
//
|
||||
// See: https://github.com/emilk/egui/issues/903
|
||||
if let Some(id) = viewport_id
|
||||
&& shared.resized_viewport == viewport_id
|
||||
{
|
||||
shared.painter.on_window_resize_state_change(id, false);
|
||||
shared.resized_viewport = None;
|
||||
}
|
||||
|
||||
match event {
|
||||
winit::event::WindowEvent::Focused(focused) => {
|
||||
let focused = if cfg!(target_os = "macos")
|
||||
@@ -799,14 +828,18 @@ impl WgpuWinitRunning<'_> {
|
||||
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
|
||||
// See: https://github.com/rust-windowing/winit/issues/208
|
||||
// This solves an issue where the app would panic when minimizing on Windows.
|
||||
if let Some(viewport_id) = viewport_id
|
||||
if let Some(id) = viewport_id
|
||||
&& let (Some(width), Some(height)) = (
|
||||
NonZeroU32::new(physical_size.width),
|
||||
NonZeroU32::new(physical_size.height),
|
||||
)
|
||||
{
|
||||
if shared.resized_viewport != viewport_id {
|
||||
shared.resized_viewport = viewport_id;
|
||||
shared.painter.on_window_resize_state_change(id, true);
|
||||
}
|
||||
shared.painter.on_window_resized(id, width, height);
|
||||
repaint_asap = true;
|
||||
shared.painter.on_window_resized(viewport_id, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use egui::{TexturesDelta, UserData, ViewportCommand};
|
||||
|
||||
use crate::{App, epi};
|
||||
use crate::{App, epi, web::web_painter::WebPainter};
|
||||
|
||||
use super::{NeedRepaint, now_sec, text_agent::TextAgent, web_painter::WebPainter as _};
|
||||
use super::{NeedRepaint, now_sec, text_agent::TextAgent};
|
||||
|
||||
pub struct AppRunner {
|
||||
#[allow(dead_code, clippy::allow_attributes)]
|
||||
pub(crate) web_options: crate::WebOptions,
|
||||
pub(crate) frame: epi::Frame,
|
||||
egui_ctx: egui::Context,
|
||||
painter: super::ActiveWebPainter,
|
||||
painter: Box<dyn WebPainter>,
|
||||
pub(crate) input: super::WebInput,
|
||||
app: Box<dyn epi::App>,
|
||||
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
|
||||
@@ -34,6 +34,10 @@ impl Drop for AppRunner {
|
||||
impl AppRunner {
|
||||
/// # Errors
|
||||
/// Failure to initialize WebGL renderer, or failure to create app.
|
||||
#[cfg_attr(
|
||||
not(feature = "wgpu_no_default_features"),
|
||||
expect(clippy::unused_async)
|
||||
)]
|
||||
pub async fn new(
|
||||
canvas: web_sys::HtmlCanvasElement,
|
||||
web_options: crate::WebOptions,
|
||||
@@ -41,7 +45,41 @@ impl AppRunner {
|
||||
text_agent: TextAgent,
|
||||
) -> Result<Self, String> {
|
||||
let egui_ctx = egui::Context::default();
|
||||
let painter = super::ActiveWebPainter::new(egui_ctx.clone(), canvas, &web_options).await?;
|
||||
|
||||
#[allow(clippy::allow_attributes, unused_assignments)]
|
||||
#[cfg(feature = "glow")]
|
||||
let mut gl = None;
|
||||
|
||||
#[allow(clippy::allow_attributes, unused_assignments)]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
let mut wgpu_render_state = None;
|
||||
|
||||
let painter = match web_options.renderer {
|
||||
#[cfg(feature = "glow")]
|
||||
epi::Renderer::Glow => {
|
||||
log::debug!("Using the glow renderer");
|
||||
let painter = super::web_painter_glow::WebPainterGlow::new(
|
||||
egui_ctx.clone(),
|
||||
canvas,
|
||||
&web_options,
|
||||
)?;
|
||||
gl = Some(painter.gl().clone());
|
||||
Box::new(painter) as Box<dyn WebPainter>
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
epi::Renderer::Wgpu => {
|
||||
log::debug!("Using the wgpu renderer");
|
||||
let painter = super::web_painter_wgpu::WebPainterWgpu::new(
|
||||
egui_ctx.clone(),
|
||||
canvas,
|
||||
&web_options,
|
||||
)
|
||||
.await?;
|
||||
wgpu_render_state = painter.render_state();
|
||||
Box::new(painter) as Box<dyn WebPainter>
|
||||
}
|
||||
};
|
||||
|
||||
let info = epi::IntegrationInfo {
|
||||
web_info: epi::WebInfo {
|
||||
@@ -65,21 +103,27 @@ impl AppRunner {
|
||||
o.zoom_factor = 1.0;
|
||||
});
|
||||
|
||||
// Tell egui right away about native_pixels_per_point
|
||||
// so that the app knows about it during app creation:
|
||||
egui_ctx.input_mut(|i| {
|
||||
let viewport_info = i.raw.viewports.entry(egui::ViewportId::ROOT).or_default();
|
||||
viewport_info.native_pixels_per_point = Some(super::native_pixels_per_point());
|
||||
i.pixels_per_point = super::native_pixels_per_point();
|
||||
});
|
||||
|
||||
let cc = epi::CreationContext {
|
||||
egui_ctx: egui_ctx.clone(),
|
||||
integration_info: info.clone(),
|
||||
storage: Some(&storage),
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
gl: Some(painter.gl().clone()),
|
||||
gl: gl.clone(),
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
get_proc_address: None,
|
||||
|
||||
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
|
||||
wgpu_render_state: painter.render_state(),
|
||||
#[cfg(all(feature = "wgpu", feature = "glow"))]
|
||||
wgpu_render_state: None,
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_render_state: wgpu_render_state.clone(),
|
||||
};
|
||||
let app = app_creator(&cc).map_err(|err| err.to_string())?;
|
||||
|
||||
@@ -88,12 +132,10 @@ impl AppRunner {
|
||||
storage: Some(Box::new(storage)),
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
gl: Some(painter.gl().clone()),
|
||||
gl,
|
||||
|
||||
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
|
||||
wgpu_render_state: painter.render_state(),
|
||||
#[cfg(all(feature = "wgpu", feature = "glow"))]
|
||||
wgpu_render_state: None,
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
wgpu_render_state,
|
||||
};
|
||||
|
||||
let needs_repaint: std::sync::Arc<NeedRepaint> =
|
||||
@@ -316,8 +358,7 @@ impl AppRunner {
|
||||
events: _, // already handled
|
||||
mutable_text_under_cursor: _, // TODO(#4569): https://github.com/emilk/egui/issues/4569
|
||||
ime,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_update: _, // not currently implemented
|
||||
accesskit_update: _, // not currently implemented
|
||||
num_completed_passes: _, // handled by `Context::run`
|
||||
request_discard_reasons: _, // handled by `Context::run`
|
||||
} = platform_output;
|
||||
|
||||
@@ -831,6 +831,7 @@ fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
|
||||
unit,
|
||||
delta,
|
||||
modifiers,
|
||||
phase: egui::TouchPhase::Move,
|
||||
}
|
||||
};
|
||||
let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
|
||||
|
||||
@@ -23,20 +23,16 @@ pub use panic_handler::{PanicHandler, PanicSummary};
|
||||
pub use web_logger::WebLogger;
|
||||
pub use web_runner::WebRunner;
|
||||
|
||||
#[cfg(not(any(feature = "glow", feature = "wgpu")))]
|
||||
#[cfg(not(any(feature = "glow", feature = "wgpu_no_default_features")))]
|
||||
compile_error!("You must enable either the 'glow' or 'wgpu' feature");
|
||||
|
||||
mod web_painter;
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
mod web_painter_glow;
|
||||
#[cfg(feature = "glow")]
|
||||
pub(crate) type ActiveWebPainter = web_painter_glow::WebPainterGlow;
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[cfg(feature = "wgpu_no_default_features")]
|
||||
mod web_painter_wgpu;
|
||||
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
|
||||
pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu;
|
||||
|
||||
pub use backend::*;
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ impl WebPainterGlow {
|
||||
self.painter.gl()
|
||||
}
|
||||
|
||||
pub async fn new(
|
||||
pub fn new(
|
||||
_ctx: egui::Context,
|
||||
canvas: HtmlCanvasElement,
|
||||
options: &WebOptions,
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::web_painter::WebPainter;
|
||||
use crate::WebOptions;
|
||||
use egui::{Event, UserData, ViewportId};
|
||||
use egui_wgpu::capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel};
|
||||
use egui_wgpu::{RenderState, SurfaceErrorAction};
|
||||
use egui_wgpu::{
|
||||
RenderState, SurfaceErrorAction,
|
||||
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
|
||||
};
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::HtmlCanvasElement;
|
||||
|
||||
use super::web_painter::WebPainter;
|
||||
|
||||
pub(crate) struct WebPainterWgpu {
|
||||
canvas: HtmlCanvasElement,
|
||||
surface: wgpu::Surface<'static>,
|
||||
@@ -23,7 +25,6 @@ pub(crate) struct WebPainterWgpu {
|
||||
}
|
||||
|
||||
impl WebPainterWgpu {
|
||||
#[expect(unused)] // only used if `wgpu` is the only active feature.
|
||||
pub fn render_state(&self) -> Option<RenderState> {
|
||||
self.render_state.clone()
|
||||
}
|
||||
@@ -55,11 +56,10 @@ impl WebPainterWgpu {
|
||||
})
|
||||
}
|
||||
|
||||
#[expect(unused)] // only used if `wgpu` is the only active feature.
|
||||
pub async fn new(
|
||||
ctx: egui::Context,
|
||||
canvas: web_sys::HtmlCanvasElement,
|
||||
options: &WebOptions,
|
||||
options: &crate::WebOptions,
|
||||
) -> Result<Self, String> {
|
||||
log::debug!("Creating wgpu painter");
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 0.33.2 - 2025-11-13
|
||||
* Fix jittering during window resize on MacOS for WGPU/Metal [#7641](https://github.com/emilk/egui/pull/7641) by [@aspcartman](https://github.com/aspcartman)
|
||||
|
||||
|
||||
## 0.33.0 - 2025-10-09
|
||||
### 🔧 Changed
|
||||
* Update wgpu to 26 and wasm-bindgen to 0.2.100 [#7540](https://github.com/emilk/egui/pull/7540) by [@Kumpelinus](https://github.com/Kumpelinus)
|
||||
|
||||
@@ -25,10 +25,13 @@ all-features = true
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
|
||||
[features]
|
||||
default = ["fragile-send-sync-non-atomic-wasm", "wgpu/default"]
|
||||
default = ["fragile-send-sync-non-atomic-wasm", "macos-window-resize-jitter-fix", "wgpu/default"]
|
||||
|
||||
## Enables the `capture` module for capturing screenshots.
|
||||
capture = ["dep:egui"]
|
||||
|
||||
## Enable [`winit`](https://docs.rs/winit) integration. On Linux, requires either `wayland` or `x11`
|
||||
winit = ["dep:winit", "winit/rwh_06"]
|
||||
winit = ["dep:winit", "winit/rwh_06", "dep:egui", "capture"]
|
||||
|
||||
## Enables Wayland support for winit.
|
||||
wayland = ["winit?/wayland"]
|
||||
@@ -43,8 +46,10 @@ x11 = ["winit?/x11"]
|
||||
## Thus that usage is guarded against with compiler errors in wgpu.
|
||||
fragile-send-sync-non-atomic-wasm = ["wgpu/fragile-send-sync-non-atomic-wasm"]
|
||||
|
||||
## Enables `present_with_transaction` surface flag temporary during window resize on MacOS.
|
||||
macos-window-resize-jitter-fix = ["wgpu/metal"]
|
||||
|
||||
[dependencies]
|
||||
egui = { workspace = true, default-features = false }
|
||||
epaint = { workspace = true, default-features = false, features = ["bytemuck"] }
|
||||
|
||||
ahash.workspace = true
|
||||
@@ -59,4 +64,5 @@ wgpu = { workspace = true, features = ["wgsl"] }
|
||||
|
||||
# Optional dependencies:
|
||||
|
||||
egui = { workspace = true, optional = true, default-features = false }
|
||||
winit = { workspace = true, optional = true, default-features = false }
|
||||
|
||||
@@ -29,6 +29,7 @@ pub use renderer::*;
|
||||
pub use setup::{NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, WgpuSetupExisting};
|
||||
|
||||
/// Helpers for capturing screenshots of the UI.
|
||||
#[cfg(feature = "capture")]
|
||||
pub mod capture;
|
||||
|
||||
/// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`].
|
||||
|
||||
@@ -14,6 +14,7 @@ struct SurfaceState {
|
||||
alpha_mode: wgpu::CompositeAlphaMode,
|
||||
width: u32,
|
||||
height: u32,
|
||||
resizing: bool,
|
||||
}
|
||||
|
||||
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
|
||||
@@ -230,6 +231,7 @@ impl Painter {
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
alpha_mode,
|
||||
resizing: false,
|
||||
},
|
||||
);
|
||||
let Some(width) = NonZeroU32::new(size.width) else {
|
||||
@@ -326,6 +328,59 @@ impl Painter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles changes of the resizing state.
|
||||
///
|
||||
/// Should be called prior to the first [`Painter::on_window_resized`] call and after the last in
|
||||
/// the chain. Used to apply platform-specific logic, e.g. OSX Metal window resize jitter fix.
|
||||
pub fn on_window_resize_state_change(&mut self, viewport_id: ViewportId, resizing: bool) {
|
||||
profiling::function_scope!();
|
||||
|
||||
let Some(state) = self.surfaces.get_mut(&viewport_id) else {
|
||||
return;
|
||||
};
|
||||
if state.resizing == resizing {
|
||||
if resizing {
|
||||
log::debug!(
|
||||
"Painter::on_window_resize_state_change() redundant call while resizing"
|
||||
);
|
||||
} else {
|
||||
log::debug!(
|
||||
"Painter::on_window_resize_state_change() redundant call after resizing"
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Resizing is a bit tricky on macOS.
|
||||
// It requires enabling ["present_with_transaction"](https://developer.apple.com/documentation/quartzcore/cametallayer/presentswithtransaction)
|
||||
// flag to avoid jittering during the resize. Even though resize jittering on macOS
|
||||
// is common across rendering backends, the solution for wgpu/metal is known.
|
||||
//
|
||||
// See https://github.com/emilk/egui/issues/903
|
||||
#[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
|
||||
{
|
||||
// SAFETY: The cast is checked with if condition. If the used backend is not metal
|
||||
// it gracefully fails. The pointer casts are valid as it's 1-to-1 type mapping.
|
||||
// This is how wgpu currently exposes this backend-specific flag.
|
||||
unsafe {
|
||||
if let Some(hal_surface) = state.surface.as_hal::<wgpu::hal::api::Metal>() {
|
||||
let raw =
|
||||
std::ptr::from_ref::<wgpu::hal::metal::Surface>(&*hal_surface).cast_mut();
|
||||
|
||||
(*raw).present_with_transaction = resizing;
|
||||
|
||||
Self::configure_surface(
|
||||
state,
|
||||
self.render_state.as_ref().unwrap(),
|
||||
&self.configuration,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.resizing = resizing;
|
||||
}
|
||||
|
||||
pub fn on_window_resized(
|
||||
&mut self,
|
||||
viewport_id: ViewportId,
|
||||
|
||||
@@ -5,6 +5,10 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 0.33.2 - 2025-11-13
|
||||
* Don't enable `arboard` on iOS [#7663](https://github.com/emilk/egui/pull/7663) by [@irh](https://github.com/irh)
|
||||
|
||||
|
||||
## 0.33.0 - 2025-10-09
|
||||
### ⭐ Added
|
||||
* Add rotation gesture support for trackpad sources [#7453](https://github.com/emilk/egui/pull/7453) by [@thatcomputerguy0101](https://github.com/thatcomputerguy0101)
|
||||
|
||||
@@ -24,7 +24,7 @@ rustdoc-args = ["--generate-link-to-definition"]
|
||||
default = ["clipboard", "links", "wayland", "winit/default", "x11"]
|
||||
|
||||
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
|
||||
accesskit = ["dep:accesskit_winit", "egui/accesskit"]
|
||||
accesskit = ["dep:accesskit_winit"]
|
||||
|
||||
# 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
|
||||
|
||||
@@ -5,7 +5,10 @@ use raw_window_handle::RawDisplayHandle;
|
||||
/// If the "clipboard" feature is off, or we cannot connect to the OS clipboard,
|
||||
/// then a fallback clipboard that just works within the same app is used instead.
|
||||
pub struct Clipboard {
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
#[cfg(all(
|
||||
not(any(target_os = "android", target_os = "ios")),
|
||||
feature = "arboard",
|
||||
))]
|
||||
arboard: Option<arboard::Clipboard>,
|
||||
|
||||
#[cfg(all(
|
||||
@@ -28,7 +31,10 @@ impl Clipboard {
|
||||
/// Construct a new instance
|
||||
pub fn new(_raw_display_handle: Option<RawDisplayHandle>) -> Self {
|
||||
Self {
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
#[cfg(all(
|
||||
not(any(target_os = "android", target_os = "ios")),
|
||||
feature = "arboard",
|
||||
))]
|
||||
arboard: init_arboard(),
|
||||
|
||||
#[cfg(all(
|
||||
@@ -68,7 +74,10 @@ impl Clipboard {
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
#[cfg(all(
|
||||
not(any(target_os = "android", target_os = "ios")),
|
||||
feature = "arboard",
|
||||
))]
|
||||
if let Some(clipboard) = &mut self.arboard {
|
||||
return match clipboard.get_text() {
|
||||
Ok(text) => Some(text),
|
||||
@@ -98,7 +107,10 @@ impl Clipboard {
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
#[cfg(all(
|
||||
not(any(target_os = "android", target_os = "ios")),
|
||||
feature = "arboard",
|
||||
))]
|
||||
if let Some(clipboard) = &mut self.arboard {
|
||||
if let Err(err) = clipboard.set_text(text) {
|
||||
log::error!("arboard copy/cut error: {err}");
|
||||
@@ -110,7 +122,10 @@ impl Clipboard {
|
||||
}
|
||||
|
||||
pub fn set_image(&mut self, image: &egui::ColorImage) {
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
#[cfg(all(
|
||||
not(any(target_os = "android", target_os = "ios")),
|
||||
feature = "arboard",
|
||||
))]
|
||||
if let Some(clipboard) = &mut self.arboard {
|
||||
if let Err(err) = clipboard.set_image(arboard::ImageData {
|
||||
width: image.width(),
|
||||
@@ -130,7 +145,10 @@ impl Clipboard {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "arboard", not(target_os = "android")))]
|
||||
#[cfg(all(
|
||||
not(any(target_os = "android", target_os = "ios")),
|
||||
feature = "arboard",
|
||||
))]
|
||||
fn init_arboard() -> Option<arboard::Clipboard> {
|
||||
profiling::function_scope!();
|
||||
|
||||
|
||||
@@ -312,8 +312,8 @@ impl State {
|
||||
consumed: self.egui_ctx.wants_pointer_input(),
|
||||
}
|
||||
}
|
||||
WindowEvent::MouseWheel { delta, .. } => {
|
||||
self.on_mouse_wheel(window, *delta);
|
||||
WindowEvent::MouseWheel { delta, phase, .. } => {
|
||||
self.on_mouse_wheel(window, *delta, *phase);
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: self.egui_ctx.wants_pointer_input(),
|
||||
@@ -545,12 +545,13 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::PanGesture { delta, .. } => {
|
||||
WindowEvent::PanGesture { delta, phase, .. } => {
|
||||
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
|
||||
|
||||
self.egui_input.events.push(egui::Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: Vec2::new(delta.x, delta.y) / pixels_per_point,
|
||||
phase: to_egui_touch_phase(*phase),
|
||||
modifiers: self.egui_input.modifiers,
|
||||
});
|
||||
EventResponse {
|
||||
@@ -680,12 +681,7 @@ impl State {
|
||||
self.egui_input.events.push(egui::Event::Touch {
|
||||
device_id: egui::TouchDeviceId(egui::epaint::util::hash(touch.device_id)),
|
||||
id: egui::TouchId::from(touch.id),
|
||||
phase: match touch.phase {
|
||||
winit::event::TouchPhase::Started => egui::TouchPhase::Start,
|
||||
winit::event::TouchPhase::Moved => egui::TouchPhase::Move,
|
||||
winit::event::TouchPhase::Ended => egui::TouchPhase::End,
|
||||
winit::event::TouchPhase::Cancelled => egui::TouchPhase::Cancel,
|
||||
},
|
||||
phase: to_egui_touch_phase(touch.phase),
|
||||
pos: egui::pos2(
|
||||
touch.location.x as f32 / pixels_per_point,
|
||||
touch.location.y as f32 / pixels_per_point,
|
||||
@@ -738,7 +734,12 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_mouse_wheel(&mut self, window: &Window, delta: winit::event::MouseScrollDelta) {
|
||||
fn on_mouse_wheel(
|
||||
&mut self,
|
||||
window: &Window,
|
||||
delta: winit::event::MouseScrollDelta,
|
||||
phase: winit::event::TouchPhase,
|
||||
) {
|
||||
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
|
||||
|
||||
{
|
||||
@@ -754,10 +755,12 @@ impl State {
|
||||
egui::vec2(x as f32, y as f32) / pixels_per_point,
|
||||
),
|
||||
};
|
||||
let phase = to_egui_touch_phase(phase);
|
||||
let modifiers = self.egui_input.modifiers;
|
||||
self.egui_input.events.push(egui::Event::MouseWheel {
|
||||
unit,
|
||||
delta,
|
||||
phase,
|
||||
modifiers,
|
||||
});
|
||||
}
|
||||
@@ -885,7 +888,6 @@ impl State {
|
||||
events: _, // handled elsewhere
|
||||
mutable_text_under_cursor: _, // only used in eframe web
|
||||
ime,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_update,
|
||||
num_completed_passes: _, // `egui::Context::run` handles this
|
||||
request_discard_reasons: _, // `egui::Context::run` handles this
|
||||
@@ -944,6 +946,9 @@ impl State {
|
||||
profiling::scope!("accesskit");
|
||||
accesskit.update_if_active(|| update);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "accesskit"))]
|
||||
let _ = accesskit_update;
|
||||
}
|
||||
|
||||
fn set_cursor_icon(&mut self, window: &Window, cursor_icon: egui::CursorIcon) {
|
||||
@@ -970,6 +975,15 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
fn to_egui_touch_phase(phase: winit::event::TouchPhase) -> egui::TouchPhase {
|
||||
match phase {
|
||||
winit::event::TouchPhase::Started => egui::TouchPhase::Start,
|
||||
winit::event::TouchPhase::Moved => egui::TouchPhase::Move,
|
||||
winit::event::TouchPhase::Ended => egui::TouchPhase::End,
|
||||
winit::event::TouchPhase::Cancelled => egui::TouchPhase::Cancel,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_egui_theme(theme: winit::window::Theme) -> Theme {
|
||||
match theme {
|
||||
winit::window::Theme::Dark => Theme::Dark,
|
||||
|
||||
@@ -26,10 +26,6 @@ rustdoc-args = ["--generate-link-to-definition"]
|
||||
[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"]
|
||||
|
||||
@@ -61,7 +57,7 @@ persistence = ["serde", "epaint/serde", "ron"]
|
||||
rayon = ["epaint/rayon"]
|
||||
|
||||
## Allow serialization using [`serde`](https://docs.rs/serde).
|
||||
serde = ["dep:serde", "epaint/serde", "accesskit?/serde"]
|
||||
serde = ["dep:serde", "epaint/serde", "accesskit/serde"]
|
||||
|
||||
## Change Vertex layout to be compatible with unity
|
||||
unity = ["epaint/unity"]
|
||||
@@ -75,6 +71,7 @@ _override_unity = ["epaint/_override_unity"]
|
||||
emath = { workspace = true, default-features = false }
|
||||
epaint = { workspace = true, default-features = false }
|
||||
|
||||
accesskit.workspace = true
|
||||
ahash.workspace = true
|
||||
bitflags.workspace = true
|
||||
log.workspace = true
|
||||
@@ -84,7 +81,6 @@ smallvec.workspace = true
|
||||
unicode-segmentation.workspace = true
|
||||
|
||||
#! ### Optional dependencies
|
||||
accesskit = { workspace = true, optional = true }
|
||||
|
||||
backtrace = { workspace = true, optional = true }
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ impl<'a> AtomKind<'a> {
|
||||
) -> (Vec2, SizedAtomKind<'a>) {
|
||||
match self {
|
||||
AtomKind::Text(text) => {
|
||||
let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode());
|
||||
let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
|
||||
let galley = text.into_galley(ui, Some(wrap_mode), available_size.x, fallback_font);
|
||||
(galley.intrinsic_size(), SizedAtomKind::Text(galley))
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ impl<'a> AtomLayout<'a> {
|
||||
|
||||
let fallback_font = fallback_font.unwrap_or_default();
|
||||
|
||||
let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode());
|
||||
let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
|
||||
|
||||
// If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`.
|
||||
// If none is found, mark the first text item as `shrink`.
|
||||
@@ -188,7 +188,7 @@ impl<'a> AtomLayout<'a> {
|
||||
|
||||
let fallback_text_color =
|
||||
fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color());
|
||||
let gap = gap.unwrap_or(ui.spacing().icon_spacing);
|
||||
let gap = gap.unwrap_or_else(|| ui.spacing().icon_spacing);
|
||||
|
||||
// The size available for the content
|
||||
let available_inner_size = ui.available_size() - frame.total_margin().sum();
|
||||
|
||||
@@ -525,11 +525,21 @@ impl Area {
|
||||
true,
|
||||
);
|
||||
|
||||
// Used to prevent drift
|
||||
let pivot_at_start_of_drag_id = id.with("pivot_at_drag_start");
|
||||
|
||||
if movable
|
||||
&& move_response.dragged()
|
||||
&& let Some(pivot_pos) = &mut state.pivot_pos
|
||||
{
|
||||
*pivot_pos += move_response.drag_delta();
|
||||
let pivot_at_start_of_drag = ctx.data_mut(|data| {
|
||||
*data.get_temp_mut_or::<Pos2>(pivot_at_start_of_drag_id, *pivot_pos)
|
||||
});
|
||||
|
||||
*pivot_pos =
|
||||
pivot_at_start_of_drag + move_response.total_drag_delta().unwrap_or_default();
|
||||
} else {
|
||||
ctx.data_mut(|data| data.remove::<Pos2>(pivot_at_start_of_drag_id));
|
||||
}
|
||||
|
||||
if (move_response.dragged() || move_response.clicked())
|
||||
@@ -543,13 +553,14 @@ impl Area {
|
||||
move_response
|
||||
};
|
||||
|
||||
if constrain {
|
||||
state.set_left_top_pos(
|
||||
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min,
|
||||
);
|
||||
}
|
||||
|
||||
state.set_left_top_pos(state.left_top_pos());
|
||||
state.set_left_top_pos(round_area_position(
|
||||
ctx,
|
||||
if constrain {
|
||||
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min
|
||||
} else {
|
||||
state.left_top_pos()
|
||||
},
|
||||
));
|
||||
|
||||
// Update response with possibly moved/constrained rect:
|
||||
move_response.rect = state.rect();
|
||||
@@ -570,6 +581,16 @@ impl Area {
|
||||
}
|
||||
}
|
||||
|
||||
fn round_area_position(ctx: &Context, pos: Pos2) -> Pos2 {
|
||||
// We round a lot of rendering to pixels, so we round the whole
|
||||
// area positions to pixels too, so avoid widgets appearing to float
|
||||
// around independently of each other when the area is dragged.
|
||||
// But just in case pixels_per_point is irrational,
|
||||
// we then also round to ui coordinates:
|
||||
|
||||
pos.round_to_pixels(ctx.pixels_per_point()).round_ui()
|
||||
}
|
||||
|
||||
impl Prepared {
|
||||
pub(crate) fn state(&self) -> &AreaState {
|
||||
&self.state
|
||||
|
||||
@@ -239,7 +239,7 @@ impl ComboBox {
|
||||
let mut ir = combo_box_dyn(
|
||||
ui,
|
||||
button_id,
|
||||
selected_text,
|
||||
selected_text.clone(),
|
||||
menu_contents,
|
||||
icon,
|
||||
wrap_mode,
|
||||
@@ -247,14 +247,16 @@ impl ComboBox {
|
||||
popup_style,
|
||||
(width, height),
|
||||
);
|
||||
ir.response.widget_info(|| {
|
||||
let mut info = WidgetInfo::new(WidgetType::ComboBox);
|
||||
info.enabled = ui.is_enabled();
|
||||
info.current_text_value = Some(selected_text.text().to_owned());
|
||||
info
|
||||
});
|
||||
if let Some(label) = label {
|
||||
ir.response.widget_info(|| {
|
||||
WidgetInfo::labeled(WidgetType::ComboBox, ui.is_enabled(), label.text())
|
||||
});
|
||||
ir.response |= ui.label(label);
|
||||
} else {
|
||||
ir.response
|
||||
.widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, ui.is_enabled(), ""));
|
||||
let label_response = ui.label(label);
|
||||
ir.response = ir.response.labelled_by(label_response.id);
|
||||
ir.response |= label_response;
|
||||
}
|
||||
ir
|
||||
})
|
||||
|
||||
@@ -161,14 +161,13 @@ impl MenuState {
|
||||
if state.last_visible_pass + 1 < pass_nr {
|
||||
state.open_item = None;
|
||||
}
|
||||
if let Some(item) = state.open_item {
|
||||
if data
|
||||
if let Some(item) = state.open_item
|
||||
&& data
|
||||
.get_temp(item.with(Self::ID))
|
||||
.is_none_or(|item: Self| item.last_visible_pass + 1 < pass_nr)
|
||||
{
|
||||
// If the open item wasn't shown for at least a frame, reset the open item
|
||||
state.open_item = None;
|
||||
}
|
||||
{
|
||||
// If the open item wasn't shown for at least a frame, reset the open item
|
||||
state.open_item = None;
|
||||
}
|
||||
let r = f(&mut state);
|
||||
data.insert_temp(state_id, state);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Containers are pieces of the UI which wraps other pieces of UI. Examples: [`Window`], [`ScrollArea`], [`Resize`], [`SidePanel`], etc.
|
||||
//! Containers are pieces of the UI which wraps other pieces of UI. Examples: [`Window`], [`ScrollArea`], [`Resize`], [`Panel`], etc.
|
||||
//!
|
||||
//! For instance, a [`Frame`] adds a frame and background to some contained UI.
|
||||
|
||||
@@ -27,7 +27,7 @@ pub use {
|
||||
frame::Frame,
|
||||
modal::{Modal, ModalResponse},
|
||||
old_popup::*,
|
||||
panel::{CentralPanel, SidePanel, TopBottomPanel},
|
||||
panel::*,
|
||||
popup::*,
|
||||
resize::Resize,
|
||||
scene::{DragPanButtons, Scene},
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
///
|
||||
/// You can show multiple modals on top of each other. The topmost modal will always be
|
||||
/// the most recently shown one.
|
||||
/// If multiple modals are newly shown in the same frame, the order of the modals not undefined
|
||||
/// If multiple modals are newly shown in the same frame, the order of the modals is undefined
|
||||
/// (either first or second could be top).
|
||||
pub struct Modal {
|
||||
pub area: Area,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -465,7 +465,7 @@ impl<'a> Popup<'a> {
|
||||
pub fn get_best_align(&self) -> RectAlign {
|
||||
let expected_popup_size = self
|
||||
.get_expected_size()
|
||||
.unwrap_or(vec2(self.width.unwrap_or(0.0), 0.0));
|
||||
.unwrap_or_else(|| vec2(self.width.unwrap_or(0.0), 0.0));
|
||||
|
||||
let Some(anchor_rect) = self.anchor.rect(self.id, &self.ctx) else {
|
||||
return self.rect_align;
|
||||
@@ -473,6 +473,7 @@ impl<'a> Popup<'a> {
|
||||
|
||||
RectAlign::find_best_align(
|
||||
#[expect(clippy::iter_on_empty_collections)]
|
||||
#[expect(clippy::or_fun_call)]
|
||||
once(self.rect_align).chain(
|
||||
self.alternative_aligns
|
||||
// Need the empty slice so the iters have the same type so we can unwrap_or
|
||||
|
||||
@@ -245,7 +245,7 @@ impl Scene {
|
||||
{
|
||||
let pointer_in_scene = to_global.inverse() * mouse_pos;
|
||||
let zoom_delta = ui.ctx().input(|i| i.zoom_delta());
|
||||
let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta);
|
||||
let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta());
|
||||
|
||||
// Most of the time we can return early. This is also important to
|
||||
// avoid `ui_from_scene` to change slightly due to floating point errors.
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
//! See [`ScrollArea`] for docs.
|
||||
|
||||
#![allow(clippy::needless_range_loop)]
|
||||
|
||||
use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
|
||||
|
||||
use emath::GuiRounding as _;
|
||||
use epaint::Margin;
|
||||
|
||||
use crate::{
|
||||
Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Sense, Ui, UiBuilder, UiKind,
|
||||
UiStackInfo, Vec2, Vec2b, emath, epaint, lerp, pass_state, pos2, remap, remap_clamp,
|
||||
Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Response, Sense, Ui, UiBuilder,
|
||||
UiKind, UiStackInfo, Vec2, Vec2b, emath, epaint, lerp, pass_state, pos2, remap, remap_clamp,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -256,7 +261,7 @@ impl AddAssign for ScrollSource {
|
||||
/// ### 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.
|
||||
/// * inner: excluding scroll bar(s). The area we clip the contents to. Includes `content_margin`.
|
||||
///
|
||||
/// If the floating scroll bars settings is turned on then `inner == outer`.
|
||||
///
|
||||
@@ -292,6 +297,8 @@ pub struct ScrollArea {
|
||||
scroll_source: ScrollSource,
|
||||
wheel_scroll_multiplier: Vec2,
|
||||
|
||||
content_margin: Option<Margin>,
|
||||
|
||||
/// If true for vertical or horizontal the scroll wheel will stick to the
|
||||
/// end position until user manually changes position. It will become true
|
||||
/// again once scroll handle makes contact with end.
|
||||
@@ -344,6 +351,7 @@ impl ScrollArea {
|
||||
on_drag_cursor: None,
|
||||
scroll_source: ScrollSource::default(),
|
||||
wheel_scroll_multiplier: Vec2::splat(1.0),
|
||||
content_margin: None,
|
||||
stick_to_end: Vec2b::FALSE,
|
||||
animated: true,
|
||||
}
|
||||
@@ -591,6 +599,18 @@ impl ScrollArea {
|
||||
self.direction_enabled[0] || self.direction_enabled[1]
|
||||
}
|
||||
|
||||
/// Extra margin added around the contents.
|
||||
///
|
||||
/// The scroll bars will be either on top of this margin, or outside of it,
|
||||
/// depending on the value of [`crate::style::ScrollStyle::floating`].
|
||||
///
|
||||
/// Default: [`crate::style::ScrollStyle::content_margin`].
|
||||
#[inline]
|
||||
pub fn content_margin(mut self, margin: impl Into<Margin>) -> Self {
|
||||
self.content_margin = Some(margin.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// The scroll handle will stick to the rightmost position even while the content size
|
||||
/// changes dynamically. This can be useful to simulate text scrollers coming in from right
|
||||
/// hand side. The scroll handle remains stuck until user manually changes position. Once "unstuck"
|
||||
@@ -642,7 +662,7 @@ struct Prepared {
|
||||
scroll_bar_visibility: ScrollBarVisibility,
|
||||
scroll_bar_rect: Option<Rect>,
|
||||
|
||||
/// Where on the screen the content is (excludes scroll bars).
|
||||
/// Where on the screen the content is (excludes scroll bars; includes `content_margin`).
|
||||
inner_rect: Rect,
|
||||
|
||||
content_ui: Ui,
|
||||
@@ -659,6 +679,9 @@ struct Prepared {
|
||||
/// not for us to handle so we save it and restore it after this [`ScrollArea`] is done.
|
||||
saved_scroll_target: [Option<pass_state::ScrollTarget>; 2],
|
||||
|
||||
/// The response from dragging the background (if enabled)
|
||||
background_drag_response: Option<Response>,
|
||||
|
||||
animated: bool,
|
||||
}
|
||||
|
||||
@@ -678,6 +701,7 @@ impl ScrollArea {
|
||||
on_drag_cursor,
|
||||
scroll_source,
|
||||
wheel_scroll_multiplier,
|
||||
content_margin: _, // Used elsewhere
|
||||
stick_to_end,
|
||||
animated,
|
||||
} = self;
|
||||
@@ -745,6 +769,12 @@ impl ScrollArea {
|
||||
}
|
||||
|
||||
let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
|
||||
|
||||
// Round to pixels to avoid widgets appearing to "float" when scrolling fractional amounts:
|
||||
let content_max_rect = content_max_rect
|
||||
.round_to_pixels(ui.pixels_per_point())
|
||||
.round_ui();
|
||||
|
||||
let mut content_ui = ui.new_child(
|
||||
UiBuilder::new()
|
||||
.ui_stack_info(UiStackInfo::new(UiKind::ScrollArea))
|
||||
@@ -772,70 +802,72 @@ impl ScrollArea {
|
||||
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
|
||||
let dt = ui.input(|i| i.stable_dt).at_most(0.1);
|
||||
|
||||
if scroll_source.drag
|
||||
&& ui.is_enabled()
|
||||
&& (state.content_is_too_large[0] || state.content_is_too_large[1])
|
||||
{
|
||||
// Drag contents to scroll (for touch screens mostly).
|
||||
// We must do this BEFORE adding content to the `ScrollArea`,
|
||||
// or we will steal input from the widgets we contain.
|
||||
let content_response_option = state
|
||||
.interact_rect
|
||||
.map(|rect| ui.interact(rect, id.with("area"), Sense::drag()));
|
||||
let background_drag_response =
|
||||
if scroll_source.drag && ui.is_enabled() && state.content_is_too_large.any() {
|
||||
// Drag contents to scroll (for touch screens mostly).
|
||||
// We must do this BEFORE adding content to the `ScrollArea`,
|
||||
// or we will steal input from the widgets we contain.
|
||||
let content_response_option = state
|
||||
.interact_rect
|
||||
.map(|rect| ui.interact(rect, id.with("area"), Sense::drag()));
|
||||
|
||||
if content_response_option
|
||||
.as_ref()
|
||||
.is_some_and(|response| response.dragged())
|
||||
{
|
||||
for d in 0..2 {
|
||||
if direction_enabled[d] {
|
||||
ui.input(|input| {
|
||||
state.offset[d] -= input.pointer.delta()[d];
|
||||
});
|
||||
state.scroll_stuck_to_end[d] = false;
|
||||
state.offset_target[d] = None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Apply the cursor velocity to the scroll area when the user releases the drag.
|
||||
if content_response_option
|
||||
.as_ref()
|
||||
.is_some_and(|response| response.drag_stopped())
|
||||
.is_some_and(|response| response.dragged())
|
||||
{
|
||||
state.vel =
|
||||
direction_enabled.to_vec2() * ui.input(|input| input.pointer.velocity());
|
||||
}
|
||||
for d in 0..2 {
|
||||
// Kinetic scrolling
|
||||
let stop_speed = 20.0; // Pixels per second.
|
||||
let friction_coeff = 1000.0; // Pixels per second squared.
|
||||
for d in 0..2 {
|
||||
if direction_enabled[d] {
|
||||
ui.input(|input| {
|
||||
state.offset[d] -= input.pointer.delta()[d];
|
||||
});
|
||||
state.scroll_stuck_to_end[d] = false;
|
||||
state.offset_target[d] = None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Apply the cursor velocity to the scroll area when the user releases the drag.
|
||||
if content_response_option
|
||||
.as_ref()
|
||||
.is_some_and(|response| response.drag_stopped())
|
||||
{
|
||||
state.vel = direction_enabled.to_vec2()
|
||||
* ui.input(|input| input.pointer.velocity());
|
||||
}
|
||||
for d in 0..2 {
|
||||
// Kinetic scrolling
|
||||
let stop_speed = 20.0; // Pixels per second.
|
||||
let friction_coeff = 1000.0; // Pixels per second squared.
|
||||
|
||||
let friction = friction_coeff * dt;
|
||||
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
|
||||
state.vel[d] = 0.0;
|
||||
} else {
|
||||
state.vel[d] -= friction * state.vel[d].signum();
|
||||
// Offset has an inverted coordinate system compared to
|
||||
// the velocity, so we subtract it instead of adding it
|
||||
state.offset[d] -= state.vel[d] * dt;
|
||||
ctx.request_repaint();
|
||||
let friction = friction_coeff * dt;
|
||||
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
|
||||
state.vel[d] = 0.0;
|
||||
} else {
|
||||
state.vel[d] -= friction * state.vel[d].signum();
|
||||
// Offset has an inverted coordinate system compared to
|
||||
// the velocity, so we subtract it instead of adding it
|
||||
state.offset[d] -= state.vel[d] * dt;
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the desired mouse cursors.
|
||||
if let Some(response) = content_response_option {
|
||||
if response.dragged() {
|
||||
if let Some(cursor) = on_drag_cursor {
|
||||
response.on_hover_cursor(cursor);
|
||||
// Set the desired mouse cursors.
|
||||
if let Some(response) = &content_response_option {
|
||||
if response.dragged()
|
||||
&& let Some(cursor) = on_drag_cursor
|
||||
{
|
||||
ui.ctx().set_cursor_icon(cursor);
|
||||
} else if response.hovered()
|
||||
&& let Some(cursor) = on_hover_cursor
|
||||
{
|
||||
ui.ctx().set_cursor_icon(cursor);
|
||||
}
|
||||
} else if response.hovered()
|
||||
&& let Some(cursor) = on_hover_cursor
|
||||
{
|
||||
response.on_hover_cursor(cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content_response_option
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Scroll with an animation if we have a target offset (that hasn't been cleared by the code
|
||||
// above).
|
||||
@@ -888,6 +920,7 @@ impl ScrollArea {
|
||||
wheel_scroll_multiplier,
|
||||
stick_to_end,
|
||||
saved_scroll_target,
|
||||
background_drag_response,
|
||||
animated,
|
||||
}
|
||||
}
|
||||
@@ -969,10 +1002,21 @@ impl ScrollArea {
|
||||
ui: &mut Ui,
|
||||
add_contents: Box<dyn FnOnce(&mut Ui, Rect) -> R + 'c>,
|
||||
) -> ScrollAreaOutput<R> {
|
||||
let margin = self
|
||||
.content_margin
|
||||
.unwrap_or_else(|| ui.spacing().scroll.content_margin);
|
||||
|
||||
let mut prepared = self.begin(ui);
|
||||
let id = prepared.id;
|
||||
let inner_rect = prepared.inner_rect;
|
||||
let inner = add_contents(&mut prepared.content_ui, prepared.viewport);
|
||||
|
||||
let inner = crate::Frame::NONE
|
||||
.inner_margin(margin)
|
||||
.show(&mut prepared.content_ui, |ui| {
|
||||
add_contents(ui, prepared.viewport)
|
||||
})
|
||||
.inner;
|
||||
|
||||
let (content_size, state) = prepared.end(ui);
|
||||
ScrollAreaOutput {
|
||||
inner,
|
||||
@@ -1003,6 +1047,7 @@ impl Prepared {
|
||||
wheel_scroll_multiplier,
|
||||
stick_to_end,
|
||||
saved_scroll_target,
|
||||
background_drag_response,
|
||||
animated,
|
||||
} = self;
|
||||
|
||||
@@ -1118,7 +1163,16 @@ impl Prepared {
|
||||
);
|
||||
|
||||
let max_offset = content_size - inner_rect.size();
|
||||
let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect);
|
||||
|
||||
// Drag-to-scroll?
|
||||
let is_dragging_background = background_drag_response
|
||||
.as_ref()
|
||||
.is_some_and(|r| r.dragged());
|
||||
|
||||
let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect)
|
||||
&& ui.ctx().dragged_id().is_none()
|
||||
|| is_dragging_background;
|
||||
|
||||
if scroll_source.mouse_wheel && ui.is_enabled() && is_hovering_outer_rect {
|
||||
let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction
|
||||
&& direction_enabled[0] != direction_enabled[1];
|
||||
@@ -1127,9 +1181,9 @@ impl Prepared {
|
||||
let scroll_delta = ui.ctx().input(|input| {
|
||||
if always_scroll_enabled_direction {
|
||||
// no bidirectional scrolling; allow horizontal scrolling without pressing shift
|
||||
input.smooth_scroll_delta[0] + input.smooth_scroll_delta[1]
|
||||
input.smooth_scroll_delta()[0] + input.smooth_scroll_delta()[1]
|
||||
} else {
|
||||
input.smooth_scroll_delta[d]
|
||||
input.smooth_scroll_delta()[d]
|
||||
}
|
||||
});
|
||||
let scroll_delta = scroll_delta * wheel_scroll_multiplier[d];
|
||||
@@ -1143,10 +1197,10 @@ impl Prepared {
|
||||
// Clear scroll delta so no parent scroll will use it:
|
||||
ui.ctx().input_mut(|input| {
|
||||
if always_scroll_enabled_direction {
|
||||
input.smooth_scroll_delta[0] = 0.0;
|
||||
input.smooth_scroll_delta[1] = 0.0;
|
||||
input.smooth_scroll_delta()[0] = 0.0;
|
||||
input.smooth_scroll_delta()[1] = 0.0;
|
||||
} else {
|
||||
input.smooth_scroll_delta[d] = 0.0;
|
||||
input.smooth_scroll_delta()[d] = 0.0;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1204,6 +1258,7 @@ impl Prepared {
|
||||
|
||||
let is_hovering_bar_area = is_hovering_outer_rect
|
||||
&& ui.rect_contains_pointer(max_bar_rect)
|
||||
&& !is_dragging_background
|
||||
|| state.scroll_bar_interaction[d];
|
||||
|
||||
let is_hovering_bar_area_t = ui
|
||||
|
||||
@@ -358,7 +358,7 @@ impl Tooltip<'_> {
|
||||
// We only show the tooltip when the mouse pointer is still.
|
||||
if !response
|
||||
.ctx
|
||||
.input(|i| i.pointer.is_still() && i.smooth_scroll_delta == Vec2::ZERO)
|
||||
.input(|i| i.pointer.is_still() && !i.is_scrolling())
|
||||
{
|
||||
// wait for mouse to stop
|
||||
response.ctx.request_repaint();
|
||||
|
||||
@@ -825,7 +825,7 @@ fn resize_response(
|
||||
area: &mut area::Prepared,
|
||||
resize_id: Id,
|
||||
) {
|
||||
let Some(mut new_rect) = move_and_resize_window(ctx, &resize_interaction) else {
|
||||
let Some(mut new_rect) = move_and_resize_window(ctx, resize_id, &resize_interaction) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -847,27 +847,38 @@ fn resize_response(
|
||||
}
|
||||
|
||||
/// Acts on outer rect (outside the stroke)
|
||||
fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Option<Rect> {
|
||||
fn move_and_resize_window(ctx: &Context, id: Id, interaction: &ResizeInteraction) -> Option<Rect> {
|
||||
// Used to prevent drift
|
||||
let rect_at_start_of_drag_id = id.with("window_rect_at_drag_start");
|
||||
|
||||
if !interaction.any_dragged() {
|
||||
ctx.data_mut(|data| {
|
||||
data.remove::<Rect>(rect_at_start_of_drag_id);
|
||||
});
|
||||
return None;
|
||||
}
|
||||
|
||||
let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?;
|
||||
let mut rect = interaction.outer_rect; // prevent drift
|
||||
let total_drag_delta = ctx.input(|i| i.pointer.total_drag_delta())?;
|
||||
|
||||
let rect_at_start_of_drag = ctx.data_mut(|data| {
|
||||
*data.get_temp_mut_or::<Rect>(rect_at_start_of_drag_id, interaction.outer_rect)
|
||||
});
|
||||
|
||||
let mut rect = rect_at_start_of_drag; // prevent drift
|
||||
|
||||
// Put the rect in the center of the stroke:
|
||||
rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);
|
||||
|
||||
if interaction.left.drag {
|
||||
rect.min.x = pointer_pos.x;
|
||||
rect.min.x += total_drag_delta.x;
|
||||
} else if interaction.right.drag {
|
||||
rect.max.x = pointer_pos.x;
|
||||
rect.max.x += total_drag_delta.x;
|
||||
}
|
||||
|
||||
if interaction.top.drag {
|
||||
rect.min.y = pointer_pos.y;
|
||||
rect.min.y += total_drag_delta.y;
|
||||
} else if interaction.bottom.drag {
|
||||
rect.max.y = pointer_pos.y;
|
||||
rect.max.y += total_drag_delta.y;
|
||||
}
|
||||
|
||||
// Return to having the rect outside the stroke:
|
||||
@@ -899,7 +910,6 @@ fn resize_interaction(
|
||||
let rect = outer_rect.shrink(window_frame.stroke.width / 2.0);
|
||||
|
||||
let side_response = |rect, id| {
|
||||
#[cfg(feature = "accesskit")]
|
||||
ctx.register_accesskit_parent(id, _accessibility_parent);
|
||||
let response = ctx.create_widget(
|
||||
WidgetRect {
|
||||
|
||||
@@ -41,7 +41,6 @@ use crate::{
|
||||
viewport::ViewportClass,
|
||||
};
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
use crate::IdMap;
|
||||
|
||||
/// Information given to the backend about when it is time to repaint the ui.
|
||||
@@ -404,7 +403,6 @@ struct ContextImpl {
|
||||
|
||||
embed_viewports: bool,
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
is_accesskit_enabled: bool,
|
||||
|
||||
loaders: Arc<Loaders>,
|
||||
@@ -507,7 +505,6 @@ impl ContextImpl {
|
||||
},
|
||||
);
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
if self.is_accesskit_enabled {
|
||||
profiling::scope!("accesskit");
|
||||
use crate::pass_state::AccessKitPassState;
|
||||
@@ -589,10 +586,10 @@ impl ContextImpl {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
fn accesskit_node_builder(&mut self, id: Id) -> &mut accesskit::Node {
|
||||
let state = self.viewport().this_pass.accesskit_state.as_mut().unwrap();
|
||||
let builders = &mut state.nodes;
|
||||
|
||||
if let std::collections::hash_map::Entry::Vacant(entry) = builders.entry(id) {
|
||||
entry.insert(Default::default());
|
||||
|
||||
@@ -614,11 +611,12 @@ impl ContextImpl {
|
||||
}
|
||||
|
||||
let parent_id = find_accesskit_parent(&state.parent_map, builders, id)
|
||||
.unwrap_or(crate::accesskit_root_id());
|
||||
.unwrap_or_else(crate::accesskit_root_id);
|
||||
|
||||
let parent_builder = builders.get_mut(&parent_id).unwrap();
|
||||
parent_builder.push_child(id.accesskit_id());
|
||||
}
|
||||
|
||||
builders.get_mut(&id).unwrap()
|
||||
}
|
||||
|
||||
@@ -766,7 +764,48 @@ impl Context {
|
||||
/// and only on the rare occasion that [`Context::request_discard`] is called.
|
||||
/// Usually, it `run_ui` will only be called once.
|
||||
///
|
||||
/// Put your widgets into a [`crate::SidePanel`], [`crate::TopBottomPanel`], [`crate::CentralPanel`], [`crate::Window`] or [`crate::Area`].
|
||||
/// The [`Ui`] given to the callback will cover the entire [`Self::content_rect`],
|
||||
/// with no margin or background color. Use [`crate::Frame`] to add that.
|
||||
///
|
||||
/// You can organize your GUI using [`crate::Panel`].
|
||||
///
|
||||
/// Instead of calling `run_ui`, you can alternatively use [`Self::begin_pass`] and [`Context::end_pass`].
|
||||
///
|
||||
/// ```
|
||||
/// // One egui context that you keep reusing:
|
||||
/// let mut ctx = egui::Context::default();
|
||||
///
|
||||
/// // Each frame:
|
||||
/// let input = egui::RawInput::default();
|
||||
/// let full_output = ctx.run_ui(input, |ui| {
|
||||
/// ui.label("Hello egui!");
|
||||
/// });
|
||||
/// // handle full_output
|
||||
/// ```
|
||||
///
|
||||
/// ## See also
|
||||
/// * [`Self::run`]
|
||||
#[must_use]
|
||||
pub fn run_ui(&self, new_input: RawInput, mut run_ui: impl FnMut(&mut Ui)) -> FullOutput {
|
||||
self.run_ui_dyn(new_input, &mut run_ui)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn run_ui_dyn(&self, new_input: RawInput, run_ui: &mut dyn FnMut(&mut Ui)) -> FullOutput {
|
||||
self.run(new_input, |ctx| {
|
||||
crate::CentralPanel::no_frame().show(ctx, |ui| {
|
||||
run_ui(ui);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/// Run the ui code for one frame.
|
||||
///
|
||||
/// At most [`Options::max_passes`] calls will be issued to `run_ui`,
|
||||
/// and only on the rare occasion that [`Context::request_discard`] is called.
|
||||
/// Usually, it `run_ui` will only be called once.
|
||||
///
|
||||
/// Put your widgets into a [`crate::Panel`], [`crate::CentralPanel`], [`crate::Window`] or [`crate::Area`].
|
||||
///
|
||||
/// Instead of calling `run`, you can alternatively use [`Self::begin_pass`] and [`Context::end_pass`].
|
||||
///
|
||||
@@ -783,8 +822,16 @@ impl Context {
|
||||
/// });
|
||||
/// // handle full_output
|
||||
/// ```
|
||||
///
|
||||
/// ## See also
|
||||
/// * [`Self::run_ui`]
|
||||
#[must_use]
|
||||
pub fn run(&self, mut new_input: RawInput, mut run_ui: impl FnMut(&Self)) -> FullOutput {
|
||||
pub fn run(&self, new_input: RawInput, mut run_ui: impl FnMut(&Self)) -> FullOutput {
|
||||
self.run_dyn(new_input, &mut run_ui)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn run_dyn(&self, mut new_input: RawInput, run_ui: &mut dyn FnMut(&Self)) -> FullOutput {
|
||||
profiling::function_scope!();
|
||||
let viewport_id = new_input.viewport_id;
|
||||
let max_passes = self.write(|ctx| ctx.memory.options.max_passes.get());
|
||||
@@ -1198,7 +1245,12 @@ impl Context {
|
||||
#[allow(clippy::let_and_return, clippy::allow_attributes)]
|
||||
let res = self.get_response(w);
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
#[cfg(debug_assertions)]
|
||||
if res.contains_pointer() {
|
||||
let plugins = self.read(|ctx| ctx.plugins.ordered_plugins());
|
||||
plugins.on_widget_under_pointer(self, &w);
|
||||
}
|
||||
|
||||
if allow_focus && w.sense.is_focusable() {
|
||||
// Make sure anything that can receive focus has an AccessKit node.
|
||||
// TODO(mwcampbell): For nodes that are filled from widget info,
|
||||
@@ -1206,7 +1258,6 @@ impl Context {
|
||||
self.accesskit_node_builder(w.id, |builder| res.fill_accesskit_node_common(builder));
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
self.write(|ctx| {
|
||||
use crate::{Align, pass_state::ScrollTarget, style::ScrollAnimation};
|
||||
let viewport = ctx.viewport_for(ctx.viewport_id());
|
||||
@@ -1214,12 +1265,14 @@ impl Context {
|
||||
viewport
|
||||
.input
|
||||
.consume_accesskit_action_requests(res.id, |request| {
|
||||
use accesskit::Action;
|
||||
|
||||
// TODO(lucasmerlin): Correctly handle the scroll unit:
|
||||
// https://github.com/AccessKit/accesskit/blob/e639c0e0d8ccbfd9dff302d972fa06f9766d608e/common/src/lib.rs#L2621
|
||||
const DISTANCE: f32 = 100.0;
|
||||
|
||||
match &request.action {
|
||||
accesskit::Action::ScrollIntoView => {
|
||||
Action::ScrollIntoView => {
|
||||
viewport.this_pass.scroll_target = [
|
||||
Some(ScrollTarget::new(
|
||||
res.rect.x_range(),
|
||||
@@ -1233,16 +1286,16 @@ impl Context {
|
||||
)),
|
||||
];
|
||||
}
|
||||
accesskit::Action::ScrollDown => {
|
||||
Action::ScrollDown => {
|
||||
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::UP;
|
||||
}
|
||||
accesskit::Action::ScrollUp => {
|
||||
Action::ScrollUp => {
|
||||
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::DOWN;
|
||||
}
|
||||
accesskit::Action::ScrollLeft => {
|
||||
Action::ScrollLeft => {
|
||||
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::LEFT;
|
||||
}
|
||||
accesskit::Action::ScrollRight => {
|
||||
Action::ScrollRight => {
|
||||
viewport.this_pass.scroll_delta.0 += DISTANCE * Vec2::RIGHT;
|
||||
}
|
||||
_ => return false,
|
||||
@@ -1335,7 +1388,6 @@ impl Context {
|
||||
res.flags.set(Flags::FAKE_PRIMARY_CLICKED, true);
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
if enabled
|
||||
&& sense.senses_click()
|
||||
&& input.has_accesskit_action_request(id, accesskit::Action::Click)
|
||||
@@ -2492,7 +2544,6 @@ impl ContextImpl {
|
||||
|
||||
let mut platform_output: PlatformOutput = std::mem::take(&mut viewport.output);
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
profiling::scope!("accesskit");
|
||||
let state = viewport.this_pass.accesskit_state.take();
|
||||
@@ -3491,9 +3542,8 @@ impl Context {
|
||||
///
|
||||
/// The `Context` lock is held while the given closure is called!
|
||||
///
|
||||
/// Returns `None` if acesskit is off.
|
||||
/// Returns `None` if accesskit is off.
|
||||
// TODO(emilk): consider making both read-only and read-write versions
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn accesskit_node_builder<R>(
|
||||
&self,
|
||||
id: Id,
|
||||
@@ -3509,7 +3559,6 @@ impl Context {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub(crate) fn register_accesskit_parent(&self, id: Id, parent_id: Id) {
|
||||
self.write(|ctx| {
|
||||
if let Some(state) = ctx.viewport().this_pass.accesskit_state.as_mut() {
|
||||
@@ -3519,13 +3568,11 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Enable generation of AccessKit tree updates in all future frames.
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn enable_accesskit(&self) {
|
||||
self.write(|ctx| ctx.is_accesskit_enabled = true);
|
||||
}
|
||||
|
||||
/// Disable generation of AccessKit tree updates in all future frames.
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn disable_accesskit(&self) {
|
||||
self.write(|ctx| ctx.is_accesskit_enabled = false);
|
||||
}
|
||||
@@ -4010,7 +4057,7 @@ impl Context {
|
||||
/// Is this specific widget being dragged?
|
||||
///
|
||||
/// A widget that sense both clicks and drags is only marked as "dragged"
|
||||
/// when the mouse has moved a bit
|
||||
/// when the mouse has moved a bit.
|
||||
///
|
||||
/// See also: [`crate::Response::dragged`].
|
||||
pub fn is_being_dragged(&self, id: Id) -> bool {
|
||||
@@ -4024,7 +4071,7 @@ impl Context {
|
||||
self.interaction_snapshot(|i| i.drag_started)
|
||||
}
|
||||
|
||||
/// This widget was being dragged, but was released this pass
|
||||
/// This widget was being dragged, but was released this pass.
|
||||
pub fn drag_stopped_id(&self) -> Option<Id> {
|
||||
self.interaction_snapshot(|i| i.drag_stopped)
|
||||
}
|
||||
|
||||
@@ -535,6 +535,11 @@ pub enum Event {
|
||||
/// as when swiping down on a touch-screen or track-pad with natural scrolling.
|
||||
delta: Vec2,
|
||||
|
||||
/// The phase of the scroll, useful for trackpads.
|
||||
///
|
||||
/// If unknown set this to [`TouchPhase::Move`].
|
||||
phase: TouchPhase,
|
||||
|
||||
/// The state of the modifier keys at the time of the event.
|
||||
modifiers: Modifiers,
|
||||
},
|
||||
@@ -543,7 +548,6 @@ pub enum Event {
|
||||
WindowFocused(bool),
|
||||
|
||||
/// An assistive technology (e.g. screen reader) requested an action.
|
||||
#[cfg(feature = "accesskit")]
|
||||
AccessKitActionRequest(accesskit::ActionRequest),
|
||||
|
||||
/// The reply of a screenshot requested with [`crate::ViewportCommand::Screenshot`].
|
||||
|
||||
@@ -128,7 +128,6 @@ pub struct PlatformOutput {
|
||||
/// The difference in the widget tree since last frame.
|
||||
///
|
||||
/// NOTE: this needs to be per-viewport.
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub accesskit_update: Option<accesskit::TreeUpdate>,
|
||||
|
||||
/// How many ui passes is this the sum of?
|
||||
@@ -175,7 +174,6 @@ impl PlatformOutput {
|
||||
mut events,
|
||||
mutable_text_under_cursor,
|
||||
ime,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_update,
|
||||
num_completed_passes,
|
||||
mut request_discard_reasons,
|
||||
@@ -190,12 +188,8 @@ impl PlatformOutput {
|
||||
self.request_discard_reasons
|
||||
.append(&mut request_discard_reasons);
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
// egui produces a complete AccessKit tree for each frame,
|
||||
// so overwrite rather than appending.
|
||||
self.accesskit_update = accesskit_update;
|
||||
}
|
||||
// egui produces a complete AccessKit tree for each frame, so overwrite rather than append:
|
||||
self.accesskit_update = accesskit_update;
|
||||
}
|
||||
|
||||
/// Take everything ephemeral (everything except `cursor_icon` currently)
|
||||
|
||||
@@ -79,7 +79,6 @@ impl Id {
|
||||
self.0.get()
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub(crate) fn accesskit_id(&self) -> accesskit::NodeId {
|
||||
self.value().into()
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
mod touch_state;
|
||||
mod wheel_state;
|
||||
|
||||
use crate::data::input::{
|
||||
Event, EventFilter, KeyboardShortcut, Modifiers, MouseWheelUnit, NUM_POINTER_BUTTONS,
|
||||
PointerButton, RawInput, TouchDeviceId, ViewportInfo,
|
||||
};
|
||||
use crate::{
|
||||
SafeAreaInsets,
|
||||
emath::{NumExt as _, Pos2, Rect, Vec2, vec2},
|
||||
util::History,
|
||||
};
|
||||
use crate::{
|
||||
data::input::{
|
||||
Event, EventFilter, KeyboardShortcut, Modifiers, NUM_POINTER_BUTTONS, PointerButton,
|
||||
RawInput, TouchDeviceId, ViewportInfo,
|
||||
},
|
||||
input_state::wheel_state::WheelState,
|
||||
};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
time::Duration,
|
||||
@@ -221,31 +225,8 @@ pub struct InputState {
|
||||
|
||||
// ----------------------------------------------
|
||||
// Scrolling:
|
||||
//
|
||||
/// Time of the last scroll event.
|
||||
last_scroll_time: f64,
|
||||
|
||||
/// Used for smoothing the scroll delta.
|
||||
unprocessed_scroll_delta: Vec2,
|
||||
|
||||
/// Used for smoothing the scroll delta when zooming.
|
||||
unprocessed_scroll_delta_for_zoom: f32,
|
||||
|
||||
/// You probably want to use [`Self::smooth_scroll_delta`] instead.
|
||||
///
|
||||
/// The raw input of how many points the user scrolled.
|
||||
///
|
||||
/// The delta dictates how the _content_ should move.
|
||||
///
|
||||
/// A positive X-value indicates the content is being moved right,
|
||||
/// as when swiping right on a touch-screen or track-pad with natural scrolling.
|
||||
///
|
||||
/// A positive Y-value indicates the content is being moved down,
|
||||
/// as when swiping down on a touch-screen or track-pad with natural scrolling.
|
||||
///
|
||||
/// When using a notched scroll-wheel this will spike very large for one frame,
|
||||
/// then drop to zero. For a smoother experience, use [`Self::smooth_scroll_delta`].
|
||||
pub raw_scroll_delta: Vec2,
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
wheel: WheelState,
|
||||
|
||||
/// How many points the user scrolled, smoothed over a few frames.
|
||||
///
|
||||
@@ -357,10 +338,7 @@ impl Default for InputState {
|
||||
pointer: Default::default(),
|
||||
touch_states: Default::default(),
|
||||
|
||||
last_scroll_time: f64::NEG_INFINITY,
|
||||
unprocessed_scroll_delta: Vec2::ZERO,
|
||||
unprocessed_scroll_delta_for_zoom: 0.0,
|
||||
raw_scroll_delta: Vec2::ZERO,
|
||||
wheel: Default::default(),
|
||||
smooth_scroll_delta: Vec2::ZERO,
|
||||
zoom_factor_delta: 1.0,
|
||||
rotation_radians: 0.0,
|
||||
@@ -415,12 +393,8 @@ impl InputState {
|
||||
let mut keys_down = self.keys_down;
|
||||
let mut zoom_factor_delta = 1.0; // TODO(emilk): smoothing for zoom factor
|
||||
let mut rotation_radians = 0.0;
|
||||
let mut raw_scroll_delta = Vec2::ZERO;
|
||||
|
||||
let mut unprocessed_scroll_delta = self.unprocessed_scroll_delta;
|
||||
let mut unprocessed_scroll_delta_for_zoom = self.unprocessed_scroll_delta_for_zoom;
|
||||
let mut smooth_scroll_delta = Vec2::ZERO;
|
||||
let mut smooth_scroll_delta_for_zoom = 0.0;
|
||||
self.wheel.smooth_wheel_delta = Vec2::ZERO;
|
||||
|
||||
for event in &mut new.events {
|
||||
match event {
|
||||
@@ -440,55 +414,18 @@ impl InputState {
|
||||
Event::MouseWheel {
|
||||
unit,
|
||||
delta,
|
||||
phase,
|
||||
modifiers,
|
||||
} => {
|
||||
let mut delta = match unit {
|
||||
MouseWheelUnit::Point => *delta,
|
||||
MouseWheelUnit::Line => options.line_scroll_speed * *delta,
|
||||
MouseWheelUnit::Page => viewport_rect.height() * *delta,
|
||||
};
|
||||
|
||||
let is_horizontal = modifiers.matches_any(options.horizontal_scroll_modifier);
|
||||
let is_vertical = modifiers.matches_any(options.vertical_scroll_modifier);
|
||||
|
||||
if is_horizontal && !is_vertical {
|
||||
// Treat all scrolling as horizontal scrolling.
|
||||
// Note: one Mac we already get horizontal scroll events when shift is down.
|
||||
delta = vec2(delta.x + delta.y, 0.0);
|
||||
}
|
||||
if !is_horizontal && is_vertical {
|
||||
// Treat all scrolling as vertical scrolling.
|
||||
delta = vec2(0.0, delta.x + delta.y);
|
||||
}
|
||||
|
||||
raw_scroll_delta += delta;
|
||||
|
||||
// Mouse wheels often go very large steps.
|
||||
// A single notch on a logitech mouse wheel connected to a Macbook returns 14.0 raw_scroll_delta.
|
||||
// So we smooth it out over several frames for a nicer user experience when scrolling in egui.
|
||||
// BUT: if the user is using a nice smooth mac trackpad, we don't add smoothing,
|
||||
// because it adds latency.
|
||||
let is_smooth = match unit {
|
||||
MouseWheelUnit::Point => delta.length() < 8.0, // a bit arbitrary here
|
||||
MouseWheelUnit::Line | MouseWheelUnit::Page => false,
|
||||
};
|
||||
|
||||
let is_zoom = modifiers.matches_any(options.zoom_modifier);
|
||||
|
||||
#[expect(clippy::collapsible_else_if)]
|
||||
if is_zoom {
|
||||
if is_smooth {
|
||||
smooth_scroll_delta_for_zoom += delta.x + delta.y;
|
||||
} else {
|
||||
unprocessed_scroll_delta_for_zoom += delta.x + delta.y;
|
||||
}
|
||||
} else {
|
||||
if is_smooth {
|
||||
smooth_scroll_delta += delta;
|
||||
} else {
|
||||
unprocessed_scroll_delta += delta;
|
||||
}
|
||||
}
|
||||
self.wheel.on_wheel_event(
|
||||
viewport_rect,
|
||||
&options,
|
||||
time,
|
||||
*unit,
|
||||
*delta,
|
||||
*phase,
|
||||
*modifiers,
|
||||
);
|
||||
}
|
||||
Event::Zoom(factor) => {
|
||||
zoom_factor_delta *= *factor;
|
||||
@@ -508,54 +445,28 @@ impl InputState {
|
||||
}
|
||||
}
|
||||
|
||||
let mut smooth_scroll_delta = Vec2::ZERO;
|
||||
|
||||
{
|
||||
let dt = stable_dt.at_most(0.1);
|
||||
let t = crate::emath::exponential_smooth_factor(0.90, 0.1, dt); // reach _% in _ seconds. TODO(emilk): parameterize
|
||||
self.wheel.after_events(time, dt);
|
||||
|
||||
if unprocessed_scroll_delta != Vec2::ZERO {
|
||||
for d in 0..2 {
|
||||
if unprocessed_scroll_delta[d].abs() < 1.0 {
|
||||
smooth_scroll_delta[d] += unprocessed_scroll_delta[d];
|
||||
unprocessed_scroll_delta[d] = 0.0;
|
||||
} else {
|
||||
let applied = t * unprocessed_scroll_delta[d];
|
||||
smooth_scroll_delta[d] += applied;
|
||||
unprocessed_scroll_delta[d] -= applied;
|
||||
}
|
||||
}
|
||||
}
|
||||
let is_zoom = self.wheel.modifiers.matches_any(options.zoom_modifier);
|
||||
|
||||
{
|
||||
// Smooth scroll-to-zoom:
|
||||
if unprocessed_scroll_delta_for_zoom.abs() < 1.0 {
|
||||
smooth_scroll_delta_for_zoom += unprocessed_scroll_delta_for_zoom;
|
||||
unprocessed_scroll_delta_for_zoom = 0.0;
|
||||
} else {
|
||||
let applied = t * unprocessed_scroll_delta_for_zoom;
|
||||
smooth_scroll_delta_for_zoom += applied;
|
||||
unprocessed_scroll_delta_for_zoom -= applied;
|
||||
}
|
||||
|
||||
zoom_factor_delta *=
|
||||
(options.scroll_zoom_speed * smooth_scroll_delta_for_zoom).exp();
|
||||
if is_zoom {
|
||||
zoom_factor_delta *= (options.scroll_zoom_speed
|
||||
* (self.wheel.smooth_wheel_delta.x + self.wheel.smooth_wheel_delta.y))
|
||||
.exp();
|
||||
} else {
|
||||
smooth_scroll_delta = self.wheel.smooth_wheel_delta;
|
||||
}
|
||||
}
|
||||
|
||||
let is_scrolling = raw_scroll_delta != Vec2::ZERO || smooth_scroll_delta != Vec2::ZERO;
|
||||
let last_scroll_time = if is_scrolling {
|
||||
time
|
||||
} else {
|
||||
self.last_scroll_time
|
||||
};
|
||||
|
||||
Self {
|
||||
pointer,
|
||||
touch_states: self.touch_states,
|
||||
|
||||
last_scroll_time,
|
||||
unprocessed_scroll_delta,
|
||||
unprocessed_scroll_delta_for_zoom,
|
||||
raw_scroll_delta,
|
||||
wheel: self.wheel,
|
||||
smooth_scroll_delta,
|
||||
zoom_factor_delta,
|
||||
rotation_radians,
|
||||
@@ -627,6 +538,22 @@ impl InputState {
|
||||
self.safe_area_insets
|
||||
}
|
||||
|
||||
/// How many points the user scrolled, smoothed over a few frames.
|
||||
///
|
||||
/// The delta dictates how the _content_ should move.
|
||||
///
|
||||
/// A positive X-value indicates the content is being moved right,
|
||||
/// as when swiping right on a touch-screen or track-pad with natural scrolling.
|
||||
///
|
||||
/// A positive Y-value indicates the content is being moved down,
|
||||
/// as when swiping down on a touch-screen or track-pad with natural scrolling.
|
||||
///
|
||||
/// [`crate::ScrollArea`] will both read and write to this field, so that
|
||||
/// at the end of the frame this will be zero if a scroll-area consumed the delta.
|
||||
pub fn smooth_scroll_delta(&self) -> Vec2 {
|
||||
self.smooth_scroll_delta
|
||||
}
|
||||
|
||||
/// Uniform zoom scale factor this frame (e.g. from ctrl-scroll or pinch gesture).
|
||||
/// * `zoom = 1`: no change
|
||||
/// * `zoom < 1`: pinch together
|
||||
@@ -705,13 +632,18 @@ impl InputState {
|
||||
#[inline(always)]
|
||||
pub fn translation_delta(&self) -> Vec2 {
|
||||
self.multi_touch()
|
||||
.map_or(self.smooth_scroll_delta, |touch| touch.translation_delta)
|
||||
.map_or(self.smooth_scroll_delta(), |touch| touch.translation_delta)
|
||||
}
|
||||
|
||||
/// How long has it been (in seconds) since the use last scrolled?
|
||||
/// True if there is an active scroll action that might scroll more when using [`Self::smooth_scroll_delta`].
|
||||
pub fn is_scrolling(&self) -> bool {
|
||||
self.wheel.is_scrolling()
|
||||
}
|
||||
|
||||
/// How long has it been (in seconds) since the last scroll event?
|
||||
#[inline(always)]
|
||||
pub fn time_since_last_scroll(&self) -> f32 {
|
||||
(self.time - self.last_scroll_time) as f32
|
||||
(self.time - self.wheel.last_wheel_event) as f32
|
||||
}
|
||||
|
||||
/// The [`crate::Context`] will call this at the beginning of each frame to see if we need a repaint.
|
||||
@@ -723,8 +655,7 @@ impl InputState {
|
||||
/// cause a repaint.
|
||||
pub(crate) fn wants_repaint_after(&self) -> Option<Duration> {
|
||||
if self.pointer.wants_repaint()
|
||||
|| self.unprocessed_scroll_delta.abs().max_elem() > 0.2
|
||||
|| self.unprocessed_scroll_delta_for_zoom.abs() > 0.2
|
||||
|| self.wheel.unprocessed_wheel_delta.abs().max_elem() > 0.2
|
||||
|| !self.events.is_empty()
|
||||
{
|
||||
// Immediate repaint
|
||||
@@ -924,7 +855,6 @@ impl InputState {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn accesskit_action_requests(
|
||||
&self,
|
||||
id: crate::Id,
|
||||
@@ -942,7 +872,6 @@ impl InputState {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn consume_accesskit_action_requests(
|
||||
&mut self,
|
||||
id: crate::Id,
|
||||
@@ -959,12 +888,10 @@ impl InputState {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn has_accesskit_action_request(&self, id: crate::Id, action: accesskit::Action) -> bool {
|
||||
self.accesskit_action_requests(id, action).next().is_some()
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn num_accesskit_action_requests(&self, id: crate::Id, action: accesskit::Action) -> usize {
|
||||
self.accesskit_action_requests(id, action).count()
|
||||
}
|
||||
@@ -1336,6 +1263,11 @@ impl PointerState {
|
||||
self.press_origin
|
||||
}
|
||||
|
||||
/// How far has the pointer moved since the start of the drag (if any)?
|
||||
pub fn total_drag_delta(&self) -> Option<Vec2> {
|
||||
Some(self.latest_pos? - self.press_origin?)
|
||||
}
|
||||
|
||||
/// When did the current click/drag originate?
|
||||
/// `None` if no mouse button is down.
|
||||
#[inline(always)]
|
||||
@@ -1598,14 +1530,9 @@ impl InputState {
|
||||
raw,
|
||||
pointer,
|
||||
touch_states,
|
||||
|
||||
last_scroll_time,
|
||||
unprocessed_scroll_delta,
|
||||
unprocessed_scroll_delta_for_zoom,
|
||||
raw_scroll_delta,
|
||||
wheel,
|
||||
smooth_scroll_delta,
|
||||
rotation_radians,
|
||||
|
||||
zoom_factor_delta,
|
||||
viewport_rect,
|
||||
safe_area_insets,
|
||||
@@ -1642,22 +1569,13 @@ impl InputState {
|
||||
});
|
||||
}
|
||||
|
||||
ui.label(format!(
|
||||
"Time since last scroll: {:.1} s",
|
||||
time - last_scroll_time
|
||||
));
|
||||
if cfg!(debug_assertions) {
|
||||
ui.label(format!(
|
||||
"unprocessed_scroll_delta: {unprocessed_scroll_delta:?} points"
|
||||
));
|
||||
ui.label(format!(
|
||||
"unprocessed_scroll_delta_for_zoom: {unprocessed_scroll_delta_for_zoom:?} points"
|
||||
));
|
||||
}
|
||||
ui.label(format!("raw_scroll_delta: {raw_scroll_delta:?} points"));
|
||||
ui.label(format!(
|
||||
"smooth_scroll_delta: {smooth_scroll_delta:?} points"
|
||||
));
|
||||
crate::containers::CollapsingHeader::new("⬍ Scroll")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
wheel.ui(ui);
|
||||
});
|
||||
|
||||
ui.label(format!("smooth_scroll_delta: {smooth_scroll_delta:4.1}x"));
|
||||
ui.label(format!("zoom_factor_delta: {zoom_factor_delta:4.2}x"));
|
||||
ui.label(format!("rotation_radians: {rotation_radians:.3} radians"));
|
||||
|
||||
|
||||
233
crates/egui/src/input_state/wheel_state.rs
Normal file
233
crates/egui/src/input_state/wheel_state.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
use emath::{Rect, Vec2, vec2};
|
||||
|
||||
use crate::{InputOptions, Modifiers, MouseWheelUnit, TouchPhase};
|
||||
|
||||
/// The current state of scrolling.
|
||||
///
|
||||
/// There are two important types of scroll input deviced:
|
||||
/// * Discreen scroll wheels on a mouse
|
||||
/// * Smooth scroll input from a trackpad
|
||||
///
|
||||
/// Scroll wheels will usually fire one single scroll event,
|
||||
/// so it is important that egui smooths it out over time.
|
||||
///
|
||||
/// On the contrary, trackpads usually provide smooth scroll input,
|
||||
/// and with kinetic scrolling (which on Mac is implemented by the OS)
|
||||
/// scroll events can arrive _after_ the user lets go of the trackpad.
|
||||
///
|
||||
/// In either case, we consider use to be scrolling until there is no more
|
||||
/// scroll events expected.
|
||||
///
|
||||
/// This means there are a few different states we can be in:
|
||||
/// * Not scrolling
|
||||
/// * "Smooth scrolling" (low-pass filter of discreet scroll events)
|
||||
/// * Trackpad-scrolling (we receive begin/end phases for these)
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Status {
|
||||
/// Not scrolling,
|
||||
Static,
|
||||
|
||||
/// We're smoothing out previous scroll events
|
||||
Smoothing,
|
||||
|
||||
// We're in-between [`TouchPhase::Start`] and [`TouchPhase::End`] of a trackpad scroll.
|
||||
InTouch,
|
||||
}
|
||||
|
||||
/// Keeps track of wheel (scroll) input.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WheelState {
|
||||
/// Are we currently in a scroll action?
|
||||
///
|
||||
/// This may be true even if no scroll events came in this frame,
|
||||
/// but we are in a kinetic scroll or in a smoothed scroll.
|
||||
pub status: Status,
|
||||
|
||||
/// The modifiers at the start of the scroll.
|
||||
pub modifiers: Modifiers,
|
||||
|
||||
/// Time of the last scroll event.
|
||||
pub last_wheel_event: f64,
|
||||
|
||||
/// Used for smoothing the scroll delta.
|
||||
pub unprocessed_wheel_delta: Vec2,
|
||||
|
||||
/// How many points the user scrolled, smoothed over a few frames.
|
||||
///
|
||||
/// The delta dictates how the _content_ should move.
|
||||
///
|
||||
/// A positive X-value indicates the content is being moved right,
|
||||
/// as when swiping right on a touch-screen or track-pad with natural scrolling.
|
||||
///
|
||||
/// A positive Y-value indicates the content is being moved down,
|
||||
/// as when swiping down on a touch-screen or track-pad with natural scrolling.
|
||||
///
|
||||
/// [`crate::ScrollArea`] will both read and write to this field, so that
|
||||
/// at the end of the frame this will be zero if a scroll-area consumed the delta.
|
||||
pub smooth_wheel_delta: Vec2,
|
||||
}
|
||||
|
||||
impl Default for WheelState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
status: Status::Static,
|
||||
modifiers: Default::default(),
|
||||
last_wheel_event: f64::NEG_INFINITY,
|
||||
unprocessed_wheel_delta: Vec2::ZERO,
|
||||
smooth_wheel_delta: Vec2::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WheelState {
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub fn on_wheel_event(
|
||||
&mut self,
|
||||
viewport_rect: Rect,
|
||||
options: &InputOptions,
|
||||
time: f64,
|
||||
unit: MouseWheelUnit,
|
||||
delta: Vec2,
|
||||
phase: TouchPhase,
|
||||
latest_modifiers: Modifiers,
|
||||
) {
|
||||
self.last_wheel_event = time;
|
||||
match phase {
|
||||
crate::TouchPhase::Start => {
|
||||
self.status = Status::InTouch;
|
||||
self.modifiers = latest_modifiers;
|
||||
}
|
||||
crate::TouchPhase::Move => {
|
||||
match self.status {
|
||||
Status::Static | Status::Smoothing => {
|
||||
self.modifiers = latest_modifiers;
|
||||
self.status = Status::Smoothing;
|
||||
}
|
||||
Status::InTouch => {
|
||||
// If the user lets go of a modifier - ignore it.
|
||||
// More kinematic scrolling may arrive.
|
||||
// But if the users presses down new modifiers - heed it!
|
||||
self.modifiers |= latest_modifiers;
|
||||
}
|
||||
}
|
||||
|
||||
let mut delta = match unit {
|
||||
MouseWheelUnit::Point => delta,
|
||||
MouseWheelUnit::Line => options.line_scroll_speed * delta,
|
||||
MouseWheelUnit::Page => viewport_rect.height() * delta,
|
||||
};
|
||||
|
||||
let is_horizontal = self
|
||||
.modifiers
|
||||
.matches_any(options.horizontal_scroll_modifier);
|
||||
let is_vertical = self.modifiers.matches_any(options.vertical_scroll_modifier);
|
||||
|
||||
if is_horizontal && !is_vertical {
|
||||
// Treat all scrolling as horizontal scrolling.
|
||||
// Note: one Mac we already get horizontal scroll events when shift is down.
|
||||
delta = vec2(delta.x + delta.y, 0.0);
|
||||
}
|
||||
if !is_horizontal && is_vertical {
|
||||
// Treat all scrolling as vertical scrolling.
|
||||
delta = vec2(0.0, delta.x + delta.y);
|
||||
}
|
||||
|
||||
// Mouse wheels often go very large steps.
|
||||
// A single notch on a logitech mouse wheel connected to a Macbook returns 14.0 raw scroll delta.
|
||||
// So we smooth it out over several frames for a nicer user experience when scrolling in egui.
|
||||
// BUT: if the user is using a nice smooth mac trackpad, we don't add smoothing,
|
||||
// because it adds latency.
|
||||
let is_smooth = self.status == Status::InTouch
|
||||
|| match unit {
|
||||
MouseWheelUnit::Point => delta.length() < 8.0, // a bit arbitrary here
|
||||
MouseWheelUnit::Line | MouseWheelUnit::Page => false,
|
||||
};
|
||||
|
||||
if is_smooth {
|
||||
self.smooth_wheel_delta += delta;
|
||||
} else {
|
||||
self.unprocessed_wheel_delta += delta;
|
||||
}
|
||||
}
|
||||
crate::TouchPhase::End | crate::TouchPhase::Cancel => {
|
||||
self.status = Status::Static;
|
||||
self.modifiers = Default::default();
|
||||
self.unprocessed_wheel_delta = Default::default();
|
||||
self.smooth_wheel_delta = Default::default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn after_events(&mut self, time: f64, dt: f32) {
|
||||
let t = crate::emath::exponential_smooth_factor(0.90, 0.1, dt); // reach _% in _ seconds. TODO(emilk): parameterize
|
||||
|
||||
if self.unprocessed_wheel_delta != Vec2::ZERO {
|
||||
for d in 0..2 {
|
||||
if self.unprocessed_wheel_delta[d].abs() < 1.0 {
|
||||
self.smooth_wheel_delta[d] += self.unprocessed_wheel_delta[d];
|
||||
self.unprocessed_wheel_delta[d] = 0.0;
|
||||
} else {
|
||||
let applied = t * self.unprocessed_wheel_delta[d];
|
||||
self.smooth_wheel_delta[d] += applied;
|
||||
self.unprocessed_wheel_delta[d] -= applied;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let time_since_last_scroll = time - self.last_wheel_event;
|
||||
|
||||
if self.status == Status::Smoothing
|
||||
&& self.smooth_wheel_delta == Vec2::ZERO
|
||||
&& 0.150 < time_since_last_scroll
|
||||
{
|
||||
// On certain platforms, like web, we don't get the start & stop scrolling events, so
|
||||
// we rely on a timer there.
|
||||
//
|
||||
// Tested on a mac touchpad 2025, where the largest observed gap between scroll events
|
||||
// was 68 ms. But we add some margin to be safe
|
||||
self.status = Status::Static;
|
||||
self.modifiers = Default::default();
|
||||
}
|
||||
}
|
||||
|
||||
/// True if there is an active scroll action that might scroll more when using [`Self::smooth_wheel_delta`].
|
||||
pub fn is_scrolling(&self) -> bool {
|
||||
self.status != Status::Static
|
||||
}
|
||||
|
||||
pub fn ui(&self, ui: &mut crate::Ui) {
|
||||
let Self {
|
||||
status,
|
||||
modifiers,
|
||||
last_wheel_event,
|
||||
unprocessed_wheel_delta,
|
||||
smooth_wheel_delta,
|
||||
} = self;
|
||||
|
||||
let time = ui.input(|i| i.time);
|
||||
|
||||
crate::Grid::new("ScrollState")
|
||||
.num_columns(2)
|
||||
.show(ui, |ui| {
|
||||
ui.label("status");
|
||||
ui.monospace(format!("{status:?}"));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("modifiers");
|
||||
ui.monospace(format!("{modifiers:?}"));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("last_wheel_event");
|
||||
ui.monospace(format!("{:.1}s ago", time - *last_wheel_event));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("unprocessed_wheel_delta");
|
||||
ui.monospace(unprocessed_wheel_delta.to_string());
|
||||
ui.end_row();
|
||||
|
||||
ui.label("smooth_wheel_delta");
|
||||
ui.monospace(smooth_wheel_delta.to_string());
|
||||
ui.end_row();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
//! which uses [`eframe`](https://docs.rs/eframe).
|
||||
//!
|
||||
//! To create a GUI using egui you first need a [`Context`] (by convention referred to by `ctx`).
|
||||
//! 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.
|
||||
//! Then you add a [`Window`] or a [`Panel`] to get a [`Ui`], which is what you'll be using to add all the buttons and labels that you need.
|
||||
//!
|
||||
//!
|
||||
//! ## Feature flags
|
||||
@@ -45,7 +45,7 @@
|
||||
//!
|
||||
//! ### Getting a [`Ui`]
|
||||
//!
|
||||
//! Use one of [`SidePanel`], [`TopBottomPanel`], [`CentralPanel`], [`Window`] or [`Area`] to
|
||||
//! Use one of [`Panel`], [`CentralPanel`], [`Window`] or [`Area`] to
|
||||
//! get access to an [`Ui`] where you can put widgets. For example:
|
||||
//!
|
||||
//! ```
|
||||
@@ -322,7 +322,7 @@
|
||||
//! when you release the panel/window shrinks again.
|
||||
//! This is an artifact of immediate mode, and here are some alternatives on how to avoid it:
|
||||
//!
|
||||
//! 1. Turn off resizing with [`Window::resizable`], [`SidePanel::resizable`], [`TopBottomPanel::resizable`].
|
||||
//! 1. Turn off resizing with [`Window::resizable`], [`Panel::resizable`].
|
||||
//! 2. Wrap your panel contents in a [`ScrollArea`], or use [`Window::vscroll`] and [`Window::hscroll`].
|
||||
//! 3. Use a justified layout:
|
||||
//!
|
||||
@@ -448,7 +448,6 @@ pub mod widgets;
|
||||
#[cfg(debug_assertions)]
|
||||
mod callstack;
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub use accesskit;
|
||||
|
||||
#[deprecated = "Use the ahash crate directly."]
|
||||
@@ -692,8 +691,8 @@ pub enum WidgetType {
|
||||
pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) {
|
||||
let ctx = Context::default();
|
||||
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
|
||||
let _ = ctx.run(Default::default(), |ctx| {
|
||||
run_ui(ctx);
|
||||
let _ = ctx.run_ui(Default::default(), |ui| {
|
||||
run_ui(ui.ctx());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -701,14 +700,11 @@ pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) {
|
||||
pub fn __run_test_ui(add_contents: impl Fn(&mut Ui)) {
|
||||
let ctx = Context::default();
|
||||
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
|
||||
let _ = ctx.run(Default::default(), |ctx| {
|
||||
crate::CentralPanel::default().show(ctx, |ui| {
|
||||
add_contents(ui);
|
||||
});
|
||||
let _ = ctx.run_ui(Default::default(), |ui| {
|
||||
add_contents(ui);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn accesskit_root_id() -> Id {
|
||||
Id::new("accesskit_root")
|
||||
}
|
||||
|
||||
@@ -470,7 +470,6 @@ pub(crate) struct Focus {
|
||||
/// The ID of a widget to give the focus to in the next frame.
|
||||
id_next_frame: Option<Id>,
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
id_requested_by_accesskit: Option<accesskit::NodeId>,
|
||||
|
||||
/// If set, the next widget that is interested in focus will automatically get it.
|
||||
@@ -529,10 +528,7 @@ impl Focus {
|
||||
}
|
||||
let event_filter = self.focused_widget.map(|w| w.filter).unwrap_or_default();
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
self.id_requested_by_accesskit = None;
|
||||
}
|
||||
self.id_requested_by_accesskit = None;
|
||||
|
||||
self.focus_direction = FocusDirection::None;
|
||||
|
||||
@@ -567,16 +563,13 @@ impl Focus {
|
||||
self.focus_direction = cardinality;
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
if let crate::Event::AccessKitActionRequest(accesskit::ActionRequest {
|
||||
action: accesskit::Action::Focus,
|
||||
target,
|
||||
data: None,
|
||||
}) = event
|
||||
{
|
||||
if let crate::Event::AccessKitActionRequest(accesskit::ActionRequest {
|
||||
action: accesskit::Action::Focus,
|
||||
target,
|
||||
data: None,
|
||||
}) = event
|
||||
{
|
||||
self.id_requested_by_accesskit = Some(*target);
|
||||
}
|
||||
self.id_requested_by_accesskit = Some(*target);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -606,14 +599,11 @@ impl Focus {
|
||||
}
|
||||
|
||||
fn interested_in_focus(&mut self, id: Id) {
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
if self.id_requested_by_accesskit == Some(id.accesskit_id()) {
|
||||
self.focused_widget = Some(FocusWidget::new(id));
|
||||
self.id_requested_by_accesskit = None;
|
||||
self.give_to_next = false;
|
||||
self.reset_focus();
|
||||
}
|
||||
if self.id_requested_by_accesskit == Some(id.accesskit_id()) {
|
||||
self.focused_widget = Some(FocusWidget::new(id));
|
||||
self.id_requested_by_accesskit = None;
|
||||
self.give_to_next = false;
|
||||
self.reset_focus();
|
||||
}
|
||||
|
||||
// The rect is updated at the end of the frame.
|
||||
@@ -1282,8 +1272,7 @@ impl Areas {
|
||||
pub fn top_layer_id(&self, order: Order) -> Option<LayerId> {
|
||||
self.order
|
||||
.iter()
|
||||
.filter(|layer| layer.order == order && !self.is_sublayer(layer))
|
||||
.next_back()
|
||||
.rfind(|layer| layer.order == order && !self.is_sublayer(layer))
|
||||
.copied()
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ fn set_menu_style(style: &mut Style) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The menu bar goes well in a [`crate::TopBottomPanel::top`],
|
||||
/// The menu bar goes well in a [`crate::Panel::top`],
|
||||
/// but can also be placed in a [`crate::Window`].
|
||||
/// In the latter case you may want to wrap it in [`Frame`].
|
||||
#[deprecated = "Use `egui::MenuBar::new().ui(` instead"]
|
||||
@@ -634,7 +634,7 @@ impl SubMenu {
|
||||
/// Usually you don't need to use it directly.
|
||||
pub struct MenuState {
|
||||
/// The opened sub-menu and its [`Id`]
|
||||
sub_menu: Option<(Id, Arc<RwLock<MenuState>>)>,
|
||||
sub_menu: Option<(Id, Arc<RwLock<Self>>)>,
|
||||
|
||||
/// Bounding box of this menu (without the sub-menu),
|
||||
/// including the frame and everything.
|
||||
|
||||
@@ -67,7 +67,6 @@ impl ScrollTarget {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
#[derive(Clone)]
|
||||
pub struct AccessKitPassState {
|
||||
pub nodes: IdMap<accesskit::Node>,
|
||||
@@ -225,7 +224,6 @@ pub struct PassState {
|
||||
/// as when swiping down on a touch-screen or track-pad with natural scrolling.
|
||||
pub scroll_delta: (Vec2, style::ScrollAnimation),
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub accesskit_state: Option<AccessKitPassState>,
|
||||
|
||||
/// Highlight these widgets the next pass.
|
||||
@@ -247,7 +245,6 @@ impl Default for PassState {
|
||||
used_by_panels: Rect::NAN,
|
||||
scroll_target: [None, None],
|
||||
scroll_delta: (Vec2::default(), style::ScrollAnimation::none()),
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_state: None,
|
||||
highlight_next_pass: Default::default(),
|
||||
|
||||
@@ -270,7 +267,6 @@ impl PassState {
|
||||
used_by_panels,
|
||||
scroll_target,
|
||||
scroll_delta,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accesskit_state,
|
||||
highlight_next_pass,
|
||||
|
||||
@@ -293,10 +289,7 @@ impl PassState {
|
||||
*debug_rect = None;
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
*accesskit_state = None;
|
||||
}
|
||||
*accesskit_state = None;
|
||||
|
||||
highlight_next_pass.clear();
|
||||
}
|
||||
|
||||
@@ -34,14 +34,21 @@ pub trait Plugin: Send + Sync + std::any::Any + 'static {
|
||||
/// Called just before the input is processed.
|
||||
///
|
||||
/// Useful to inspect or modify the input.
|
||||
/// Since this is called outside a pass, don't show ui here.
|
||||
/// Since this is called outside a pass, don't show ui here. Using `Context::debug_painter` is fine though.
|
||||
fn input_hook(&mut self, input: &mut RawInput) {}
|
||||
|
||||
/// Called just before the output is passed to the backend.
|
||||
///
|
||||
/// Useful to inspect or modify the output.
|
||||
/// Since this is called outside a pass, don't show ui here.
|
||||
/// Since this is called outside a pass, don't show ui here. Using `Context::debug_painter` is fine though.
|
||||
fn output_hook(&mut self, output: &mut FullOutput) {}
|
||||
|
||||
/// Called when a widget is created and is under the pointer.
|
||||
///
|
||||
/// Useful for capturing a stack trace so that widgets can be mapped back to their source code.
|
||||
/// Since this is called outside a pass, don't show ui here. Using `Context::debug_painter` is fine though.
|
||||
#[cfg(debug_assertions)]
|
||||
fn on_widget_under_pointer(&mut self, ctx: &Context, widget: &crate::WidgetRect) {}
|
||||
}
|
||||
|
||||
pub(crate) struct PluginHandle {
|
||||
@@ -167,6 +174,14 @@ impl PluginsOrdered {
|
||||
plugin.output_hook(output);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub fn on_widget_under_pointer(&self, ctx: &Context, widget: &crate::WidgetRect) {
|
||||
profiling::scope!("plugins", "on_widget_under_pointer");
|
||||
self.for_each_dyn(|plugin| {
|
||||
plugin.on_widget_under_pointer(ctx, widget);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugins {
|
||||
@@ -216,7 +231,7 @@ impl Plugin for CallbackPlugin {
|
||||
profiling::function_scope!();
|
||||
|
||||
for (_debug_name, cb) in &self.on_begin_plugins {
|
||||
profiling::scope!(*_debug_name);
|
||||
profiling::scope!("on_begin_pass", *_debug_name);
|
||||
(cb)(ctx);
|
||||
}
|
||||
}
|
||||
@@ -225,7 +240,7 @@ impl Plugin for CallbackPlugin {
|
||||
profiling::function_scope!();
|
||||
|
||||
for (_debug_name, cb) in &self.on_end_plugins {
|
||||
profiling::scope!(*_debug_name);
|
||||
profiling::scope!("on_end_pass", *_debug_name);
|
||||
(cb)(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,7 +396,7 @@ impl Response {
|
||||
self.drag_stopped() && self.ctx.input(|i| i.pointer.button_released(button))
|
||||
}
|
||||
|
||||
/// If dragged, how many points were we dragged and in what direction?
|
||||
/// If dragged, how many points were we dragged in since last frame?
|
||||
#[inline]
|
||||
pub fn drag_delta(&self) -> Vec2 {
|
||||
if self.dragged() {
|
||||
@@ -410,7 +410,22 @@ impl Response {
|
||||
}
|
||||
}
|
||||
|
||||
/// If dragged, how far did the mouse move?
|
||||
/// If dragged, how many points have we been dragged since the start of the drag?
|
||||
#[inline]
|
||||
pub fn total_drag_delta(&self) -> Option<Vec2> {
|
||||
if self.dragged() {
|
||||
let mut delta = self.ctx.input(|i| i.pointer.total_drag_delta())?;
|
||||
if let Some(from_global) = self.ctx.layer_transform_from_global(self.layer_id) {
|
||||
delta *= from_global.scaling;
|
||||
}
|
||||
Some(delta)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// If dragged, how far did the mouse move since last frame?
|
||||
///
|
||||
/// This will use raw mouse movement if provided by the integration, otherwise will fall back to [`Response::drag_delta`]
|
||||
/// Raw mouse movement is unaccelerated and unclamped by screen boundaries, and does not relate to any position on the screen.
|
||||
/// This may be useful in certain situations such as draggable values and 3D cameras, where screen position does not matter.
|
||||
@@ -418,7 +433,7 @@ impl Response {
|
||||
pub fn drag_motion(&self) -> Vec2 {
|
||||
if self.dragged() {
|
||||
self.ctx
|
||||
.input(|i| i.pointer.motion().unwrap_or(i.pointer.delta()))
|
||||
.input(|i| i.pointer.motion().unwrap_or_else(|| i.pointer.delta()))
|
||||
} else {
|
||||
Vec2::ZERO
|
||||
}
|
||||
@@ -457,7 +472,7 @@ impl Response {
|
||||
///
|
||||
/// Only returns something if [`Self::contains_pointer`] is true,
|
||||
/// the user is drag-dropping something of this type,
|
||||
/// and they released it this frame
|
||||
/// and they released it this frame.
|
||||
#[doc(alias = "drag and drop")]
|
||||
pub fn dnd_release_payload<Payload: Any + Send + Sync>(&self) -> Option<Arc<Payload>> {
|
||||
// NOTE: we use `response.contains_pointer` here instead of `hovered`, because
|
||||
@@ -709,10 +724,9 @@ impl Response {
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn interact(&self, sense: Sense) -> Self {
|
||||
if (self.sense | sense) == self.sense {
|
||||
// Early-out: we already sense everything we need to sense.
|
||||
return self.clone();
|
||||
}
|
||||
// We could check here if the new Sense equals the old one to avoid the extra create_widget
|
||||
// call. But that would break calling `interact` on a response from `Context::read_response`
|
||||
// or `Ui::response`. (See https://github.com/emilk/egui/pull/7713 for more details.)
|
||||
|
||||
self.ctx.create_widget(
|
||||
WidgetRect {
|
||||
@@ -793,7 +807,6 @@ impl Response {
|
||||
if let Some(event) = event {
|
||||
self.output_event(event);
|
||||
} else {
|
||||
#[cfg(feature = "accesskit")]
|
||||
self.ctx.accesskit_node_builder(self.id, |builder| {
|
||||
self.fill_accesskit_node_from_widget_info(builder, make_info());
|
||||
});
|
||||
@@ -803,7 +816,6 @@ impl Response {
|
||||
}
|
||||
|
||||
pub fn output_event(&self, event: crate::output::OutputEvent) {
|
||||
#[cfg(feature = "accesskit")]
|
||||
self.ctx.accesskit_node_builder(self.id, |builder| {
|
||||
self.fill_accesskit_node_from_widget_info(builder, event.widget_info().clone());
|
||||
});
|
||||
@@ -814,7 +826,6 @@ impl Response {
|
||||
self.ctx.output_mut(|o| o.events.push(event));
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub(crate) fn fill_accesskit_node_common(&self, builder: &mut accesskit::Node) {
|
||||
if !self.enabled() {
|
||||
builder.set_disabled();
|
||||
@@ -833,7 +844,6 @@ impl Response {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
fn fill_accesskit_node_from_widget_info(
|
||||
&self,
|
||||
builder: &mut accesskit::Node,
|
||||
@@ -908,14 +918,9 @@ impl Response {
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn labelled_by(self, id: Id) -> Self {
|
||||
#[cfg(feature = "accesskit")]
|
||||
self.ctx.accesskit_node_builder(self.id, |builder| {
|
||||
builder.push_labelled_by(id.accesskit_id());
|
||||
});
|
||||
#[cfg(not(feature = "accesskit"))]
|
||||
{
|
||||
let _ = id;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
@@ -508,6 +508,12 @@ pub struct ScrollStyle {
|
||||
/// it more promiment.
|
||||
pub floating: bool,
|
||||
|
||||
/// Extra margin added around the contents of a [`crate::ScrollArea`].
|
||||
///
|
||||
/// The scroll bars will be either on top of this margin, or outside of it,
|
||||
/// depending on the value of [`Self::floating`].
|
||||
pub content_margin: Margin,
|
||||
|
||||
/// The width of the scroll bars at it largest.
|
||||
pub bar_width: f32,
|
||||
|
||||
@@ -591,6 +597,7 @@ impl ScrollStyle {
|
||||
pub fn solid() -> Self {
|
||||
Self {
|
||||
floating: false,
|
||||
content_margin: Margin::ZERO,
|
||||
bar_width: 6.0,
|
||||
handle_min_length: 12.0,
|
||||
bar_inner_margin: 4.0,
|
||||
@@ -672,6 +679,9 @@ impl ScrollStyle {
|
||||
pub fn details_ui(&mut self, ui: &mut Ui) {
|
||||
let Self {
|
||||
floating,
|
||||
|
||||
content_margin,
|
||||
|
||||
bar_width,
|
||||
handle_min_length,
|
||||
bar_inner_margin,
|
||||
@@ -695,6 +705,11 @@ impl ScrollStyle {
|
||||
ui.selectable_value(floating, true, "Floating");
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Content margin:");
|
||||
content_margin.ui(ui);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(DragValue::new(bar_width).range(0.0..=32.0));
|
||||
ui.label("Full bar width");
|
||||
@@ -1129,7 +1144,10 @@ impl Visuals {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct Selection {
|
||||
/// Background color behind selected text and other selectable buttons.
|
||||
pub bg_fill: Color32,
|
||||
|
||||
/// Color of selected text.
|
||||
pub stroke: Stroke,
|
||||
}
|
||||
|
||||
@@ -1821,6 +1839,10 @@ impl Spacing {
|
||||
ui.add(window_margin);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("ScrollArea margin");
|
||||
scroll.content_margin.ui(ui);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Menu margin");
|
||||
ui.add(menu_margin);
|
||||
ui.end_row();
|
||||
|
||||
@@ -42,8 +42,9 @@ pub fn update_accesskit_for_text_widget(
|
||||
|
||||
for (row_index, row) in galley.rows.iter().enumerate() {
|
||||
let row_id = parent_id.with(row_index);
|
||||
#[cfg(feature = "accesskit")]
|
||||
|
||||
ctx.register_accesskit_parent(row_id, parent_id);
|
||||
|
||||
ctx.accesskit_node_builder(row_id, |builder| {
|
||||
builder.set_role(accesskit::Role::TextRun);
|
||||
let rect = global_from_galley * row.rect_without_leading_space();
|
||||
|
||||
@@ -190,7 +190,6 @@ impl CCursorRange {
|
||||
..
|
||||
} => self.on_key_press(os, galley, modifiers, *key),
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
Event::AccessKitActionRequest(accesskit::ActionRequest {
|
||||
action: accesskit::Action::SetTextSelection,
|
||||
target,
|
||||
@@ -220,7 +219,6 @@ impl CCursorRange {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
fn ccursor_from_accesskit_text_position(
|
||||
id: Id,
|
||||
galley: &Galley,
|
||||
|
||||
@@ -624,7 +624,6 @@ impl LabelSelectionState {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
super::accesskit_text::update_accesskit_for_text_widget(
|
||||
ui.ctx(),
|
||||
response.id,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Helpers regarding text selection for labels and text edit.
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub mod accesskit_text;
|
||||
|
||||
mod cursor_range;
|
||||
|
||||
@@ -208,25 +208,33 @@ fn ccursor_previous_line(text: &str, ccursor: CCursor) -> CCursor {
|
||||
}
|
||||
}
|
||||
|
||||
fn next_word_boundary_char_index(text: &str, index: usize) -> usize {
|
||||
for word in text.split_word_bound_indices() {
|
||||
fn next_word_boundary_char_index(text: &str, cursor_ci: usize) -> usize {
|
||||
for (word_byte_index, word) in text.split_word_bound_indices() {
|
||||
let word_ci = char_index_from_byte_index(text, word_byte_index);
|
||||
|
||||
// We consider `.` a word boundary.
|
||||
// At least that's how Mac works when navigating something like `www.example.com`.
|
||||
for (dot_ci_offset, chr) in word.chars().enumerate() {
|
||||
let dot_ci = word_ci + dot_ci_offset;
|
||||
if chr == '.' && cursor_ci < dot_ci {
|
||||
return dot_ci;
|
||||
}
|
||||
}
|
||||
|
||||
// Splitting considers contiguous whitespace as one word, such words must be skipped,
|
||||
// this handles cases for example ' abc' (a space and a word), the cursor is at the beginning
|
||||
// (before space) - this jumps at the end of 'abc' (this is consistent with text editors
|
||||
// or browsers)
|
||||
let ci = char_index_from_byte_index(text, word.0);
|
||||
if ci > index && !skip_word(word.1) {
|
||||
return ci;
|
||||
if cursor_ci < word_ci && !all_word_chars(word) {
|
||||
return word_ci;
|
||||
}
|
||||
}
|
||||
|
||||
char_index_from_byte_index(text, text.len())
|
||||
}
|
||||
|
||||
fn skip_word(text: &str) -> bool {
|
||||
// skip words that contain anything other than alphanumeric characters and underscore
|
||||
// (i.e. whitespace, dashes, etc.)
|
||||
!text.chars().any(|c| !is_word_char(c))
|
||||
fn all_word_chars(text: &str) -> bool {
|
||||
text.chars().all(is_word_char)
|
||||
}
|
||||
|
||||
fn next_line_boundary_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
|
||||
@@ -337,6 +345,12 @@ mod test {
|
||||
assert_eq!(next_word_boundary_char_index("", 0), 0);
|
||||
assert_eq!(next_word_boundary_char_index("", 1), 0);
|
||||
|
||||
// ASCII only
|
||||
let text = "abc.def.ghi";
|
||||
assert_eq!(next_word_boundary_char_index(text, 1), 3);
|
||||
assert_eq!(next_word_boundary_char_index(text, 3), 7);
|
||||
assert_eq!(next_word_boundary_char_index(text, 7), 11);
|
||||
|
||||
// Unicode graphemes, some of which consist of multiple Unicode characters,
|
||||
// !!! Unicode character is not always what is tranditionally considered a character,
|
||||
// the values below are correct despite not seeming that way on the first look,
|
||||
|
||||
@@ -25,13 +25,16 @@ pub fn paint_text_selection(
|
||||
// and so we need to clone it if it is shared:
|
||||
let galley: &mut Galley = Arc::make_mut(galley);
|
||||
|
||||
let color = visuals.selection.bg_fill;
|
||||
let background_color = visuals.selection.bg_fill;
|
||||
let text_color = visuals.selection.stroke.color;
|
||||
|
||||
let [min, max] = cursor_range.sorted_cursors();
|
||||
let min = galley.layout_from_cursor(min);
|
||||
let max = galley.layout_from_cursor(max);
|
||||
|
||||
for ri in min.row..=max.row {
|
||||
let row = Arc::make_mut(&mut galley.rows[ri].row);
|
||||
let placed_row = &mut galley.rows[ri];
|
||||
let row = Arc::make_mut(&mut placed_row.row);
|
||||
|
||||
let left = if ri == min.row {
|
||||
row.x_offset(min.column)
|
||||
@@ -41,7 +44,7 @@ pub fn paint_text_selection(
|
||||
let right = if ri == max.row {
|
||||
row.x_offset(max.column)
|
||||
} else {
|
||||
let newline_size = if row.ends_with_newline {
|
||||
let newline_size = if placed_row.ends_with_newline {
|
||||
row.height() / 2.0 // visualize that we select the newline
|
||||
} else {
|
||||
0.0
|
||||
@@ -52,6 +55,31 @@ pub fn paint_text_selection(
|
||||
let rect = Rect::from_min_max(pos2(left, 0.0), pos2(right, row.size.y));
|
||||
let mesh = &mut row.visuals.mesh;
|
||||
|
||||
if !row.glyphs.is_empty() {
|
||||
// Change color of the selected text:
|
||||
let first_glyph_index = if ri == min.row { min.column } else { 0 };
|
||||
let last_glyph_index = if ri == max.row {
|
||||
max.column
|
||||
} else {
|
||||
row.glyphs.len() - 1
|
||||
};
|
||||
|
||||
let first_vertex_index = row
|
||||
.glyphs
|
||||
.get(first_glyph_index)
|
||||
.map_or(row.visuals.glyph_vertex_range.start, |g| {
|
||||
g.first_vertex as _
|
||||
});
|
||||
let last_vertex_index = row
|
||||
.glyphs
|
||||
.get(last_glyph_index)
|
||||
.map_or(row.visuals.glyph_vertex_range.end, |g| g.first_vertex as _);
|
||||
|
||||
for vi in first_vertex_index..last_vertex_index {
|
||||
mesh.vertices[vi].color = text_color;
|
||||
}
|
||||
}
|
||||
|
||||
// Time to insert the selection rectangle into the row mesh.
|
||||
// It should be on top (after) of any background in the galley,
|
||||
// but behind (before) any glyphs. The row visuals has this information:
|
||||
@@ -59,7 +87,7 @@ pub fn paint_text_selection(
|
||||
|
||||
// Start by appending the selection rectangle to end of the mesh, as two triangles (= 6 indices):
|
||||
let num_indices_before = mesh.indices.len();
|
||||
mesh.add_colored_rect(rect, color);
|
||||
mesh.add_colored_rect(rect, background_color);
|
||||
assert_eq!(
|
||||
num_indices_before + 6,
|
||||
mesh.indices.len(),
|
||||
|
||||
@@ -119,7 +119,7 @@ impl Ui {
|
||||
/// Create a new top-level [`Ui`].
|
||||
///
|
||||
/// Normally you would not use this directly, but instead use
|
||||
/// [`crate::SidePanel`], [`crate::TopBottomPanel`], [`crate::CentralPanel`], [`crate::Window`] or [`crate::Area`].
|
||||
/// [`crate::Panel`], [`crate::CentralPanel`], [`crate::Window`] or [`crate::Area`].
|
||||
pub fn new(ctx: Context, id: Id, ui_builder: UiBuilder) -> Self {
|
||||
let UiBuilder {
|
||||
id_salt,
|
||||
@@ -133,11 +133,10 @@ impl Ui {
|
||||
sizing_pass,
|
||||
style,
|
||||
sense,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accessibility_parent,
|
||||
} = ui_builder;
|
||||
|
||||
let layer_id = layer_id.unwrap_or(LayerId::background());
|
||||
let layer_id = layer_id.unwrap_or_else(LayerId::background);
|
||||
|
||||
debug_assert!(
|
||||
id_salt.is_none(),
|
||||
@@ -149,7 +148,7 @@ impl Ui {
|
||||
let layout = layout.unwrap_or_default();
|
||||
let disabled = disabled || invisible;
|
||||
let style = style.unwrap_or_else(|| ctx.style());
|
||||
let sense = sense.unwrap_or(Sense::hover());
|
||||
let sense = sense.unwrap_or_else(Sense::hover);
|
||||
|
||||
let placer = Placer::new(max_rect, layout);
|
||||
let ui_stack = UiStack {
|
||||
@@ -175,7 +174,6 @@ impl Ui {
|
||||
min_rect_already_remembered: false,
|
||||
};
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
if let Some(accessibility_parent) = accessibility_parent {
|
||||
ui.ctx()
|
||||
.register_accesskit_parent(ui.unique_id, accessibility_parent);
|
||||
@@ -202,7 +200,6 @@ impl Ui {
|
||||
ui.set_invisible();
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
ui.ctx().accesskit_node_builder(ui.unique_id, |node| {
|
||||
node.set_role(accesskit::Role::GenericContainer);
|
||||
});
|
||||
@@ -273,7 +270,6 @@ impl Ui {
|
||||
sizing_pass,
|
||||
style,
|
||||
sense,
|
||||
#[cfg(feature = "accesskit")]
|
||||
accessibility_parent,
|
||||
} = ui_builder;
|
||||
|
||||
@@ -281,7 +277,7 @@ impl Ui {
|
||||
|
||||
let id_salt = id_salt.unwrap_or_else(|| Id::from("child"));
|
||||
let max_rect = max_rect.unwrap_or_else(|| self.available_rect_before_wrap());
|
||||
let mut layout = layout.unwrap_or(*self.layout());
|
||||
let mut layout = layout.unwrap_or_else(|| *self.layout());
|
||||
let enabled = self.enabled && !disabled && !invisible;
|
||||
if let Some(layer_id) = layer_id {
|
||||
painter.set_layer_id(layer_id);
|
||||
@@ -291,7 +287,7 @@ impl Ui {
|
||||
}
|
||||
let sizing_pass = self.sizing_pass || sizing_pass;
|
||||
let style = style.unwrap_or_else(|| self.style.clone());
|
||||
let sense = sense.unwrap_or(Sense::hover());
|
||||
let sense = sense.unwrap_or_else(Sense::hover);
|
||||
|
||||
if sizing_pass {
|
||||
// During the sizing pass we want widgets to use up as little space as possible,
|
||||
@@ -343,7 +339,6 @@ impl Ui {
|
||||
child_ui.disable();
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
child_ui.ctx().register_accesskit_parent(
|
||||
child_ui.unique_id,
|
||||
accessibility_parent.unwrap_or(self.unique_id),
|
||||
@@ -363,7 +358,6 @@ impl Ui {
|
||||
true,
|
||||
);
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
child_ui
|
||||
.ctx()
|
||||
.accesskit_node_builder(child_ui.unique_id, |node| {
|
||||
@@ -1129,7 +1123,6 @@ impl Ui {
|
||||
impl Ui {
|
||||
/// Check for clicks, drags and/or hover on a specific region of this [`Ui`].
|
||||
pub fn interact(&self, rect: Rect, id: Id, sense: Sense) -> Response {
|
||||
#[cfg(feature = "accesskit")]
|
||||
self.ctx().register_accesskit_parent(id, self.unique_id);
|
||||
|
||||
self.ctx().create_widget(
|
||||
@@ -3011,7 +3004,7 @@ impl Ui {
|
||||
///
|
||||
/// Returns the dropped item, if it was released this frame.
|
||||
///
|
||||
/// The given frame is used for its margins, but it color is ignored.
|
||||
/// The given frame is used for its margins, but the color is ignored.
|
||||
#[doc(alias = "drag and drop")]
|
||||
pub fn dnd_drop_zone<Payload, R>(
|
||||
&mut self,
|
||||
|
||||
@@ -24,7 +24,6 @@ pub struct UiBuilder {
|
||||
pub sizing_pass: bool,
|
||||
pub style: Option<Arc<Style>>,
|
||||
pub sense: Option<Sense>,
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub accessibility_parent: Option<Id>,
|
||||
}
|
||||
|
||||
@@ -187,15 +186,9 @@ impl UiBuilder {
|
||||
///
|
||||
/// This will override the automatic parent assignment for accessibility purposes.
|
||||
/// If not set, the parent [`Ui`]'s ID will be used as the accessibility parent.
|
||||
///
|
||||
/// This does nothing if the `accesskit` feature is not enabled.
|
||||
#[cfg_attr(not(feature = "accesskit"), expect(unused_mut, unused_variables))]
|
||||
#[inline]
|
||||
pub fn accessibility_parent(mut self, parent_id: Id) -> Self {
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
self.accessibility_parent = Some(parent_id);
|
||||
}
|
||||
self.accessibility_parent = Some(parent_id);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,16 +12,16 @@ pub enum UiKind {
|
||||
/// A [`crate::CentralPanel`].
|
||||
CentralPanel,
|
||||
|
||||
/// A left [`crate::SidePanel`].
|
||||
/// A left [`crate::Panel`].
|
||||
LeftPanel,
|
||||
|
||||
/// A right [`crate::SidePanel`].
|
||||
/// A right [`crate::Panel`].
|
||||
RightPanel,
|
||||
|
||||
/// A top [`crate::TopBottomPanel`].
|
||||
/// A top [`crate::Panel`].
|
||||
TopPanel,
|
||||
|
||||
/// A bottom [`crate::TopBottomPanel`].
|
||||
/// A bottom [`crate::Panel`].
|
||||
BottomPanel,
|
||||
|
||||
/// A modal [`crate::Modal`].
|
||||
@@ -209,7 +209,7 @@ pub struct UiStack {
|
||||
pub layout_direction: Direction,
|
||||
pub min_rect: Rect,
|
||||
pub max_rect: Rect,
|
||||
pub parent: Option<Arc<UiStack>>,
|
||||
pub parent: Option<Arc<Self>>,
|
||||
}
|
||||
|
||||
// these methods act on this specific node
|
||||
|
||||
@@ -223,22 +223,29 @@ impl<'a> Button<'a> {
|
||||
///
|
||||
/// See also [`Self::right_text`].
|
||||
#[inline]
|
||||
pub fn shortcut_text(mut self, shortcut_text: impl Into<Atom<'a>>) -> Self {
|
||||
let mut atom = shortcut_text.into();
|
||||
atom.kind = match atom.kind {
|
||||
AtomKind::Text(text) => AtomKind::Text(text.weak()),
|
||||
other => other,
|
||||
};
|
||||
pub fn shortcut_text(mut self, shortcut_text: impl IntoAtoms<'a>) -> Self {
|
||||
self.layout.push_right(Atom::grow());
|
||||
self.layout.push_right(atom);
|
||||
|
||||
for mut atom in shortcut_text.into_atoms() {
|
||||
atom.kind = match atom.kind {
|
||||
AtomKind::Text(text) => AtomKind::Text(text.weak()),
|
||||
other => other,
|
||||
};
|
||||
self.layout.push_right(atom);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Show some text on the right side of the button.
|
||||
#[inline]
|
||||
pub fn right_text(mut self, right_text: impl Into<Atom<'a>>) -> Self {
|
||||
pub fn right_text(mut self, right_text: impl IntoAtoms<'a>) -> Self {
|
||||
self.layout.push_right(Atom::grow());
|
||||
self.layout.push_right(right_text.into());
|
||||
|
||||
for atom in right_text.into_atoms() {
|
||||
self.layout.push_right(atom);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@@ -489,27 +489,21 @@ impl Widget for DragValue<'_> {
|
||||
- input.count_and_consume_key(Modifiers::NONE, Key::ArrowDown) as f64;
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
use accesskit::Action;
|
||||
change += input.num_accesskit_action_requests(id, Action::Increment) as f64
|
||||
- input.num_accesskit_action_requests(id, Action::Decrement) as f64;
|
||||
}
|
||||
use accesskit::Action;
|
||||
change += input.num_accesskit_action_requests(id, Action::Increment) as f64
|
||||
- input.num_accesskit_action_requests(id, Action::Decrement) as f64;
|
||||
|
||||
change
|
||||
});
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
ui.input(|input| {
|
||||
use accesskit::{Action, ActionData};
|
||||
ui.input(|input| {
|
||||
for request in input.accesskit_action_requests(id, Action::SetValue) {
|
||||
if let Some(ActionData::NumericValue(new_value)) = request.data {
|
||||
value = new_value;
|
||||
}
|
||||
for request in input.accesskit_action_requests(id, Action::SetValue) {
|
||||
if let Some(ActionData::NumericValue(new_value)) = request.data {
|
||||
value = new_value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if clamp_existing_to_range {
|
||||
value = clamp_value_to_range(value, range.clone());
|
||||
@@ -669,7 +663,6 @@ impl Widget for DragValue<'_> {
|
||||
|
||||
response.widget_info(|| WidgetInfo::drag_value(ui.is_enabled(), value));
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
ui.ctx().accesskit_node_builder(response.id, |builder| {
|
||||
use accesskit::Action;
|
||||
// If either end of the range is unbounded, it's better
|
||||
|
||||
@@ -681,7 +681,7 @@ pub fn paint_texture_load_result(
|
||||
}
|
||||
Ok(TexturePoll::Pending { .. }) => {
|
||||
let show_loading_spinner =
|
||||
show_loading_spinner.unwrap_or(ui.visuals().image_loading_spinners);
|
||||
show_loading_spinner.unwrap_or_else(|| ui.visuals().image_loading_spinners);
|
||||
if show_loading_spinner {
|
||||
Spinner::new().paint_at(ui, rect);
|
||||
}
|
||||
|
||||
@@ -248,7 +248,9 @@ impl Label {
|
||||
layout_job.halign = Align::LEFT;
|
||||
layout_job.justify = false;
|
||||
} else {
|
||||
layout_job.halign = self.halign.unwrap_or(ui.layout().horizontal_placement());
|
||||
layout_job.halign = self
|
||||
.halign
|
||||
.unwrap_or_else(|| ui.layout().horizontal_placement());
|
||||
layout_job.justify = ui.layout().horizontal_justify();
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ impl Widget for ProgressBar {
|
||||
|
||||
let desired_width =
|
||||
desired_width.unwrap_or_else(|| ui.available_size_before_wrap().x.at_least(96.0));
|
||||
let height = desired_height.unwrap_or(ui.spacing().interact_size.y);
|
||||
let height = desired_height.unwrap_or_else(|| ui.spacing().interact_size.y);
|
||||
let (outer_rect, response) =
|
||||
ui.allocate_exact_size(vec2(desired_width, height), Sense::hover());
|
||||
|
||||
|
||||
@@ -716,14 +716,11 @@ impl Slider<'_> {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
ui.input(|input| {
|
||||
use accesskit::Action;
|
||||
ui.input(|input| {
|
||||
decrement += input.num_accesskit_action_requests(response.id, Action::Decrement);
|
||||
increment += input.num_accesskit_action_requests(response.id, Action::Increment);
|
||||
});
|
||||
}
|
||||
decrement += input.num_accesskit_action_requests(response.id, Action::Decrement);
|
||||
increment += input.num_accesskit_action_requests(response.id, Action::Increment);
|
||||
});
|
||||
|
||||
let kb_step = increment as f32 - decrement as f32;
|
||||
|
||||
@@ -759,17 +756,14 @@ impl Slider<'_> {
|
||||
self.set_value(new_value);
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
ui.input(|input| {
|
||||
use accesskit::{Action, ActionData};
|
||||
ui.input(|input| {
|
||||
for request in input.accesskit_action_requests(response.id, Action::SetValue) {
|
||||
if let Some(ActionData::NumericValue(new_value)) = request.data {
|
||||
self.set_value(new_value);
|
||||
}
|
||||
for request in input.accesskit_action_requests(response.id, Action::SetValue) {
|
||||
if let Some(ActionData::NumericValue(new_value)) = request.data {
|
||||
self.set_value(new_value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Paint it:
|
||||
if ui.is_rect_visible(response.rect) {
|
||||
@@ -978,7 +972,6 @@ impl Slider<'_> {
|
||||
}
|
||||
response.widget_info(|| WidgetInfo::slider(ui.is_enabled(), value, self.text.text()));
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
ui.ctx().accesskit_node_builder(response.id, |builder| {
|
||||
use accesskit::Action;
|
||||
builder.set_min_numeric_value(*self.range.start());
|
||||
|
||||
@@ -496,7 +496,7 @@ impl TextEdit<'_> {
|
||||
} = self;
|
||||
|
||||
let text_color = text_color
|
||||
.or(ui.visuals().override_text_color)
|
||||
.or_else(|| ui.visuals().override_text_color)
|
||||
// .unwrap_or_else(|| ui.style().interact(&response).text_color()); // too bright
|
||||
.unwrap_or_else(|| ui.visuals().widgets.inactive.text_color());
|
||||
|
||||
@@ -691,7 +691,7 @@ impl TextEdit<'_> {
|
||||
if ui.is_rect_visible(rect) {
|
||||
if text.as_str().is_empty() && !hint_text.is_empty() {
|
||||
let hint_text_color = ui.visuals().weak_text_color();
|
||||
let hint_text_font_id = hint_text_font.unwrap_or(font_id.into());
|
||||
let hint_text_font_id = hint_text_font.unwrap_or_else(|| font_id.into());
|
||||
let galley = if multiline {
|
||||
hint_text.into_galley(
|
||||
ui,
|
||||
@@ -844,7 +844,6 @@ impl TextEdit<'_> {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
{
|
||||
let role = if password {
|
||||
accesskit::Role::PasswordInput
|
||||
|
||||
@@ -23,7 +23,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
|
||||
[features]
|
||||
default = ["glow", "persistence"]
|
||||
default = ["wgpu", "persistence"]
|
||||
|
||||
# image_viewer adds about 0.9 MB of WASM
|
||||
web_app = ["http", "persistence"]
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use std::mem;
|
||||
|
||||
use accesskit::{Action, ActionRequest, NodeId};
|
||||
use accesskit_consumer::{FilterResult, Node, Tree, TreeChangeHandler};
|
||||
|
||||
use eframe::epaint::text::TextWrapMode;
|
||||
use egui::collapsing_header::CollapsingState;
|
||||
use egui::{
|
||||
Button, Color32, Context, Event, Frame, FullOutput, Id, Key, KeyboardShortcut, Label,
|
||||
Modifiers, RawInput, RichText, ScrollArea, SidePanel, TopBottomPanel, Ui,
|
||||
Modifiers, Panel, RawInput, RichText, ScrollArea, Ui, collapsing_header::CollapsingState,
|
||||
};
|
||||
use std::mem;
|
||||
|
||||
/// This [`egui::Plugin`] adds an inspector Panel.
|
||||
///
|
||||
@@ -86,10 +87,10 @@ impl egui::Plugin for AccessibilityInspectorPlugin {
|
||||
|
||||
ctx.enable_accesskit();
|
||||
|
||||
SidePanel::right(Self::id()).show(ctx, |ui| {
|
||||
Panel::right(Self::id()).show(ctx, |ui| {
|
||||
ui.heading("🔎 AccessKit Inspector");
|
||||
if let Some(selected_node) = self.selected_node {
|
||||
TopBottomPanel::bottom(Self::id().with("details_panel"))
|
||||
Panel::bottom(Self::id().with("details_panel"))
|
||||
.frame(Frame::new())
|
||||
.show_separator_line(false)
|
||||
.show_inside(ui, |ui| {
|
||||
@@ -198,8 +199,8 @@ impl AccessibilityInspectorPlugin {
|
||||
}
|
||||
let label = node
|
||||
.label()
|
||||
.or(node.value())
|
||||
.unwrap_or(node.id().0.to_string());
|
||||
.or_else(|| node.value())
|
||||
.unwrap_or_else(|| node.id().0.to_string());
|
||||
let label = format!("({:?}) {}", node.role(), label);
|
||||
|
||||
// Safety: This is safe since the `accesskit::NodeId` was created from an `egui::Id`.
|
||||
|
||||
@@ -22,26 +22,27 @@ impl Custom3d {
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for Custom3d {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::ScrollArea::both()
|
||||
.auto_shrink(false)
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("The triangle is being painted using ");
|
||||
ui.hyperlink_to("glow", "https://github.com/grovesNL/glow");
|
||||
ui.label(" (OpenGL).");
|
||||
});
|
||||
ui.label("It's not a very impressive demo, but it shows you can embed 3D inside of egui.");
|
||||
|
||||
egui::Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
self.custom_painting(ui);
|
||||
});
|
||||
ui.label("Drag to rotate!");
|
||||
ui.add(egui_demo_lib::egui_github_link_file!());
|
||||
impl crate::DemoApp for Custom3d {
|
||||
fn demo_ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
// TODO(emilk): Use `ScrollArea::inner_margin`
|
||||
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||
egui::ScrollArea::both().auto_shrink(false).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("The triangle is being painted using ");
|
||||
ui.hyperlink_to("glow", "https://github.com/grovesNL/glow");
|
||||
ui.label(" (OpenGL).");
|
||||
});
|
||||
ui.label(
|
||||
"It's not a very impressive demo, but it shows you can embed 3D inside of egui.",
|
||||
);
|
||||
|
||||
egui::Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
self.custom_painting(ui);
|
||||
});
|
||||
ui.label("Drag to rotate!");
|
||||
ui.add(egui_demo_lib::egui_github_link_file!());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -98,26 +98,27 @@ impl Custom3d {
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for Custom3d {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::ScrollArea::both()
|
||||
.auto_shrink(false)
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("The triangle is being painted using ");
|
||||
ui.hyperlink_to("WGPU", "https://wgpu.rs");
|
||||
ui.label(" (Portable Rust graphics API awesomeness)");
|
||||
});
|
||||
ui.label("It's not a very impressive demo, but it shows you can embed 3D inside of egui.");
|
||||
|
||||
egui::Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
self.custom_painting(ui);
|
||||
});
|
||||
ui.label("Drag to rotate!");
|
||||
ui.add(egui_demo_lib::egui_github_link_file!());
|
||||
impl crate::DemoApp for Custom3d {
|
||||
fn demo_ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
// TODO(emilk): Use `ScrollArea::inner_margin`
|
||||
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||
egui::ScrollArea::both().auto_shrink(false).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("The triangle is being painted using ");
|
||||
ui.hyperlink_to("WGPU", "https://wgpu.rs");
|
||||
ui.label(" (Portable Rust graphics API awesomeness)");
|
||||
});
|
||||
ui.label(
|
||||
"It's not a very impressive demo, but it shows you can embed 3D inside of egui.",
|
||||
);
|
||||
|
||||
egui::Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
self.custom_painting(ui);
|
||||
});
|
||||
ui.label("Drag to rotate!");
|
||||
ui.add(egui_demo_lib::egui_github_link_file!());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,16 +59,16 @@ impl Default for HttpApp {
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for HttpApp {
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
egui::TopBottomPanel::bottom("http_bottom").show(ctx, |ui| {
|
||||
impl crate::DemoApp for HttpApp {
|
||||
fn demo_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
egui::Panel::bottom("http_bottom").show_inside(ui, |ui| {
|
||||
let layout = egui::Layout::top_down(egui::Align::Center).with_main_justify(true);
|
||||
ui.allocate_ui_with_layout(ui.available_size(), layout, |ui| {
|
||||
ui.add(egui_demo_lib::egui_github_link_file!())
|
||||
})
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||
let prev_url = self.url.clone();
|
||||
let trigger_fetch = ui_url(ui, frame, &mut self.url);
|
||||
|
||||
@@ -80,7 +80,7 @@ impl eframe::App for HttpApp {
|
||||
});
|
||||
|
||||
if trigger_fetch {
|
||||
let ctx = ctx.clone();
|
||||
let ctx = ui.ctx().clone();
|
||||
let (sender, promise) = Promise::new();
|
||||
let request = ehttp::Request::get(&self.url);
|
||||
ehttp::fetch(request, move |response| {
|
||||
|
||||
@@ -2,8 +2,6 @@ use egui::ImageFit;
|
||||
use egui::Slider;
|
||||
use egui::Vec2;
|
||||
use egui::emath::Rot2;
|
||||
use egui::panel::Side;
|
||||
use egui::panel::TopBottomSide;
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct ImageViewer {
|
||||
@@ -50,15 +48,15 @@ impl Default for ImageViewer {
|
||||
}
|
||||
}
|
||||
|
||||
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| {
|
||||
impl crate::DemoApp for ImageViewer {
|
||||
fn demo_ui(&mut self, ui: &mut egui::Ui, _: &mut eframe::Frame) {
|
||||
egui::Panel::top("url bar").show_inside(ui, |ui| {
|
||||
ui.horizontal_centered(|ui| {
|
||||
let label = ui.label("URI:");
|
||||
ui.text_edit_singleline(&mut self.uri_edit_text)
|
||||
.labelled_by(label.id);
|
||||
if ui.small_button("✔").clicked() {
|
||||
ctx.forget_image(&self.current_uri);
|
||||
ui.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();
|
||||
}
|
||||
@@ -73,7 +71,7 @@ impl eframe::App for ImageViewer {
|
||||
});
|
||||
});
|
||||
|
||||
egui::SidePanel::new(Side::Left, "controls").show(ctx, |ui| {
|
||||
egui::Panel::left("controls").show_inside(ui, |ui| {
|
||||
// uv
|
||||
ui.label("UV");
|
||||
ui.add(Slider::new(&mut self.image_options.uv.min.x, 0.0..=1.0).text("min x"));
|
||||
@@ -199,7 +197,7 @@ impl eframe::App for ImageViewer {
|
||||
}
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||
egui::ScrollArea::both().show(ui, |ui| {
|
||||
let mut image = egui::Image::from_uri(&self.current_uri);
|
||||
image = image.uv(self.image_options.uv);
|
||||
|
||||
@@ -15,6 +15,14 @@ pub(crate) fn seconds_since_midnight() -> f64 {
|
||||
time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64)
|
||||
}
|
||||
|
||||
/// Trait that wraps different parts of the demo app.
|
||||
pub trait DemoApp {
|
||||
fn demo_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame);
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
#[cfg(feature = "accessibility_inspector")]
|
||||
pub mod accessibility_inspector;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use egui_demo_lib::is_mobile;
|
||||
use egui_demo_lib::{DemoWindows, is_mobile};
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
use eframe::glow;
|
||||
@@ -6,29 +6,25 @@ use eframe::glow;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use core::any::Any;
|
||||
|
||||
use crate::DemoApp;
|
||||
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
struct EasyMarkApp {
|
||||
editor: egui_demo_lib::easy_mark::EasyMarkEditor,
|
||||
}
|
||||
|
||||
impl eframe::App for EasyMarkApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
self.editor.panels(ctx);
|
||||
impl DemoApp for EasyMarkApp {
|
||||
fn demo_ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
self.editor.panels(ui);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct DemoApp {
|
||||
demo_windows: egui_demo_lib::DemoWindows,
|
||||
}
|
||||
|
||||
impl eframe::App for DemoApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
self.demo_windows.ui(ctx);
|
||||
impl DemoApp for DemoWindows {
|
||||
fn demo_ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
self.ui(ui);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,17 +37,17 @@ pub struct FractalClockApp {
|
||||
pub mock_time: Option<f64>,
|
||||
}
|
||||
|
||||
impl eframe::App for FractalClockApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default()
|
||||
.frame(
|
||||
egui::Frame::dark_canvas(&ctx.style())
|
||||
.stroke(egui::Stroke::NONE)
|
||||
.corner_radius(0),
|
||||
)
|
||||
.show(ctx, |ui| {
|
||||
self.fractal_clock
|
||||
.ui(ui, self.mock_time.or(Some(crate::seconds_since_midnight())));
|
||||
impl DemoApp for FractalClockApp {
|
||||
fn demo_ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
egui::Frame::dark_canvas(ui.style())
|
||||
.stroke(egui::Stroke::NONE)
|
||||
.corner_radius(0)
|
||||
.show(ui, |ui| {
|
||||
self.fractal_clock.ui(
|
||||
ui,
|
||||
self.mock_time
|
||||
.or_else(|| Some(crate::seconds_since_midnight())),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -64,13 +60,13 @@ pub struct ColorTestApp {
|
||||
color_test: egui_demo_lib::ColorTest,
|
||||
}
|
||||
|
||||
impl eframe::App for ColorTestApp {
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
impl DemoApp for ColorTestApp {
|
||||
fn demo_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||
if frame.is_web() {
|
||||
ui.label(
|
||||
"NOTE: Some old browsers stuck on WebGL1 without sRGB support will not pass the color test.",
|
||||
);
|
||||
"NOTE: Some old browsers stuck on WebGL1 without sRGB support will not pass the color test.",
|
||||
);
|
||||
ui.separator();
|
||||
}
|
||||
egui::ScrollArea::both().auto_shrink(false).show(ui, |ui| {
|
||||
@@ -155,7 +151,7 @@ enum Command {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct State {
|
||||
demo: DemoApp,
|
||||
demo: DemoWindows,
|
||||
easy_mark_editor: EasyMarkApp,
|
||||
#[cfg(feature = "http")]
|
||||
http: crate::apps::HttpApp,
|
||||
@@ -209,34 +205,34 @@ impl WrapApp {
|
||||
|
||||
pub fn apps_iter_mut(
|
||||
&mut self,
|
||||
) -> impl Iterator<Item = (&'static str, Anchor, &mut dyn eframe::App)> {
|
||||
) -> impl Iterator<Item = (&'static str, Anchor, &mut dyn DemoApp)> {
|
||||
let mut vec = vec![
|
||||
(
|
||||
"✨ Demos",
|
||||
Anchor::Demo,
|
||||
&mut self.state.demo as &mut dyn eframe::App,
|
||||
&mut self.state.demo as &mut dyn DemoApp,
|
||||
),
|
||||
(
|
||||
"🖹 EasyMark editor",
|
||||
Anchor::EasyMarkEditor,
|
||||
&mut self.state.easy_mark_editor as &mut dyn eframe::App,
|
||||
&mut self.state.easy_mark_editor as &mut dyn DemoApp,
|
||||
),
|
||||
#[cfg(feature = "http")]
|
||||
(
|
||||
"⬇ HTTP",
|
||||
Anchor::Http,
|
||||
&mut self.state.http as &mut dyn eframe::App,
|
||||
&mut self.state.http as &mut dyn DemoApp,
|
||||
),
|
||||
(
|
||||
"🕑 Fractal Clock",
|
||||
Anchor::Clock,
|
||||
&mut self.state.clock as &mut dyn eframe::App,
|
||||
&mut self.state.clock as &mut dyn DemoApp,
|
||||
),
|
||||
#[cfg(feature = "image_viewer")]
|
||||
(
|
||||
"🖼 Image Viewer",
|
||||
Anchor::ImageViewer,
|
||||
&mut self.state.image_viewer as &mut dyn eframe::App,
|
||||
&mut self.state.image_viewer as &mut dyn DemoApp,
|
||||
),
|
||||
];
|
||||
|
||||
@@ -245,14 +241,14 @@ impl WrapApp {
|
||||
vec.push((
|
||||
"🔺 3D painting",
|
||||
Anchor::Custom3d,
|
||||
custom3d as &mut dyn eframe::App,
|
||||
custom3d as &mut dyn DemoApp,
|
||||
));
|
||||
}
|
||||
|
||||
vec.push((
|
||||
"🎨 Rendering test",
|
||||
Anchor::Rendering,
|
||||
&mut self.state.rendering_test as &mut dyn eframe::App,
|
||||
&mut self.state.rendering_test as &mut dyn DemoApp,
|
||||
));
|
||||
|
||||
vec.into_iter()
|
||||
@@ -295,7 +291,7 @@ impl eframe::App for WrapApp {
|
||||
}
|
||||
|
||||
let mut cmd = Command::Nothing;
|
||||
egui::TopBottomPanel::top("wrap_app_top_bar")
|
||||
egui::Panel::top("wrap_app_top_bar")
|
||||
.frame(egui::Frame::new().inner_margin(4))
|
||||
.show(ctx, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
@@ -306,11 +302,13 @@ impl eframe::App for WrapApp {
|
||||
|
||||
self.state.backend_panel.update(ctx, frame);
|
||||
|
||||
if !is_mobile(ctx) {
|
||||
cmd = self.backend_panel(ctx, frame);
|
||||
}
|
||||
egui::CentralPanel::no_frame().show(ctx, |ui| {
|
||||
if !is_mobile(ctx) {
|
||||
cmd = self.backend_panel(ui, frame);
|
||||
}
|
||||
|
||||
self.show_selected_app(ctx, frame);
|
||||
self.show_selected_app(ui, frame);
|
||||
});
|
||||
|
||||
self.state.backend_panel.end_of_frame(ctx);
|
||||
|
||||
@@ -333,17 +331,16 @@ impl eframe::App for WrapApp {
|
||||
}
|
||||
|
||||
impl WrapApp {
|
||||
fn backend_panel(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) -> Command {
|
||||
fn backend_panel(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) -> Command {
|
||||
// The backend-panel can be toggled on/off.
|
||||
// We show a little animation when the user switches it.
|
||||
let is_open =
|
||||
self.state.backend_panel.open || ctx.memory(|mem| mem.everything_is_visible());
|
||||
let is_open = self.state.backend_panel.open || ui.memory(|mem| mem.everything_is_visible());
|
||||
|
||||
let mut cmd = Command::Nothing;
|
||||
|
||||
egui::SidePanel::left("backend_panel")
|
||||
egui::Panel::left("backend_panel")
|
||||
.resizable(false)
|
||||
.show_animated(ctx, is_open, |ui| {
|
||||
.show_animated_inside(ui, is_open, |ui| {
|
||||
ui.add_space(4.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("💻 Backend");
|
||||
@@ -393,11 +390,11 @@ impl WrapApp {
|
||||
});
|
||||
}
|
||||
|
||||
fn show_selected_app(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
fn show_selected_app(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
let selected_anchor = self.state.selected_anchor;
|
||||
for (_name, anchor, app) in self.apps_iter_mut() {
|
||||
if anchor == selected_anchor || ctx.memory(|mem| mem.everything_is_visible()) {
|
||||
app.update(ctx, frame);
|
||||
if anchor == selected_anchor || ui.memory(|mem| mem.everything_is_visible()) {
|
||||
app.demo_ui(ui, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:44a68dc4d3aeebeb2d296c5c8e03aac330e1e4552364084347b710326c88f70c
|
||||
size 335794
|
||||
oid sha256:784cbcdfd8deaf61e7b663f9416d67724e6a6a189a20ba3351908aa5c5f2deff
|
||||
size 336159
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e9a760fe4a695e6321f00e40bfa76fd0195bee7157a1217572765e3f146ea2cc
|
||||
size 93640
|
||||
oid sha256:4cdde1dda0e64f584c769c72f5910a7035e6a4a86a074b590e88365f12570109
|
||||
size 94062
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a1670bbfc1f0a71e20cbbeb73625c148b680963bc503d9b48e9cc43e704d7c54
|
||||
size 181671
|
||||
oid sha256:824d941ea538fd44fc374f5df1893eee2309004c0ee5e69a97f1c84a74b2b423
|
||||
size 182128
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dc9c22567b76193a7f6753c4217adb3c92afa921c488ba1cf2e14b403814e7ac
|
||||
size 99841
|
||||
oid sha256:44ea7ac8c8e22eb51fbcb63f00c8510de0e6ae126d19ab44c5d708d979b5362b
|
||||
size 100345
|
||||
|
||||
@@ -28,8 +28,8 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
// The most end-to-end benchmark.
|
||||
c.bench_function("demo_with_tessellate__realistic", |b| {
|
||||
b.iter(|| {
|
||||
let full_output = ctx.run(RawInput::default(), |ctx| {
|
||||
demo_windows.ui(ctx);
|
||||
let full_output = ctx.run_ui(RawInput::default(), |ui| {
|
||||
demo_windows.ui(ui);
|
||||
});
|
||||
ctx.tessellate(full_output.shapes, full_output.pixels_per_point)
|
||||
});
|
||||
@@ -37,14 +37,14 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
|
||||
c.bench_function("demo_no_tessellate", |b| {
|
||||
b.iter(|| {
|
||||
ctx.run(RawInput::default(), |ctx| {
|
||||
demo_windows.ui(ctx);
|
||||
ctx.run_ui(RawInput::default(), |ui| {
|
||||
demo_windows.ui(ui);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
let full_output = ctx.run(RawInput::default(), |ctx| {
|
||||
demo_windows.ui(ctx);
|
||||
let full_output = ctx.run_ui(RawInput::default(), |ui| {
|
||||
demo_windows.ui(ui);
|
||||
});
|
||||
c.bench_function("demo_only_tessellate", |b| {
|
||||
b.iter(|| ctx.tessellate(full_output.shapes.clone(), full_output.pixels_per_point));
|
||||
@@ -57,8 +57,8 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
let mut demo_windows = egui_demo_lib::DemoWindows::default();
|
||||
c.bench_function("demo_full_no_tessellate", |b| {
|
||||
b.iter(|| {
|
||||
ctx.run(RawInput::default(), |ctx| {
|
||||
demo_windows.ui(ctx);
|
||||
ctx.run_ui(RawInput::default(), |ui| {
|
||||
demo_windows.ui(ui);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -195,11 +195,11 @@ impl Default for DemoWindows {
|
||||
|
||||
impl DemoWindows {
|
||||
/// Show the app ui (menu bar and windows).
|
||||
pub fn ui(&mut self, ctx: &Context) {
|
||||
if is_mobile(ctx) {
|
||||
self.mobile_ui(ctx);
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
if is_mobile(ui.ctx()) {
|
||||
self.mobile_ui(ui);
|
||||
} else {
|
||||
self.desktop_ui(ctx);
|
||||
self.desktop_ui(ui);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,36 +207,36 @@ impl DemoWindows {
|
||||
self.open.contains(About::default().name())
|
||||
}
|
||||
|
||||
fn mobile_ui(&mut self, ctx: &Context) {
|
||||
fn mobile_ui(&mut self, ui: &mut egui::Ui) {
|
||||
if self.about_is_open() {
|
||||
let mut close = false;
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink(false)
|
||||
.show(ui, |ui| {
|
||||
self.groups.about.ui(ui);
|
||||
ui.add_space(12.0);
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
if ui
|
||||
.button(egui::RichText::new("Continue to the demo!").size(20.0))
|
||||
.clicked()
|
||||
{
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink(false)
|
||||
.show(ui, |ui| {
|
||||
self.groups.about.ui(ui);
|
||||
ui.add_space(12.0);
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
if ui
|
||||
.button(egui::RichText::new("Continue to the demo!").size(20.0))
|
||||
.clicked()
|
||||
{
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if close {
|
||||
set_open(&mut self.open, About::default().name(), false);
|
||||
}
|
||||
} else {
|
||||
self.mobile_top_bar(ctx);
|
||||
self.groups.windows(ctx, &mut self.open);
|
||||
self.mobile_top_bar(ui);
|
||||
self.groups.windows(ui.ctx(), &mut self.open);
|
||||
}
|
||||
}
|
||||
|
||||
fn mobile_top_bar(&mut self, ctx: &Context) {
|
||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||
fn mobile_top_bar(&mut self, ui: &mut egui::Ui) {
|
||||
egui::Panel::top("menu_bar").show_inside(ui, |ui| {
|
||||
menu::MenuBar::new()
|
||||
.config(menu::MenuConfig::new().style(StyleModifier::default()))
|
||||
.ui(ui, |ui| {
|
||||
@@ -261,12 +261,12 @@ impl DemoWindows {
|
||||
});
|
||||
}
|
||||
|
||||
fn desktop_ui(&mut self, ctx: &Context) {
|
||||
egui::SidePanel::right("egui_demo_panel")
|
||||
fn desktop_ui(&mut self, ui: &mut egui::Ui) {
|
||||
egui::Panel::right("egui_demo_panel")
|
||||
.resizable(false)
|
||||
.default_width(160.0)
|
||||
.min_width(160.0)
|
||||
.show(ctx, |ui| {
|
||||
.default_size(160.0)
|
||||
.min_size(160.0)
|
||||
.show_inside(ui, |ui| {
|
||||
ui.add_space(4.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("✒ egui demos");
|
||||
@@ -289,13 +289,13 @@ impl DemoWindows {
|
||||
self.demo_list_ui(ui);
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||
egui::Panel::top("menu_bar").show_inside(ui, |ui| {
|
||||
menu::MenuBar::new().ui(ui, |ui| {
|
||||
file_menu_button(ui);
|
||||
});
|
||||
});
|
||||
|
||||
self.groups.windows(ctx, &mut self.open);
|
||||
self.groups.windows(ui.ctx(), &mut self.open);
|
||||
}
|
||||
|
||||
fn demo_list_ui(&mut self, ui: &mut egui::Ui) {
|
||||
|
||||
@@ -451,7 +451,7 @@ enum Action {
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
struct Tree(Vec<Tree>);
|
||||
struct Tree(Vec<Self>);
|
||||
|
||||
impl Tree {
|
||||
pub fn demo() -> Self {
|
||||
|
||||
@@ -22,9 +22,9 @@ impl crate::View for Panels {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
// Note that the order we add the panels is very important!
|
||||
|
||||
egui::TopBottomPanel::top("top_panel")
|
||||
egui::Panel::top("top_panel")
|
||||
.resizable(true)
|
||||
.min_height(32.0)
|
||||
.min_size(32.0)
|
||||
.show_inside(ui, |ui| {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
@@ -34,10 +34,10 @@ impl crate::View for Panels {
|
||||
});
|
||||
});
|
||||
|
||||
egui::SidePanel::left("left_panel")
|
||||
egui::Panel::left("left_panel")
|
||||
.resizable(true)
|
||||
.default_width(150.0)
|
||||
.width_range(80.0..=200.0)
|
||||
.default_size(150.0)
|
||||
.size_range(80.0..=200.0)
|
||||
.show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Left Panel");
|
||||
@@ -47,10 +47,10 @@ impl crate::View for Panels {
|
||||
});
|
||||
});
|
||||
|
||||
egui::SidePanel::right("right_panel")
|
||||
egui::Panel::right("right_panel")
|
||||
.resizable(true)
|
||||
.default_width(150.0)
|
||||
.width_range(80.0..=200.0)
|
||||
.default_size(150.0)
|
||||
.size_range(80.0..=200.0)
|
||||
.show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Right Panel");
|
||||
@@ -60,9 +60,9 @@ impl crate::View for Panels {
|
||||
});
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::bottom("bottom_panel")
|
||||
egui::Panel::bottom("bottom_panel")
|
||||
.resizable(false)
|
||||
.min_height(0.0)
|
||||
.min_size(0.0)
|
||||
.show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Bottom Panel");
|
||||
@@ -72,6 +72,7 @@ impl crate::View for Panels {
|
||||
});
|
||||
});
|
||||
|
||||
// TODO(emilk): This extra panel is superfluous - just use what's left of `ui` instead
|
||||
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Central Panel");
|
||||
|
||||
@@ -357,11 +357,13 @@ fn rect_shape_ui(ui: &mut egui::Ui, shape: &mut RectShape) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::View as _;
|
||||
use egui_kittest::SnapshotResults;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn snapshot_tessellation_test() {
|
||||
let mut results = SnapshotResults::new();
|
||||
for (name, shape) in TessellationTest::interesting_shapes() {
|
||||
let mut test = TessellationTest {
|
||||
shape,
|
||||
@@ -375,6 +377,7 @@ mod tests {
|
||||
harness.run();
|
||||
|
||||
harness.snapshot(format!("tessellation_test/{name}"));
|
||||
results.extend_harness(&mut harness);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ impl crate::View for Tooltips {
|
||||
ui.add(crate::egui_github_link_file_line!());
|
||||
});
|
||||
|
||||
egui::SidePanel::right("scroll_test").show_inside(ui, |ui| {
|
||||
egui::Panel::right("scroll_test").show_inside(ui, |ui| {
|
||||
ui.label(
|
||||
"The scroll area below has many labels with interactive tooltips. \
|
||||
The purpose is to test that the tooltips close when you scroll.",
|
||||
|
||||
@@ -310,7 +310,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::View as _;
|
||||
use egui::Vec2;
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::{Harness, SnapshotResults};
|
||||
|
||||
#[test]
|
||||
pub fn should_match_screenshot() {
|
||||
@@ -320,6 +320,8 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut results = SnapshotResults::new();
|
||||
|
||||
for pixels_per_point in [1, 2] {
|
||||
for theme in [egui::Theme::Light, egui::Theme::Dark] {
|
||||
let mut harness = Harness::builder()
|
||||
@@ -339,6 +341,7 @@ mod tests {
|
||||
};
|
||||
let image_name = format!("widget_gallery_{theme_name}_x{pixels_per_point}");
|
||||
harness.snapshot(&image_name);
|
||||
results.extend_harness(&mut harness);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,15 +32,15 @@ impl Default for EasyMarkEditor {
|
||||
}
|
||||
|
||||
impl EasyMarkEditor {
|
||||
pub fn panels(&mut self, ctx: &egui::Context) {
|
||||
egui::TopBottomPanel::bottom("easy_mark_bottom").show(ctx, |ui| {
|
||||
pub fn panels(&mut self, ui: &mut egui::Ui) {
|
||||
egui::Panel::bottom("easy_mark_bottom").show_inside(ui, |ui| {
|
||||
let layout = egui::Layout::top_down(egui::Align::Center).with_main_justify(true);
|
||||
ui.allocate_ui_with_layout(ui.available_size(), layout, |ui| {
|
||||
ui.add(crate::egui_github_link_file!())
|
||||
})
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -73,8 +73,8 @@ fn test_egui_e2e() {
|
||||
|
||||
const NUM_FRAMES: usize = 5;
|
||||
for _ in 0..NUM_FRAMES {
|
||||
let full_output = ctx.run(raw_input.clone(), |ctx| {
|
||||
demo_windows.ui(ctx);
|
||||
let full_output = ctx.run_ui(raw_input.clone(), |ui| {
|
||||
demo_windows.ui(ui);
|
||||
});
|
||||
let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point);
|
||||
assert!(!clipped_primitives.is_empty());
|
||||
@@ -92,8 +92,8 @@ fn test_egui_zero_window_size() {
|
||||
|
||||
const NUM_FRAMES: usize = 5;
|
||||
for _ in 0..NUM_FRAMES {
|
||||
let full_output = ctx.run(raw_input.clone(), |ctx| {
|
||||
demo_windows.ui(ctx);
|
||||
let full_output = ctx.run_ui(raw_input.clone(), |ui| {
|
||||
demo_windows.ui(ui);
|
||||
});
|
||||
let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point);
|
||||
assert!(
|
||||
|
||||
@@ -3,6 +3,7 @@ use egui_kittest::Harness;
|
||||
|
||||
#[test]
|
||||
fn test_image_blending() {
|
||||
let mut results = egui_kittest::SnapshotResults::new();
|
||||
for pixels_per_point in [1.0, 2.0] {
|
||||
let mut harness = Harness::builder()
|
||||
.with_pixels_per_point(pixels_per_point)
|
||||
@@ -21,5 +22,6 @@ fn test_image_blending() {
|
||||
harness.run();
|
||||
harness.fit_contents();
|
||||
harness.snapshot(format!("image_blending/image_x{pixels_per_point}"));
|
||||
results.extend_harness(&mut harness);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use egui_kittest::Harness;
|
||||
use egui::{Color32, accesskit::Role};
|
||||
use egui_kittest::{Harness, kittest::Queryable as _};
|
||||
|
||||
#[test]
|
||||
fn test_kerning() {
|
||||
let mut results = egui_kittest::SnapshotResults::new();
|
||||
for pixels_per_point in [1.0, 2.0] {
|
||||
for theme in [egui::Theme::Dark, egui::Theme::Light] {
|
||||
let mut harness = Harness::builder()
|
||||
@@ -23,12 +25,14 @@ fn test_kerning() {
|
||||
egui::Theme::Light => "light",
|
||||
}
|
||||
));
|
||||
results.extend_harness(&mut harness);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_italics() {
|
||||
let mut results = egui_kittest::SnapshotResults::new();
|
||||
for pixels_per_point in [1.0, 2.0_f32.sqrt(), 2.0] {
|
||||
for theme in [egui::Theme::Dark, egui::Theme::Light] {
|
||||
let mut harness = Harness::builder()
|
||||
@@ -42,12 +46,34 @@ fn test_italics() {
|
||||
harness.run();
|
||||
harness.fit_contents();
|
||||
harness.snapshot(format!(
|
||||
"image_blending/image_{theme}_x{pixels_per_point:.2}",
|
||||
"italics/image_{theme}_x{pixels_per_point:.2}",
|
||||
theme = match theme {
|
||||
egui::Theme::Dark => "dark",
|
||||
egui::Theme::Light => "light",
|
||||
}
|
||||
));
|
||||
results.extend_harness(&mut harness);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_text_selection() {
|
||||
let mut harness = Harness::builder().build_ui(|ui| {
|
||||
let visuals = ui.visuals_mut();
|
||||
visuals.selection.bg_fill = Color32::LIGHT_GREEN;
|
||||
visuals.selection.stroke.color = Color32::DARK_BLUE;
|
||||
|
||||
ui.label("Some varied ☺ text :)\nAnd it has a second line!");
|
||||
});
|
||||
harness.run();
|
||||
harness.fit_contents();
|
||||
|
||||
// Drag to select text:
|
||||
let label = harness.get_by_role(Role::Label);
|
||||
harness.drag_at(label.rect().lerp_inside([0.2, 0.25]));
|
||||
harness.drop_at(label.rect().lerp_inside([0.6, 0.75]));
|
||||
harness.run();
|
||||
|
||||
harness.snapshot("text_selection");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aff927596be5db77349ec0bbdcc852a0b1467e94c2a553a740a383ae318bad18
|
||||
oid sha256:bb3f7b5f790830b46d1410c2bbb5e19c6beb403f8fe979eb8d250fba4f89be3e
|
||||
size 51670
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9d6f055247034fa13ab55c9ec1fca275e6c23999c9a7e01c87af1fcc930faac6
|
||||
size 66777
|
||||
oid sha256:170cee9d72a4ab59aa2faf1b77aff4a9eee64f3380aa3f1b256340d88b1dabc2
|
||||
size 66525
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c91f592571ba654d0a96791662ae7530a1db4c1630b57c795d1c006ea6e46f19
|
||||
size 256975
|
||||
oid sha256:f7a7d0e2618b852b5966073438c95cb62901d5410c1473639920b0b0bf2ec59b
|
||||
size 256913
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user