mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -04:00
Merge branch 'main' into theme_plugin
# Conflicts: # crates/egui/src/widget_style.rs
This commit is contained in:
60
CHANGELOG.md
60
CHANGELOG.md
@@ -14,6 +14,66 @@ 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.
|
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.36.1 - 2026-08-07
|
||||||
|
* Fix `Sense::drag` detecting drags when clicking widget above it [#8396](https://github.com/emilk/egui/pull/8396) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||||
|
|
||||||
|
|
||||||
|
## 0.36.0 - 2026-08-05
|
||||||
|
|
||||||
|
### Highlights ✨
|
||||||
|
|
||||||
|
This release drastically improves the mobile keyboard experience (when using eframe web). It also adds drag-to-open
|
||||||
|
panels, window chrome theme sync and a lot of small bug fixes and improvements!
|
||||||
|
|
||||||
|
#### Improved mobile keyboard support
|
||||||
|
|
||||||
|
Autocomplete, autocorrect and IMEs now work correctly on iOS and android (on eframe web)!
|
||||||
|
|
||||||
|
https://github.com/user-attachments/assets/b0aa1084-0755-4e47-a0b3-0ce6890aca85
|
||||||
|
|
||||||
|
- via [#8045](https://github.com/emilk/egui/pull/8045) by [@umajho](https://github.com/umajho) (and [@rustbasic](https://github.com/rustbasic) who helped a lot with testing)
|
||||||
|
|
||||||
|
#### Drag to reopen panels
|
||||||
|
|
||||||
|
You can now reopen closed panels by dragging the handle:
|
||||||
|
|
||||||
|
https://github.com/user-attachments/assets/e02895c4-248e-4e22-9694-bdfcd9839bfd
|
||||||
|
|
||||||
|
#### Window decoration theme is now synced with app theme
|
||||||
|
|
||||||
|
Previously, when switching themes, the window chrome (OS titlebar) stayed in the OS theme. Now, it syncs with the app theme:
|
||||||
|
|
||||||
|
https://github.com/user-attachments/assets/6b393057-fdfc-431b-b997-744bc183a8ac
|
||||||
|
|
||||||
|
|
||||||
|
### ⭐ Added
|
||||||
|
* Add `BoxedWidget`: dynamically dispatched widgets [#8378](https://github.com/emilk/egui/pull/8378) by [@emilk](https://github.com/emilk)
|
||||||
|
* Add `WidgetText::size` [#8377](https://github.com/emilk/egui/pull/8377) by [@emilk](https://github.com/emilk)
|
||||||
|
* Add `LayoutJob::clear` [#8376](https://github.com/emilk/egui/pull/8376) by [@emilk](https://github.com/emilk)
|
||||||
|
* Sync window theme with egui theme [#8299](https://github.com/emilk/egui/pull/8299) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||||
|
* Add `egui::Window::title_frame` [#8353](https://github.com/emilk/egui/pull/8353) by [@Its-Just-Nans](https://github.com/Its-Just-Nans)
|
||||||
|
* Add `extra_text_line_spacing` to control vertical spacing between text lines [#8040](https://github.com/emilk/egui/pull/8040) by [@rustbasic](https://github.com/rustbasic)
|
||||||
|
* Add drag-to-open for collapsible panels [#8363](https://github.com/emilk/egui/pull/8363) by [@emilk](https://github.com/emilk)
|
||||||
|
|
||||||
|
### 🔧 Changed
|
||||||
|
* Rerun `sizing_pass` when reopening popup [#8315](https://github.com/emilk/egui/pull/8315) by [@yay](https://github.com/yay)
|
||||||
|
* Update MSRV from 1.92 to 1.95 [#8348](https://github.com/emilk/egui/pull/8348) by [@emilk](https://github.com/emilk)
|
||||||
|
* Treat a press that leaves a widget as a drag [#8365](https://github.com/emilk/egui/pull/8365) by [@emilk](https://github.com/emilk)
|
||||||
|
|
||||||
|
### 🔥 Removed
|
||||||
|
* Remove `Modifiers` from `RawInput` and make it a `egui::Event` [#8336](https://github.com/emilk/egui/pull/8336) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||||
|
* Remove `clip_rect_margin` [#8366](https://github.com/emilk/egui/pull/8366) by [@emilk](https://github.com/emilk)
|
||||||
|
|
||||||
|
### 🐛 Fixed
|
||||||
|
* Improve backtrace trimming for cranelift [#8294](https://github.com/emilk/egui/pull/8294) by [@emilk](https://github.com/emilk)
|
||||||
|
* Prevent accidentally dropping `TexturesDelta` [#8356](https://github.com/emilk/egui/pull/8356) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||||
|
* Make non-interactive tooltips not interactable [#8362](https://github.com/emilk/egui/pull/8362) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||||
|
* Panels: Take separator line width into account [#8367](https://github.com/emilk/egui/pull/8367) by [@emilk](https://github.com/emilk)
|
||||||
|
* Fix TextEdit hint text not following horizontal_align/vertical_align [#8332](https://github.com/emilk/egui/pull/8332) by [@thedavidweng](https://github.com/thedavidweng)
|
||||||
|
* Fix ScrollArea failure by handling horizontal and vertical scrolling separately in the missing place [#8275](https://github.com/emilk/egui/pull/8275) by [@rustbasic](https://github.com/rustbasic)
|
||||||
|
* Fix window with a `Grid` being widenable but not shrinkable again [#8386](https://github.com/emilk/egui/pull/8386) by [@emilk](https://github.com/emilk)
|
||||||
|
|
||||||
|
|
||||||
## 0.35.0 - 2026-06-25 - Inspection, egui_mcp, classes and improved IME
|
## 0.35.0 - 2026-06-25 - Inspection, egui_mcp, classes and improved IME
|
||||||
|
|
||||||
### Highlights
|
### Highlights
|
||||||
|
|||||||
384
Cargo.lock
384
Cargo.lock
File diff suppressed because it is too large
Load Diff
235
Cargo.toml
235
Cargo.toml
@@ -25,7 +25,7 @@ members = [
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
version = "0.35.0"
|
version = "0.36.1"
|
||||||
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
@@ -56,23 +56,23 @@ opt-level = 2
|
|||||||
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
emath = { version = "0.35.0", path = "crates/emath", default-features = false }
|
emath = { version = "0.36.1", path = "crates/emath", default-features = false }
|
||||||
ecolor = { version = "0.35.0", path = "crates/ecolor", default-features = false }
|
ecolor = { version = "0.36.1", path = "crates/ecolor", default-features = false }
|
||||||
epaint = { version = "0.35.0", path = "crates/epaint", default-features = false }
|
epaint = { version = "0.36.1", path = "crates/epaint", default-features = false }
|
||||||
epaint_default_fonts = { version = "0.35.0", path = "crates/epaint_default_fonts" }
|
epaint_default_fonts = { version = "0.36.1", path = "crates/epaint_default_fonts" }
|
||||||
egui = { version = "0.35.0", path = "crates/egui", default-features = false }
|
egui = { version = "0.36.1", path = "crates/egui", default-features = false }
|
||||||
egui-winit = { version = "0.35.0", path = "crates/egui-winit", default-features = false }
|
egui-winit = { version = "0.36.1", path = "crates/egui-winit", default-features = false }
|
||||||
egui_extras = { version = "0.35.0", path = "crates/egui_extras", default-features = false }
|
egui_extras = { version = "0.36.1", path = "crates/egui_extras", default-features = false }
|
||||||
egui-wgpu = { version = "0.35.0", path = "crates/egui-wgpu", default-features = false }
|
egui-wgpu = { version = "0.36.1", path = "crates/egui-wgpu", default-features = false }
|
||||||
egui_demo_lib = { version = "0.35.0", path = "crates/egui_demo_lib", default-features = false }
|
egui_demo_lib = { version = "0.36.1", path = "crates/egui_demo_lib", default-features = false }
|
||||||
egui_glow = { version = "0.35.0", path = "crates/egui_glow", default-features = false }
|
egui_glow = { version = "0.36.1", path = "crates/egui_glow", default-features = false }
|
||||||
egui_inspection = { version = "0.35.0", path = "crates/egui_inspection", default-features = false }
|
egui_inspection = { version = "0.36.1", path = "crates/egui_inspection", default-features = false }
|
||||||
egui_kittest = { version = "0.35.0", path = "crates/egui_kittest", default-features = false }
|
egui_kittest = { version = "0.36.1", path = "crates/egui_kittest", default-features = false }
|
||||||
eframe = { version = "0.35.0", path = "crates/eframe", default-features = false }
|
eframe = { version = "0.36.1", path = "crates/eframe", default-features = false }
|
||||||
|
|
||||||
accesskit = "0.24.1"
|
accesskit = "0.24.1"
|
||||||
accesskit_consumer = "0.35.0" # Can't update to 0.36+: kittest 0.4 pins accesskit_consumer 0.35, so bumping splits it into two versions
|
accesskit_consumer = "0.35.0" # Can't update to 0.36+: kittest 0.4 pins accesskit_consumer 0.35, so bumping splits it into two versions
|
||||||
accesskit_winit = "0.32.0" # Can't update to 0.33: it needs accesskit_macos 0.26.2, which pulls accesskit_consumer 0.37, duplicating the 0.35 that kittest 0.4 needs
|
accesskit_winit = "0.32.0" # Can't update to 0.33: it needs accesskit_macos 0.26.2, which pulls accesskit_consumer 0.37, duplicating the 0.35 that kittest 0.4 needs. For the same reason, `accesskit_macos` is held at 0.26.0 in `Cargo.lock`.
|
||||||
ahash = { version = "0.8.12", default-features = false, features = [
|
ahash = { version = "0.8.12", default-features = false, features = [
|
||||||
"no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
|
"no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
|
||||||
"std",
|
"std",
|
||||||
@@ -89,23 +89,19 @@ dify = { version = "0.8.0", default-features = false }
|
|||||||
directories = "6.0"
|
directories = "6.0"
|
||||||
document-features = "0.2.12"
|
document-features = "0.2.12"
|
||||||
ehttp = { version = "0.7.1", default-features = false }
|
ehttp = { version = "0.7.1", default-features = false }
|
||||||
enum-map = "2.7"
|
enum-map = "2.7" # Can't update to 3.1: its `enum-map-derive` moved to syn 3, duplicating the syn 2 that most other proc-macro crates still use
|
||||||
env_logger = { version = "0.11.8", default-features = false } # 0.11.9+ pulls env_filter 1.x, duplicating the 0.1.x that android_logger needs
|
env_logger = { version = "0.11.8", default-features = false } # 0.11.9+ pulls env_filter 1.x/2.x, duplicating the 0.1.x that android_logger needs
|
||||||
font-types = { version = "0.11.3", default-features = false, features = [
|
font-types = { version = "0.12.2", default-features = false, features = ["std"] }
|
||||||
"std",
|
glow = "0.17.0" # Can't update to 0.18: wgpu 30's wgpu-hal pins glow 0.17, so bumping splits it into two versions
|
||||||
] } # Can't update to 0.12: vello_cpu's glifo 0.1.1 pins font-types 0.11 (via skrifa/read-fonts), so bumping splits it into two versions
|
|
||||||
glow = "0.17.0"
|
|
||||||
glutin = { version = "0.32.3", default-features = false }
|
glutin = { version = "0.32.3", default-features = false }
|
||||||
glutin-winit = { version = "0.5.0", default-features = false }
|
glutin-winit = { version = "0.5.0", default-features = false }
|
||||||
harfrust = "0.7.0" # Can't update to 0.8+: newer versions need read-fonts 0.40+/font-types 0.12, but vello_cpu's glifo 0.1.1 pins read-fonts 0.39/font-types 0.11, so bumping duplicates them
|
harfrust = "0.12.0"
|
||||||
home = "0.5.12"
|
|
||||||
image = { version = "0.25.6", default-features = false } # Can't update to 0.25.7+: it needs png 0.18, which only matches resvg once resvg moves to tiny-skia 0.12 — blocked, see resvg below
|
image = { version = "0.25.6", default-features = false } # Can't update to 0.25.7+: it needs png 0.18, which only matches resvg once resvg moves to tiny-skia 0.12 — blocked, see resvg below
|
||||||
itertools = "0.15.0"
|
itertools = "0.15.0"
|
||||||
jiff = { version = "0.2.29", default-features = false }
|
jiff = { version = "0.2.35", default-features = false }
|
||||||
js-sys = "0.3.103"
|
js-sys = "0.3.103"
|
||||||
kittest = { version = "0.4.0" }
|
kittest = { version = "0.4.0" }
|
||||||
log = { version = "0.4.33", features = ["std"] }
|
log = { version = "0.4.33", features = ["std"] }
|
||||||
memoffset = "0.9.1"
|
|
||||||
mimalloc = "0.1.52"
|
mimalloc = "0.1.52"
|
||||||
mime_guess2 = { version = "2.3", default-features = false }
|
mime_guess2 = { version = "2.3", default-features = false }
|
||||||
mint = "0.5.9"
|
mint = "0.5.9"
|
||||||
@@ -114,42 +110,43 @@ objc2 = "0.6.4"
|
|||||||
objc2-app-kit = { version = "0.3.2", default-features = false }
|
objc2-app-kit = { version = "0.3.2", default-features = false }
|
||||||
objc2-foundation = { version = "0.3.2", default-features = false }
|
objc2-foundation = { version = "0.3.2", default-features = false }
|
||||||
objc2-ui-kit = { version = "0.3.2", default-features = false }
|
objc2-ui-kit = { version = "0.3.2", default-features = false }
|
||||||
open = "5.3"
|
open = "5.4"
|
||||||
parking_lot = "0.12.5"
|
parking_lot = "0.12.5"
|
||||||
percent-encoding = "2.3"
|
percent-encoding = "2.3"
|
||||||
poll-promise = { version = "0.3.0", default-features = false }
|
poll-promise = { version = "0.3.0", default-features = false }
|
||||||
pollster = "0.4.0"
|
pollster = "1.0"
|
||||||
profiling = { version = "1.0", default-features = false }
|
profiling = { version = "1.0", default-features = false }
|
||||||
puffin = "0.20.0"
|
puffin = "0.20.0"
|
||||||
puffin_http = "0.17.0"
|
puffin_http = "0.17.0"
|
||||||
rand = "0.10.1"
|
rand = "0.10.2"
|
||||||
raw-window-handle = "0.6.2"
|
raw-window-handle = "0.6.2"
|
||||||
rayon = "1.12"
|
rayon = "1.12"
|
||||||
resvg = { version = "0.45.1", default-features = false } # Can't update to 0.47: it needs tiny-skia 0.12, but winit 0.30's sctk-adwaita is stuck on tiny-skia 0.11, so bumping duplicates tiny-skia. (0.46 keeps tiny-skia 0.11 but its fontconfig-parser duplicates roxmltree.)
|
resvg = { version = "0.45.1", default-features = false } # Can't update to 0.47+: it needs tiny-skia 0.12, but winit 0.30's sctk-adwaita 0.10 is stuck on tiny-skia 0.11, so bumping duplicates tiny-skia. (0.46 keeps tiny-skia 0.11 but its fontconfig-parser duplicates roxmltree.)
|
||||||
rfd = "0.17.2"
|
rfd = "0.17.2"
|
||||||
rmp-serde = "1.3"
|
rmp-serde = "1.3"
|
||||||
ron = "0.12.2"
|
ron = "0.12.2"
|
||||||
self_cell = "1.2"
|
self_cell = "1.3"
|
||||||
|
# `serde_derive` is held at 1.0.228 in `Cargo.lock`: 1.0.229 moved to syn 3, duplicating the syn 2 that most other proc-macro crates still use
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_bytes = "0.11.19"
|
serde_bytes = "0.11.19"
|
||||||
similar-asserts = "2.0"
|
similar-asserts = "2.0"
|
||||||
skrifa = { version = "0.42.1", default-features = false, features = [
|
skrifa = { version = "0.44.0", default-features = false, features = [
|
||||||
"std",
|
"std",
|
||||||
"autohint_shaping",
|
"autohint_shaping",
|
||||||
] } # Can't update to 0.43: vello_cpu's glifo 0.1.1 pins skrifa 0.42, so bumping splits it into two versions
|
] } # Can't update to 0.45: it needs read-fonts 0.42, but harfrust 0.12 pins read-fonts 0.41, so bumping splits read-fonts into two versions
|
||||||
smallvec = "1.15"
|
smallvec = "1.15"
|
||||||
smithay-clipboard = "0.7.2" # 0.7.3 pulls smithay-client-toolkit 0.20 (calloop 0.14), duplicating the 0.19/0.13 that winit 0.30 needs
|
smithay-clipboard = "0.7.2" # 0.7.3 pulls smithay-client-toolkit 0.20 (calloop 0.14), duplicating the 0.19/0.13 that winit 0.30 needs
|
||||||
static_assertions = "1.1"
|
static_assertions = "1.1"
|
||||||
syntect = { version = "5.3", default-features = false }
|
syntect = { version = "5.3", default-features = false }
|
||||||
tempfile = "3.27"
|
tempfile = "3.27"
|
||||||
thiserror = "2.0"
|
thiserror = "2.0" # Held at 2.0.18 in `Cargo.lock`: `thiserror-impl` 2.0.19 moved to syn 3, duplicating the syn 2 that most other proc-macro crates still use
|
||||||
tokio = "1.52"
|
tokio = "1.53"
|
||||||
toml = { version = "1.0", default-features = false }
|
toml = { version = "1.1", default-features = false }
|
||||||
type-map = "0.5.1"
|
type-map = "0.5.1"
|
||||||
unicode_names2 = { version = "3.1", default-features = false }
|
unicode_names2 = { version = "3.1", default-features = false }
|
||||||
unicode-general-category = "1.1"
|
unicode-general-category = "1.1"
|
||||||
unicode-segmentation = "1.13"
|
unicode-segmentation = "1.13"
|
||||||
vello_cpu = { version = "0.0.9", default-features = false, features = [
|
vello_cpu = { version = "0.2.0", default-features = false, features = [
|
||||||
"std",
|
"std",
|
||||||
"u8_pipeline",
|
"u8_pipeline",
|
||||||
"f32_pipeline",
|
"f32_pipeline",
|
||||||
@@ -159,7 +156,7 @@ wasm-bindgen-futures = "0.4.76"
|
|||||||
wayland-cursor = { version = "0.31.14", default-features = false }
|
wayland-cursor = { version = "0.31.14", default-features = false }
|
||||||
web-sys = "0.3.103"
|
web-sys = "0.3.103"
|
||||||
web-time = "1.1" # Timekeeping for native and web
|
web-time = "1.1" # Timekeeping for native and web
|
||||||
webbrowser = "1.2"
|
webbrowser = "1.2.2" # 1.2.2 fixes RUSTSEC-2026-0257 (`BROWSER` argument injection)
|
||||||
wgpu = { version = "30.0", default-features = false, features = ["std"] }
|
wgpu = { version = "30.0", default-features = false, features = ["std"] }
|
||||||
windows-sys = "0.61.2"
|
windows-sys = "0.61.2"
|
||||||
winit = { version = "0.30.13", default-features = false }
|
winit = { version = "0.30.13", default-features = false }
|
||||||
@@ -167,16 +164,10 @@ winit = { version = "0.30.13", default-features = false }
|
|||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
unsafe_code = "deny"
|
unsafe_code = "deny"
|
||||||
|
|
||||||
elided_lifetimes_in_paths = "warn"
|
|
||||||
future_incompatible = { level = "warn", priority = -1 }
|
future_incompatible = { level = "warn", priority = -1 }
|
||||||
nonstandard_style = { level = "warn", priority = -1 }
|
nonstandard_style = { level = "warn", priority = -1 }
|
||||||
rust_2018_idioms = { level = "warn", priority = -1 }
|
rust_2018_idioms = { level = "warn", priority = -1 }
|
||||||
rust_2021_prelude_collisions = "warn"
|
|
||||||
semicolon_in_expressions_from_macros = "warn"
|
|
||||||
trivial_numeric_casts = "warn"
|
trivial_numeric_casts = "warn"
|
||||||
unexpected_cfgs = "warn"
|
|
||||||
unsafe_op_in_unsafe_fn = "warn" # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668
|
|
||||||
unused_extern_crates = "warn"
|
|
||||||
unused_import_braces = "warn"
|
unused_import_braces = "warn"
|
||||||
unused_lifetimes = "warn"
|
unused_lifetimes = "warn"
|
||||||
|
|
||||||
@@ -185,216 +176,128 @@ unused_qualifications = "allow"
|
|||||||
|
|
||||||
[workspace.lints.rustdoc]
|
[workspace.lints.rustdoc]
|
||||||
all = "warn"
|
all = "warn"
|
||||||
missing_crate_level_docs = "warn"
|
|
||||||
broken_intra_doc_links = "warn"
|
|
||||||
|
|
||||||
# See also clippy.toml
|
# See also clippy.toml
|
||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
|
# `all` = `correctness` + `suspicious` + `style` + `complexity` + `perf`.
|
||||||
|
# The remaining groups are `nursery` and `restriction`,
|
||||||
|
# which are not meant to be enabled wholesale - we cherry-pick from them below.
|
||||||
all = { level = "warn", priority = -1 }
|
all = { level = "warn", priority = -1 }
|
||||||
|
cargo = { level = "warn", priority = -1 }
|
||||||
|
pedantic = { level = "warn", priority = -1 }
|
||||||
|
|
||||||
allow_attributes = "warn"
|
allow_attributes = "warn"
|
||||||
as_ptr_cast_mut = "warn"
|
as_ptr_cast_mut = "warn"
|
||||||
await_holding_lock = "warn"
|
|
||||||
bool_to_int_with_if = "warn"
|
|
||||||
branches_sharing_code = "warn"
|
branches_sharing_code = "warn"
|
||||||
char_lit_as_u8 = "warn"
|
|
||||||
checked_conversions = "warn"
|
|
||||||
clear_with_drain = "warn"
|
clear_with_drain = "warn"
|
||||||
clone_on_ref_ptr = "warn"
|
clone_on_ref_ptr = "warn"
|
||||||
cloned_instead_of_copied = "warn"
|
|
||||||
coerce_container_to_any = "warn"
|
coerce_container_to_any = "warn"
|
||||||
dbg_macro = "warn"
|
dbg_macro = "warn"
|
||||||
debug_assert_with_mut_call = "warn"
|
debug_assert_with_mut_call = "warn"
|
||||||
decimal_bitwise_operands = "warn"
|
|
||||||
default_union_representation = "warn"
|
default_union_representation = "warn"
|
||||||
derive_partial_eq_without_eq = "warn"
|
derive_partial_eq_without_eq = "warn"
|
||||||
disallowed_macros = "warn" # See clippy.toml
|
|
||||||
disallowed_methods = "warn" # See clippy.toml
|
|
||||||
disallowed_names = "warn" # See clippy.toml
|
|
||||||
disallowed_script_idents = "warn" # See clippy.toml
|
disallowed_script_idents = "warn" # See clippy.toml
|
||||||
disallowed_types = "warn" # See clippy.toml
|
|
||||||
doc_broken_link = "warn"
|
|
||||||
doc_comment_double_space_linebreaks = "warn"
|
|
||||||
doc_include_without_cfg = "warn"
|
doc_include_without_cfg = "warn"
|
||||||
doc_link_with_quotes = "warn"
|
|
||||||
doc_markdown = "warn"
|
|
||||||
duration_suboptimal_units = "warn"
|
|
||||||
elidable_lifetime_names = "warn"
|
|
||||||
empty_enum_variants_with_brackets = "warn"
|
empty_enum_variants_with_brackets = "warn"
|
||||||
empty_enums = "warn"
|
|
||||||
empty_line_after_outer_attr = "warn"
|
|
||||||
enum_glob_use = "warn"
|
|
||||||
equatable_if_let = "warn"
|
equatable_if_let = "warn"
|
||||||
exit = "warn"
|
exit = "warn"
|
||||||
expl_impl_clone_on_copy = "warn"
|
|
||||||
explicit_deref_methods = "warn"
|
|
||||||
explicit_into_iter_loop = "warn"
|
|
||||||
explicit_iter_loop = "warn"
|
|
||||||
fallible_impl_from = "warn"
|
fallible_impl_from = "warn"
|
||||||
filter_map_next = "warn"
|
|
||||||
flat_map_option = "warn"
|
|
||||||
float_cmp_const = "warn"
|
float_cmp_const = "warn"
|
||||||
fn_params_excessive_bools = "warn"
|
|
||||||
fn_to_numeric_cast_any = "warn"
|
fn_to_numeric_cast_any = "warn"
|
||||||
format_push_string = "warn"
|
|
||||||
from_iter_instead_of_collect = "warn"
|
|
||||||
get_unwrap = "warn"
|
get_unwrap = "warn"
|
||||||
if_let_mutex = "warn"
|
|
||||||
ignore_without_reason = "warn"
|
|
||||||
ignored_unit_patterns = "warn"
|
|
||||||
implicit_clone = "warn"
|
|
||||||
implied_bounds_in_impls = "warn"
|
|
||||||
imprecise_flops = "warn"
|
imprecise_flops = "warn"
|
||||||
inconsistent_struct_constructor = "warn"
|
|
||||||
index_refutable_slice = "warn"
|
|
||||||
inefficient_to_string = "warn"
|
|
||||||
infinite_loop = "warn"
|
infinite_loop = "warn"
|
||||||
into_iter_without_iter = "warn"
|
|
||||||
invalid_upcast_comparisons = "warn"
|
|
||||||
ip_constant = "warn"
|
|
||||||
iter_filter_is_ok = "warn"
|
|
||||||
iter_filter_is_some = "warn"
|
|
||||||
iter_not_returning_iterator = "warn"
|
|
||||||
iter_on_empty_collections = "warn"
|
iter_on_empty_collections = "warn"
|
||||||
iter_on_single_items = "warn"
|
iter_on_single_items = "warn"
|
||||||
iter_over_hash_type = "warn"
|
iter_over_hash_type = "warn"
|
||||||
iter_without_into_iter = "warn"
|
|
||||||
large_digit_groups = "warn"
|
|
||||||
large_futures = "warn"
|
|
||||||
large_include_file = "warn"
|
large_include_file = "warn"
|
||||||
large_stack_arrays = "warn"
|
|
||||||
large_stack_frames = "warn"
|
large_stack_frames = "warn"
|
||||||
large_types_passed_by_value = "warn"
|
|
||||||
let_unit_value = "warn"
|
|
||||||
linkedlist = "warn"
|
|
||||||
literal_string_with_formatting_args = "warn"
|
literal_string_with_formatting_args = "warn"
|
||||||
lossy_float_literal = "warn"
|
lossy_float_literal = "warn"
|
||||||
macro_use_imports = "warn"
|
|
||||||
manual_assert = "warn"
|
|
||||||
manual_clamp = "warn"
|
|
||||||
manual_ilog2 = "warn"
|
|
||||||
manual_instant_elapsed = "warn"
|
|
||||||
manual_is_power_of_two = "warn"
|
|
||||||
manual_is_variant_and = "warn"
|
|
||||||
manual_let_else = "warn"
|
|
||||||
manual_midpoint = "warn" # NOTE `midpoint` is often a lot slower for floats, so we have our own `emath::fast_midpoint` function.
|
|
||||||
manual_ok_or = "warn"
|
|
||||||
manual_string_new = "warn"
|
|
||||||
map_err_ignore = "warn"
|
map_err_ignore = "warn"
|
||||||
map_flatten = "warn"
|
|
||||||
match_bool = "warn"
|
|
||||||
match_same_arms = "warn"
|
|
||||||
match_wild_err_arm = "warn"
|
|
||||||
match_wildcard_for_single_variants = "warn"
|
|
||||||
mem_forget = "warn"
|
mem_forget = "warn"
|
||||||
mismatching_type_param_order = "warn"
|
|
||||||
missing_assert_message = "warn"
|
missing_assert_message = "warn"
|
||||||
missing_enforced_import_renames = "warn"
|
|
||||||
missing_errors_doc = "warn"
|
|
||||||
missing_fields_in_debug = "warn"
|
|
||||||
missing_safety_doc = "warn"
|
|
||||||
mixed_attributes_style = "warn"
|
|
||||||
mut_mut = "warn"
|
|
||||||
mutex_integer = "warn"
|
mutex_integer = "warn"
|
||||||
needless_borrow = "warn"
|
|
||||||
needless_continue = "warn"
|
|
||||||
needless_for_each = "warn"
|
|
||||||
needless_pass_by_ref_mut = "warn"
|
needless_pass_by_ref_mut = "warn"
|
||||||
needless_pass_by_value = "warn"
|
|
||||||
needless_raw_string_hashes = "warn"
|
|
||||||
needless_type_cast = "warn"
|
needless_type_cast = "warn"
|
||||||
negative_feature_names = "warn"
|
|
||||||
non_std_lazy_statics = "warn"
|
|
||||||
non_zero_suggestions = "warn"
|
non_zero_suggestions = "warn"
|
||||||
nonstandard_macro_braces = "warn"
|
nonstandard_macro_braces = "warn"
|
||||||
only_used_in_recursion = "warn"
|
|
||||||
option_as_ref_cloned = "warn"
|
|
||||||
option_option = "warn"
|
|
||||||
or_fun_call = "warn"
|
or_fun_call = "warn"
|
||||||
path_buf_push_overwrite = "warn"
|
path_buf_push_overwrite = "warn"
|
||||||
pathbuf_init_then_push = "warn"
|
pathbuf_init_then_push = "warn"
|
||||||
precedence_bits = "warn"
|
precedence_bits = "warn"
|
||||||
print_stderr = "warn"
|
print_stderr = "warn"
|
||||||
print_stdout = "warn"
|
print_stdout = "warn"
|
||||||
ptr_as_ptr = "warn"
|
|
||||||
ptr_cast_constness = "warn"
|
|
||||||
pub_underscore_fields = "warn"
|
|
||||||
pub_without_shorthand = "warn"
|
pub_without_shorthand = "warn"
|
||||||
rc_mutex = "warn"
|
rc_mutex = "warn"
|
||||||
readonly_write_lock = "warn"
|
|
||||||
redundant_type_annotations = "warn"
|
redundant_type_annotations = "warn"
|
||||||
ref_as_ptr = "warn"
|
|
||||||
ref_option = "warn"
|
|
||||||
ref_option_ref = "warn"
|
|
||||||
ref_patterns = "warn"
|
ref_patterns = "warn"
|
||||||
rest_pat_in_fully_bound_structs = "warn"
|
rest_pat_in_fully_bound_structs = "warn"
|
||||||
return_and_then = "warn"
|
return_and_then = "warn"
|
||||||
same_functions_in_if_condition = "warn"
|
|
||||||
same_length_and_capacity = "warn"
|
|
||||||
self_only_used_in_recursion = "warn"
|
|
||||||
semicolon_if_nothing_returned = "warn"
|
|
||||||
set_contains_or_insert = "warn"
|
set_contains_or_insert = "warn"
|
||||||
single_char_pattern = "warn"
|
|
||||||
single_match_else = "warn"
|
|
||||||
single_option_map = "warn"
|
single_option_map = "warn"
|
||||||
str_split_at_newline = "warn"
|
std_instead_of_core = "warn"
|
||||||
str_to_string = "warn"
|
str_to_string = "warn"
|
||||||
string_add = "warn"
|
string_add = "warn"
|
||||||
string_add_assign = "warn"
|
|
||||||
string_lit_as_bytes = "warn"
|
string_lit_as_bytes = "warn"
|
||||||
string_lit_chars_any = "warn"
|
string_lit_chars_any = "warn"
|
||||||
suspicious_command_arg_space = "warn"
|
|
||||||
suspicious_xor_used_as_pow = "warn"
|
suspicious_xor_used_as_pow = "warn"
|
||||||
todo = "warn"
|
todo = "warn"
|
||||||
too_long_first_doc_paragraph = "warn"
|
too_long_first_doc_paragraph = "warn"
|
||||||
too_many_arguments = "warn"
|
|
||||||
trailing_empty_array = "warn"
|
trailing_empty_array = "warn"
|
||||||
trait_duplication_in_bounds = "warn"
|
trait_duplication_in_bounds = "warn"
|
||||||
transmute_ptr_to_ptr = "warn"
|
|
||||||
tuple_array_conversions = "warn"
|
tuple_array_conversions = "warn"
|
||||||
unchecked_time_subtraction = "warn"
|
|
||||||
undocumented_unsafe_blocks = "warn"
|
undocumented_unsafe_blocks = "warn"
|
||||||
unimplemented = "warn"
|
unimplemented = "warn"
|
||||||
uninhabited_references = "warn"
|
uninhabited_references = "warn"
|
||||||
uninlined_format_args = "warn"
|
|
||||||
unnecessary_box_returns = "warn"
|
|
||||||
unnecessary_debug_formatting = "warn"
|
|
||||||
unnecessary_literal_bound = "warn"
|
|
||||||
unnecessary_safety_comment = "warn"
|
unnecessary_safety_comment = "warn"
|
||||||
unnecessary_safety_doc = "warn"
|
unnecessary_safety_doc = "warn"
|
||||||
unnecessary_self_imports = "warn"
|
unnecessary_self_imports = "warn"
|
||||||
unnecessary_semicolon = "warn"
|
|
||||||
unnecessary_struct_initialization = "warn"
|
unnecessary_struct_initialization = "warn"
|
||||||
unnecessary_trailing_comma = "warn"
|
|
||||||
unnecessary_wraps = "warn"
|
|
||||||
unnested_or_patterns = "warn"
|
|
||||||
unused_async = "warn"
|
|
||||||
unused_peekable = "warn"
|
unused_peekable = "warn"
|
||||||
unused_rounding = "warn"
|
unused_rounding = "warn"
|
||||||
unused_self = "warn"
|
|
||||||
unused_trait_names = "warn"
|
unused_trait_names = "warn"
|
||||||
unwrap_used = "warn"
|
unwrap_used = "warn"
|
||||||
use_self = "warn"
|
use_self = "warn"
|
||||||
useless_let_if_seq = "warn"
|
useless_let_if_seq = "warn"
|
||||||
useless_transmute = "warn"
|
|
||||||
verbose_file_reads = "warn"
|
verbose_file_reads = "warn"
|
||||||
wildcard_dependencies = "warn"
|
|
||||||
zero_sized_map_values = "warn"
|
|
||||||
|
|
||||||
|
|
||||||
# TODO(emilk): maybe enable more of these lints?
|
# Pedantic lints we opt out of, with the number of hits at the time we enabled `pedantic`:
|
||||||
cast_possible_wrap = "allow"
|
cast_lossless = "allow" # 204 hits
|
||||||
|
cast_possible_truncation = "allow" # 287 hits
|
||||||
|
cast_possible_wrap = "allow" # 43 hits
|
||||||
|
cast_precision_loss = "allow" # 200 hits
|
||||||
|
cast_sign_loss = "allow" # 98 hits
|
||||||
comparison_chain = "allow"
|
comparison_chain = "allow"
|
||||||
|
default_trait_access = "allow" # 278 hits
|
||||||
|
float_cmp = "allow" # exact float comparisons are usually intentional (`float_cmp_const` is still on)
|
||||||
|
inline_always = "allow" # 271 hits; we know what we are doing
|
||||||
|
items_after_statements = "allow" # 82 hits
|
||||||
|
many_single_char_names = "allow" # `r, g, b, a` and `h, s, v` are fine
|
||||||
|
missing_panics_doc = "allow" # 68 hits
|
||||||
|
must_use_candidate = "allow" # 1169 hits
|
||||||
|
redundant_closure_for_method_calls = "allow" # 89 hits
|
||||||
|
return_self_not_must_use = "allow" # 246 hits
|
||||||
should_panic_without_expect = "allow"
|
should_panic_without_expect = "allow"
|
||||||
|
similar_names = "allow" # too many false positives, e.g. `encoder`/`encoded`
|
||||||
|
struct_excessive_bools = "allow" # 32 hits
|
||||||
|
struct_field_names = "allow" # 23 hits
|
||||||
too_many_lines = "allow"
|
too_many_lines = "allow"
|
||||||
|
trivially_copy_pass_by_ref = "allow" # 119 hits
|
||||||
|
unreadable_literal = "allow" # 513 hits
|
||||||
|
used_underscore_binding = "allow" # 25 hits
|
||||||
|
|
||||||
# These are meh:
|
# Other:
|
||||||
assigning_clones = "allow" # No please
|
assigning_clones = "allow" # No please
|
||||||
cast_possible_truncation = "allow" # too many hits
|
|
||||||
let_underscore_must_use = "allow"
|
|
||||||
let_underscore_untyped = "allow"
|
|
||||||
manual_range_contains = "allow" # this one is just worse imho
|
manual_range_contains = "allow" # this one is just worse imho
|
||||||
map_unwrap_or = "allow" # so is this one
|
map_unwrap_or = "allow" # so is this one
|
||||||
|
multiple_crate_versions = "allow" # we handle this with `cargo deny`
|
||||||
|
wildcard_imports = "allow" # `use crate::*` is useful to avoid merge conflicts when adding/removing imports
|
||||||
|
|
||||||
|
# NOTE: these are in `restriction`/`nursery`, so the `allow` is a no-op today.
|
||||||
|
# We keep them to record our intent in case we ever enable those groups.
|
||||||
|
let_underscore_must_use = "allow"
|
||||||
|
let_underscore_untyped = "allow"
|
||||||
self_named_module_files = "allow" # Disabled waiting on https://github.com/rust-lang/rust-clippy/issues/9602
|
self_named_module_files = "allow" # Disabled waiting on https://github.com/rust-lang/rust-clippy/issues/9602
|
||||||
significant_drop_tightening = "allow" # Too many false positives
|
significant_drop_tightening = "allow" # Too many false positives
|
||||||
wildcard_imports = "allow" # `use crate::*` is useful to avoid merge conflicts when adding/removing imports
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ ui.image(egui::include_image!("ferris.png"));
|
|||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
There are simple examples in [the `examples/` folder](https://github.com/emilk/egui/blob/main/examples/). If you want to write a web app, then go to <https://github.com/emilk/eframe_template/> and follow the instructions. The official docs are at <https://docs.rs/egui>. For inspiration and more examples, check out the [the egui web demo](https://www.egui.rs/#demo) and follow the links in it to its source code.
|
There are simple examples in [the `examples/` folder](https://github.com/emilk/egui/blob/main/examples/). If you want to write a web app, then go to <https://github.com/emilk/eframe_template/> and follow the instructions. The official docs are at <https://docs.rs/egui>. For inspiration and more examples, check out [the egui web demo](https://www.egui.rs/#demo) and follow the links in it to its source code.
|
||||||
|
|
||||||
If you want to integrate egui into an existing engine, go to the [Integrations](#integrations) section.
|
If you want to integrate egui into an existing engine, go to the [Integrations](#integrations) section.
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ 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.
|
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.36.1 - 2026-08-07
|
||||||
|
Nothing new
|
||||||
|
|
||||||
|
|
||||||
|
## 0.36.0 - 2026-08-05
|
||||||
|
Nothing new
|
||||||
|
|
||||||
## 0.35.0 - 2026-06-25
|
## 0.35.0 - 2026-06-25
|
||||||
Nothing new
|
Nothing new
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::{Rgba, fast_round, linear_f32_from_linear_u8};
|
use crate::{Rgba, fast_round, mul_frac_round};
|
||||||
|
|
||||||
/// This format is used for space-efficient color representation (32 bits).
|
/// This format is used for space-efficient color representation (32 bits).
|
||||||
///
|
///
|
||||||
@@ -30,15 +30,15 @@ use crate::{Rgba, fast_round, linear_f32_from_linear_u8};
|
|||||||
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
|
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
|
||||||
pub struct Color32(pub(crate) [u8; 4]);
|
pub struct Color32(pub(crate) [u8; 4]);
|
||||||
|
|
||||||
impl std::fmt::Debug for Color32 {
|
impl core::fmt::Debug for Color32 {
|
||||||
/// Prints the contents with premultiplied alpha!
|
/// Prints the contents with premultiplied alpha!
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
let [r, g, b, a] = self.0;
|
let [r, g, b, a] = self.0;
|
||||||
write!(f, "#{r:02X}_{g:02X}_{b:02X}_{a:02X}")
|
write!(f, "#{r:02X}_{g:02X}_{b:02X}_{a:02X}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Index<usize> for Color32 {
|
impl core::ops::Index<usize> for Color32 {
|
||||||
type Output = u8;
|
type Output = u8;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -47,7 +47,7 @@ impl std::ops::Index<usize> for Color32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::IndexMut<usize> for Color32 {
|
impl core::ops::IndexMut<usize> for Color32 {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn index_mut(&mut self, index: usize) -> &mut u8 {
|
fn index_mut(&mut self, index: usize) -> &mut u8 {
|
||||||
&mut self.0[index]
|
&mut self.0[index]
|
||||||
@@ -131,35 +131,10 @@ impl Color32 {
|
|||||||
/// but for transparent colors what you get back might be slightly different (rounding errors).
|
/// but for transparent colors what you get back might be slightly different (rounding errors).
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||||
use std::sync::OnceLock;
|
Self::from_rgba_unmultiplied_const(r, g, b, a)
|
||||||
match a {
|
|
||||||
// common-case optimization:
|
|
||||||
0 => Self::TRANSPARENT,
|
|
||||||
|
|
||||||
// common-case optimization:
|
|
||||||
255 => Self::from_rgb(r, g, b),
|
|
||||||
|
|
||||||
a => {
|
|
||||||
static LOOKUP_TABLE: OnceLock<Box<[u8]>> = OnceLock::new();
|
|
||||||
let lut = LOOKUP_TABLE.get_or_init(|| {
|
|
||||||
(0..=u16::MAX)
|
|
||||||
.map(|i| {
|
|
||||||
let [value, alpha] = i.to_ne_bytes();
|
|
||||||
fast_round(value as f32 * linear_f32_from_linear_u8(alpha))
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
});
|
|
||||||
|
|
||||||
let [r, g, b] =
|
|
||||||
[r, g, b].map(|value| lut[usize::from(u16::from_ne_bytes([value, a]))]);
|
|
||||||
Self::from_rgba_premultiplied(r, g, b, a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Same as [`Self::from_rgba_unmultiplied`], but can be used in a const context.
|
/// This is the same as [`Self::from_rgba_unmultiplied`], but for const contexts.
|
||||||
///
|
|
||||||
/// It is slightly slower when operating on non-const data.
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn from_rgba_unmultiplied_const(r: u8, g: u8, b: u8, a: u8) -> Self {
|
pub const fn from_rgba_unmultiplied_const(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||||
match a {
|
match a {
|
||||||
@@ -170,9 +145,9 @@ impl Color32 {
|
|||||||
255 => Self::from_rgb(r, g, b),
|
255 => Self::from_rgb(r, g, b),
|
||||||
|
|
||||||
a => {
|
a => {
|
||||||
let r = fast_round(r as f32 * linear_f32_from_linear_u8(a));
|
let r = mul_frac_round(r, a);
|
||||||
let g = fast_round(g as f32 * linear_f32_from_linear_u8(a));
|
let g = mul_frac_round(g, a);
|
||||||
let b = fast_round(b as f32 * linear_f32_from_linear_u8(a));
|
let b = mul_frac_round(b, a);
|
||||||
Self::from_rgba_premultiplied(r, g, b, a)
|
Self::from_rgba_premultiplied(r, g, b, a)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -378,7 +353,7 @@ impl Color32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Mul for Color32 {
|
impl core::ops::Mul for Color32 {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
/// Fast gamma-space multiplication.
|
/// Fast gamma-space multiplication.
|
||||||
@@ -393,7 +368,7 @@ impl std::ops::Mul for Color32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Add for Color32 {
|
impl core::ops::Add for Color32 {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -489,7 +464,7 @@ mod test {
|
|||||||
} else {
|
} else {
|
||||||
// There will be small rounding errors whenever the alpha is not 0 or 255,
|
// There will be small rounding errors whenever the alpha is not 0 or 255,
|
||||||
// because we multiply and then unmultiply the alpha.
|
// because we multiply and then unmultiply the alpha.
|
||||||
for (&a, &b) in std::iter::zip(&in_rgba, &out_rgba) {
|
for (&a, &b) in core::iter::zip(&in_rgba, &out_rgba) {
|
||||||
assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}");
|
assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -535,4 +510,14 @@ mod test {
|
|||||||
Color32::from_rgba_unmultiplied(255, 0, 0, 128)
|
Color32::from_rgba_unmultiplied(255, 0, 0, 128)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mul_frac_round_vs_old() {
|
||||||
|
for x in (0..=255u8).step_by(4) {
|
||||||
|
for a in (1..=255u8).step_by(4) {
|
||||||
|
let old = fast_round(x as f32 * crate::linear_f32_from_linear_u8(a));
|
||||||
|
assert_eq!(old, mul_frac_round(x, a));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//! Supports the 3, 4, 6, and 8-digit formats, according to the specification in
|
//! Supports the 3, 4, 6, and 8-digit formats, according to the specification in
|
||||||
//! <https://drafts.csswg.org/css-color-4/#hex-color>
|
//! <https://drafts.csswg.org/css-color-4/#hex-color>
|
||||||
|
|
||||||
use std::{fmt::Display, str::FromStr};
|
use core::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
use crate::Color32;
|
use crate::Color32;
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ pub enum HexColor {
|
|||||||
pub enum ParseHexColorError {
|
pub enum ParseHexColorError {
|
||||||
MissingHash,
|
MissingHash,
|
||||||
InvalidLength,
|
InvalidLength,
|
||||||
InvalidInt(std::num::ParseIntError),
|
InvalidInt(core::num::ParseIntError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for HexColor {
|
impl FromStr for HexColor {
|
||||||
@@ -45,7 +45,7 @@ impl FromStr for HexColor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Display for HexColor {
|
impl Display for HexColor {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Hex3(color) => {
|
Self::Hex3(color) => {
|
||||||
let [r, g, b, _] = color.to_srgba_unmultiplied().map(|u| u >> 4);
|
let [r, g, b, _] = color.to_srgba_unmultiplied().map(|u| u >> 4);
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ impl Hsva {
|
|||||||
/// From linear RGBA with premultiplied alpha
|
/// From linear RGBA with premultiplied alpha
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||||
#![expect(clippy::many_single_char_names)]
|
|
||||||
if a <= 0.0 {
|
if a <= 0.0 {
|
||||||
if r == 0.0 && b == 0.0 && a == 0.0 {
|
if r == 0.0 && b == 0.0 && a == 0.0 {
|
||||||
Self::default()
|
Self::default()
|
||||||
@@ -57,7 +56,6 @@ impl Hsva {
|
|||||||
/// From linear RGBA without premultiplied alpha
|
/// From linear RGBA without premultiplied alpha
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||||
#![expect(clippy::many_single_char_names)]
|
|
||||||
let (h, s, v) = hsv_from_rgb([r, g, b]);
|
let (h, s, v) = hsv_from_rgb([r, g, b]);
|
||||||
Self { h, s, v, a }
|
Self { h, s, v, a }
|
||||||
}
|
}
|
||||||
@@ -189,7 +187,6 @@ impl From<Color32> for Hsva {
|
|||||||
/// All ranges in 0-1, rgb is linear.
|
/// All ranges in 0-1, rgb is linear.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) {
|
pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) {
|
||||||
#![expect(clippy::many_single_char_names)]
|
|
||||||
let min = r.min(g.min(b));
|
let min = r.min(g.min(b));
|
||||||
let max = r.max(g.max(b)); // value
|
let max = r.max(g.max(b)); // value
|
||||||
|
|
||||||
@@ -213,7 +210,6 @@ pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) {
|
|||||||
/// All ranges in 0-1, rgb is linear.
|
/// All ranges in 0-1, rgb is linear.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] {
|
pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] {
|
||||||
#![expect(clippy::many_single_char_names)]
|
|
||||||
let h = (h.fract() + 1.0).fract(); // wrap
|
let h = (h.fract() + 1.0).fract(); // wrap
|
||||||
let s = s.clamp(0.0, 1.0);
|
let s = s.clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,17 @@ const fn fast_round(r: f32) -> u8 {
|
|||||||
(r + 0.5) as _ // rust does a saturating cast since 1.45
|
(r + 0.5) as _ // rust does a saturating cast since 1.45
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compute val * (frac/255) with no floating point or divisions.
|
||||||
|
#[inline]
|
||||||
|
const fn mul_frac_round(val: u8, frac: u8) -> u8 {
|
||||||
|
// Treat this as a simple fixed point calculation
|
||||||
|
let p = (val as u16) * (frac as u16) + 128;
|
||||||
|
((p + (p >> 8)) >> 8) as u8
|
||||||
|
// Logic split out a bit more.
|
||||||
|
//let p = (val as u16) * (frac as u16) + 127; // + 127 to round or remove to truncate.
|
||||||
|
//return ((p + 1 + (p >> 8)) >> 8) as u8;
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn test_srgba_conversion() {
|
pub fn test_srgba_conversion() {
|
||||||
for b in 0..=255 {
|
for b in 0..=255 {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::Color32;
|
|||||||
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
|
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
|
||||||
pub struct Rgba(pub(crate) [f32; 4]);
|
pub struct Rgba(pub(crate) [f32; 4]);
|
||||||
|
|
||||||
impl std::ops::Index<usize> for Rgba {
|
impl core::ops::Index<usize> for Rgba {
|
||||||
type Output = f32;
|
type Output = f32;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -18,7 +18,7 @@ impl std::ops::Index<usize> for Rgba {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::IndexMut<usize> for Rgba {
|
impl core::ops::IndexMut<usize> for Rgba {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn index_mut(&mut self, index: usize) -> &mut f32 {
|
fn index_mut(&mut self, index: usize) -> &mut f32 {
|
||||||
&mut self.0[index]
|
&mut self.0[index]
|
||||||
@@ -27,20 +27,20 @@ impl std::ops::IndexMut<usize> for Rgba {
|
|||||||
|
|
||||||
/// Deterministically hash an `f32`, treating all NANs as equal, and ignoring the sign of zero.
|
/// Deterministically hash an `f32`, treating all NANs as equal, and ignoring the sign of zero.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn f32_hash<H: std::hash::Hasher>(state: &mut H, f: f32) {
|
pub(crate) fn f32_hash<H: core::hash::Hasher>(state: &mut H, f: f32) {
|
||||||
if f == 0.0 {
|
if f == 0.0 {
|
||||||
state.write_u8(0);
|
state.write_u8(0);
|
||||||
} else if f.is_nan() {
|
} else if f.is_nan() {
|
||||||
state.write_u8(1);
|
state.write_u8(1);
|
||||||
} else {
|
} else {
|
||||||
use std::hash::Hash as _;
|
use core::hash::Hash as _;
|
||||||
f.to_bits().hash(state);
|
f.to_bits().hash(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::hash::Hash for Rgba {
|
impl core::hash::Hash for Rgba {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||||
crate::f32_hash(state, self.0[0]);
|
crate::f32_hash(state, self.0[0]);
|
||||||
crate::f32_hash(state, self.0[1]);
|
crate::f32_hash(state, self.0[1]);
|
||||||
crate::f32_hash(state, self.0[2]);
|
crate::f32_hash(state, self.0[2]);
|
||||||
@@ -219,7 +219,7 @@ impl Rgba {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Add for Rgba {
|
impl core::ops::Add for Rgba {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -233,7 +233,7 @@ impl std::ops::Add for Rgba {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Mul for Rgba {
|
impl core::ops::Mul for Rgba {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -247,7 +247,7 @@ impl std::ops::Mul for Rgba {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Mul<f32> for Rgba {
|
impl core::ops::Mul<f32> for Rgba {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -261,7 +261,7 @@ impl std::ops::Mul<f32> for Rgba {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Mul<Rgba> for f32 {
|
impl core::ops::Mul<Rgba> for f32 {
|
||||||
type Output = Rgba;
|
type Output = Rgba;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -336,7 +336,7 @@ mod test {
|
|||||||
} else {
|
} else {
|
||||||
// There will be small rounding errors whenever the alpha is not 0 or 255,
|
// There will be small rounding errors whenever the alpha is not 0 or 255,
|
||||||
// because we multiply and then unmultiply the alpha.
|
// because we multiply and then unmultiply the alpha.
|
||||||
for (&a, &b) in std::iter::zip(&in_rgba, &out_rgba) {
|
for (&a, &b) in core::iter::zip(&in_rgba, &out_rgba) {
|
||||||
assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}");
|
assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,24 @@ 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.
|
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.36.1 - 2026-08-07
|
||||||
|
Nothing new
|
||||||
|
|
||||||
|
|
||||||
|
## 0.36.0 - 2026-08-05
|
||||||
|
### 🔧 Changed
|
||||||
|
* Improve robustness of text input handling for `eframe/web` [#8045](https://github.com/emilk/egui/pull/8045) by [@umajho](https://github.com/umajho)
|
||||||
|
* Eframe: make webbrowser dependency optional [#8372](https://github.com/emilk/egui/pull/8372) by [@wyvernbw](https://github.com/wyvernbw)
|
||||||
|
* Store `web_sys::File` inside of `DroppedFile` [#8354](https://github.com/emilk/egui/pull/8354) by [@grtlr](https://github.com/grtlr)
|
||||||
|
|
||||||
|
### 🐛 Fixed
|
||||||
|
* Web: don't scroll host page when text agent or canvas grabs focus [#8296](https://github.com/emilk/egui/pull/8296) by [@emilk](https://github.com/emilk)
|
||||||
|
* Fix missing modifier events on eframe web, handle physical keys [#8345](https://github.com/emilk/egui/pull/8345) by [@lucasmerlin](https://github.com/lucasmerlin)
|
||||||
|
* Web: Avoid panic from lost texture updates when loaded on a background tab [#8313](https://github.com/emilk/egui/pull/8313) by [@kevinmehall](https://github.com/kevinmehall)
|
||||||
|
* Web: anchor the text agent to the canvas [#8297](https://github.com/emilk/egui/pull/8297) by [@emilk](https://github.com/emilk)
|
||||||
|
* Never run an egui pass when nothing will be shown [#8387](https://github.com/emilk/egui/pull/8387) by [@emilk](https://github.com/emilk)
|
||||||
|
|
||||||
|
|
||||||
## 0.35.0 - 2026-06-25
|
## 0.35.0 - 2026-06-25
|
||||||
### ⭐ Added
|
### ⭐ Added
|
||||||
* Add Context::set_cursor_image for OS-level custom cursors [#8155](https://github.com/emilk/egui/pull/8155) by [@all3f0r1](https://github.com/all3f0r1)
|
* Add Context::set_cursor_image for OS-level custom cursors [#8155](https://github.com/emilk/egui/pull/8155) by [@all3f0r1](https://github.com/all3f0r1)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ workspace = true
|
|||||||
default = [
|
default = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"default_fonts",
|
"default_fonts",
|
||||||
|
"links",
|
||||||
"wayland", # Required for Linux support (including CI!)
|
"wayland", # Required for Linux support (including CI!)
|
||||||
"web_screen_reader",
|
"web_screen_reader",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
@@ -66,7 +67,7 @@ experimental = ["egui/experimental"]
|
|||||||
glow = ["dep:egui_glow", "dep:glow", "dep:glutin-winit", "dep:glutin"]
|
glow = ["dep:egui_glow", "dep:glow", "dep:glutin-winit", "dep:glutin"]
|
||||||
|
|
||||||
## Enable saving app state to disk.
|
## Enable saving app state to disk.
|
||||||
persistence = ["dep:home", "egui-winit/serde", "egui/persistence", "ron", "serde"]
|
persistence = ["egui-winit/serde", "egui/persistence", "ron", "serde"]
|
||||||
|
|
||||||
## Enables wayland support and fixes clipboard issue.
|
## Enables wayland support and fixes clipboard issue.
|
||||||
##
|
##
|
||||||
@@ -128,6 +129,9 @@ __screenshot = []
|
|||||||
## and capture screenshots. Off unless the env var is set; no-op on wasm.
|
## and capture screenshots. Off unless the env var is set; no-op on wasm.
|
||||||
inspection = ["dep:egui_inspection", "accesskit"]
|
inspection = ["dep:egui_inspection", "accesskit"]
|
||||||
|
|
||||||
|
## Enables the `links` feature on `egui-winit`, allowing for links to open in browser.
|
||||||
|
links = ["egui-winit/links"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
egui = { workspace = true, default-features = false, features = ["bytemuck"] }
|
egui = { workspace = true, default-features = false, features = ["bytemuck"] }
|
||||||
|
|
||||||
@@ -151,7 +155,7 @@ serde = { workspace = true, optional = true }
|
|||||||
# -------------------------------------------
|
# -------------------------------------------
|
||||||
# native:
|
# native:
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
egui-winit = { workspace = true, default-features = false, features = ["clipboard", "links"] }
|
egui-winit = { workspace = true, default-features = false, features = ["clipboard"] }
|
||||||
image = { workspace = true, features = ["png"] } # Needed for app icon
|
image = { workspace = true, features = ["png"] } # Needed for app icon
|
||||||
winit = { workspace = true, default-features = false, features = ["rwh_06"] }
|
winit = { workspace = true, default-features = false, features = ["rwh_06"] }
|
||||||
|
|
||||||
@@ -167,7 +171,6 @@ glutin-winit = { workspace = true, optional = true, default-features = false, fe
|
|||||||
"egl",
|
"egl",
|
||||||
"wgl",
|
"wgl",
|
||||||
] }
|
] }
|
||||||
home = { workspace = true, optional = true }
|
|
||||||
|
|
||||||
# mac:
|
# mac:
|
||||||
[target.'cfg(any(target_os = "macos"))'.dependencies]
|
[target.'cfg(any(target_os = "macos"))'.dependencies]
|
||||||
@@ -212,7 +215,6 @@ image = { workspace = true, features = ["png"] } # For copying images
|
|||||||
js-sys.workspace = true
|
js-sys.workspace = true
|
||||||
percent-encoding.workspace = true
|
percent-encoding.workspace = true
|
||||||
wasm-bindgen.workspace = true
|
wasm-bindgen.workspace = true
|
||||||
wasm-bindgen-futures.workspace = true
|
|
||||||
web-sys = { workspace = true, features = [
|
web-sys = { workspace = true, features = [
|
||||||
"AddEventListenerOptions",
|
"AddEventListenerOptions",
|
||||||
"BinaryType",
|
"BinaryType",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
#![warn(missing_docs)] // Let's keep `epi` well-documented.
|
#![warn(missing_docs)] // Let's keep `epi` well-documented.
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use std::any::Any;
|
use core::any::Any;
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||||
@@ -41,7 +41,7 @@ pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)
|
|||||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||||
pub type WindowBuilderHook = Box<dyn FnOnce(egui::ViewportBuilder) -> egui::ViewportBuilder>;
|
pub type WindowBuilderHook = Box<dyn FnOnce(egui::ViewportBuilder) -> egui::ViewportBuilder>;
|
||||||
|
|
||||||
type DynError = Box<dyn std::error::Error + Send + Sync>;
|
type DynError = Box<dyn core::error::Error + Send + Sync>;
|
||||||
|
|
||||||
/// This is how your app is created.
|
/// This is how your app is created.
|
||||||
///
|
///
|
||||||
@@ -73,7 +73,7 @@ pub struct CreationContext<'s> {
|
|||||||
/// The `get_proc_address` wrapper of underlying GL context
|
/// The `get_proc_address` wrapper of underlying GL context
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
pub get_proc_address:
|
pub get_proc_address:
|
||||||
Option<std::sync::Arc<dyn Fn(&std::ffi::CStr) -> *const std::ffi::c_void + Send + Sync>>,
|
Option<std::sync::Arc<dyn Fn(&core::ffi::CStr) -> *const core::ffi::c_void + Send + Sync>>,
|
||||||
|
|
||||||
/// The underlying WGPU render state.
|
/// The underlying WGPU render state.
|
||||||
///
|
///
|
||||||
@@ -155,6 +155,12 @@ pub trait App {
|
|||||||
///
|
///
|
||||||
/// You may NOT show any ui or do any painting during the call to [`Self::logic`].
|
/// You may NOT show any ui or do any painting during the call to [`Self::logic`].
|
||||||
///
|
///
|
||||||
|
/// While the window is hidden, `eframe` runs no egui pass at all (so that no ui state is
|
||||||
|
/// disturbed), and calls this via [`egui::Context::run_logic`] instead.
|
||||||
|
/// You can then still tell that the window is hidden with
|
||||||
|
/// [`egui::InputState::viewport`], but the rest of [`egui::Context::input`]
|
||||||
|
/// (events, time, …) is that of the last shown frame.
|
||||||
|
///
|
||||||
/// The [`egui::Context`] can be cloned and saved if you like.
|
/// The [`egui::Context`] can be cloned and saved if you like.
|
||||||
///
|
///
|
||||||
/// To force another call to [`Self::logic`], call [`egui::Context::request_repaint`] at any time (e.g. from another thread).
|
/// To force another call to [`Self::logic`], call [`egui::Context::request_repaint`] at any time (e.g. from another thread).
|
||||||
@@ -225,8 +231,8 @@ pub trait App {
|
|||||||
// Settings:
|
// Settings:
|
||||||
|
|
||||||
/// Time between automatic calls to [`Self::save`]
|
/// Time between automatic calls to [`Self::save`]
|
||||||
fn auto_save_interval(&self) -> std::time::Duration {
|
fn auto_save_interval(&self) -> core::time::Duration {
|
||||||
std::time::Duration::from_secs(30)
|
core::time::Duration::from_secs(30)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Background color values for the app, e.g. what is sent to `gl.clearColor`.
|
/// Background color values for the app, e.g. what is sent to `gl.clearColor`.
|
||||||
@@ -615,8 +621,8 @@ impl Default for Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||||
impl std::fmt::Display for Renderer {
|
impl core::fmt::Display for Renderer {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
Self::Glow => "glow".fmt(f),
|
Self::Glow => "glow".fmt(f),
|
||||||
@@ -628,7 +634,7 @@ impl std::fmt::Display for Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||||
impl std::str::FromStr for Renderer {
|
impl core::str::FromStr for Renderer {
|
||||||
type Err = String;
|
type Err = String;
|
||||||
|
|
||||||
fn from_str(name: &str) -> Result<Self, String> {
|
fn from_str(name: &str) -> Result<Self, String> {
|
||||||
|
|||||||
@@ -503,7 +503,7 @@ pub fn run_ui_native(
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// Something went wrong in user code when creating the app.
|
/// Something went wrong in user code when creating the app.
|
||||||
AppCreation(Box<dyn std::error::Error + Send + Sync>),
|
AppCreation(Box<dyn core::error::Error + Send + Sync>),
|
||||||
|
|
||||||
/// An error from [`winit`].
|
/// An error from [`winit`].
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
@@ -519,7 +519,7 @@ pub enum Error {
|
|||||||
|
|
||||||
/// An error from [`glutin`] when using [`glow`].
|
/// An error from [`glutin`] when using [`glow`].
|
||||||
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
|
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
|
||||||
NoGlutinConfigs(glutin::config::ConfigTemplate, Box<dyn std::error::Error>),
|
NoGlutinConfigs(glutin::config::ConfigTemplate, Box<dyn core::error::Error>),
|
||||||
|
|
||||||
/// An error from [`glutin`] when using [`glow`].
|
/// An error from [`glutin`] when using [`glow`].
|
||||||
#[cfg(feature = "glow")]
|
#[cfg(feature = "glow")]
|
||||||
@@ -530,7 +530,7 @@ pub enum Error {
|
|||||||
Wgpu(egui_wgpu::WgpuError),
|
Wgpu(egui_wgpu::WgpuError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for Error {}
|
impl core::error::Error for Error {}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
impl From<winit::error::OsError> for Error {
|
impl From<winit::error::OsError> for Error {
|
||||||
@@ -572,8 +572,8 @@ impl From<egui_wgpu::WgpuError> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Error {
|
impl core::fmt::Display for Error {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::AppCreation(err) => write!(f, "app creation error: {err}"),
|
Self::AppCreation(err) => write!(f, "app creation error: {err}"),
|
||||||
|
|
||||||
@@ -614,4 +614,4 @@ impl std::fmt::Display for Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Short for `Result<T, eframe::Error>`.
|
/// Short for `Result<T, eframe::Error>`.
|
||||||
pub type Result<T = (), E = Error> = std::result::Result<T, E>;
|
pub type Result<T = (), E = Error> = core::result::Result<T, E>;
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
|||||||
)
|
)
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
return std::ptr::null_mut();
|
return core::ptr::null_mut();
|
||||||
}
|
}
|
||||||
|
|
||||||
// SAFETY: Creating an HICON which should be readonly on our data.
|
// SAFETY: Creating an HICON which should be readonly on our data.
|
||||||
@@ -161,16 +161,16 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
|||||||
if icon_big.is_null() {
|
if icon_big.is_null() {
|
||||||
log::warn!("Failed to create HICON (for big icon) from embedded png data.");
|
log::warn!("Failed to create HICON (for big icon) from embedded png data.");
|
||||||
return AppIconStatus::NotSetIgnored; // We could try independently with the small icon but what's the point, it would look bad!
|
return AppIconStatus::NotSetIgnored; // We could try independently with the small icon but what's the point, it would look bad!
|
||||||
} else {
|
}
|
||||||
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
|
|
||||||
unsafe {
|
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
|
||||||
SendMessageW(
|
unsafe {
|
||||||
window_handle,
|
SendMessageW(
|
||||||
WM_SETICON,
|
window_handle,
|
||||||
ICON_BIG as usize,
|
WM_SETICON,
|
||||||
icon_big as isize,
|
ICON_BIG as usize,
|
||||||
);
|
icon_big as isize,
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -180,16 +180,16 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
|||||||
if icon_small.is_null() {
|
if icon_small.is_null() {
|
||||||
log::warn!("Failed to create HICON (for small icon) from embedded png data.");
|
log::warn!("Failed to create HICON (for small icon) from embedded png data.");
|
||||||
return AppIconStatus::NotSetIgnored;
|
return AppIconStatus::NotSetIgnored;
|
||||||
} else {
|
}
|
||||||
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
|
|
||||||
unsafe {
|
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
|
||||||
SendMessageW(
|
unsafe {
|
||||||
window_handle,
|
SendMessageW(
|
||||||
WM_SETICON,
|
window_handle,
|
||||||
ICON_SMALL as usize,
|
WM_SETICON,
|
||||||
icon_small as isize,
|
ICON_SMALL as usize,
|
||||||
);
|
icon_small as isize,
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ pub fn viewport_builder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match std::mem::take(&mut native_options.window_builder) {
|
match core::mem::take(&mut native_options.window_builder) {
|
||||||
Some(hook) => hook(viewport_builder),
|
Some(hook) => hook(viewport_builder),
|
||||||
None => viewport_builder,
|
None => viewport_builder,
|
||||||
}
|
}
|
||||||
@@ -156,6 +156,11 @@ pub struct EpiIntegration {
|
|||||||
pub beginning: Instant,
|
pub beginning: Instant,
|
||||||
is_first_frame: bool,
|
is_first_frame: bool,
|
||||||
pub egui_ctx: egui::Context,
|
pub egui_ctx: egui::Context,
|
||||||
|
|
||||||
|
/// Input that we have received, but not yet given to egui,
|
||||||
|
/// because we haven't run any pass since (see [`Self::update_logic_only`]).
|
||||||
|
pending_raw_input: egui::RawInput,
|
||||||
|
|
||||||
pending_full_output: egui::FullOutput,
|
pending_full_output: egui::FullOutput,
|
||||||
|
|
||||||
/// When set, it is time to close the native window.
|
/// When set, it is time to close the native window.
|
||||||
@@ -215,6 +220,7 @@ impl EpiIntegration {
|
|||||||
Self {
|
Self {
|
||||||
frame,
|
frame,
|
||||||
last_auto_save: Instant::now(),
|
last_auto_save: Instant::now(),
|
||||||
|
pending_raw_input: Default::default(),
|
||||||
pending_full_output: Default::default(),
|
pending_full_output: Default::default(),
|
||||||
close: false,
|
close: false,
|
||||||
can_drag_window: false,
|
can_drag_window: false,
|
||||||
@@ -262,57 +268,109 @@ impl EpiIntegration {
|
|||||||
|
|
||||||
/// Run user code - this can create immediate viewports, so hold no locks over this!
|
/// Run user code - this can create immediate viewports, so hold no locks over this!
|
||||||
///
|
///
|
||||||
/// If `viewport_ui_cb` is None, we are in the root viewport and will call [`crate::App::ui`].
|
/// If `viewport_ui_cb` is None, we are in the root viewport and will call
|
||||||
|
/// [`crate::App::logic`] and [`crate::App::ui`].
|
||||||
|
///
|
||||||
|
/// Only call this when the ui will actually be shown;
|
||||||
|
/// use [`Self::update_logic_only`] otherwise.
|
||||||
pub fn update(
|
pub fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
app: &mut dyn epi::App,
|
app: &mut dyn epi::App,
|
||||||
viewport_ui_cb: Option<&DeferredViewportUiCallback>,
|
viewport_ui_cb: Option<&DeferredViewportUiCallback>,
|
||||||
mut raw_input: egui::RawInput,
|
raw_input: egui::RawInput,
|
||||||
is_visible: bool,
|
|
||||||
) -> egui::FullOutput {
|
) -> egui::FullOutput {
|
||||||
raw_input.time = Some(self.beginning.elapsed().as_secs_f64());
|
let raw_input = self.prepare_raw_input(app, raw_input);
|
||||||
|
|
||||||
let close_requested = raw_input.viewport().close_requested();
|
let close_requested = raw_input.viewport().close_requested();
|
||||||
|
|
||||||
app.raw_input_hook(&self.egui_ctx, &mut raw_input);
|
let is_root_viewport = viewport_ui_cb.is_none();
|
||||||
|
|
||||||
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
|
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
|
||||||
if let Some(viewport_ui_cb) = viewport_ui_cb {
|
if let Some(viewport_ui_cb) = viewport_ui_cb {
|
||||||
// Child viewport
|
// Child viewport
|
||||||
if is_visible {
|
profiling::scope!("viewport_callback");
|
||||||
profiling::scope!("viewport_callback");
|
viewport_ui_cb(ui);
|
||||||
viewport_ui_cb(ui);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
{
|
{
|
||||||
profiling::scope!("App::logic");
|
profiling::scope!("App::logic");
|
||||||
app.logic(ui.ctx(), &mut self.frame);
|
app.logic(ui.ctx(), &mut self.frame);
|
||||||
}
|
}
|
||||||
|
{
|
||||||
if is_visible {
|
profiling::scope!("App::ui");
|
||||||
{
|
app.ui(ui, &mut self.frame);
|
||||||
profiling::scope!("App::ui");
|
|
||||||
app.ui(ui, &mut self.frame);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let is_root_viewport = viewport_ui_cb.is_none();
|
|
||||||
if is_root_viewport && close_requested {
|
if is_root_viewport && close_requested {
|
||||||
let canceled = full_output.viewport_output[&ViewportId::ROOT]
|
let canceled = full_output.viewport_output[&ViewportId::ROOT]
|
||||||
.commands
|
.commands
|
||||||
.contains(&egui::ViewportCommand::CancelClose);
|
.contains(&egui::ViewportCommand::CancelClose);
|
||||||
if canceled {
|
self.handle_close_request(canceled);
|
||||||
log::debug!("Closing of root viewport canceled with ViewportCommand::CancelClose");
|
|
||||||
} else {
|
|
||||||
log::debug!("Closing root viewport (ViewportCommand::CancelClose was not sent)");
|
|
||||||
self.close = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.pending_full_output.append(full_output);
|
self.pending_full_output.append(full_output);
|
||||||
std::mem::take(&mut self.pending_full_output)
|
core::mem::take(&mut self.pending_full_output)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Let the app tick its logic without showing any ui,
|
||||||
|
/// because the window is minimized or occluded.
|
||||||
|
///
|
||||||
|
/// No egui pass is run, so all ui state is left untouched:
|
||||||
|
/// the app will find everything where it left it once the window is visible again.
|
||||||
|
///
|
||||||
|
/// Only call this for the root viewport: only it has [`crate::App::logic`].
|
||||||
|
pub fn update_logic_only(
|
||||||
|
&mut self,
|
||||||
|
app: &mut dyn epi::App,
|
||||||
|
raw_input: egui::RawInput,
|
||||||
|
) -> egui::LogicOutput {
|
||||||
|
let raw_input = self.prepare_raw_input(app, raw_input);
|
||||||
|
|
||||||
|
let close_requested = raw_input.viewport().close_requested();
|
||||||
|
|
||||||
|
let logic_output = self.egui_ctx.run_logic(&raw_input, |ctx| {
|
||||||
|
profiling::scope!("App::logic");
|
||||||
|
app.logic(ctx, &mut self.frame);
|
||||||
|
});
|
||||||
|
|
||||||
|
// No pass consumed the input, so save it for the next one:
|
||||||
|
self.pending_raw_input = raw_input;
|
||||||
|
|
||||||
|
if close_requested {
|
||||||
|
let canceled = logic_output
|
||||||
|
.viewport_commands
|
||||||
|
.get(&ViewportId::ROOT)
|
||||||
|
.is_some_and(|commands| commands.contains(&egui::ViewportCommand::CancelClose));
|
||||||
|
self.handle_close_request(canceled);
|
||||||
|
}
|
||||||
|
|
||||||
|
logic_output
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepend any input we couldn't give to egui earlier, set the time, and run the app hook.
|
||||||
|
fn prepare_raw_input(
|
||||||
|
&mut self,
|
||||||
|
app: &mut dyn epi::App,
|
||||||
|
new_input: egui::RawInput,
|
||||||
|
) -> egui::RawInput {
|
||||||
|
let mut raw_input = core::mem::take(&mut self.pending_raw_input);
|
||||||
|
raw_input.append(new_input); // The new input wins where they overlap
|
||||||
|
|
||||||
|
raw_input.time = Some(self.beginning.elapsed().as_secs_f64());
|
||||||
|
|
||||||
|
app.raw_input_hook(&self.egui_ctx, &mut raw_input);
|
||||||
|
|
||||||
|
raw_input
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_close_request(&mut self, canceled: bool) {
|
||||||
|
if canceled {
|
||||||
|
log::debug!("Closing of root viewport canceled with ViewportCommand::CancelClose");
|
||||||
|
} else {
|
||||||
|
log::debug!("Closing root viewport (ViewportCommand::CancelClose was not sent)");
|
||||||
|
self.close = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn report_frame_time(&mut self, seconds: f32) {
|
pub fn report_frame_time(&mut self, seconds: f32) {
|
||||||
@@ -321,7 +379,7 @@ impl EpiIntegration {
|
|||||||
|
|
||||||
pub fn post_rendering(&mut self, window: &winit::window::Window) {
|
pub fn post_rendering(&mut self, window: &winit::window::Window) {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
if std::mem::take(&mut self.is_first_frame) {
|
if core::mem::take(&mut self.is_first_frame) {
|
||||||
// We keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
|
// We keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
|
||||||
window.set_visible(true);
|
window.set_visible(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::cell::Cell;
|
use core::cell::Cell;
|
||||||
use winit::event_loop::ActiveEventLoop;
|
use winit::event_loop::ActiveEventLoop;
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
@@ -14,7 +14,7 @@ impl EventLoopGuard {
|
|||||||
cell.get().is_none(),
|
cell.get().is_none(),
|
||||||
"Attempted to set a new event loop while one is already set"
|
"Attempted to set a new event loop while one is already set"
|
||||||
);
|
);
|
||||||
cell.set(Some(std::ptr::from_ref::<ActiveEventLoop>(event_loop)));
|
cell.set(Some(core::ptr::from_ref::<ActiveEventLoop>(event_loop)));
|
||||||
});
|
});
|
||||||
Self
|
Self
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub fn storage_dir(app_id: &str) -> Option<PathBuf> {
|
|||||||
OS::Nix => var_os("XDG_DATA_HOME")
|
OS::Nix => var_os("XDG_DATA_HOME")
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.filter(|p| p.is_absolute())
|
.filter(|p| p.is_absolute())
|
||||||
.or_else(|| home::home_dir().map(|p| p.join(".local").join("share")))
|
.or_else(|| std::env::home_dir().map(|p| p.join(".local").join("share")))
|
||||||
.map(|p| {
|
.map(|p| {
|
||||||
p.join(
|
p.join(
|
||||||
app_id
|
app_id
|
||||||
@@ -29,7 +29,7 @@ pub fn storage_dir(app_id: &str) -> Option<PathBuf> {
|
|||||||
.replace(|c: char| c.is_ascii_whitespace(), ""),
|
.replace(|c: char| c.is_ascii_whitespace(), ""),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
OS::Mac => home::home_dir().map(|p| {
|
OS::Mac => std::env::home_dir().map(|p| {
|
||||||
p.join("Library")
|
p.join("Library")
|
||||||
.join("Application Support")
|
.join("Application Support")
|
||||||
.join(app_id.replace(|c: char| c.is_ascii_whitespace(), "-"))
|
.join(app_id.replace(|c: char| c.is_ascii_whitespace(), "-"))
|
||||||
@@ -44,10 +44,10 @@ pub fn storage_dir(app_id: &str) -> Option<PathBuf> {
|
|||||||
#[cfg(all(windows, not(target_vendor = "uwp")))]
|
#[cfg(all(windows, not(target_vendor = "uwp")))]
|
||||||
#[expect(unsafe_code)]
|
#[expect(unsafe_code)]
|
||||||
fn roaming_appdata() -> Option<PathBuf> {
|
fn roaming_appdata() -> Option<PathBuf> {
|
||||||
|
use core::ptr;
|
||||||
|
use core::slice;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::os::windows::ffi::OsStringExt as _;
|
use std::os::windows::ffi::OsStringExt as _;
|
||||||
use std::ptr;
|
|
||||||
use std::slice;
|
|
||||||
|
|
||||||
use windows_sys::Win32::Foundation::S_OK;
|
use windows_sys::Win32::Foundation::S_OK;
|
||||||
use windows_sys::Win32::System::Com::CoTaskMemFree;
|
use windows_sys::Win32::System::Com::CoTaskMemFree;
|
||||||
@@ -66,8 +66,8 @@ fn roaming_appdata() -> Option<PathBuf> {
|
|||||||
SHGetKnownFolderPath(
|
SHGetKnownFolderPath(
|
||||||
&FOLDERID_RoamingAppData,
|
&FOLDERID_RoamingAppData,
|
||||||
KF_FLAG_DONT_VERIFY as u32,
|
KF_FLAG_DONT_VERIFY as u32,
|
||||||
std::ptr::null_mut(),
|
core::ptr::null_mut(),
|
||||||
&mut path_raw,
|
&raw mut path_raw,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
#![expect(clippy::undocumented_unsafe_blocks)]
|
#![expect(clippy::undocumented_unsafe_blocks)]
|
||||||
#![expect(clippy::unwrap_used)]
|
#![expect(clippy::unwrap_used)]
|
||||||
|
|
||||||
use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant};
|
use core::{cell::RefCell, num::NonZeroU32};
|
||||||
|
use std::{rc::Rc, sync::Arc, time::Instant};
|
||||||
|
|
||||||
use egui_winit::ActionRequested;
|
use egui_winit::ActionRequested;
|
||||||
use glutin::{
|
use glutin::{
|
||||||
@@ -31,16 +32,17 @@ use egui::{
|
|||||||
};
|
};
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
use egui_winit::accesskit_winit;
|
use egui_winit::accesskit_winit;
|
||||||
|
use log::warn;
|
||||||
use crate::{
|
|
||||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
|
||||||
native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
epi_integration, event_loop_context,
|
epi_integration, event_loop_context,
|
||||||
winit_integration::{EventResult, UserEvent, WinitApp, create_egui_context},
|
winit_integration::{EventResult, UserEvent, WinitApp, create_egui_context},
|
||||||
};
|
};
|
||||||
|
use crate::epaint::textures::TexturesDelta;
|
||||||
|
use crate::{
|
||||||
|
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
||||||
|
native::{epi_integration::EpiIntegration, winit_integration::sleep_if_invisible_or_minimized},
|
||||||
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Types:
|
// Types:
|
||||||
@@ -73,6 +75,16 @@ struct GlowWinitRunning<'app> {
|
|||||||
|
|
||||||
// NOTE: one painter shared by all viewports.
|
// NOTE: one painter shared by all viewports.
|
||||||
painter: Rc<RefCell<egui_glow::Painter>>,
|
painter: Rc<RefCell<egui_glow::Painter>>,
|
||||||
|
|
||||||
|
/// Any not yet applied deltas for this app.
|
||||||
|
pending_deltas: TexturesDelta,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for GlowWinitRunning<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Avoid debug panic when dropping unapplied deltas on teardown
|
||||||
|
self.pending_deltas.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This struct will contain both persistent and temporary glutin state.
|
/// This struct will contain both persistent and temporary glutin state.
|
||||||
@@ -114,6 +126,9 @@ struct Viewport {
|
|||||||
info: ViewportInfo,
|
info: ViewportInfo,
|
||||||
actions_requested: Vec<egui_winit::ActionRequested>,
|
actions_requested: Vec<egui_winit::ActionRequested>,
|
||||||
|
|
||||||
|
/// Any not yet applied deltas for this viewport.
|
||||||
|
pending_delta: TexturesDelta,
|
||||||
|
|
||||||
/// The user-callback that shows the ui.
|
/// The user-callback that shows the ui.
|
||||||
/// None for immediate viewports.
|
/// None for immediate viewports.
|
||||||
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
|
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
|
||||||
@@ -125,6 +140,34 @@ struct Viewport {
|
|||||||
egui_winit: Option<egui_winit::State>,
|
egui_winit: Option<egui_winit::State>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Viewport {
|
||||||
|
/// Apply the commands, or defer them until we have a window.
|
||||||
|
fn process_commands(
|
||||||
|
&mut self,
|
||||||
|
egui_ctx: &egui::Context,
|
||||||
|
mut commands: Vec<egui::ViewportCommand>,
|
||||||
|
) {
|
||||||
|
self.deferred_commands.append(&mut commands);
|
||||||
|
|
||||||
|
if let Some(window) = &self.window {
|
||||||
|
egui_winit::process_viewport_commands(
|
||||||
|
egui_ctx,
|
||||||
|
&mut self.info,
|
||||||
|
core::mem::take(&mut self.deferred_commands),
|
||||||
|
window,
|
||||||
|
&mut self.actions_requested,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Viewport {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Avoid debug panic when dropping unapplied deltas on teardown
|
||||||
|
self.pending_delta.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
impl<'app> GlowWinitApp<'app> {
|
impl<'app> GlowWinitApp<'app> {
|
||||||
@@ -296,7 +339,7 @@ impl<'app> GlowWinitApp<'app> {
|
|||||||
log::warn!("set_cursor_hittest(false) failed: {err}");
|
log::warn!("set_cursor_hittest(false) failed: {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let app_creator = std::mem::take(&mut self.app_creator)
|
let app_creator = core::mem::take(&mut self.app_creator)
|
||||||
.expect("Single-use AppCreator has unexpectedly already been taken");
|
.expect("Single-use AppCreator has unexpectedly already been taken");
|
||||||
|
|
||||||
crate::maybe_attach_inspection_plugin(&integration.egui_ctx, Some(self.app_name.clone()));
|
crate::maybe_attach_inspection_plugin(&integration.egui_ctx, Some(self.app_name.clone()));
|
||||||
@@ -353,6 +396,7 @@ impl<'app> GlowWinitApp<'app> {
|
|||||||
app,
|
app,
|
||||||
glutin,
|
glutin,
|
||||||
painter,
|
painter,
|
||||||
|
pending_deltas: Default::default(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -557,7 +601,7 @@ impl GlowWinitRunning<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (raw_input, viewport_ui_cb, is_visible, run_ui) = {
|
let (raw_input, viewport_ui_cb, is_visible, show_ui) = {
|
||||||
let mut glutin = self.glutin.borrow_mut();
|
let mut glutin = self.glutin.borrow_mut();
|
||||||
let egui_ctx = glutin.egui_ctx.clone();
|
let egui_ctx = glutin.egui_ctx.clone();
|
||||||
let Some(viewport) = glutin.viewports.get_mut(&viewport_id) else {
|
let Some(viewport) = glutin.viewports.get_mut(&viewport_id) else {
|
||||||
@@ -576,7 +620,7 @@ impl GlowWinitRunning<'_> {
|
|||||||
let mut raw_input = egui_winit.take_egui_input(window);
|
let mut raw_input = egui_winit.take_egui_input(window);
|
||||||
let viewport_ui_cb = viewport.viewport_ui_cb.clone();
|
let viewport_ui_cb = viewport.viewport_ui_cb.clone();
|
||||||
|
|
||||||
let run_ui =
|
let show_ui =
|
||||||
is_visible || is_viewport_or_descendant_visible(&glutin.viewports, viewport_id);
|
is_visible || is_viewport_or_descendant_visible(&glutin.viewports, viewport_id);
|
||||||
|
|
||||||
self.integration.pre_update();
|
self.integration.pre_update();
|
||||||
@@ -588,9 +632,58 @@ impl GlowWinitRunning<'_> {
|
|||||||
.map(|(id, viewport)| (*id, viewport.info.clone()))
|
.map(|(id, viewport)| (*id, viewport.info.clone()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
(raw_input, viewport_ui_cb, is_visible, run_ui)
|
(raw_input, viewport_ui_cb, is_visible, show_ui)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !show_ui {
|
||||||
|
// Nothing will be shown, so we run no egui pass at all.
|
||||||
|
// That way all ui state is left untouched, and is still there
|
||||||
|
// when this viewport becomes visible again.
|
||||||
|
let is_root_viewport = viewport_ui_cb.is_none();
|
||||||
|
if is_root_viewport {
|
||||||
|
// The app logic keeps ticking, so it can e.g. ask to be shown again:
|
||||||
|
let egui::LogicOutput {
|
||||||
|
platform_output,
|
||||||
|
viewport_commands,
|
||||||
|
} = self
|
||||||
|
.integration
|
||||||
|
.update_logic_only(self.app.as_mut(), raw_input);
|
||||||
|
|
||||||
|
let mut glutin = self.glutin.borrow_mut();
|
||||||
|
if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) {
|
||||||
|
viewport.info.events.clear(); // they should have been processed
|
||||||
|
if let Some(window) = viewport.window.clone()
|
||||||
|
&& let Some(egui_winit) = viewport.egui_winit.as_mut()
|
||||||
|
{
|
||||||
|
egui_winit.handle_platform_output_with_event_loop(
|
||||||
|
&window,
|
||||||
|
event_loop,
|
||||||
|
platform_output,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (id, commands) in viewport_commands {
|
||||||
|
if let Some(viewport) = glutin.viewports.get_mut(&id) {
|
||||||
|
viewport.process_commands(&self.integration.egui_ctx, commands);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep_if_invisible_or_minimized(
|
||||||
|
self.glutin
|
||||||
|
.borrow()
|
||||||
|
.viewports
|
||||||
|
.get(&viewport_id)
|
||||||
|
.and_then(|viewport| viewport.window.as_deref()),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(if self.integration.should_close() {
|
||||||
|
EventResult::CloseRequested
|
||||||
|
} else {
|
||||||
|
EventResult::Wait
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// HACK: In order to get the right clear_color, the system theme needs to be set, which
|
// HACK: In order to get the right clear_color, the system theme needs to be set, which
|
||||||
// usually only happens in the `update` call. So we call Options::begin_pass early
|
// usually only happens in the `update` call. So we call Options::begin_pass early
|
||||||
// to set the right theme. Without this there would be a black flash on the first frame.
|
// to set the right theme. Without this there would be a black flash on the first frame.
|
||||||
@@ -639,12 +732,9 @@ impl GlowWinitRunning<'_> {
|
|||||||
// The update function, which could call immediate viewports,
|
// The update function, which could call immediate viewports,
|
||||||
// so make sure we don't hold any locks here required by the immediate viewports rendeer.
|
// so make sure we don't hold any locks here required by the immediate viewports rendeer.
|
||||||
|
|
||||||
let full_output = self.integration.update(
|
let full_output =
|
||||||
self.app.as_mut(),
|
self.integration
|
||||||
viewport_ui_cb.as_deref(),
|
.update(self.app.as_mut(), viewport_ui_cb.as_deref(), raw_input);
|
||||||
raw_input,
|
|
||||||
run_ui,
|
|
||||||
);
|
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
@@ -653,6 +743,7 @@ impl GlowWinitRunning<'_> {
|
|||||||
app,
|
app,
|
||||||
glutin,
|
glutin,
|
||||||
painter,
|
painter,
|
||||||
|
pending_deltas,
|
||||||
..
|
..
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
@@ -666,6 +757,7 @@ impl GlowWinitRunning<'_> {
|
|||||||
pixels_per_point,
|
pixels_per_point,
|
||||||
viewport_output,
|
viewport_output,
|
||||||
} = full_output;
|
} = full_output;
|
||||||
|
pending_deltas.append(textures_delta);
|
||||||
|
|
||||||
glutin.remove_viewports_not_in(&viewport_output);
|
glutin.remove_viewports_not_in(&viewport_output);
|
||||||
|
|
||||||
@@ -687,30 +779,28 @@ impl GlowWinitRunning<'_> {
|
|||||||
|
|
||||||
egui_winit.handle_platform_output_with_event_loop(&window, event_loop, platform_output);
|
egui_winit.handle_platform_output_with_event_loop(&window, event_loop, platform_output);
|
||||||
|
|
||||||
// Upload textures even when not visible: the atlas dirty region is already
|
|
||||||
// consumed, so dropping the delta would desync the font texture.
|
|
||||||
let has_texture_updates = !textures_delta.set.is_empty() || !textures_delta.free.is_empty();
|
|
||||||
if is_visible || has_texture_updates {
|
|
||||||
// We may need to switch contexts again, because of immediate viewports:
|
|
||||||
frame_timer.pause();
|
|
||||||
change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
|
|
||||||
frame_timer.resume();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (id, image_delta) in &textures_delta.set {
|
|
||||||
painter.set_texture(*id, image_delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_visible {
|
if is_visible {
|
||||||
let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point);
|
let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point);
|
||||||
|
|
||||||
|
{
|
||||||
|
// We may need to switch contexts again, because of immediate viewports:
|
||||||
|
frame_timer.pause();
|
||||||
|
change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
|
||||||
|
frame_timer.resume();
|
||||||
|
}
|
||||||
|
|
||||||
let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
|
let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
|
||||||
|
|
||||||
if !clear_before_update {
|
if !clear_before_update {
|
||||||
painter.clear(screen_size_in_pixels, clear_color);
|
painter.clear(screen_size_in_pixels, clear_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
painter.paint_primitives(screen_size_in_pixels, pixels_per_point, &clipped_primitives);
|
painter.paint_and_update_textures(
|
||||||
|
screen_size_in_pixels,
|
||||||
|
pixels_per_point,
|
||||||
|
&clipped_primitives,
|
||||||
|
pending_deltas,
|
||||||
|
);
|
||||||
|
|
||||||
{
|
{
|
||||||
for action in viewport.actions_requested.drain(..) {
|
for action in viewport.actions_requested.drain(..) {
|
||||||
@@ -772,25 +862,13 @@ impl GlowWinitRunning<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Free textures *after* painting, since they may still be used in the frame we just drew.
|
|
||||||
for id in &textures_delta.free {
|
|
||||||
painter.free_texture(*id);
|
|
||||||
}
|
|
||||||
|
|
||||||
glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output);
|
glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output);
|
||||||
|
|
||||||
integration.report_frame_time(frame_timer.total_time_sec()); // don't count auto-save time as part of regular frame time
|
integration.report_frame_time(frame_timer.total_time_sec()); // don't count auto-save time as part of regular frame time
|
||||||
|
|
||||||
integration.maybe_autosave(app.as_mut(), Some(&window));
|
integration.maybe_autosave(app.as_mut(), Some(&window));
|
||||||
|
|
||||||
if is_invisible_or_minimized(&window) {
|
sleep_if_invisible_or_minimized(Some(&window));
|
||||||
// On Mac, a minimized Window uses up all CPU:
|
|
||||||
// https://github.com/emilk/egui/issues/325
|
|
||||||
// On Windows, an invisible window also uses up all CPU:
|
|
||||||
// https://github.com/emilk/egui/issues/7776
|
|
||||||
profiling::scope!("minimized_sleep");
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
||||||
}
|
|
||||||
|
|
||||||
if integration.should_close() {
|
if integration.should_close() {
|
||||||
Ok(EventResult::CloseRequested)
|
Ok(EventResult::CloseRequested)
|
||||||
@@ -1120,6 +1198,7 @@ impl GlutinWindowContext {
|
|||||||
deferred_commands: vec![],
|
deferred_commands: vec![],
|
||||||
info: viewport_info,
|
info: viewport_info,
|
||||||
actions_requested: Default::default(),
|
actions_requested: Default::default(),
|
||||||
|
pending_delta: Default::default(),
|
||||||
viewport_ui_cb: None,
|
viewport_ui_cb: None,
|
||||||
gl_surface: None,
|
gl_surface: None,
|
||||||
window: window.map(Arc::new),
|
window: window.map(Arc::new),
|
||||||
@@ -1330,7 +1409,7 @@ impl GlutinWindowContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_proc_address(&self, addr: &std::ffi::CStr) -> *const std::ffi::c_void {
|
fn get_proc_address(&self, addr: &core::ffi::CStr) -> *const core::ffi::c_void {
|
||||||
self.gl_config.display().get_proc_address(addr)
|
self.gl_config.display().get_proc_address(addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1362,7 +1441,7 @@ impl GlutinWindowContext {
|
|||||||
class,
|
class,
|
||||||
builder,
|
builder,
|
||||||
viewport_ui_cb,
|
viewport_ui_cb,
|
||||||
mut commands,
|
commands,
|
||||||
repaint_delay: _, // ignored - we listened to the repaint callback instead
|
repaint_delay: _, // ignored - we listened to the repaint callback instead
|
||||||
},
|
},
|
||||||
) in viewport_output.clone()
|
) in viewport_output.clone()
|
||||||
@@ -1377,25 +1456,18 @@ impl GlutinWindowContext {
|
|||||||
viewport_ui_cb,
|
viewport_ui_cb,
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(window) = &viewport.window {
|
let old_inner_size = viewport.window.as_ref().map(|window| window.inner_size());
|
||||||
let old_inner_size = window.inner_size();
|
|
||||||
|
|
||||||
viewport.deferred_commands.append(&mut commands);
|
viewport.process_commands(egui_ctx, commands);
|
||||||
|
|
||||||
egui_winit::process_viewport_commands(
|
// For Wayland : https://github.com/emilk/egui/issues/4196
|
||||||
egui_ctx,
|
if cfg!(target_os = "linux")
|
||||||
&mut viewport.info,
|
&& let Some(window) = &viewport.window
|
||||||
std::mem::take(&mut viewport.deferred_commands),
|
&& let Some(old_inner_size) = old_inner_size
|
||||||
window,
|
{
|
||||||
&mut viewport.actions_requested,
|
let new_inner_size = window.inner_size();
|
||||||
);
|
if new_inner_size != old_inner_size {
|
||||||
|
self.resize(viewport_id, new_inner_size);
|
||||||
// For Wayland : https://github.com/emilk/egui/issues/4196
|
|
||||||
if cfg!(target_os = "linux") {
|
|
||||||
let new_inner_size = window.inner_size();
|
|
||||||
if new_inner_size != old_inner_size {
|
|
||||||
self.resize(viewport_id, new_inner_size);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1436,6 +1508,7 @@ fn initialize_or_update_viewport(
|
|||||||
deferred_commands: vec![],
|
deferred_commands: vec![],
|
||||||
info: Default::default(),
|
info: Default::default(),
|
||||||
actions_requested: Default::default(),
|
actions_requested: Default::default(),
|
||||||
|
pending_delta: Default::default(),
|
||||||
viewport_ui_cb,
|
viewport_ui_cb,
|
||||||
window: None,
|
window: None,
|
||||||
egui_winit: None,
|
egui_winit: None,
|
||||||
@@ -1584,8 +1657,10 @@ fn render_immediate_viewport(
|
|||||||
} = &mut *glutin;
|
} = &mut *glutin;
|
||||||
|
|
||||||
let Some(viewport) = viewports.get_mut(&viewport_id) else {
|
let Some(viewport) = viewports.get_mut(&viewport_id) else {
|
||||||
|
warn!("Viewport disappeared unexpectedly!");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
viewport.pending_delta.append(textures_delta);
|
||||||
|
|
||||||
viewport.info.events.clear(); // they should have been processed
|
viewport.info.events.clear(); // they should have been processed
|
||||||
|
|
||||||
@@ -1621,7 +1696,7 @@ fn render_immediate_viewport(
|
|||||||
screen_size_in_pixels,
|
screen_size_in_pixels,
|
||||||
pixels_per_point,
|
pixels_per_point,
|
||||||
&clipped_primitives,
|
&clipped_primitives,
|
||||||
&textures_delta,
|
&mut viewport.pending_delta,
|
||||||
);
|
);
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -1645,7 +1720,7 @@ fn save_screenshot_and_exit(
|
|||||||
screen_size_in_pixels: [u32; 2],
|
screen_size_in_pixels: [u32; 2],
|
||||||
) {
|
) {
|
||||||
assert!(
|
assert!(
|
||||||
path.ends_with(".png"),
|
egui::load::has_extension(path, "png"),
|
||||||
"Expected EFRAME_SCREENSHOT_TO to end with '.png', got {path:?}"
|
"Expected EFRAME_SCREENSHOT_TO to end with '.png', got {path:?}"
|
||||||
);
|
);
|
||||||
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
|
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::time::{Duration, Instant};
|
use core::time::Duration;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
use winit::{
|
use winit::{
|
||||||
application::ApplicationHandler,
|
application::ApplicationHandler,
|
||||||
@@ -41,7 +42,7 @@ fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result<EventLoo
|
|||||||
))
|
))
|
||||||
})?);
|
})?);
|
||||||
|
|
||||||
if let Some(hook) = std::mem::take(&mut native_options.event_loop_builder) {
|
if let Some(hook) = core::mem::take(&mut native_options.event_loop_builder) {
|
||||||
hook(&mut builder);
|
hook(&mut builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ fn with_event_loop<R>(
|
|||||||
mut native_options: epi::NativeOptions,
|
mut native_options: epi::NativeOptions,
|
||||||
f: impl FnOnce(&mut EventLoop<UserEvent>, epi::NativeOptions) -> R,
|
f: impl FnOnce(&mut EventLoop<UserEvent>, epi::NativeOptions) -> R,
|
||||||
) -> Result<R> {
|
) -> Result<R> {
|
||||||
thread_local!(static EVENT_LOOP: std::cell::RefCell<Option<EventLoop<UserEvent>>> = const { std::cell::RefCell::new(None) });
|
thread_local!(static EVENT_LOOP: core::cell::RefCell<Option<EventLoop<UserEvent>>> = const { core::cell::RefCell::new(None) });
|
||||||
|
|
||||||
EVENT_LOOP.with(|event_loop| {
|
EVENT_LOOP.with(|event_loop| {
|
||||||
// Since we want to reference NativeOptions when creating the EventLoop we can't
|
// Since we want to reference NativeOptions when creating the EventLoop we can't
|
||||||
@@ -206,7 +207,12 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
|||||||
invisible_window_ids.push(*window_id);
|
invisible_window_ids.push(*window_id);
|
||||||
} else {
|
} else {
|
||||||
log::trace!("request_redraw for {window_id:?}");
|
log::trace!("request_redraw for {window_id:?}");
|
||||||
event_loop.set_control_flow(ControlFlow::Poll);
|
// Don't switch to `ControlFlow::Poll` here. `request_redraw`
|
||||||
|
// is enough to wake the event loop, and on Wayland the
|
||||||
|
// `RedrawRequested` event is only delivered once the
|
||||||
|
// compositor sends a frame callback. Polling in the meantime
|
||||||
|
// busy-loops a whole CPU core.
|
||||||
|
// See https://github.com/emilk/egui/issues/8326.
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -236,10 +242,16 @@ impl<T: WinitApp> WinitAppWrapper<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always set an explicit, sleeping control flow. Previously we only set
|
||||||
|
// `WaitUntil` when a repaint was already scheduled, which meant that a
|
||||||
|
// `ControlFlow::Poll` set earlier was never undone once the last timed
|
||||||
|
// repaint had been consumed, leaving the loop spinning.
|
||||||
|
// See https://github.com/emilk/egui/issues/8326.
|
||||||
let next_repaint_time = self.windows_next_repaint_times.values().min().copied();
|
let next_repaint_time = self.windows_next_repaint_times.values().min().copied();
|
||||||
if let Some(next_repaint_time) = next_repaint_time {
|
event_loop.set_control_flow(match next_repaint_time {
|
||||||
event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time));
|
Some(next_repaint_time) => ControlFlow::WaitUntil(next_repaint_time),
|
||||||
}
|
None => ControlFlow::Wait,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,7 +562,7 @@ impl<'a> EframeWinitApplication<'a> {
|
|||||||
pub fn pump_eframe_app(
|
pub fn pump_eframe_app(
|
||||||
&mut self,
|
&mut self,
|
||||||
event_loop: &mut EventLoop<UserEvent>,
|
event_loop: &mut EventLoop<UserEvent>,
|
||||||
timeout: Option<std::time::Duration>,
|
timeout: Option<core::time::Duration>,
|
||||||
) -> EframePumpStatus {
|
) -> EframePumpStatus {
|
||||||
use winit::platform::pump_events::{EventLoopExtPumpEvents as _, PumpStatus};
|
use winit::platform::pump_events::{EventLoopExtPumpEvents as _, PumpStatus};
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
//! There is a bunch of improvements we could do,
|
//! There is a bunch of improvements we could do,
|
||||||
//! like removing a bunch of `unwraps`.
|
//! like removing a bunch of `unwraps`.
|
||||||
|
|
||||||
use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant};
|
use core::{cell::RefCell, num::NonZeroU32};
|
||||||
|
use std::{rc::Rc, sync::Arc, time::Instant};
|
||||||
|
|
||||||
use egui_winit::ActionRequested;
|
use egui_winit::ActionRequested;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
@@ -17,19 +18,20 @@ use winit::{
|
|||||||
|
|
||||||
use ahash::HashMap;
|
use ahash::HashMap;
|
||||||
use egui::{
|
use egui::{
|
||||||
DeferredViewportUiCallback, FullOutput, ImmediateViewport, OrderedViewportIdMap,
|
DeferredViewportUiCallback, FullOutput, ImmediateViewport, OrderedViewportIdMap, TexturesDelta,
|
||||||
ViewportBuilder, ViewportClass, ViewportId, ViewportIdPair, ViewportIdSet, ViewportInfo,
|
ViewportBuilder, ViewportClass, ViewportId, ViewportIdPair, ViewportIdSet, ViewportInfo,
|
||||||
ViewportOutput,
|
ViewportOutput,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
use egui_winit::accesskit_winit;
|
use egui_winit::accesskit_winit;
|
||||||
|
use log::warn;
|
||||||
use winit_integration::UserEvent;
|
use winit_integration::UserEvent;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
||||||
native::{
|
native::{
|
||||||
epi_integration::EpiIntegration,
|
epi_integration::EpiIntegration,
|
||||||
winit_integration::{EventResult, is_invisible_or_minimized},
|
winit_integration::{EventResult, sleep_if_invisible_or_minimized},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,6 +67,15 @@ struct WgpuWinitRunning<'app> {
|
|||||||
|
|
||||||
/// Wrapped in an `Rc<RefCell<…>>` so it can be re-entrantly shared via a weak-pointer.
|
/// Wrapped in an `Rc<RefCell<…>>` so it can be re-entrantly shared via a weak-pointer.
|
||||||
shared: Rc<RefCell<SharedState>>,
|
shared: Rc<RefCell<SharedState>>,
|
||||||
|
|
||||||
|
pending_deltas: TexturesDelta,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for WgpuWinitRunning<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Avoid debug panic when dropping unapplied deltas on teardown
|
||||||
|
self.pending_deltas.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Everything needed by the immediate viewport renderer.\
|
/// Everything needed by the immediate viewport renderer.\
|
||||||
@@ -91,6 +102,9 @@ pub struct Viewport {
|
|||||||
info: ViewportInfo,
|
info: ViewportInfo,
|
||||||
actions_requested: Vec<ActionRequested>,
|
actions_requested: Vec<ActionRequested>,
|
||||||
|
|
||||||
|
/// Any not yet applied deltas for this viewport.
|
||||||
|
pending_delta: TexturesDelta,
|
||||||
|
|
||||||
/// `None` for sync viewports.
|
/// `None` for sync viewports.
|
||||||
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
|
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
|
||||||
|
|
||||||
@@ -102,6 +116,13 @@ pub struct Viewport {
|
|||||||
egui_winit: Option<egui_winit::State>,
|
egui_winit: Option<egui_winit::State>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for Viewport {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Avoid debug panic when dropping unapplied deltas on teardown
|
||||||
|
self.pending_delta.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
impl<'app> WgpuWinitApp<'app> {
|
impl<'app> WgpuWinitApp<'app> {
|
||||||
@@ -289,7 +310,7 @@ impl<'app> WgpuWinitApp<'app> {
|
|||||||
egui_winit.init_accesskit(event_loop, &window, event_loop_proxy);
|
egui_winit.init_accesskit(event_loop, &window, event_loop_proxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
let app_creator = std::mem::take(&mut self.app_creator)
|
let app_creator = core::mem::take(&mut self.app_creator)
|
||||||
.expect("Single-use AppCreator has unexpectedly already been taken");
|
.expect("Single-use AppCreator has unexpectedly already been taken");
|
||||||
|
|
||||||
crate::maybe_attach_inspection_plugin(&egui_ctx, Some(self.app_name.clone()));
|
crate::maybe_attach_inspection_plugin(&egui_ctx, Some(self.app_name.clone()));
|
||||||
@@ -328,6 +349,7 @@ impl<'app> WgpuWinitApp<'app> {
|
|||||||
viewport_ui_cb: None,
|
viewport_ui_cb: None,
|
||||||
window: Some(window),
|
window: Some(window),
|
||||||
egui_winit: Some(egui_winit),
|
egui_winit: Some(egui_winit),
|
||||||
|
pending_delta: Default::default(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -358,6 +380,7 @@ impl<'app> WgpuWinitApp<'app> {
|
|||||||
integration,
|
integration,
|
||||||
app,
|
app,
|
||||||
shared,
|
shared,
|
||||||
|
pending_deltas: Default::default(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -596,12 +619,13 @@ impl WgpuWinitRunning<'_> {
|
|||||||
app,
|
app,
|
||||||
integration,
|
integration,
|
||||||
shared,
|
shared,
|
||||||
|
pending_deltas,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let mut frame_timer = crate::stopwatch::Stopwatch::new();
|
let mut frame_timer = crate::stopwatch::Stopwatch::new();
|
||||||
frame_timer.start();
|
frame_timer.start();
|
||||||
|
|
||||||
let (viewport_ui_cb, raw_input, is_visible, run_ui) = {
|
let (viewport_ui_cb, raw_input, is_visible, show_ui) = {
|
||||||
profiling::scope!("Prepare");
|
profiling::scope!("Prepare");
|
||||||
let mut shared_lock = shared.borrow_mut();
|
let mut shared_lock = shared.borrow_mut();
|
||||||
|
|
||||||
@@ -657,7 +681,7 @@ impl WgpuWinitRunning<'_> {
|
|||||||
};
|
};
|
||||||
let mut raw_input = egui_winit.take_egui_input(window);
|
let mut raw_input = egui_winit.take_egui_input(window);
|
||||||
|
|
||||||
let run_ui = is_visible || is_viewport_or_descendant_visible(viewports, viewport_id);
|
let show_ui = is_visible || is_viewport_or_descendant_visible(viewports, viewport_id);
|
||||||
|
|
||||||
integration.pre_update();
|
integration.pre_update();
|
||||||
|
|
||||||
@@ -669,15 +693,67 @@ impl WgpuWinitRunning<'_> {
|
|||||||
|
|
||||||
painter.handle_screenshots(&mut raw_input.events);
|
painter.handle_screenshots(&mut raw_input.events);
|
||||||
|
|
||||||
(viewport_ui_cb, raw_input, is_visible, run_ui)
|
(viewport_ui_cb, raw_input, is_visible, show_ui)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !show_ui {
|
||||||
|
// Nothing will be shown, so we run no egui pass at all.
|
||||||
|
// That way all ui state is left untouched, and is still there
|
||||||
|
// when this viewport becomes visible again.
|
||||||
|
let is_root_viewport = viewport_ui_cb.is_none();
|
||||||
|
if is_root_viewport {
|
||||||
|
// The app logic keeps ticking, so it can e.g. ask to be shown again:
|
||||||
|
let egui::LogicOutput {
|
||||||
|
platform_output,
|
||||||
|
viewport_commands,
|
||||||
|
} = integration.update_logic_only(app.as_mut(), raw_input);
|
||||||
|
|
||||||
|
let mut shared_mut = shared.borrow_mut();
|
||||||
|
let SharedState { viewports, .. } = &mut *shared_mut;
|
||||||
|
|
||||||
|
if let Some(viewport) = viewports.get_mut(&viewport_id) {
|
||||||
|
viewport.info.events.clear(); // they should have been processed
|
||||||
|
if let Viewport {
|
||||||
|
window: Some(window),
|
||||||
|
egui_winit: Some(egui_winit),
|
||||||
|
..
|
||||||
|
} = viewport
|
||||||
|
{
|
||||||
|
egui_winit.handle_platform_output_with_event_loop(
|
||||||
|
window,
|
||||||
|
event_loop,
|
||||||
|
platform_output,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (id, commands) in viewport_commands {
|
||||||
|
if let Some(viewport) = viewports.get_mut(&id) {
|
||||||
|
viewport.process_commands(&integration.egui_ctx, commands);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep_if_invisible_or_minimized(
|
||||||
|
shared
|
||||||
|
.borrow()
|
||||||
|
.viewports
|
||||||
|
.get(&viewport_id)
|
||||||
|
.and_then(|viewport| viewport.window.as_deref()),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(if integration.should_close() {
|
||||||
|
EventResult::CloseRequested
|
||||||
|
} else {
|
||||||
|
EventResult::Wait
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
// Runs the update, which could call immediate viewports,
|
// Runs the update, which could call immediate viewports,
|
||||||
// so make sure we hold no locks here!
|
// so make sure we hold no locks here!
|
||||||
let full_output =
|
let full_output = integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input);
|
||||||
integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input, run_ui);
|
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
@@ -699,6 +775,8 @@ impl WgpuWinitRunning<'_> {
|
|||||||
viewport_output,
|
viewport_output,
|
||||||
} = full_output;
|
} = full_output;
|
||||||
|
|
||||||
|
pending_deltas.append(textures_delta);
|
||||||
|
|
||||||
remove_viewports_not_in(viewports, painter, viewport_from_window, &viewport_output);
|
remove_viewports_not_in(viewports, painter, viewport_from_window, &viewport_output);
|
||||||
|
|
||||||
let Some(viewport) = viewports.get_mut(&viewport_id) else {
|
let Some(viewport) = viewports.get_mut(&viewport_id) else {
|
||||||
@@ -735,7 +813,7 @@ impl WgpuWinitRunning<'_> {
|
|||||||
pixels_per_point,
|
pixels_per_point,
|
||||||
app.clear_color(&egui_ctx.global_style().visuals),
|
app.clear_color(&egui_ctx.global_style().visuals),
|
||||||
&clipped_primitives,
|
&clipped_primitives,
|
||||||
&textures_delta,
|
pending_deltas,
|
||||||
screenshot_commands,
|
screenshot_commands,
|
||||||
window,
|
window,
|
||||||
);
|
);
|
||||||
@@ -796,16 +874,7 @@ impl WgpuWinitRunning<'_> {
|
|||||||
|
|
||||||
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
|
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
|
||||||
|
|
||||||
if let Some(window) = window
|
sleep_if_invisible_or_minimized(window.map(|window| window.as_ref()));
|
||||||
&& is_invisible_or_minimized(window)
|
|
||||||
{
|
|
||||||
// On Mac, a minimized Window uses up all CPU:
|
|
||||||
// https://github.com/emilk/egui/issues/325
|
|
||||||
// On Windows, an invisible window also uses up all CPU:
|
|
||||||
// https://github.com/emilk/egui/issues/7776
|
|
||||||
profiling::scope!("minimized_sleep");
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
||||||
}
|
|
||||||
|
|
||||||
if integration.should_close() {
|
if integration.should_close() {
|
||||||
Ok(EventResult::CloseRequested)
|
Ok(EventResult::CloseRequested)
|
||||||
@@ -960,6 +1029,25 @@ impl WgpuWinitRunning<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Viewport {
|
impl Viewport {
|
||||||
|
/// Apply the commands, or defer them until we have a window.
|
||||||
|
fn process_commands(
|
||||||
|
&mut self,
|
||||||
|
egui_ctx: &egui::Context,
|
||||||
|
mut commands: Vec<egui::ViewportCommand>,
|
||||||
|
) {
|
||||||
|
self.deferred_commands.append(&mut commands);
|
||||||
|
|
||||||
|
if let Some(window) = self.window.as_ref() {
|
||||||
|
egui_winit::process_viewport_commands(
|
||||||
|
egui_ctx,
|
||||||
|
&mut self.info,
|
||||||
|
core::mem::take(&mut self.deferred_commands),
|
||||||
|
window,
|
||||||
|
&mut self.actions_requested,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create winit window, if needed.
|
/// Create winit window, if needed.
|
||||||
fn initialize_window(
|
fn initialize_window(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -1125,8 +1213,11 @@ fn render_immediate_viewport(
|
|||||||
} = &mut *shared_mut;
|
} = &mut *shared_mut;
|
||||||
|
|
||||||
let Some(viewport) = viewports.get_mut(&ids.this) else {
|
let Some(viewport) = viewports.get_mut(&ids.this) else {
|
||||||
|
warn!("Viewport disappeared unexpectedly!");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
viewport.pending_delta.append(textures_delta);
|
||||||
|
|
||||||
viewport.info.events.clear(); // they should have been processed
|
viewport.info.events.clear(); // they should have been processed
|
||||||
let (Some(egui_winit), Some(window)) = (&mut viewport.egui_winit, &viewport.window) else {
|
let (Some(egui_winit), Some(window)) = (&mut viewport.egui_winit, &viewport.window) else {
|
||||||
return;
|
return;
|
||||||
@@ -1149,7 +1240,7 @@ fn render_immediate_viewport(
|
|||||||
pixels_per_point,
|
pixels_per_point,
|
||||||
[0.0, 0.0, 0.0, 0.0],
|
[0.0, 0.0, 0.0, 0.0],
|
||||||
&clipped_primitives,
|
&clipped_primitives,
|
||||||
&textures_delta,
|
&mut viewport.pending_delta,
|
||||||
vec![],
|
vec![],
|
||||||
window,
|
window,
|
||||||
);
|
);
|
||||||
@@ -1194,7 +1285,7 @@ fn handle_viewport_output(
|
|||||||
class,
|
class,
|
||||||
builder,
|
builder,
|
||||||
viewport_ui_cb,
|
viewport_ui_cb,
|
||||||
mut commands,
|
commands,
|
||||||
repaint_delay: _, // ignored - we listened to the repaint callback instead
|
repaint_delay: _, // ignored - we listened to the repaint callback instead
|
||||||
},
|
},
|
||||||
) in viewport_output.clone()
|
) in viewport_output.clone()
|
||||||
@@ -1204,30 +1295,23 @@ fn handle_viewport_output(
|
|||||||
let viewport =
|
let viewport =
|
||||||
initialize_or_update_viewport(viewports, ids, class, builder, viewport_ui_cb, painter);
|
initialize_or_update_viewport(viewports, ids, class, builder, viewport_ui_cb, painter);
|
||||||
|
|
||||||
if let Some(window) = viewport.window.as_ref() {
|
let old_inner_size = viewport.window.as_ref().map(|window| window.inner_size());
|
||||||
let old_inner_size = window.inner_size();
|
|
||||||
|
|
||||||
viewport.deferred_commands.append(&mut commands);
|
viewport.process_commands(egui_ctx, commands);
|
||||||
|
|
||||||
egui_winit::process_viewport_commands(
|
// For Wayland : https://github.com/emilk/egui/issues/4196
|
||||||
egui_ctx,
|
if cfg!(target_os = "linux")
|
||||||
&mut viewport.info,
|
&& let Some(window) = viewport.window.as_ref()
|
||||||
std::mem::take(&mut viewport.deferred_commands),
|
&& let Some(old_inner_size) = old_inner_size
|
||||||
window,
|
{
|
||||||
&mut viewport.actions_requested,
|
let new_inner_size = window.inner_size();
|
||||||
);
|
if new_inner_size != old_inner_size
|
||||||
|
&& let (Some(width), Some(height)) = (
|
||||||
// For Wayland : https://github.com/emilk/egui/issues/4196
|
NonZeroU32::new(new_inner_size.width),
|
||||||
if cfg!(target_os = "linux") {
|
NonZeroU32::new(new_inner_size.height),
|
||||||
let new_inner_size = window.inner_size();
|
)
|
||||||
if new_inner_size != old_inner_size
|
{
|
||||||
&& let (Some(width), Some(height)) = (
|
painter.on_window_resized(viewport_id, width, height);
|
||||||
NonZeroU32::new(new_inner_size.width),
|
|
||||||
NonZeroU32::new(new_inner_size.height),
|
|
||||||
)
|
|
||||||
{
|
|
||||||
painter.on_window_resized(viewport_id, width, height);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1268,6 +1352,7 @@ fn initialize_or_update_viewport<'a>(
|
|||||||
viewport_ui_cb,
|
viewport_ui_cb,
|
||||||
window: None,
|
window: None,
|
||||||
egui_winit: None,
|
egui_winit: None,
|
||||||
|
pending_delta: Default::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ pub fn is_invisible_or_minimized(window: &Window) -> bool {
|
|||||||
window.is_visible() == Some(false) || window.is_minimized() == Some(true)
|
window.is_visible() == Some(false) || window.is_minimized() == Some(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// On Mac, a minimized window uses up all CPU:
|
||||||
|
/// <https://github.com/emilk/egui/issues/325>
|
||||||
|
///
|
||||||
|
/// On Windows, an invisible window also uses up all CPU:
|
||||||
|
/// <https://github.com/emilk/egui/issues/7776>
|
||||||
|
pub fn sleep_if_invisible_or_minimized(window: Option<&Window>) {
|
||||||
|
if window.is_some_and(is_invisible_or_minimized) {
|
||||||
|
profiling::scope!("minimized_sleep");
|
||||||
|
std::thread::sleep(core::time::Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create an egui context, restoring it from storage if possible.
|
/// Create an egui context, restoring it from storage if possible.
|
||||||
pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context {
|
pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|||||||
@@ -280,52 +280,71 @@ impl AppRunner {
|
|||||||
.and_then(|v| v.visible())
|
.and_then(|v| v.visible())
|
||||||
.unwrap_or(true);
|
.unwrap_or(true);
|
||||||
|
|
||||||
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
|
if is_visible {
|
||||||
self.app.logic(ui.ctx(), &mut self.frame);
|
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
|
||||||
|
self.app.logic(ui.ctx(), &mut self.frame);
|
||||||
if is_visible {
|
|
||||||
self.app.ui(ui, &mut self.frame);
|
self.app.ui(ui, &mut self.frame);
|
||||||
}
|
});
|
||||||
});
|
let egui::FullOutput {
|
||||||
let egui::FullOutput {
|
platform_output,
|
||||||
platform_output,
|
textures_delta,
|
||||||
textures_delta,
|
shapes,
|
||||||
shapes,
|
pixels_per_point,
|
||||||
pixels_per_point,
|
viewport_output,
|
||||||
viewport_output,
|
} = full_output;
|
||||||
} = full_output;
|
|
||||||
|
|
||||||
if viewport_output.len() > 1 {
|
if viewport_output.len() > 1 {
|
||||||
log::warn!("Multiple viewports not yet supported on the web");
|
log::warn!("Multiple viewports not yet supported on the web");
|
||||||
}
|
|
||||||
for (_viewport_id, viewport_output) in viewport_output {
|
|
||||||
for command in viewport_output.commands {
|
|
||||||
match command {
|
|
||||||
ViewportCommand::Screenshot(user_data) => {
|
|
||||||
self.screenshot_commands_with_frame_delay
|
|
||||||
.push((user_data, 1));
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// TODO(emilk): handle some of the commands
|
|
||||||
log::warn!(
|
|
||||||
"Unhandled egui viewport command: {command:?} - not implemented in web backend"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
self.handle_viewport_commands(
|
||||||
|
viewport_output
|
||||||
|
.into_values()
|
||||||
|
.flat_map(|viewport_output| viewport_output.commands),
|
||||||
|
);
|
||||||
|
|
||||||
self.handle_platform_output(platform_output);
|
self.handle_platform_output(platform_output);
|
||||||
if is_visible || !textures_delta.is_empty() {
|
|
||||||
self.textures_delta.append(textures_delta);
|
self.textures_delta.append(textures_delta);
|
||||||
self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point));
|
self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point));
|
||||||
|
} else {
|
||||||
|
// The tab is hidden, so we run no egui pass at all.
|
||||||
|
// That way all ui state is left untouched, and is still there
|
||||||
|
// when the tab is shown again.
|
||||||
|
|
||||||
|
let egui::LogicOutput {
|
||||||
|
platform_output,
|
||||||
|
viewport_commands,
|
||||||
|
} = self.egui_ctx.run_logic(&raw_input, |ctx| {
|
||||||
|
self.app.logic(ctx, &mut self.frame);
|
||||||
|
});
|
||||||
|
|
||||||
|
// No pass consumed the input, so save it for the next one:
|
||||||
|
self.input.raw.append(raw_input);
|
||||||
|
|
||||||
|
self.handle_viewport_commands(viewport_commands.into_values().flatten());
|
||||||
|
self.handle_platform_output(platform_output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_viewport_commands(&mut self, commands: impl Iterator<Item = ViewportCommand>) {
|
||||||
|
for command in commands {
|
||||||
|
match command {
|
||||||
|
ViewportCommand::Screenshot(user_data) => {
|
||||||
|
self.screenshot_commands_with_frame_delay
|
||||||
|
.push((user_data, 1));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// TODO(emilk): handle some of the commands
|
||||||
|
log::warn!(
|
||||||
|
"Unhandled egui viewport command: {command:?} - not implemented in web backend"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paint the results of the last call to [`Self::logic`].
|
/// Paint the results of the last call to [`Self::logic`].
|
||||||
pub fn paint(&mut self) {
|
pub fn paint(&mut self) {
|
||||||
let textures_delta = std::mem::take(&mut self.textures_delta);
|
let clipped_primitives = core::mem::take(&mut self.clipped_primitives);
|
||||||
let clipped_primitives = std::mem::take(&mut self.clipped_primitives);
|
|
||||||
|
|
||||||
if let Some(clipped_primitives) = clipped_primitives {
|
if let Some(clipped_primitives) = clipped_primitives {
|
||||||
let mut screenshot_commands = vec![];
|
let mut screenshot_commands = vec![];
|
||||||
@@ -347,7 +366,7 @@ impl AppRunner {
|
|||||||
self.app.clear_color(&self.egui_ctx.global_style().visuals),
|
self.app.clear_color(&self.egui_ctx.global_style().visuals),
|
||||||
&clipped_primitives,
|
&clipped_primitives,
|
||||||
self.egui_ctx.pixels_per_point(),
|
self.egui_ctx.pixels_per_point(),
|
||||||
&textures_delta,
|
&mut self.textures_delta,
|
||||||
screenshot_commands,
|
screenshot_commands,
|
||||||
) {
|
) {
|
||||||
log::error!("Failed to paint: {}", super::string_from_js_value(&err));
|
log::error!("Failed to paint: {}", super::string_from_js_value(&err));
|
||||||
@@ -395,7 +414,10 @@ impl AppRunner {
|
|||||||
|
|
||||||
if self.has_focus() {
|
if self.has_focus() {
|
||||||
// The eframe app has focus.
|
// The eframe app has focus.
|
||||||
if ime.is_some() {
|
if let Some(ime) = ime {
|
||||||
|
if ime.should_interrupt_composition {
|
||||||
|
self.text_agent.interrupt_ime_composition();
|
||||||
|
}
|
||||||
// We are editing text: give the focus to the text agent.
|
// We are editing text: give the focus to the text agent.
|
||||||
self.text_agent.focus();
|
self.text_agent.focus();
|
||||||
} else {
|
} else {
|
||||||
@@ -407,7 +429,7 @@ impl AppRunner {
|
|||||||
|
|
||||||
if let Err(err) = self
|
if let Err(err) = self
|
||||||
.text_agent
|
.text_agent
|
||||||
.move_to(ime, self.canvas(), self.egui_ctx.zoom_factor())
|
.update(ime, self.canvas(), self.egui_ctx.zoom_factor())
|
||||||
{
|
{
|
||||||
log::error!(
|
log::error!(
|
||||||
"failed to update text agent position: {}",
|
"failed to update text agent position: {}",
|
||||||
|
|||||||
45
crates/eframe/src/web/dropped_file.rs
Normal file
45
crates/eframe/src/web/dropped_file.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use core::{future::Future, pin::Pin};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct WebFile {
|
||||||
|
file: web_sys::File,
|
||||||
|
// We store a `PathBuf` here so that we can hand out `Path`s
|
||||||
|
// without allocating each time.
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<web_sys::File> for WebFile {
|
||||||
|
fn from(file: web_sys::File) -> Self {
|
||||||
|
let path = file.name().into();
|
||||||
|
Self { file, path }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl egui::DroppedFile for WebFile {
|
||||||
|
fn path(&self) -> &Path {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes_async(&self) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + '_>> {
|
||||||
|
let file = self.file.clone();
|
||||||
|
Box::pin(async move {
|
||||||
|
if file.size() > f64::from(u32::MAX) {
|
||||||
|
return Err(format!(
|
||||||
|
"File is too large: browser file reads are limited to {} bytes",
|
||||||
|
u32::MAX
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let array_buffer = file
|
||||||
|
.array_buffer()
|
||||||
|
.await
|
||||||
|
.map_err(|err| crate::web::string_from_js_value(&err))?;
|
||||||
|
Ok(js_sys::Uint8Array::new(&array_buffer).to_vec())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn web_file(&self) -> Option<&web_sys::File> {
|
||||||
|
Some(&self.file)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -190,11 +190,6 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.is_composing() || event.key_code() == 229 {
|
|
||||||
// https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let modifiers = modifiers_from_kb_event(&event);
|
let modifiers = modifiers_from_kb_event(&event);
|
||||||
runner.input.set_modifiers(modifiers);
|
runner.input.set_modifiers(modifiers);
|
||||||
|
|
||||||
@@ -978,62 +973,25 @@ fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result
|
|||||||
event.prevent_default();
|
event.prevent_default();
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
runner_ref.add_event_listener(target, "drop", {
|
runner_ref.add_event_listener(target, "drop", |event: web_sys::DragEvent, runner| {
|
||||||
let runner_ref = runner_ref.clone();
|
if let Some(data_transfer) = event.data_transfer() {
|
||||||
|
// TODO(https://github.com/emilk/egui/issues/3702): support dropping folders
|
||||||
|
runner.input.raw.hovered_files.clear();
|
||||||
|
runner.needs_repaint.repaint_asap();
|
||||||
|
|
||||||
move |event: web_sys::DragEvent, runner| {
|
if let Some(files) = data_transfer.files() {
|
||||||
if let Some(data_transfer) = event.data_transfer() {
|
for i in 0..files.length() {
|
||||||
// TODO(https://github.com/emilk/egui/issues/3702): support dropping folders
|
if let Some(file) = files.get(i) {
|
||||||
runner.input.raw.hovered_files.clear();
|
log::debug!("Dropped {:?} ({} bytes)", file.name(), file.size());
|
||||||
runner.needs_repaint.repaint_asap();
|
|
||||||
|
|
||||||
if let Some(files) = data_transfer.files() {
|
runner.input.raw.dropped_files.push(std::sync::Arc::new(
|
||||||
for i in 0..files.length() {
|
super::dropped_file::WebFile::from(file),
|
||||||
if let Some(file) = files.get(i) {
|
));
|
||||||
let name = file.name();
|
|
||||||
let mime = file.type_();
|
|
||||||
let last_modified = std::time::UNIX_EPOCH
|
|
||||||
+ std::time::Duration::from_millis(file.last_modified() as u64);
|
|
||||||
|
|
||||||
log::debug!("Loading {:?} ({} bytes)…", name, file.size());
|
|
||||||
|
|
||||||
let future = wasm_bindgen_futures::JsFuture::from(file.array_buffer());
|
|
||||||
|
|
||||||
let runner_ref = runner_ref.clone();
|
|
||||||
let future = async move {
|
|
||||||
match future.await {
|
|
||||||
Ok(array_buffer) => {
|
|
||||||
let bytes = js_sys::Uint8Array::new(&array_buffer).to_vec();
|
|
||||||
log::debug!("Loaded {:?} ({} bytes).", name, bytes.len());
|
|
||||||
|
|
||||||
if let Some(mut runner_lock) = runner_ref.try_lock() {
|
|
||||||
runner_lock.input.raw.dropped_files.push(
|
|
||||||
egui::DroppedFile {
|
|
||||||
name,
|
|
||||||
mime,
|
|
||||||
last_modified: Some(last_modified),
|
|
||||||
bytes: Some(bytes.into()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
runner_lock.needs_repaint.repaint_asap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log::error!(
|
|
||||||
"Failed to read file: {}",
|
|
||||||
string_from_js_value(&err)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
wasm_bindgen_futures::spawn_local(future);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
event.stop_propagation();
|
|
||||||
event.prevent_default();
|
|
||||||
}
|
}
|
||||||
|
event.stop_propagation();
|
||||||
|
event.prevent_default();
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ pub fn primary_touch_pos(
|
|||||||
event: &web_sys::TouchEvent,
|
event: &web_sys::TouchEvent,
|
||||||
) -> Option<(egui::Pos2, web_sys::Touch)> {
|
) -> Option<(egui::Pos2, web_sys::Touch)> {
|
||||||
// On touchend we don't get anything in `touches`, but we still get `changed_touches`, so include those:
|
// On touchend we don't get anything in `touches`, but we still get `changed_touches`, so include those:
|
||||||
let all_touches: Vec<_> = std::iter::chain(
|
let all_touches: Vec<_> = core::iter::chain(
|
||||||
(0..event.touches().length()).filter_map(|i| event.touches().get(i)),
|
(0..event.touches().length()).filter_map(|i| event.touches().get(i)),
|
||||||
(0..event.changed_touches().length()).filter_map(|i| event.changed_touches().get(i)),
|
(0..event.changed_touches().length()).filter_map(|i| event.changed_touches().get(i)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
mod app_runner;
|
mod app_runner;
|
||||||
mod backend;
|
mod backend;
|
||||||
|
mod dropped_file;
|
||||||
mod events;
|
mod events;
|
||||||
mod input;
|
mod input;
|
||||||
mod panic_handler;
|
mod panic_handler;
|
||||||
@@ -207,13 +208,12 @@ fn set_clipboard_text(s: &str) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let promise = window.navigator().clipboard().write_text(s);
|
let promise = window.navigator().clipboard().write_text(s);
|
||||||
let future = wasm_bindgen_futures::JsFuture::from(promise);
|
|
||||||
let future = async move {
|
let future = async move {
|
||||||
if let Err(err) = future.await {
|
if let Err(err) = promise.await {
|
||||||
log::error!("Copy/cut action failed: {}", string_from_js_value(&err));
|
log::error!("Copy/cut action failed: {}", string_from_js_value(&err));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
wasm_bindgen_futures::spawn_local(future);
|
js_sys::futures::spawn_local(future);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,16 +248,15 @@ fn set_clipboard_image(image: &egui::ColorImage) {
|
|||||||
};
|
};
|
||||||
let items = js_sys::Array::of1(&item);
|
let items = js_sys::Array::of1(&item);
|
||||||
let promise = window.navigator().clipboard().write(&items);
|
let promise = window.navigator().clipboard().write(&items);
|
||||||
let future = wasm_bindgen_futures::JsFuture::from(promise);
|
|
||||||
let future = async move {
|
let future = async move {
|
||||||
if let Err(err) = future.await {
|
if let Err(err) = promise.await {
|
||||||
log::error!(
|
log::error!(
|
||||||
"Copy/cut image action failed: {}",
|
"Copy/cut image action failed: {}",
|
||||||
string_from_js_value(&err)
|
string_from_js_value(&err)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
wasm_bindgen_futures::spawn_local(future);
|
js_sys::futures::spawn_local(future);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
//! The text agent is a hidden `<input>` element used to capture
|
//! The text agent is a hidden `<input>` element used to capture
|
||||||
//! IME and mobile keyboard input events.
|
//! IME and mobile keyboard input events.
|
||||||
|
|
||||||
use std::cell::Cell;
|
use core::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use web_sys::Document;
|
|
||||||
|
|
||||||
use super::{AppRunner, WebRunner};
|
use super::{AppRunner, WebRunner};
|
||||||
|
|
||||||
pub struct TextAgent {
|
pub struct TextAgent {
|
||||||
input: web_sys::HtmlInputElement,
|
input: web_sys::HtmlInputElement,
|
||||||
prev_ime_output: Cell<Option<egui::output::IMEOutput>>,
|
input_state: Rc<RefCell<InputState>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextAgent {
|
impl TextAgent {
|
||||||
@@ -19,7 +19,8 @@ impl TextAgent {
|
|||||||
runner_ref: &WebRunner,
|
runner_ref: &WebRunner,
|
||||||
canvas: &web_sys::HtmlCanvasElement,
|
canvas: &web_sys::HtmlCanvasElement,
|
||||||
) -> Result<Self, JsValue> {
|
) -> Result<Self, JsValue> {
|
||||||
let document = web_sys::window().unwrap().document().unwrap();
|
let window = web_sys::window().unwrap();
|
||||||
|
let document = window.document().unwrap();
|
||||||
|
|
||||||
// create an `<input>` element
|
// create an `<input>` element
|
||||||
let input = document
|
let input = document
|
||||||
@@ -27,11 +28,11 @@ impl TextAgent {
|
|||||||
.dyn_into::<web_sys::HtmlInputElement>()?;
|
.dyn_into::<web_sys::HtmlInputElement>()?;
|
||||||
input.set_type("text");
|
input.set_type("text");
|
||||||
input.set_attribute("autocapitalize", "off")?;
|
input.set_attribute("autocapitalize", "off")?;
|
||||||
|
let input_state = Rc::new(RefCell::new(InputState::new(input.clone())));
|
||||||
|
|
||||||
// Hide the element, and park it over the canvas
|
// Hide the element, and park it over the top-left corner of the canvas
|
||||||
// so that focusing it can never scroll some other part
|
// so that focusing it can never scroll some other part
|
||||||
// of the page into view.
|
// of the page into view.
|
||||||
let canvas_rect = super::canvas_content_rect(canvas);
|
|
||||||
let style = input.style();
|
let style = input.style();
|
||||||
style.set_property("background-color", "transparent")?;
|
style.set_property("background-color", "transparent")?;
|
||||||
style.set_property("border", "none")?;
|
style.set_property("border", "none")?;
|
||||||
@@ -40,21 +41,22 @@ impl TextAgent {
|
|||||||
style.set_property("height", "1px")?;
|
style.set_property("height", "1px")?;
|
||||||
style.set_property("caret-color", "transparent")?;
|
style.set_property("caret-color", "transparent")?;
|
||||||
style.set_property("position", "absolute")?;
|
style.set_property("position", "absolute")?;
|
||||||
style.set_property("top", &format!("{}px", canvas_rect.min.y))?;
|
style.set_property("top", &format!("{}px", canvas.offset_top()))?;
|
||||||
style.set_property("left", &format!("{}px", canvas_rect.min.x))?;
|
style.set_property("left", &format!("{}px", canvas.offset_left()))?;
|
||||||
// Prevent auto-zoom on mobile browsers (requires at least 16px).
|
// Prevent auto-zoom on mobile browsers (requires at least 16px).
|
||||||
style.set_property("font-size", "16px")?;
|
style.set_property("font-size", "16px")?;
|
||||||
|
|
||||||
let root = canvas.get_root_node();
|
// Insert the input as a sibling of the canvas, so that its
|
||||||
if root.has_type::<Document>() {
|
// `position: absolute` resolves against the same containing block
|
||||||
// root object is a document, append to its body
|
// as the canvas' `offset_top`/`offset_left`.
|
||||||
root.dyn_into::<Document>()?
|
// This anchors the input to the canvas regardless of how the page
|
||||||
.body()
|
// is scrolled or how the canvas is embedded, and also works when
|
||||||
.unwrap()
|
// the canvas is inside a shadow DOM.
|
||||||
.append_child(&input)?;
|
if let Some(parent) = canvas.parent_node() {
|
||||||
} else {
|
parent.insert_before(&input, canvas.next_sibling().as_ref())?;
|
||||||
// append input into root directly
|
} else if let Some(body) = document.body() {
|
||||||
root.append_child(&input)?;
|
log::warn!("Canvas has no parent element - appending text agent to document body");
|
||||||
|
body.append_child(&input)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focus the app on startup, without scrolling the page.
|
// Focus the app on startup, without scrolling the page.
|
||||||
@@ -66,152 +68,67 @@ impl TextAgent {
|
|||||||
|
|
||||||
// attach event listeners
|
// attach event listeners
|
||||||
|
|
||||||
let on_input = {
|
runner_ref.add_event_listener(
|
||||||
let input = input.clone();
|
&input,
|
||||||
move |event: web_sys::InputEvent, runner: &mut AppRunner| {
|
"compositionstart",
|
||||||
let text = input.value();
|
|
||||||
// Workaround for an Android Gboard issue: after typing a word,
|
|
||||||
// the user has to delete invisible characters (whose count
|
|
||||||
// matches the length of the current suggestion) before actual
|
|
||||||
// characters are deleted, unless the focus has been reset.
|
|
||||||
//
|
|
||||||
// this issue appears to have been fixed in Gboard sometime
|
|
||||||
// between versions 14.7.09 and 17.0.12.
|
|
||||||
if !event.is_composing() {
|
|
||||||
input.blur().ok();
|
|
||||||
super::focus_without_scroll(&input).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.is_composing() {
|
|
||||||
// if `is_composing` is true, then user is using IME, for
|
|
||||||
// example: emoji, pinyin, kanji, hangul, etc. In that case,
|
|
||||||
// the browser emits both `input` and `compositionupdate`
|
|
||||||
// events.
|
|
||||||
// We handle the composition update here instead of in the
|
|
||||||
// `compositionupdate` event because the selection range
|
|
||||||
// has not yet been updated when `compositionupdate` fires.
|
|
||||||
|
|
||||||
let Some(text) = event.data() else { return };
|
|
||||||
let selection_start = input
|
|
||||||
.selection_start()
|
|
||||||
.unwrap_or(None)
|
|
||||||
.map(|pos| pos as usize);
|
|
||||||
let selection_end = input
|
|
||||||
.selection_end()
|
|
||||||
.unwrap_or(None)
|
|
||||||
.map(|pos| pos as usize);
|
|
||||||
let active_range_chars = if let Some(selection_start) = selection_start
|
|
||||||
&& let Some(selection_end) = selection_end
|
|
||||||
{
|
|
||||||
let text_utf16 = text.encode_utf16().collect::<Vec<u16>>();
|
|
||||||
let text_before_selection =
|
|
||||||
String::from_utf16_lossy(&text_utf16[..selection_start]);
|
|
||||||
let text_in_selection =
|
|
||||||
String::from_utf16_lossy(&text_utf16[selection_start..selection_end]);
|
|
||||||
let count_before_selection = text_before_selection.chars().count();
|
|
||||||
let count_in_selection = text_in_selection.chars().count();
|
|
||||||
Some(count_before_selection..count_before_selection + count_in_selection)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let event = egui::Event::Ime(egui::ImeEvent::Preedit {
|
|
||||||
text,
|
|
||||||
active_range_chars,
|
|
||||||
});
|
|
||||||
runner.input.raw.events.push(event);
|
|
||||||
} else {
|
|
||||||
if text.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.set_value("");
|
|
||||||
let event = egui::Event::Text(text);
|
|
||||||
runner.input.raw.events.push(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
runner.needs_repaint.repaint_asap();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let on_composition_start = {
|
|
||||||
move |_: web_sys::CompositionEvent, runner: &mut AppRunner| {
|
move |_: web_sys::CompositionEvent, runner: &mut AppRunner| {
|
||||||
// Repaint moves the text agent into place,
|
// Repaint moves the text agent into place,
|
||||||
// see `move_to` in `AppRunner::handle_platform_output`.
|
// see `AppRunner::handle_platform_output`, which calls
|
||||||
|
// `TextAgent::update`.
|
||||||
runner.needs_repaint.repaint_asap();
|
runner.needs_repaint.repaint_asap();
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
runner_ref.add_event_listener(&input, "input", {
|
||||||
|
let input_state = Rc::clone(&input_state);
|
||||||
|
move |event: web_sys::InputEvent, runner: &mut AppRunner| {
|
||||||
|
input_state.borrow_mut().handle_input_event(&event, runner);
|
||||||
}
|
}
|
||||||
};
|
})?;
|
||||||
|
runner_ref.add_event_listener(&input, "compositionend", {
|
||||||
let on_composition_end = {
|
let input_state = Rc::clone(&input_state);
|
||||||
let input = input.clone();
|
move |_event: web_sys::CompositionEvent, runner: &mut AppRunner| {
|
||||||
move |event: web_sys::CompositionEvent, runner: &mut AppRunner| {
|
input_state
|
||||||
let Some(text) = event.data() else { return };
|
.borrow_mut()
|
||||||
input.set_value("");
|
.handle_composition_end_event(runner);
|
||||||
let event = egui::Event::Ime(egui::ImeEvent::Commit(text));
|
|
||||||
runner.input.raw.events.push(event);
|
|
||||||
runner.needs_repaint.repaint_asap();
|
|
||||||
}
|
}
|
||||||
};
|
})?;
|
||||||
|
|
||||||
runner_ref.add_event_listener(&input, "input", on_input)?;
|
runner_ref.add_event_listener(&input, "keydown", {
|
||||||
runner_ref.add_event_listener(&input, "compositionstart", on_composition_start)?;
|
let input_state = Rc::clone(&input_state);
|
||||||
runner_ref.add_event_listener(&input, "compositionend", on_composition_end)?;
|
move |event: web_sys::KeyboardEvent, runner: &mut AppRunner| {
|
||||||
|
let is_consumed = InputState::handle_keydown_event(&input_state, &event);
|
||||||
|
if !is_consumed {
|
||||||
|
// The canvas doesn't get keydown/keyup events when the text agent is focused,
|
||||||
|
// so we need to forward them to the runner:
|
||||||
|
super::events::on_keydown(event, runner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
runner_ref.add_event_listener(&input, "keyup", {
|
||||||
|
let input_state = Rc::clone(&input_state);
|
||||||
|
move |event: web_sys::KeyboardEvent, runner: &mut AppRunner| {
|
||||||
|
let is_consumed = InputState::handle_keyup_event(&input_state, &event);
|
||||||
|
if !is_consumed {
|
||||||
|
// The canvas doesn't get keydown/keyup events when the text agent is focused,
|
||||||
|
// so we need to forward them to the runner:
|
||||||
|
super::events::on_keyup(event, runner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
// The canvas doesn't get keydown/keyup events when the text agent is focused,
|
Ok(Self { input, input_state })
|
||||||
// so we need to forward them to the runner:
|
|
||||||
runner_ref.add_event_listener(&input, "keydown", super::events::on_keydown)?;
|
|
||||||
runner_ref.add_event_listener(&input, "keyup", super::events::on_keyup)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
input,
|
|
||||||
prev_ime_output: Default::default(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_to(
|
pub fn update(
|
||||||
&self,
|
&self,
|
||||||
ime: Option<egui::output::IMEOutput>,
|
ime: Option<egui::output::IMEOutput>,
|
||||||
canvas: &web_sys::HtmlCanvasElement,
|
canvas: &web_sys::HtmlCanvasElement,
|
||||||
zoom_factor: f32,
|
zoom_factor: f32,
|
||||||
) -> Result<(), JsValue> {
|
) -> Result<(), JsValue> {
|
||||||
// Don't move the text agent unless the position actually changed:
|
self.input_state
|
||||||
if self.prev_ime_output.get() == ime {
|
.borrow_mut()
|
||||||
return Ok(());
|
.update(ime, canvas, zoom_factor)
|
||||||
}
|
|
||||||
self.prev_ime_output.set(ime);
|
|
||||||
|
|
||||||
let Some(ime) = ime else { return Ok(()) };
|
|
||||||
|
|
||||||
if ime.should_interrupt_composition {
|
|
||||||
// no-op for now: currently, the text agent is sizeless, so any
|
|
||||||
// click shifts focus to the canvas, which naturally interrupts the
|
|
||||||
// composition.
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut canvas_rect = super::canvas_content_rect(canvas);
|
|
||||||
// Fix for safari with virtual keyboard flapping position
|
|
||||||
if is_mobile_safari() {
|
|
||||||
canvas_rect.min.y = canvas.offset_top() as f32;
|
|
||||||
}
|
|
||||||
let cursor_rect = ime.cursor_rect.translate(canvas_rect.min.to_vec2());
|
|
||||||
|
|
||||||
let style = self.input.style();
|
|
||||||
let native_ppp = super::native_pixels_per_point();
|
|
||||||
|
|
||||||
// Clamp the input position within the canvas width to prevent unwanted horizontal scrolling.
|
|
||||||
let logical_canvas_width = canvas.width() as f32 / native_ppp;
|
|
||||||
let visible_x = cursor_rect.center().x * zoom_factor;
|
|
||||||
let clamped_x = visible_x.clamp(0.0, logical_canvas_width);
|
|
||||||
|
|
||||||
// Clamp the input position within the canvas height to prevent unwanted vertical scrolling.
|
|
||||||
let logical_canvas_height = canvas.height() as f32 / native_ppp;
|
|
||||||
let visible_y = cursor_rect.center().y * zoom_factor;
|
|
||||||
let clamped_y = visible_y.clamp(0.0, logical_canvas_height);
|
|
||||||
|
|
||||||
// This is where the IME input will point to:
|
|
||||||
style.set_property("left", &format!("{clamped_x}px"))?;
|
|
||||||
style.set_property("top", &format!("{clamped_y}px"))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_focus(&self, on: bool) {
|
pub fn set_focus(&self, on: bool) {
|
||||||
@@ -248,6 +165,11 @@ impl TextAgent {
|
|||||||
if let Err(err) = self.input.blur() {
|
if let Err(err) = self.input.blur() {
|
||||||
log::error!("failed to set focus: {}", super::string_from_js_value(&err));
|
log::error!("failed to set focus: {}", super::string_from_js_value(&err));
|
||||||
}
|
}
|
||||||
|
self.input_state.borrow_mut().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn interrupt_ime_composition(&self) {
|
||||||
|
self.input_state.borrow_mut().clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,15 +179,273 @@ impl Drop for TextAgent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the app is likely running on a mobile device on navigator Safari.
|
struct InputState {
|
||||||
fn is_mobile_safari() -> bool {
|
input: web_sys::HtmlInputElement,
|
||||||
(|| {
|
last_text: String,
|
||||||
let user_agent = web_sys::window()?.navigator().user_agent().ok()?;
|
ime_output: Option<egui::output::IMEOutput>,
|
||||||
let is_ios = user_agent.contains("iPhone")
|
keydown_special_case: KeydownSpecialCase,
|
||||||
|| user_agent.contains("iPad")
|
}
|
||||||
|| user_agent.contains("iPod");
|
|
||||||
let is_safari = user_agent.contains("Safari");
|
#[derive(Clone, Copy)]
|
||||||
Some(is_ios && is_safari)
|
enum KeydownSpecialCase {
|
||||||
})()
|
None,
|
||||||
.unwrap_or(false)
|
|
||||||
|
/// On Android Gboard 14.7.09, when suggestions remain visible while typing
|
||||||
|
/// letters without IME composition (e.g., Latin or Cyrillic), pressing
|
||||||
|
/// Backspace produces key code 229 instead of the expected Backspace key
|
||||||
|
/// code.
|
||||||
|
/// Without the workaround, users have to press Backspace twice before text
|
||||||
|
/// starts being deleted.
|
||||||
|
///
|
||||||
|
/// This workaround is also required for Android Gboard corrections and
|
||||||
|
/// completions (e.g., `tex|` -> `Texas`) to work correctly. In these
|
||||||
|
/// cases, a `deleteContentBackward` input event fires first (e.g., to
|
||||||
|
/// delete `tex`), followed by an `insertText` input event (e.g., to insert
|
||||||
|
/// `Texas`).
|
||||||
|
///
|
||||||
|
/// Since it is difficult to distinguish between a Backspace press and a
|
||||||
|
/// correction or completion (e.g., when the state is `t|`, it is unclear
|
||||||
|
/// whether the user wants to delete `t` or replace it with `Texas`), we
|
||||||
|
/// send a `DeleteSurrounding` IME event in all cases instead of
|
||||||
|
/// synthetically generating Backspace press and release events.
|
||||||
|
AndroidKeycode229,
|
||||||
|
|
||||||
|
/// iOS (18.6)'s built-in Korean keyboard uses `deleteContentBackward` to
|
||||||
|
/// compose Hangul characters. In these cases, the key code is 0.
|
||||||
|
IosKeycode0,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputState {
|
||||||
|
fn new(input: web_sys::HtmlInputElement) -> Self {
|
||||||
|
Self {
|
||||||
|
input,
|
||||||
|
last_text: String::new(),
|
||||||
|
ime_output: None,
|
||||||
|
keydown_special_case: KeydownSpecialCase::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
ime: Option<egui::output::IMEOutput>,
|
||||||
|
canvas: &web_sys::HtmlCanvasElement,
|
||||||
|
zoom_factor: f32,
|
||||||
|
) -> Result<(), JsValue> {
|
||||||
|
// Don't move the text agent unless the position actually changed:
|
||||||
|
if self.ime_output == ime {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.ime_output = ime;
|
||||||
|
|
||||||
|
let Some(ime) = ime else { return Ok(()) };
|
||||||
|
|
||||||
|
// NOTE: we don't set the input's `type` to `password` based on
|
||||||
|
// `ime.purpose`, because that would confuse some password managers.
|
||||||
|
// For example, Chrome's password manager will always think the last
|
||||||
|
// letter typed in the password field is the password.
|
||||||
|
|
||||||
|
let style = self.input.style();
|
||||||
|
let native_ppp = super::native_pixels_per_point();
|
||||||
|
|
||||||
|
// The input is a sibling of the canvas (see `attach`), so we position
|
||||||
|
// it relative to the same containing block using the canvas offset.
|
||||||
|
// Unlike `get_bounding_client_rect`, the offset is unaffected by page
|
||||||
|
// scrolling, and doesn't flap when the virtual keyboard is shown on
|
||||||
|
// mobile Safari.
|
||||||
|
|
||||||
|
// Clamp the input position within the canvas width to prevent unwanted horizontal scrolling.
|
||||||
|
let logical_canvas_width = canvas.width() as f32 / native_ppp;
|
||||||
|
let visible_x = ime.cursor_rect.center().x * zoom_factor;
|
||||||
|
let clamped_x = visible_x.clamp(0.0, logical_canvas_width);
|
||||||
|
|
||||||
|
// Clamp the input position within the canvas height to prevent unwanted vertical scrolling.
|
||||||
|
let logical_canvas_height = canvas.height() as f32 / native_ppp;
|
||||||
|
let visible_y = ime.cursor_rect.center().y * zoom_factor;
|
||||||
|
let clamped_y = visible_y.clamp(0.0, logical_canvas_height);
|
||||||
|
|
||||||
|
// This is where the IME input will point to:
|
||||||
|
style.set_property(
|
||||||
|
"left",
|
||||||
|
&format!("{}px", canvas.offset_left() as f32 + clamped_x),
|
||||||
|
)?;
|
||||||
|
style.set_property(
|
||||||
|
"top",
|
||||||
|
&format!("{}px", canvas.offset_top() as f32 + clamped_y),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear(&mut self) {
|
||||||
|
self.input.set_value("");
|
||||||
|
self.last_text.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_input_event(&mut self, event: &web_sys::InputEvent, runner: &mut AppRunner) {
|
||||||
|
if self
|
||||||
|
.ime_output
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|ime| ime.purpose == egui::IMEPurpose::Password)
|
||||||
|
{
|
||||||
|
self.handle_input_event_password(event, runner);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let input_type = event.input_type();
|
||||||
|
|
||||||
|
if !event.is_composing()
|
||||||
|
&& input_type != "insertText"
|
||||||
|
// iOS uses this for corrections and completions (e.g., `tex|` ->
|
||||||
|
// `Texas`).
|
||||||
|
&& input_type != "insertReplacementText"
|
||||||
|
&& (matches!(self.keydown_special_case, KeydownSpecialCase::None)
|
||||||
|
|| input_type != "deleteContentBackward")
|
||||||
|
{
|
||||||
|
self.clear();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = self.input.value();
|
||||||
|
|
||||||
|
let prefix_len = longest_common_prefix_length(&text, &self.last_text);
|
||||||
|
let last_text_len = self.last_text.chars().count();
|
||||||
|
if prefix_len < last_text_len {
|
||||||
|
let out_event = egui::Event::Ime(egui::ImeEvent::DeleteSurrounding {
|
||||||
|
before_chars: last_text_len - prefix_len,
|
||||||
|
after_chars: 0,
|
||||||
|
});
|
||||||
|
runner.input.raw.events.push(out_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
let preedit_text: String = text.chars().skip(prefix_len).collect();
|
||||||
|
let out_event = if event.is_composing() {
|
||||||
|
// We handle the composition update here instead of in a
|
||||||
|
// `compositionupdate` event because the selection range
|
||||||
|
// has not yet been updated when `compositionupdate` fires.
|
||||||
|
let active_range_chars = self.active_range_chars(&text, prefix_len);
|
||||||
|
egui::Event::Ime(egui::ImeEvent::Preedit {
|
||||||
|
text: preedit_text,
|
||||||
|
active_range_chars,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
egui::Event::Text(preedit_text)
|
||||||
|
};
|
||||||
|
runner.input.raw.events.push(out_event);
|
||||||
|
|
||||||
|
if event.is_composing() {
|
||||||
|
self.last_text = text.chars().take(prefix_len).collect();
|
||||||
|
} else {
|
||||||
|
self.last_text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
runner.needs_repaint.repaint_asap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_input_event_password(&mut self, event: &web_sys::InputEvent, runner: &mut AppRunner) {
|
||||||
|
let input_type = event.input_type();
|
||||||
|
|
||||||
|
if input_type != "insertText" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = self.input.value();
|
||||||
|
|
||||||
|
runner.input.raw.events.push(egui::Event::Text(text));
|
||||||
|
self.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the active range (cursor or conversion segment) within the
|
||||||
|
/// preedit text, based on the selection in the input element.
|
||||||
|
///
|
||||||
|
/// `text` is the full `input.value()`, and `prefix_len_chars` is the
|
||||||
|
/// number of chars at the start of `text` that are committed (not part
|
||||||
|
/// of the preedit). `selectionStart`/`selectionEnd` are UTF-16 offsets
|
||||||
|
/// within the full `input.value()`, so they are adjusted to be relative
|
||||||
|
/// to the preedit text.
|
||||||
|
fn active_range_chars(
|
||||||
|
&self,
|
||||||
|
text: &str,
|
||||||
|
prefix_len_chars: usize,
|
||||||
|
) -> Option<core::ops::Range<usize>> {
|
||||||
|
let selection_start = self.input.selection_start().unwrap_or(None)? as usize;
|
||||||
|
let selection_end = self.input.selection_end().unwrap_or(None)? as usize;
|
||||||
|
|
||||||
|
let text_utf16 = text.encode_utf16().collect::<Vec<u16>>();
|
||||||
|
if selection_start > text_utf16.len() || selection_end > text_utf16.len() {
|
||||||
|
// This can occur on Android Chrome. see discussion in:
|
||||||
|
// <https://github.com/emilk/egui/pull/8045>.
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text_before_selection = String::from_utf16_lossy(&text_utf16[..selection_start]);
|
||||||
|
let text_in_selection =
|
||||||
|
String::from_utf16_lossy(&text_utf16[selection_start..selection_end]);
|
||||||
|
let count_before_selection = text_before_selection.chars().count();
|
||||||
|
let count_in_selection = text_in_selection.chars().count();
|
||||||
|
|
||||||
|
// Adjust for the committed prefix to get the range within the preedit text.
|
||||||
|
let start = count_before_selection.saturating_sub(prefix_len_chars);
|
||||||
|
let end = start + count_in_selection;
|
||||||
|
Some(start..end)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_composition_end_event(&mut self, runner: &mut AppRunner) {
|
||||||
|
let text = self.input.value();
|
||||||
|
|
||||||
|
let commit_text = {
|
||||||
|
let prefix_len = self.last_text.chars().count();
|
||||||
|
text.chars().skip(prefix_len).collect::<String>()
|
||||||
|
};
|
||||||
|
let out_event = egui::Event::Ime(egui::ImeEvent::Commit(commit_text));
|
||||||
|
runner.input.raw.events.push(out_event);
|
||||||
|
|
||||||
|
self.last_text = text;
|
||||||
|
|
||||||
|
runner.needs_repaint.repaint_asap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Returns
|
||||||
|
/// Whether the event is consumed. If `true`, the caller should not do
|
||||||
|
/// further processing for this event.
|
||||||
|
fn handle_keydown_event(input_state: &RefCell<Self>, event: &web_sys::KeyboardEvent) -> bool {
|
||||||
|
// Platform-sniffing methods are unreliable, so they are not used as
|
||||||
|
// guards here.
|
||||||
|
let special_case = match event.key_code() {
|
||||||
|
229 => KeydownSpecialCase::AndroidKeycode229,
|
||||||
|
0 => KeydownSpecialCase::IosKeycode0,
|
||||||
|
_ => KeydownSpecialCase::None,
|
||||||
|
};
|
||||||
|
input_state.borrow_mut().keydown_special_case = special_case;
|
||||||
|
|
||||||
|
// https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
|
||||||
|
if event.is_composing() || !matches!(special_case, KeydownSpecialCase::None) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
if event.key().chars().count() > 1
|
||||||
|
|| event.ctrl_key()
|
||||||
|
|| event.alt_key()
|
||||||
|
|| event.meta_key()
|
||||||
|
{
|
||||||
|
input_state.borrow_mut().clear();
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ## Returns
|
||||||
|
/// Whether the event is consumed. If `true`, the caller should not do
|
||||||
|
/// further processing for this event.
|
||||||
|
fn handle_keyup_event(input_state: &RefCell<Self>, event: &web_sys::KeyboardEvent) -> bool {
|
||||||
|
input_state.borrow_mut().keydown_special_case = KeydownSpecialCase::None;
|
||||||
|
|
||||||
|
// https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
|
||||||
|
event.is_composing() || event.key_code() == 229
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn longest_common_prefix_length(a: &str, b: &str) -> usize {
|
||||||
|
core::iter::zip(a.chars(), b.chars())
|
||||||
|
.take_while(|(a, b)| a == b)
|
||||||
|
.count()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ pub(crate) trait WebPainter {
|
|||||||
clear_color: [f32; 4],
|
clear_color: [f32; 4],
|
||||||
clipped_primitives: &[egui::ClippedPrimitive],
|
clipped_primitives: &[egui::ClippedPrimitive],
|
||||||
pixels_per_point: f32,
|
pixels_per_point: f32,
|
||||||
textures_delta: &egui::TexturesDelta,
|
textures_delta: &mut egui::TexturesDelta,
|
||||||
capture: Vec<UserData>,
|
capture: Vec<UserData>,
|
||||||
) -> Result<(), JsValue>;
|
) -> Result<(), JsValue>;
|
||||||
|
|
||||||
|
|||||||
@@ -61,13 +61,16 @@ impl WebPainter for WebPainterGlow {
|
|||||||
clear_color: [f32; 4],
|
clear_color: [f32; 4],
|
||||||
clipped_primitives: &[egui::ClippedPrimitive],
|
clipped_primitives: &[egui::ClippedPrimitive],
|
||||||
pixels_per_point: f32,
|
pixels_per_point: f32,
|
||||||
textures_delta: &egui::TexturesDelta,
|
textures_delta: &mut egui::TexturesDelta,
|
||||||
capture: Vec<UserData>,
|
capture: Vec<UserData>,
|
||||||
) -> Result<(), JsValue> {
|
) -> Result<(), JsValue> {
|
||||||
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
|
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
|
||||||
|
|
||||||
for (id, image_delta) in &textures_delta.set {
|
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
|
||||||
self.painter.set_texture(*id, image_delta);
|
for (id, image_deltas) in textures_delta.set.drain() {
|
||||||
|
for image_delta in image_deltas {
|
||||||
|
self.painter.set_texture(id, &image_delta);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
egui_glow::painter::clear(self.painter.gl(), canvas_dimension, clear_color);
|
egui_glow::painter::clear(self.painter.gl(), canvas_dimension, clear_color);
|
||||||
@@ -79,7 +82,8 @@ impl WebPainter for WebPainterGlow {
|
|||||||
self.screenshots.push((image, capture));
|
self.screenshots.push((image, capture));
|
||||||
}
|
}
|
||||||
|
|
||||||
for &id in &textures_delta.free {
|
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
|
||||||
|
for id in textures_delta.free.drain() {
|
||||||
self.painter.free_texture(id);
|
self.painter.free_texture(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ impl WebPainter for WebPainterWgpu {
|
|||||||
clear_color: [f32; 4],
|
clear_color: [f32; 4],
|
||||||
clipped_primitives: &[egui::ClippedPrimitive],
|
clipped_primitives: &[egui::ClippedPrimitive],
|
||||||
pixels_per_point: f32,
|
pixels_per_point: f32,
|
||||||
textures_delta: &egui::TexturesDelta,
|
textures_delta: &mut egui::TexturesDelta,
|
||||||
capture_data: Vec<UserData>,
|
capture_data: Vec<UserData>,
|
||||||
) -> Result<(), JsValue> {
|
) -> Result<(), JsValue> {
|
||||||
let capture = !capture_data.is_empty();
|
let capture = !capture_data.is_empty();
|
||||||
@@ -210,13 +210,16 @@ impl WebPainter for WebPainterWgpu {
|
|||||||
|
|
||||||
let user_cmd_bufs = {
|
let user_cmd_bufs = {
|
||||||
let mut renderer = render_state.renderer.write();
|
let mut renderer = render_state.renderer.write();
|
||||||
for (id, image_delta) in &textures_delta.set {
|
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
|
||||||
renderer.update_texture(
|
for (id, image_deltas) in textures_delta.set.drain() {
|
||||||
&render_state.device,
|
for image_delta in image_deltas {
|
||||||
&render_state.queue,
|
renderer.update_texture(
|
||||||
*id,
|
&render_state.device,
|
||||||
image_delta,
|
&render_state.queue,
|
||||||
);
|
id,
|
||||||
|
&image_delta,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.update_buffers(
|
renderer.update_buffers(
|
||||||
@@ -365,7 +368,7 @@ impl WebPainter for WebPainterWgpu {
|
|||||||
// Submit the commands: both the main buffer and user-defined ones.
|
// Submit the commands: both the main buffer and user-defined ones.
|
||||||
render_state
|
render_state
|
||||||
.queue
|
.queue
|
||||||
.submit(std::iter::chain(user_cmd_bufs, [encoder.finish()]));
|
.submit(core::iter::chain(user_cmd_bufs, [encoder.finish()]));
|
||||||
|
|
||||||
if let Some((frame, capture_buffer)) = frame_and_capture_buffer {
|
if let Some((frame, capture_buffer)) = frame_and_capture_buffer {
|
||||||
if let Some(capture_buffer) = capture_buffer
|
if let Some(capture_buffer) = capture_buffer
|
||||||
@@ -388,8 +391,9 @@ impl WebPainter for WebPainterWgpu {
|
|||||||
// However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
|
// However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
|
||||||
{
|
{
|
||||||
let mut renderer = render_state.renderer.write();
|
let mut renderer = render_state.renderer.write();
|
||||||
for id in &textures_delta.free {
|
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
|
||||||
renderer.free_texture(id);
|
for id in textures_delta.free.drain() {
|
||||||
|
renderer.free_texture(&id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::{cell::RefCell, rc::Rc};
|
use core::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
@@ -107,7 +108,7 @@ impl WebRunner {
|
|||||||
|
|
||||||
fn unsubscribe_from_all_events(&self) {
|
fn unsubscribe_from_all_events(&self) {
|
||||||
let events_to_unsubscribe: Vec<_> =
|
let events_to_unsubscribe: Vec<_> =
|
||||||
std::mem::take(&mut *self.events_to_unsubscribe.borrow_mut());
|
core::mem::take(&mut *self.events_to_unsubscribe.borrow_mut());
|
||||||
|
|
||||||
if !events_to_unsubscribe.is_empty() {
|
if !events_to_unsubscribe.is_empty() {
|
||||||
log::debug!("Unsubscribing from {} events", events_to_unsubscribe.len());
|
log::debug!("Unsubscribing from {} events", events_to_unsubscribe.len());
|
||||||
@@ -139,7 +140,7 @@ impl WebRunner {
|
|||||||
|
|
||||||
/// Returns `None` if there has been a panic, or if we have been destroyed.
|
/// Returns `None` if there has been a panic, or if we have been destroyed.
|
||||||
/// In that case, just return to JS.
|
/// In that case, just return to JS.
|
||||||
pub(crate) fn try_lock(&self) -> Option<std::cell::RefMut<'_, AppRunner>> {
|
pub(crate) fn try_lock(&self) -> Option<core::cell::RefMut<'_, AppRunner>> {
|
||||||
if self.panic_handler.has_panicked() {
|
if self.panic_handler.has_panicked() {
|
||||||
// Unsubscribe from all events so that we don't get any more callbacks
|
// Unsubscribe from all events so that we don't get any more callbacks
|
||||||
// that will try to access the poisoned runner.
|
// that will try to access the poisoned runner.
|
||||||
@@ -147,7 +148,7 @@ impl WebRunner {
|
|||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let lock = self.app_runner.try_borrow_mut().ok()?;
|
let lock = self.app_runner.try_borrow_mut().ok()?;
|
||||||
std::cell::RefMut::filter_map(lock, |lock| -> Option<&mut AppRunner> { lock.as_mut() })
|
core::cell::RefMut::filter_map(lock, |lock| -> Option<&mut AppRunner> { lock.as_mut() })
|
||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,9 +159,9 @@ impl WebRunner {
|
|||||||
/// and return `None` if this runner has panicked.
|
/// and return `None` if this runner has panicked.
|
||||||
pub fn app_mut<ConcreteApp: 'static + App>(
|
pub fn app_mut<ConcreteApp: 'static + App>(
|
||||||
&self,
|
&self,
|
||||||
) -> Option<std::cell::RefMut<'_, ConcreteApp>> {
|
) -> Option<core::cell::RefMut<'_, ConcreteApp>> {
|
||||||
self.try_lock()
|
self.try_lock()
|
||||||
.map(|lock| std::cell::RefMut::map(lock, |runner| runner.app_mut::<ConcreteApp>()))
|
.map(|lock| core::cell::RefMut::map(lock, |runner| runner.app_mut::<ConcreteApp>()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience function to reduce boilerplate and ensure that all event handlers
|
/// Convenience function to reduce boilerplate and ensure that all event handlers
|
||||||
|
|||||||
@@ -6,6 +6,16 @@ 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.
|
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.36.1 - 2026-08-07
|
||||||
|
Nothing new
|
||||||
|
|
||||||
|
|
||||||
|
## 0.36.0 - 2026-08-05
|
||||||
|
* Upgrade wgpu to v30 [#8289](https://github.com/emilk/egui/pull/8289) by [@akx](https://github.com/akx)
|
||||||
|
* Fix: ensure mapped range is dropped before unmapping buffer in capture [#8337](https://github.com/emilk/egui/pull/8337) by [@MagicCrazyMan](https://github.com/MagicCrazyMan)
|
||||||
|
* Make wgpu Instance public [#8321](https://github.com/emilk/egui/pull/8321) by [@oleflb](https://github.com/oleflb)
|
||||||
|
|
||||||
|
|
||||||
## 0.35.0 - 2026-06-25
|
## 0.35.0 - 2026-06-25
|
||||||
* Call `pre_present_notify` before presenting [#8089](https://github.com/emilk/egui/pull/8089) by [@dimtpap](https://github.com/dimtpap)
|
* Call `pre_present_notify` before presenting [#8089](https://github.com/emilk/egui/pull/8089) by [@dimtpap](https://github.com/dimtpap)
|
||||||
* Wgpu: Allow configuring VSync and frame latency at runtime [#8114](https://github.com/emilk/egui/pull/8114) by [@emilk](https://github.com/emilk)
|
* Wgpu: Allow configuring VSync and frame latency at runtime [#8114](https://github.com/emilk/egui/pull/8114) by [@emilk](https://github.com/emilk)
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ struct BufferPadding {
|
|||||||
|
|
||||||
impl BufferPadding {
|
impl BufferPadding {
|
||||||
fn new(width: u32) -> Self {
|
fn new(width: u32) -> Self {
|
||||||
let bytes_per_pixel = std::mem::size_of::<u32>() as u32;
|
let bytes_per_pixel = core::mem::size_of::<u32>() as u32;
|
||||||
let unpadded_bytes_per_row = width * bytes_per_pixel;
|
let unpadded_bytes_per_row = width * bytes_per_pixel;
|
||||||
let padded_bytes_per_row =
|
let padded_bytes_per_row =
|
||||||
wgpu::util::align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
|
wgpu::util::align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
|
||||||
|
|||||||
@@ -98,20 +98,31 @@ fn vs_main(
|
|||||||
@group(1) @binding(0) var r_tex_color: texture_2d<f32>;
|
@group(1) @binding(0) var r_tex_color: texture_2d<f32>;
|
||||||
@group(1) @binding(1) var r_tex_sampler: sampler;
|
@group(1) @binding(1) var r_tex_sampler: sampler;
|
||||||
|
|
||||||
|
/// 1 if the texture sampler uses nearest filtering, 0 if linear.
|
||||||
|
/// Only read when `predictable_texture_filtering` is on.
|
||||||
|
@group(1) @binding(2) var<uniform> r_tex_nearest_filtering: u32;
|
||||||
|
|
||||||
fn sample_texture(in: VertexOutput) -> vec4<f32> {
|
fn sample_texture(in: VertexOutput) -> vec4<f32> {
|
||||||
if r_locals.predictable_texture_filtering == 0 {
|
if r_locals.predictable_texture_filtering == 0 {
|
||||||
// Hardware filtering: fast, but varies across GPUs and drivers.
|
// Hardware filtering: fast, but varies across GPUs and drivers.
|
||||||
return textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
|
return textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
|
||||||
} else {
|
} else {
|
||||||
// Manual bilinear filtering with four taps at pixel centers using textureLoad
|
|
||||||
let texture_size = vec2<i32>(textureDimensions(r_tex_color, 0));
|
let texture_size = vec2<i32>(textureDimensions(r_tex_color, 0));
|
||||||
let texture_size_f = vec2<f32>(texture_size);
|
let texture_size_f = vec2<f32>(texture_size);
|
||||||
|
let max_coord = texture_size - vec2<i32>(1, 1);
|
||||||
|
|
||||||
|
if r_tex_nearest_filtering == 1 {
|
||||||
|
// Nearest filtering: load the texel under the sample position.
|
||||||
|
let texel = clamp(vec2<i32>(in.tex_coord * texture_size_f), vec2<i32>(0, 0), max_coord);
|
||||||
|
return textureLoad(r_tex_color, texel, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual bilinear filtering with four taps at pixel centers using textureLoad
|
||||||
let pixel_coord = in.tex_coord * texture_size_f - 0.5;
|
let pixel_coord = in.tex_coord * texture_size_f - 0.5;
|
||||||
let pixel_fract = fract(pixel_coord);
|
let pixel_fract = fract(pixel_coord);
|
||||||
let pixel_floor = vec2<i32>(floor(pixel_coord));
|
let pixel_floor = vec2<i32>(floor(pixel_coord));
|
||||||
|
|
||||||
// Manual texture clamping
|
// Manual texture clamping
|
||||||
let max_coord = texture_size - vec2<i32>(1, 1);
|
|
||||||
let p00 = clamp(pixel_floor + vec2<i32>(0, 0), vec2<i32>(0, 0), max_coord);
|
let p00 = clamp(pixel_floor + vec2<i32>(0, 0), vec2<i32>(0, 0), max_coord);
|
||||||
let p10 = clamp(pixel_floor + vec2<i32>(1, 0), vec2<i32>(0, 0), max_coord);
|
let p10 = clamp(pixel_floor + vec2<i32>(1, 0), vec2<i32>(0, 0), max_coord);
|
||||||
let p01 = clamp(pixel_floor + vec2<i32>(0, 1), vec2<i32>(0, 0), max_coord);
|
let p01 = clamp(pixel_floor + vec2<i32>(0, 1), vec2<i32>(0, 0), max_coord);
|
||||||
|
|||||||
@@ -115,6 +115,9 @@ pub struct RenderState {
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub available_adapters: Vec<wgpu::Adapter>,
|
pub available_adapters: Vec<wgpu::Adapter>,
|
||||||
|
|
||||||
|
/// Wgpu instance used for creating surfaces and adapters.
|
||||||
|
pub instance: wgpu::Instance,
|
||||||
|
|
||||||
/// Wgpu device used for rendering, created from the adapter.
|
/// Wgpu device used for rendering, created from the adapter.
|
||||||
pub device: wgpu::Device,
|
pub device: wgpu::Device,
|
||||||
|
|
||||||
@@ -218,7 +221,7 @@ impl RenderState {
|
|||||||
instance.enumerate_adapters(backends).await
|
instance.enumerate_adapters(backends).await
|
||||||
};
|
};
|
||||||
|
|
||||||
let (adapter, device, queue) = match config.wgpu_setup.clone() {
|
let (instance, adapter, device, queue) = match config.wgpu_setup.clone() {
|
||||||
WgpuSetup::CreateNew(WgpuSetupCreateNew {
|
WgpuSetup::CreateNew(WgpuSetupCreateNew {
|
||||||
instance_descriptor: _,
|
instance_descriptor: _,
|
||||||
display_handle: _,
|
display_handle: _,
|
||||||
@@ -253,14 +256,14 @@ impl RenderState {
|
|||||||
.await?
|
.await?
|
||||||
};
|
};
|
||||||
|
|
||||||
(adapter, device, queue)
|
(instance.clone(), adapter, device, queue)
|
||||||
}
|
}
|
||||||
WgpuSetup::Existing(WgpuSetupExisting {
|
WgpuSetup::Existing(WgpuSetupExisting {
|
||||||
instance: _,
|
instance,
|
||||||
adapter,
|
adapter,
|
||||||
device,
|
device,
|
||||||
queue,
|
queue,
|
||||||
}) => (adapter, device, queue),
|
}) => (instance, adapter, device, queue),
|
||||||
};
|
};
|
||||||
|
|
||||||
log_adapter_info(&adapter.get_info());
|
log_adapter_info(&adapter.get_info());
|
||||||
@@ -280,6 +283,7 @@ impl RenderState {
|
|||||||
// It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint.
|
// It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint.
|
||||||
#[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm
|
#[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
instance,
|
||||||
adapter,
|
adapter,
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
available_adapters,
|
available_adapters,
|
||||||
@@ -354,8 +358,8 @@ fn wgpu_config_impl_send_sync() {
|
|||||||
assert_send_sync::<WgpuConfiguration>();
|
assert_send_sync::<WgpuConfiguration>();
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for WgpuConfiguration {
|
impl core::fmt::Debug for WgpuConfiguration {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
let Self {
|
let Self {
|
||||||
surface,
|
surface,
|
||||||
wgpu_setup,
|
wgpu_setup,
|
||||||
@@ -482,7 +486,7 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
|
|||||||
// > name: "Apple M1 Pro", device_type: IntegratedGpu, backend: Metal, driver: "", driver_info: ""
|
// > name: "Apple M1 Pro", device_type: IntegratedGpu, backend: Metal, driver: "", driver_info: ""
|
||||||
// > name: "ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)", device_type: IntegratedGpu, backend: Gl, driver: "", driver_info: ""
|
// > name: "ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)", device_type: IntegratedGpu, backend: Gl, driver: "", driver_info: ""
|
||||||
|
|
||||||
use std::fmt::Write as _;
|
use core::fmt::Write as _;
|
||||||
|
|
||||||
let mut summary = format!("backend: {backend:?}, device_type: {device_type:?}");
|
let mut summary = format!("backend: {backend:?}, device_type: {device_type:?}");
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::{borrow::Cow, num::NonZeroU64, ops::Range};
|
use core::{num::NonZeroU64, ops::Range};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use ahash::HashMap;
|
use ahash::HashMap;
|
||||||
use bytemuck::Zeroable as _;
|
use bytemuck::Zeroable as _;
|
||||||
@@ -244,6 +245,12 @@ pub struct Renderer {
|
|||||||
uniform_bind_group: wgpu::BindGroup,
|
uniform_bind_group: wgpu::BindGroup,
|
||||||
texture_bind_group_layout: wgpu::BindGroupLayout,
|
texture_bind_group_layout: wgpu::BindGroupLayout,
|
||||||
|
|
||||||
|
/// Uniform buffers each holding a single `u32`:
|
||||||
|
/// 1 if the texture sampler uses nearest filtering, 0 otherwise.
|
||||||
|
/// Indexed by that flag value.
|
||||||
|
/// Read by the shader when `predictable_texture_filtering` is on.
|
||||||
|
nearest_filtering_flag_buffers: [wgpu::Buffer; 2],
|
||||||
|
|
||||||
/// Map of egui texture IDs to textures and their associated bindgroups (texture view +
|
/// Map of egui texture IDs to textures and their associated bindgroups (texture view +
|
||||||
/// sampler). The texture may be None if the `TextureId` is just a handle to a user-provided
|
/// sampler). The texture may be None if the `TextureId` is just a handle to a user-provided
|
||||||
/// sampler.
|
/// sampler.
|
||||||
@@ -299,7 +306,9 @@ impl Renderer {
|
|||||||
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
|
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
|
||||||
ty: wgpu::BindingType::Buffer {
|
ty: wgpu::BindingType::Buffer {
|
||||||
has_dynamic_offset: false,
|
has_dynamic_offset: false,
|
||||||
min_binding_size: NonZeroU64::new(std::mem::size_of::<UniformBuffer>() as _),
|
min_binding_size: NonZeroU64::new(
|
||||||
|
core::mem::size_of::<UniformBuffer>() as _
|
||||||
|
),
|
||||||
ty: wgpu::BufferBindingType::Uniform,
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
},
|
},
|
||||||
count: None,
|
count: None,
|
||||||
@@ -344,10 +353,28 @@ impl Renderer {
|
|||||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||||
count: None,
|
count: None,
|
||||||
},
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 2,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Buffer {
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: NonZeroU64::new(core::mem::size_of::<u32>() as _),
|
||||||
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let nearest_filtering_flag_buffers = [0_u32, 1_u32].map(|flag| {
|
||||||
|
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: Some(&format!("egui_nearest_filtering_flag_{flag}")),
|
||||||
|
contents: bytemuck::bytes_of(&flag),
|
||||||
|
usage: wgpu::BufferUsages::UNIFORM,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
label: Some("egui_pipeline_layout"),
|
label: Some("egui_pipeline_layout"),
|
||||||
bind_group_layouts: &[
|
bind_group_layouts: &[
|
||||||
@@ -434,9 +461,9 @@ impl Renderer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const VERTEX_BUFFER_START_CAPACITY: wgpu::BufferAddress =
|
const VERTEX_BUFFER_START_CAPACITY: wgpu::BufferAddress =
|
||||||
(std::mem::size_of::<Vertex>() * 1024) as _;
|
(core::mem::size_of::<Vertex>() * 1024) as _;
|
||||||
const INDEX_BUFFER_START_CAPACITY: wgpu::BufferAddress =
|
const INDEX_BUFFER_START_CAPACITY: wgpu::BufferAddress =
|
||||||
(std::mem::size_of::<u32>() * 1024 * 3) as _;
|
(core::mem::size_of::<u32>() * 1024 * 3) as _;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
pipeline,
|
pipeline,
|
||||||
@@ -455,6 +482,7 @@ impl Renderer {
|
|||||||
previous_uniform_buffer_content: UniformBuffer::zeroed(),
|
previous_uniform_buffer_content: UniformBuffer::zeroed(),
|
||||||
uniform_bind_group,
|
uniform_bind_group,
|
||||||
texture_bind_group_layout,
|
texture_bind_group_layout,
|
||||||
|
nearest_filtering_flag_buffers,
|
||||||
textures: HashMap::default(),
|
textures: HashMap::default(),
|
||||||
next_user_texture_id: 0,
|
next_user_texture_id: 0,
|
||||||
samplers: HashMap::default(),
|
samplers: HashMap::default(),
|
||||||
@@ -706,6 +734,8 @@ impl Renderer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let bind_group = bind_group.unwrap_or_else(|| {
|
let bind_group = bind_group.unwrap_or_else(|| {
|
||||||
|
let nearest =
|
||||||
|
image_delta.options.magnification == epaint::textures::TextureFilter::Nearest;
|
||||||
let sampler = self
|
let sampler = self
|
||||||
.samplers
|
.samplers
|
||||||
.entry(image_delta.options)
|
.entry(image_delta.options)
|
||||||
@@ -724,6 +754,11 @@ impl Renderer {
|
|||||||
binding: 1,
|
binding: 1,
|
||||||
resource: wgpu::BindingResource::Sampler(sampler),
|
resource: wgpu::BindingResource::Sampler(sampler),
|
||||||
},
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 2,
|
||||||
|
resource: self.nearest_filtering_flag_buffers[usize::from(nearest)]
|
||||||
|
.as_entire_binding(),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -826,6 +861,7 @@ impl Renderer {
|
|||||||
) -> epaint::TextureId {
|
) -> epaint::TextureId {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|
||||||
|
let nearest = sampler_descriptor.mag_filter == wgpu::FilterMode::Nearest;
|
||||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
compare: None,
|
compare: None,
|
||||||
..sampler_descriptor
|
..sampler_descriptor
|
||||||
@@ -843,6 +879,11 @@ impl Renderer {
|
|||||||
binding: 1,
|
binding: 1,
|
||||||
resource: wgpu::BindingResource::Sampler(&sampler),
|
resource: wgpu::BindingResource::Sampler(&sampler),
|
||||||
},
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 2,
|
||||||
|
resource: self.nearest_filtering_flag_buffers[usize::from(nearest)]
|
||||||
|
.as_entire_binding(),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -882,6 +923,7 @@ impl Renderer {
|
|||||||
.get_mut(&id)
|
.get_mut(&id)
|
||||||
.expect("Tried to update a texture that has not been allocated yet.");
|
.expect("Tried to update a texture that has not been allocated yet.");
|
||||||
|
|
||||||
|
let nearest = sampler_descriptor.mag_filter == wgpu::FilterMode::Nearest;
|
||||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
compare: None,
|
compare: None,
|
||||||
..sampler_descriptor
|
..sampler_descriptor
|
||||||
@@ -899,6 +941,11 @@ impl Renderer {
|
|||||||
binding: 1,
|
binding: 1,
|
||||||
resource: wgpu::BindingResource::Sampler(&sampler),
|
resource: wgpu::BindingResource::Sampler(&sampler),
|
||||||
},
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 2,
|
||||||
|
resource: self.nearest_filtering_flag_buffers[usize::from(nearest)]
|
||||||
|
.as_entire_binding(),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -962,7 +1009,7 @@ impl Renderer {
|
|||||||
|
|
||||||
self.index_buffer.slices.clear();
|
self.index_buffer.slices.clear();
|
||||||
|
|
||||||
let required_index_buffer_size = (std::mem::size_of::<u32>() * index_count) as u64;
|
let required_index_buffer_size = (core::mem::size_of::<u32>() * index_count) as u64;
|
||||||
if self.index_buffer.capacity < required_index_buffer_size {
|
if self.index_buffer.capacity < required_index_buffer_size {
|
||||||
// Resize index buffer if needed.
|
// Resize index buffer if needed.
|
||||||
self.index_buffer.capacity =
|
self.index_buffer.capacity =
|
||||||
@@ -989,7 +1036,7 @@ impl Renderer {
|
|||||||
for epaint::ClippedPrimitive { primitive, .. } in paint_jobs {
|
for epaint::ClippedPrimitive { primitive, .. } in paint_jobs {
|
||||||
match primitive {
|
match primitive {
|
||||||
Primitive::Mesh(mesh) => {
|
Primitive::Mesh(mesh) => {
|
||||||
let size = mesh.indices.len() * std::mem::size_of::<u32>();
|
let size = mesh.indices.len() * core::mem::size_of::<u32>();
|
||||||
let slice = index_offset..(size + index_offset);
|
let slice = index_offset..(size + index_offset);
|
||||||
index_buffer_staging
|
index_buffer_staging
|
||||||
.slice(slice.clone())
|
.slice(slice.clone())
|
||||||
@@ -1006,7 +1053,8 @@ impl Renderer {
|
|||||||
|
|
||||||
self.vertex_buffer.slices.clear();
|
self.vertex_buffer.slices.clear();
|
||||||
|
|
||||||
let required_vertex_buffer_size = (std::mem::size_of::<Vertex>() * vertex_count) as u64;
|
let required_vertex_buffer_size =
|
||||||
|
(core::mem::size_of::<Vertex>() * vertex_count) as u64;
|
||||||
if self.vertex_buffer.capacity < required_vertex_buffer_size {
|
if self.vertex_buffer.capacity < required_vertex_buffer_size {
|
||||||
// Resize vertex buffer if needed.
|
// Resize vertex buffer if needed.
|
||||||
self.vertex_buffer.capacity =
|
self.vertex_buffer.capacity =
|
||||||
@@ -1034,7 +1082,7 @@ impl Renderer {
|
|||||||
for epaint::ClippedPrimitive { primitive, .. } in paint_jobs {
|
for epaint::ClippedPrimitive { primitive, .. } in paint_jobs {
|
||||||
match primitive {
|
match primitive {
|
||||||
Primitive::Mesh(mesh) => {
|
Primitive::Mesh(mesh) => {
|
||||||
let size = mesh.vertices.len() * std::mem::size_of::<Vertex>();
|
let size = mesh.vertices.len() * core::mem::size_of::<Vertex>();
|
||||||
let slice = vertex_offset..(size + vertex_offset);
|
let slice = vertex_offset..(size + vertex_offset);
|
||||||
vertex_buffer_staging
|
vertex_buffer_staging
|
||||||
.slice(slice.clone())
|
.slice(slice.clone())
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use std::sync::Arc;
|
|||||||
/// Automatically implemented for all types that satisfy the bounds
|
/// Automatically implemented for all types that satisfy the bounds
|
||||||
/// (including [`winit::event_loop::OwnedDisplayHandle`]).
|
/// (including [`winit::event_loop::OwnedDisplayHandle`]).
|
||||||
pub trait EguiDisplayHandle:
|
pub trait EguiDisplayHandle:
|
||||||
wgpu::rwh::HasDisplayHandle + std::fmt::Debug + Send + Sync + 'static
|
wgpu::rwh::HasDisplayHandle + core::fmt::Debug + Send + Sync + 'static
|
||||||
{
|
{
|
||||||
/// Clone into a `Box<dyn WgpuHasDisplayHandle>` for [`wgpu::InstanceDescriptor::display`].
|
/// Clone into a `Box<dyn WgpuHasDisplayHandle>` for [`wgpu::InstanceDescriptor::display`].
|
||||||
fn clone_for_wgpu(&self) -> Box<dyn wgpu::wgt::WgpuHasDisplayHandle>;
|
fn clone_for_wgpu(&self) -> Box<dyn wgpu::wgt::WgpuHasDisplayHandle>;
|
||||||
@@ -27,7 +27,7 @@ impl Clone for Box<dyn EguiDisplayHandle> {
|
|||||||
|
|
||||||
impl<T> EguiDisplayHandle for T
|
impl<T> EguiDisplayHandle for T
|
||||||
where
|
where
|
||||||
T: wgpu::rwh::HasDisplayHandle + Clone + std::fmt::Debug + Send + Sync + 'static,
|
T: wgpu::rwh::HasDisplayHandle + Clone + core::fmt::Debug + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
fn clone_for_wgpu(&self) -> Box<dyn wgpu::wgt::WgpuHasDisplayHandle> {
|
fn clone_for_wgpu(&self) -> Box<dyn wgpu::wgt::WgpuHasDisplayHandle> {
|
||||||
Box::new(self.clone())
|
Box::new(self.clone())
|
||||||
@@ -77,8 +77,8 @@ impl WgpuSetup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for WgpuSetup {
|
impl core::fmt::Debug for WgpuSetup {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::CreateNew(create_new) => f
|
Self::CreateNew(create_new) => f
|
||||||
.debug_tuple("WgpuSetup::CreateNew")
|
.debug_tuple("WgpuSetup::CreateNew")
|
||||||
@@ -295,8 +295,8 @@ impl Clone for WgpuSetupCreateNew {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for WgpuSetupCreateNew {
|
impl core::fmt::Debug for WgpuSetupCreateNew {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
let Self {
|
let Self {
|
||||||
instance_descriptor,
|
instance_descriptor,
|
||||||
display_handle,
|
display_handle,
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ use crate::{
|
|||||||
RendererOptions,
|
RendererOptions,
|
||||||
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
|
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
|
||||||
};
|
};
|
||||||
|
use core::num::NonZeroU32;
|
||||||
use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet};
|
use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet};
|
||||||
use std::{num::NonZeroU32, sync::Arc};
|
use std::sync::Arc;
|
||||||
|
|
||||||
struct SurfaceState {
|
struct SurfaceState {
|
||||||
surface: wgpu::Surface<'static>,
|
surface: wgpu::Surface<'static>,
|
||||||
@@ -478,7 +479,7 @@ impl Painter {
|
|||||||
pixels_per_point: f32,
|
pixels_per_point: f32,
|
||||||
clear_color: [f32; 4],
|
clear_color: [f32; 4],
|
||||||
clipped_primitives: &[epaint::ClippedPrimitive],
|
clipped_primitives: &[epaint::ClippedPrimitive],
|
||||||
textures_delta: &epaint::textures::TexturesDelta,
|
textures_delta: &mut epaint::textures::TexturesDelta,
|
||||||
capture_data: Vec<UserData>,
|
capture_data: Vec<UserData>,
|
||||||
window: &Arc<winit::window::Window>,
|
window: &Arc<winit::window::Window>,
|
||||||
) -> f32 {
|
) -> f32 {
|
||||||
@@ -545,21 +546,6 @@ impl Painter {
|
|||||||
commands_submitted: false,
|
commands_submitted: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
|
||||||
// Upload textures before the surface-dependent early-returns below:
|
|
||||||
// uploads only need the device + queue, and the atlas dirty region is
|
|
||||||
// already consumed, so dropping the delta would desync the font texture.
|
|
||||||
let mut renderer = render_state.renderer.write();
|
|
||||||
for (id, image_delta) in &textures_delta.set {
|
|
||||||
renderer.update_texture(
|
|
||||||
&render_state.device,
|
|
||||||
&render_state.queue,
|
|
||||||
*id,
|
|
||||||
image_delta,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(surface_state) = self.surfaces.get_mut(&viewport_id) else {
|
let Some(surface_state) = self.surfaces.get_mut(&viewport_id) else {
|
||||||
return vsync_sec;
|
return vsync_sec;
|
||||||
};
|
};
|
||||||
@@ -579,6 +565,18 @@ impl Painter {
|
|||||||
|
|
||||||
let user_cmd_bufs = {
|
let user_cmd_bufs = {
|
||||||
let mut renderer = render_state.renderer.write();
|
let mut renderer = render_state.renderer.write();
|
||||||
|
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
|
||||||
|
for (id, image_deltas) in textures_delta.set.drain() {
|
||||||
|
for image_delta in image_deltas {
|
||||||
|
renderer.update_texture(
|
||||||
|
&render_state.device,
|
||||||
|
&render_state.queue,
|
||||||
|
id,
|
||||||
|
&image_delta,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderer.update_buffers(
|
renderer.update_buffers(
|
||||||
&render_state.device,
|
&render_state.device,
|
||||||
&render_state.queue,
|
&render_state.queue,
|
||||||
@@ -730,7 +728,7 @@ impl Painter {
|
|||||||
let start = web_time::Instant::now();
|
let start = web_time::Instant::now();
|
||||||
render_state
|
render_state
|
||||||
.queue
|
.queue
|
||||||
.submit(std::iter::chain(user_cmd_bufs, [encoded]));
|
.submit(core::iter::chain(user_cmd_bufs, [encoded]));
|
||||||
vsync_sec += start.elapsed().as_secs_f32();
|
vsync_sec += start.elapsed().as_secs_f32();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -742,8 +740,9 @@ impl Painter {
|
|||||||
// However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
|
// However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
|
||||||
{
|
{
|
||||||
let mut renderer = render_state.renderer.write();
|
let mut renderer = render_state.renderer.write();
|
||||||
for id in &textures_delta.free {
|
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
|
||||||
renderer.free_texture(id);
|
for id in textures_delta.free.drain() {
|
||||||
|
renderer.free_texture(&id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ 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.
|
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.36.1 - 2026-08-07
|
||||||
|
Nothing new
|
||||||
|
|
||||||
|
|
||||||
|
## 0.36.0 - 2026-08-05
|
||||||
|
Nothing new
|
||||||
|
|
||||||
|
|
||||||
## 0.35.0 - 2026-06-25
|
## 0.35.0 - 2026-06-25
|
||||||
* Delegate handling of IME interruptions to integrations to fix virtual keyboard flickering on web [#8078](https://github.com/emilk/egui/pull/8078) by [@umajho](https://github.com/umajho)
|
* Delegate handling of IME interruptions to integrations to fix virtual keyboard flickering on web [#8078](https://github.com/emilk/egui/pull/8078) by [@umajho](https://github.com/umajho)
|
||||||
* Always enable windows undecorated shadows [#8169](https://github.com/emilk/egui/pull/8169) by [@Wumpf](https://github.com/Wumpf)
|
* Always enable windows undecorated shadows [#8169](https://github.com/emilk/egui/pull/8169) by [@Wumpf](https://github.com/Wumpf)
|
||||||
|
|||||||
22
crates/egui-winit/src/dropped_file.rs
Normal file
22
crates/egui-winit/src/dropped_file.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct NativeFile {
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PathBuf> for NativeFile {
|
||||||
|
fn from(path: PathBuf) -> Self {
|
||||||
|
Self { path }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl egui::DroppedFile for NativeFile {
|
||||||
|
fn path(&self) -> &Path {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes(&self) -> Result<Vec<u8>, String> {
|
||||||
|
std::fs::read(&self.path).map_err(|err| err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ use egui::{Pos2, Rect, Theme, Vec2, ViewportBuilder, ViewportCommand, ViewportId
|
|||||||
pub use winit;
|
pub use winit;
|
||||||
|
|
||||||
pub mod clipboard;
|
pub mod clipboard;
|
||||||
|
mod dropped_file;
|
||||||
mod safe_area;
|
mod safe_area;
|
||||||
mod window_settings;
|
mod window_settings;
|
||||||
|
|
||||||
@@ -28,6 +29,8 @@ pub use window_settings::WindowSettings;
|
|||||||
|
|
||||||
use raw_window_handle::HasDisplayHandle;
|
use raw_window_handle::HasDisplayHandle;
|
||||||
|
|
||||||
|
use dropped_file::NativeFile;
|
||||||
|
|
||||||
use winit::{
|
use winit::{
|
||||||
dpi::{PhysicalPosition, PhysicalSize},
|
dpi::{PhysicalPosition, PhysicalSize},
|
||||||
event::ElementState,
|
event::ElementState,
|
||||||
@@ -121,6 +124,7 @@ pub struct State {
|
|||||||
|
|
||||||
allow_ime: bool,
|
allow_ime: bool,
|
||||||
ime_rect_px: Option<egui::Rect>,
|
ime_rect_px: Option<egui::Rect>,
|
||||||
|
old_ime_purpose: egui::IMEPurpose,
|
||||||
|
|
||||||
/// Used by [`State::try_on_ime_processed_keyboard_input`] to track key
|
/// Used by [`State::try_on_ime_processed_keyboard_input`] to track key
|
||||||
/// release events that should be filtered out. See comments in that method
|
/// release events that should be filtered out. See comments in that method
|
||||||
@@ -171,6 +175,7 @@ impl State {
|
|||||||
|
|
||||||
allow_ime: false,
|
allow_ime: false,
|
||||||
ime_rect_px: None,
|
ime_rect_px: None,
|
||||||
|
old_ime_purpose: egui::IMEPurpose::Normal,
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pressed_processed_physical_keys: HashSet::new(),
|
pressed_processed_physical_keys: HashSet::new(),
|
||||||
};
|
};
|
||||||
@@ -468,10 +473,9 @@ impl State {
|
|||||||
}
|
}
|
||||||
WindowEvent::DroppedFile(path) => {
|
WindowEvent::DroppedFile(path) => {
|
||||||
self.egui_input.hovered_files.clear();
|
self.egui_input.hovered_files.clear();
|
||||||
self.egui_input.dropped_files.push(egui::DroppedFile {
|
self.egui_input
|
||||||
path: Some(path.clone()),
|
.dropped_files
|
||||||
..Default::default()
|
.push(std::sync::Arc::new(NativeFile::from(path.clone())));
|
||||||
});
|
|
||||||
EventResponse {
|
EventResponse {
|
||||||
repaint: true,
|
repaint: true,
|
||||||
consumed: false,
|
consumed: false,
|
||||||
@@ -1158,6 +1162,11 @@ impl State {
|
|||||||
window.set_ime_allowed(true);
|
window.set_ime_allowed(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ime.purpose != self.old_ime_purpose {
|
||||||
|
self.old_ime_purpose = ime.purpose;
|
||||||
|
window.set_ime_purpose(to_winit_ime_purpose(ime.purpose));
|
||||||
|
}
|
||||||
|
|
||||||
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
|
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
|
||||||
let ime_rect_px = pixels_per_point * ime.rect;
|
let ime_rect_px = pixels_per_point * ime.rect;
|
||||||
if self.ime_rect_px != Some(ime_rect_px)
|
if self.ime_rect_px != Some(ime_rect_px)
|
||||||
@@ -1880,11 +1889,7 @@ fn process_viewport_command(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
ViewportCommand::IMEAllowed(v) => window.set_ime_allowed(v),
|
ViewportCommand::IMEAllowed(v) => window.set_ime_allowed(v),
|
||||||
ViewportCommand::IMEPurpose(p) => window.set_ime_purpose(match p {
|
ViewportCommand::IMEPurpose(p) => window.set_ime_purpose(to_winit_ime_purpose(p)),
|
||||||
egui::viewport::IMEPurpose::Password => winit::window::ImePurpose::Password,
|
|
||||||
egui::viewport::IMEPurpose::Terminal => winit::window::ImePurpose::Terminal,
|
|
||||||
egui::viewport::IMEPurpose::Normal => winit::window::ImePurpose::Normal,
|
|
||||||
}),
|
|
||||||
ViewportCommand::Focus => {
|
ViewportCommand::Focus => {
|
||||||
if !window.has_focus() {
|
if !window.has_focus() {
|
||||||
window.focus_window();
|
window.focus_window();
|
||||||
@@ -1945,6 +1950,14 @@ fn process_viewport_command(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn to_winit_ime_purpose(purpose: egui::IMEPurpose) -> winit::window::ImePurpose {
|
||||||
|
match purpose {
|
||||||
|
egui::IMEPurpose::Password => winit::window::ImePurpose::Password,
|
||||||
|
egui::IMEPurpose::Terminal => winit::window::ImePurpose::Terminal,
|
||||||
|
egui::IMEPurpose::Normal => winit::window::ImePurpose::Normal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Build and intitlaize a window.
|
/// Build and intitlaize a window.
|
||||||
///
|
///
|
||||||
/// Wrapper around `create_winit_window_builder` and `apply_viewport_builder_to_window`.
|
/// Wrapper around `create_winit_window_builder` and `apply_viewport_builder_to_window`.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ workspace = true
|
|||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
rustdoc-args = ["--generate-link-to-definition"]
|
rustdoc-args = ["--generate-link-to-definition"]
|
||||||
|
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|
||||||
@@ -96,3 +97,9 @@ document-features = { workspace = true, optional = true }
|
|||||||
|
|
||||||
ron = { workspace = true, optional = true }
|
ron = { workspace = true, optional = true }
|
||||||
serde = { workspace = true, optional = true, features = ["derive", "rc"] }
|
serde = { workspace = true, optional = true, features = ["derive", "rc"] }
|
||||||
|
|
||||||
|
|
||||||
|
# web:
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
# For `DroppedFile`, which hands web apps a file handle instead of its contents.
|
||||||
|
web-sys = { workspace = true, features = ["File"] }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::{AtomLayout, FontSelection, Image, ImageSource, SizedAtomKind, Ui, WidgetText};
|
use crate::{AtomLayout, FontSelection, Image, ImageSource, SizedAtomKind, Ui, WidgetText};
|
||||||
|
use core::fmt::Debug;
|
||||||
use emath::Vec2;
|
use emath::Vec2;
|
||||||
use epaint::text::TextWrapMode;
|
use epaint::text::TextWrapMode;
|
||||||
use std::fmt::Debug;
|
|
||||||
|
|
||||||
/// Args passed when sizing an [`super::Atom`]
|
/// Args passed when sizing an [`super::Atom`]
|
||||||
pub struct IntoSizedArgs {
|
pub struct IntoSizedArgs {
|
||||||
@@ -90,7 +90,7 @@ impl Clone for AtomKind<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for AtomKind<'_> {
|
impl Debug for AtomKind<'_> {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
AtomKind::Empty => write!(f, "AtomKind::Empty"),
|
AtomKind::Empty => write!(f, "AtomKind::Empty"),
|
||||||
AtomKind::Text(text) => write!(f, "AtomKind::Text({text:?})"),
|
AtomKind::Text(text) => write!(f, "AtomKind::Text({text:?})"),
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ use crate::{
|
|||||||
AtomKind, Atoms, Direction, FontSelection, Frame, Id, Image, IntoAtoms, Response, Sense,
|
AtomKind, Atoms, Direction, FontSelection, Frame, Id, Image, IntoAtoms, Response, Sense,
|
||||||
SizedAtom, SizedAtomKind, Stroke, Ui, Widget, text_selection::LabelSelectionState,
|
SizedAtom, SizedAtomKind, Stroke, Ui, Widget, text_selection::LabelSelectionState,
|
||||||
};
|
};
|
||||||
|
use core::ops::{Deref, DerefMut};
|
||||||
use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2};
|
use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2};
|
||||||
use epaint::text::TextWrapMode;
|
use epaint::text::TextWrapMode;
|
||||||
use epaint::{Color32, Galley};
|
use epaint::{Color32, Galley};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// The `(main, cross)` axis indices for `direction`, for indexing a [`Vec2`] (0 = x, 1 = y).
|
/// The `(main, cross)` axis indices for `direction`, for indexing a [`Vec2`] (0 = x, 1 = y).
|
||||||
@@ -557,7 +557,7 @@ impl<'atom> SizedAtomLayout<'atom> {
|
|||||||
F: FnMut(SizedAtomKind<'atom>) -> SizedAtomKind<'atom>,
|
F: FnMut(SizedAtomKind<'atom>) -> SizedAtomKind<'atom>,
|
||||||
{
|
{
|
||||||
for kind in self.iter_kinds_mut() {
|
for kind in self.iter_kinds_mut() {
|
||||||
*kind = f(std::mem::take(kind));
|
*kind = f(core::mem::take(kind));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{Atom, AtomKind, Image, WidgetText};
|
use crate::{Atom, AtomKind, Image, WidgetText};
|
||||||
|
use core::ops::{Deref, DerefMut};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
|
|
||||||
/// A list of [`Atom`]s.
|
/// A list of [`Atom`]s.
|
||||||
///
|
///
|
||||||
@@ -41,7 +41,7 @@ impl<'a> Atoms<'a> {
|
|||||||
///
|
///
|
||||||
/// If you have weird lifetime issues with this, use [`Self::push_left`] in a loop instead.
|
/// If you have weird lifetime issues with this, use [`Self::push_left`] in a loop instead.
|
||||||
pub fn extend_left(&mut self, mut atoms: Self) {
|
pub fn extend_left(&mut self, mut atoms: Self) {
|
||||||
std::mem::swap(&mut atoms.0, &mut self.0);
|
core::mem::swap(&mut atoms.0, &mut self.0);
|
||||||
self.0.extend(atoms.0);
|
self.0.extend(atoms.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ impl<'a> Atoms<'a> {
|
|||||||
|
|
||||||
pub fn map_atoms(&mut self, mut f: impl FnMut(Atom<'a>) -> Atom<'a>) {
|
pub fn map_atoms(&mut self, mut f: impl FnMut(Atom<'a>) -> Atom<'a>) {
|
||||||
self.iter_mut()
|
self.iter_mut()
|
||||||
.for_each(|atom| *atom = f(std::mem::take(atom)));
|
.for_each(|atom| *atom = f(core::mem::take(atom)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn map_kind<F>(&mut self, mut f: F)
|
pub fn map_kind<F>(&mut self, mut f: F)
|
||||||
@@ -136,7 +136,7 @@ impl<'a> Atoms<'a> {
|
|||||||
F: FnMut(AtomKind<'a>) -> AtomKind<'a>,
|
F: FnMut(AtomKind<'a>) -> AtomKind<'a>,
|
||||||
{
|
{
|
||||||
for kind in self.iter_kinds_mut() {
|
for kind in self.iter_kinds_mut() {
|
||||||
*kind = f(std::mem::take(kind));
|
*kind = f(core::mem::take(kind));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
crates/egui/src/cache/cache_storage.rs
vendored
10
crates/egui/src/cache/cache_storage.rs
vendored
@@ -23,18 +23,18 @@ use super::CacheTrait;
|
|||||||
/// ```
|
/// ```
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct CacheStorage {
|
pub struct CacheStorage {
|
||||||
caches: ahash::HashMap<std::any::TypeId, Box<dyn CacheTrait>>,
|
caches: ahash::HashMap<core::any::TypeId, Box<dyn CacheTrait>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CacheStorage {
|
impl CacheStorage {
|
||||||
pub fn cache<Cache: CacheTrait + Default>(&mut self) -> &mut Cache {
|
pub fn cache<Cache: CacheTrait + Default>(&mut self) -> &mut Cache {
|
||||||
let cache = self
|
let cache = self
|
||||||
.caches
|
.caches
|
||||||
.entry(std::any::TypeId::of::<Cache>())
|
.entry(core::any::TypeId::of::<Cache>())
|
||||||
.or_insert_with(|| Box::<Cache>::default());
|
.or_insert_with(|| Box::<Cache>::default());
|
||||||
|
|
||||||
#[expect(clippy::unwrap_used)]
|
#[expect(clippy::unwrap_used)]
|
||||||
(cache.as_mut() as &mut dyn std::any::Any)
|
(cache.as_mut() as &mut dyn core::any::Any)
|
||||||
.downcast_mut::<Cache>()
|
.downcast_mut::<Cache>()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
@@ -60,8 +60,8 @@ impl Clone for CacheStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for CacheStorage {
|
impl core::fmt::Debug for CacheStorage {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"FrameCacheStorage[{} caches with {} elements]",
|
"FrameCacheStorage[{} caches with {} elements]",
|
||||||
|
|||||||
2
crates/egui/src/cache/cache_trait.rs
vendored
2
crates/egui/src/cache/cache_trait.rs
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// A cache, storing some value for some length of time.
|
/// A cache, storing some value for some length of time.
|
||||||
#[expect(clippy::len_without_is_empty)]
|
#[expect(clippy::len_without_is_empty)]
|
||||||
pub trait CacheTrait: 'static + Send + Sync + std::any::Any {
|
pub trait CacheTrait: 'static + Send + Sync + core::any::Any {
|
||||||
/// Call once per frame to evict cache.
|
/// Call once per frame to evict cache.
|
||||||
fn update(&mut self);
|
fn update(&mut self);
|
||||||
|
|
||||||
|
|||||||
2
crates/egui/src/cache/frame_cache.rs
vendored
2
crates/egui/src/cache/frame_cache.rs
vendored
@@ -48,7 +48,7 @@ impl<Value, Computer> FrameCache<Value, Computer> {
|
|||||||
/// or recompute and store in the cache.
|
/// or recompute and store in the cache.
|
||||||
pub fn get<Key>(&mut self, key: Key) -> &Value
|
pub fn get<Key>(&mut self, key: Key) -> &Value
|
||||||
where
|
where
|
||||||
Key: Copy + std::hash::Hash,
|
Key: Copy + core::hash::Hash,
|
||||||
Computer: ComputerMut<Key, Value>,
|
Computer: ComputerMut<Key, Value>,
|
||||||
{
|
{
|
||||||
let hash = crate::util::hash(key);
|
let hash = crate::util::hash(key);
|
||||||
|
|||||||
2
crates/egui/src/cache/frame_publisher.rs
vendored
2
crates/egui/src/cache/frame_publisher.rs
vendored
@@ -1,4 +1,4 @@
|
|||||||
use std::hash::Hash;
|
use core::hash::Hash;
|
||||||
|
|
||||||
use super::CacheTrait;
|
use super::CacheTrait;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::fmt::Write as _;
|
use core::fmt::Write as _;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct Frame {
|
struct Frame {
|
||||||
@@ -239,7 +239,7 @@ fn test_shorten_path() {
|
|||||||
),
|
),
|
||||||
("/weird/path/file.rs", "/weird/path/file.rs"),
|
("/weird/path/file.rs", "/weird/path/file.rs"),
|
||||||
] {
|
] {
|
||||||
use std::str::FromStr as _;
|
use core::str::FromStr as _;
|
||||||
let before = std::path::PathBuf::from_str(before).unwrap();
|
let before = std::path::PathBuf::from_str(before).unwrap();
|
||||||
assert_eq!(shorten_source_file_path(&before), after);
|
assert_eq!(shorten_source_file_path(&before), after);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -454,14 +454,12 @@ impl Area {
|
|||||||
state.size = None;
|
state.size = None;
|
||||||
}
|
}
|
||||||
state.pivot = pivot;
|
state.pivot = pivot;
|
||||||
state.interactable = interactable;
|
|
||||||
if let Some(new_pos) = new_pos {
|
if let Some(new_pos) = new_pos {
|
||||||
state.pivot_pos = Some(new_pos);
|
state.pivot_pos = Some(new_pos);
|
||||||
}
|
}
|
||||||
state.pivot_pos.get_or_insert_with(|| {
|
state.pivot_pos.get_or_insert_with(|| {
|
||||||
default_pos.unwrap_or_else(|| automatic_area_position(ctx, constrain_rect, layer_id))
|
default_pos.unwrap_or_else(|| automatic_area_position(ctx, constrain_rect, layer_id))
|
||||||
});
|
});
|
||||||
state.interactable = interactable;
|
|
||||||
|
|
||||||
let size = *state.size.get_or_insert_with(|| {
|
let size = *state.size.get_or_insert_with(|| {
|
||||||
sizing_pass = true;
|
sizing_pass = true;
|
||||||
@@ -484,6 +482,10 @@ impl Area {
|
|||||||
size
|
size
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// We should never be interactable during a sizing pass, since then we are shown at a different
|
||||||
|
// size which might interfere with hover state of the hovered widget causing popup feedback loops.
|
||||||
|
state.interactable = interactable && !sizing_pass;
|
||||||
|
|
||||||
// TODO(emilk): if last frame was sizing pass, it should be considered invisible for smoother fade-in
|
// TODO(emilk): if last frame was sizing pass, it should be considered invisible for smoother fade-in
|
||||||
let visible_last_frame = ctx.memory(|mem| mem.areas().visible_last_frame(&layer_id));
|
let visible_last_frame = ctx.memory(|mem| mem.areas().visible_last_frame(&layer_id));
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#[expect(unused_imports)]
|
#[expect(unused_imports)]
|
||||||
use crate::{Ui, UiBuilder};
|
use crate::{Ui, UiBuilder};
|
||||||
use std::sync::atomic::AtomicBool;
|
use core::sync::atomic::AtomicBool;
|
||||||
|
|
||||||
/// A tag to mark a container as closable.
|
/// A tag to mark a container as closable.
|
||||||
///
|
///
|
||||||
@@ -18,11 +18,12 @@ impl ClosableTag {
|
|||||||
|
|
||||||
/// Set close to `true`
|
/// Set close to `true`
|
||||||
pub fn set_close(&self) {
|
pub fn set_close(&self) {
|
||||||
self.close.store(true, std::sync::atomic::Ordering::Relaxed);
|
self.close
|
||||||
|
.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if [`ClosableTag::set_close`] has been called.
|
/// Returns `true` if [`ClosableTag::set_close`] has been called.
|
||||||
pub fn should_close(&self) -> bool {
|
pub fn should_close(&self) -> bool {
|
||||||
self.close.load(std::sync::atomic::Ordering::Relaxed)
|
self.close.load(core::sync::atomic::Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) {
|
|||||||
let rect = Rect::from_center_size(rect.center(), vec2(rect.width(), rect.height()) * 0.75);
|
let rect = Rect::from_center_size(rect.center(), vec2(rect.width(), rect.height()) * 0.75);
|
||||||
let rect = rect.expand(visuals.expansion);
|
let rect = rect.expand(visuals.expansion);
|
||||||
let mut points = vec![rect.left_top(), rect.right_top(), rect.center_bottom()];
|
let mut points = vec![rect.left_top(), rect.right_top(), rect.center_bottom()];
|
||||||
use std::f32::consts::TAU;
|
use core::f32::consts::TAU;
|
||||||
let rotation = emath::Rot2::from_angle(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0));
|
let rotation = emath::Rot2::from_angle(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0));
|
||||||
for p in &mut points {
|
for p in &mut points {
|
||||||
*p = rect.center() + rotation * (*p - rect.center());
|
*p = rect.center() + rotation * (*p - rect.center());
|
||||||
|
|||||||
@@ -143,12 +143,12 @@ pub struct Frame {
|
|||||||
#[test]
|
#[test]
|
||||||
fn frame_size() {
|
fn frame_size() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
std::mem::size_of::<Frame>(),
|
core::mem::size_of::<Frame>(),
|
||||||
32,
|
32,
|
||||||
"Frame changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it."
|
"Frame changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it."
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
std::mem::size_of::<Frame>() <= 64,
|
core::mem::size_of::<Frame>() <= 64,
|
||||||
"Frame is getting way too big!"
|
"Frame is getting way too big!"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,14 +18,24 @@
|
|||||||
use emath::GuiRounding as _;
|
use emath::GuiRounding as _;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Align, Context, CursorIcon, Frame, Id, InnerResponse, Layout, NumExt as _, Rangef, Rect,
|
Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, Margin, NumExt as _,
|
||||||
Response, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp,
|
Order, Rangef, Rect, Response, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 {
|
fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 {
|
||||||
ctx.animate_bool_responsive(id, is_expanded)
|
ctx.animate_bool_responsive(id, is_expanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [`Id`] of a panel's resize-handle widget.
|
||||||
|
///
|
||||||
|
/// A panel registers its handle under this same id whether it is open,
|
||||||
|
/// mid-slide, or fully collapsed — that is what lets one uninterrupted drag
|
||||||
|
/// collapse the panel and pull it back open. [`Panel::show_switched`] points
|
||||||
|
/// both of its panels at one shared handle the same way.
|
||||||
|
fn resize_widget_id(id_source: Id) -> Id {
|
||||||
|
id_source.with("__resize")
|
||||||
|
}
|
||||||
|
|
||||||
/// State regarding panels.
|
/// State regarding panels.
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
@@ -126,6 +136,22 @@ impl PanelSide {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The component of `margin` on the panel's _resizable_ edge,
|
||||||
|
/// i.e. the edge facing the rest of the ui, where the separator line goes.
|
||||||
|
fn resize_margin(self, mut margin: Margin) -> i8 {
|
||||||
|
*self.resize_margin_mut(&mut margin)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mutable version of [`Self::resize_margin`].
|
||||||
|
fn resize_margin_mut(self, margin: &mut Margin) -> &mut i8 {
|
||||||
|
match self {
|
||||||
|
Self::Left => &mut margin.right,
|
||||||
|
Self::Right => &mut margin.left,
|
||||||
|
Self::Top => &mut margin.bottom,
|
||||||
|
Self::Bottom => &mut margin.top,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Resize by keeping `self` side fixed, and moving the opposite side.
|
/// Resize by keeping `self` side fixed, and moving the opposite side.
|
||||||
fn set_rect_size(self, rect: &mut Rect, size: f32) {
|
fn set_rect_size(self, rect: &mut Rect, size: f32) {
|
||||||
match self {
|
match self {
|
||||||
@@ -182,6 +208,7 @@ pub struct Panel {
|
|||||||
id: Id,
|
id: Id,
|
||||||
frame: Option<Frame>,
|
frame: Option<Frame>,
|
||||||
resizable: bool,
|
resizable: bool,
|
||||||
|
drag_to_open: bool,
|
||||||
show_separator_line: bool,
|
show_separator_line: bool,
|
||||||
|
|
||||||
/// _Outer_ size (including [`Frame`] margin & border):
|
/// _Outer_ size (including [`Frame`] margin & border):
|
||||||
@@ -267,6 +294,7 @@ impl Panel {
|
|||||||
id: id.into(),
|
id: id.into(),
|
||||||
frame: None,
|
frame: None,
|
||||||
resizable: true,
|
resizable: true,
|
||||||
|
drag_to_open: true,
|
||||||
show_separator_line: true,
|
show_separator_line: true,
|
||||||
default_outer_size,
|
default_outer_size,
|
||||||
outer_size_range,
|
outer_size_range,
|
||||||
@@ -296,8 +324,39 @@ impl Panel {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Can a fully collapsed panel be dragged back open?
|
||||||
|
///
|
||||||
|
/// Default: `true`.
|
||||||
|
///
|
||||||
|
/// When enabled, a panel that [`Self::show_collapsible`] has collapsed all
|
||||||
|
/// the way still leaves a thin grab handle at its fixed edge. The handle is
|
||||||
|
/// invisible until hovered, at which point it lights up like a normal resize
|
||||||
|
/// handle. Dragging it outward past [`Self::min_size`] — or double-clicking
|
||||||
|
/// it — reopens the panel.
|
||||||
|
///
|
||||||
|
/// This is the counterpart to drag-to-collapse, and like it requires
|
||||||
|
/// [`Self::resizable`] to be `true`.
|
||||||
|
#[inline]
|
||||||
|
pub fn drag_to_open(mut self, drag_to_open: bool) -> Self {
|
||||||
|
self.drag_to_open = drag_to_open;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Show a separator line, even when not interacting with it?
|
/// Show a separator line, even when not interacting with it?
|
||||||
///
|
///
|
||||||
|
/// The separator line sits on the panel's inner edge, i.e. the edge facing the rest of the ui.
|
||||||
|
/// It is painted _outside_ the [`Frame`]'s outline, in room the panel reserves for it in the
|
||||||
|
/// frame's [`Frame::outer_margin`], so that going from the panel contents outwards you get:
|
||||||
|
///
|
||||||
|
/// contents | [`Frame::inner_margin`] | [`Frame::stroke`] | separator line | [`Frame::outer_margin`]
|
||||||
|
///
|
||||||
|
/// Turning this off removes that reserved room too, so the panel gets no permanent gap along
|
||||||
|
/// that edge.
|
||||||
|
///
|
||||||
|
/// A `resizable` panel still shows a line while hovered or dragged, regardless of this setting.
|
||||||
|
/// With this setting off there is no room reserved for it, so that transient line is painted
|
||||||
|
/// just outside the frame's outline, overlapping the [`Frame::outer_margin`].
|
||||||
|
///
|
||||||
/// Default: `true`.
|
/// Default: `true`.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn show_separator_line(mut self, show_separator_line: bool) -> Self {
|
pub fn show_separator_line(mut self, show_separator_line: bool) -> Self {
|
||||||
@@ -386,6 +445,9 @@ impl Panel {
|
|||||||
/// to `true` if the user drags the handle outward while the panel is closed.
|
/// to `true` if the user drags the handle outward while the panel is closed.
|
||||||
/// When [`Self::resizable`] is `true`, double-clicking the resize edge also
|
/// When [`Self::resizable`] is `true`, double-clicking the resize edge also
|
||||||
/// flips `*is_expanded`.
|
/// flips `*is_expanded`.
|
||||||
|
///
|
||||||
|
/// A fully collapsed panel keeps a thin grab handle at its fixed edge, so the
|
||||||
|
/// user can drag it back open. See [`Self::drag_to_open`] to opt out.
|
||||||
pub fn show_collapsible<R>(
|
pub fn show_collapsible<R>(
|
||||||
self,
|
self,
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
@@ -395,10 +457,11 @@ impl Panel {
|
|||||||
let how_expanded = animate_expansion(ui, self.id.with("animation"), *is_expanded);
|
let how_expanded = animate_expansion(ui, self.id.with("animation"), *is_expanded);
|
||||||
|
|
||||||
if how_expanded == 0.0 {
|
if how_expanded == 0.0 {
|
||||||
// Panel is fully closed. If the user is still dragging the resize handle
|
// Panel is fully closed, but we still leave a grab handle at its fixed
|
||||||
// from a previous frame, keep its widget id alive so they can drag the
|
// edge so the user can drag it back open.
|
||||||
// panel back out without releasing.
|
if self.resizable && self.drag_to_open {
|
||||||
self.keep_drag_alive_for_reopen(ui, is_expanded);
|
self.collapsed_resize_handle(ui, is_expanded);
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure the ids of the next widgets are the same whether we show the panel or not:
|
// Make sure the ids of the next widgets are the same whether we show the panel or not:
|
||||||
ui.skip_ahead_auto_ids(1);
|
ui.skip_ahead_auto_ids(1);
|
||||||
@@ -407,7 +470,7 @@ impl Panel {
|
|||||||
|
|
||||||
// Don't lose the drag during the slide-back-open animation:
|
// Don't lose the drag during the slide-back-open animation:
|
||||||
let drag_in_progress = ui
|
let drag_in_progress = ui
|
||||||
.read_response(self.id.with("__resize"))
|
.read_response(self.resize_id())
|
||||||
.is_some_and(|r| r.dragged());
|
.is_some_and(|r| r.dragged());
|
||||||
|
|
||||||
let panel = if how_expanded < 1.0 {
|
let panel = if how_expanded < 1.0 {
|
||||||
@@ -520,20 +583,11 @@ impl Panel {
|
|||||||
|
|
||||||
// Is the resize handle currently being dragged?
|
// Is the resize handle currently being dragged?
|
||||||
let drag_in_progress = ui
|
let drag_in_progress = ui
|
||||||
.read_response(resize_id_source.with("__resize"))
|
.read_response(resize_widget_id(resize_id_source))
|
||||||
.is_some_and(|r| r.dragged());
|
.is_some_and(|r| r.dragged());
|
||||||
|
|
||||||
let animation_id = expanded_panel.id.with("animation");
|
let animation_id = expanded_panel.id.with("animation");
|
||||||
// While the user is dragging, snap the animation to the target so the
|
let how_expanded = animate_expansion(ui, animation_id, *is_expanded);
|
||||||
// drag (which sets `outer_size` directly from the pointer) doesn't fight
|
|
||||||
// a simultaneous slide. Without this, drag-to-expand visibly jumps as
|
|
||||||
// the slide animation tries to grow from 0 while the pointer is already
|
|
||||||
// at the expanded size.
|
|
||||||
let how_expanded = if drag_in_progress {
|
|
||||||
ui.animate_bool_with_time(animation_id, *is_expanded, 0.0)
|
|
||||||
} else {
|
|
||||||
animate_expansion(ui, animation_id, *is_expanded)
|
|
||||||
};
|
|
||||||
|
|
||||||
// When expanding, the user sees the expanded content the moment animation starts.
|
// When expanding, the user sees the expanded content the moment animation starts.
|
||||||
// When collapsing, keep showing the expanded content until past the midpoint,
|
// When collapsing, keep showing the expanded content until past the midpoint,
|
||||||
@@ -556,7 +610,19 @@ impl Panel {
|
|||||||
let panel = if how_expanded < 1.0 {
|
let panel = if how_expanded < 1.0 {
|
||||||
// Animate the visible size from collapsed_size to expanded_size,
|
// Animate the visible size from collapsed_size to expanded_size,
|
||||||
// so the slide picks up where the collapsed panel left off.
|
// so the slide picks up where the collapsed panel left off.
|
||||||
let expanded_size = expanded_panel.outer_size(ui);
|
let expanded_size = if drag_in_progress {
|
||||||
|
// During a drag the pointer sets the size, clamped to `min_size`
|
||||||
|
// — so that, not the (stale) persisted size, is where the slide
|
||||||
|
// meets the collapsed panel, whether opening or closing. Get it
|
||||||
|
// wrong and the panel jumps the gap between the two sizes in one
|
||||||
|
// frame.
|
||||||
|
expanded_panel
|
||||||
|
.outer_size_range
|
||||||
|
.min
|
||||||
|
.at_least(collapse_threshold)
|
||||||
|
} else {
|
||||||
|
expanded_panel.outer_size(ui)
|
||||||
|
};
|
||||||
let visible_size = lerp(collapse_threshold..=expanded_size, how_expanded);
|
let visible_size = lerp(collapse_threshold..=expanded_size, how_expanded);
|
||||||
let slide_fraction = if 0.0 < expanded_size {
|
let slide_fraction = if 0.0 < expanded_size {
|
||||||
visible_size / expanded_size
|
visible_size / expanded_size
|
||||||
@@ -673,7 +739,7 @@ impl Panel {
|
|||||||
// released size gets persisted into [`PanelState`] — without this the
|
// released size gets persisted into [`PanelState`] — without this the
|
||||||
// store-skipped-during-drag rule would leave the stored size at the
|
// store-skipped-during-drag rule would leave the stored size at the
|
||||||
// pre-drag value.
|
// pre-drag value.
|
||||||
let resize_id = self.resize_id_source.unwrap_or(id).with("__resize");
|
let resize_id = self.resize_id();
|
||||||
let resize_response = parent_ui.read_response(resize_id);
|
let resize_response = parent_ui.read_response(resize_id);
|
||||||
|
|
||||||
// Double-click on the resize edge toggles `*is_expanded` for the
|
// Double-click on the resize edge toggles `*is_expanded` for the
|
||||||
@@ -831,21 +897,34 @@ impl Panel {
|
|||||||
.store(parent_ui, id);
|
.store(parent_ui, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide the separator once the panel is mostly slid off — at that point
|
// The highlight follows the pointer all the way down to zero size, where
|
||||||
// the line would just be a stray dash hovering near the parent edge.
|
// `collapsed_resize_handle` picks it straight up again — so the user never
|
||||||
if 0.01 < self.slide_fraction {
|
// loses sight of the edge they are dragging. The dim idle separator does
|
||||||
let stroke = if is_resizing {
|
// get hidden once the panel is mostly slid off, since there it would just
|
||||||
parent_ui.style().visuals.widgets.active.fg_stroke // highly visible
|
// be a stray dash hovering near the parent edge.
|
||||||
} else if resize_hover {
|
let stroke = if is_resizing {
|
||||||
parent_ui.style().visuals.widgets.hovered.fg_stroke // highly visible
|
parent_ui.style().visuals.widgets.active.fg_stroke // highly visible
|
||||||
} else if show_separator_line {
|
} else if resize_hover {
|
||||||
// TODO(emilk): distinguish resizable from non-resizable
|
parent_ui.style().visuals.widgets.hovered.fg_stroke // highly visible
|
||||||
parent_ui.style().visuals.widgets.noninteractive.bg_stroke // dim
|
} else if show_separator_line && 0.01 < self.slide_fraction {
|
||||||
} else {
|
// TODO(emilk): distinguish resizable from non-resizable
|
||||||
Stroke::NONE
|
parent_ui.style().visuals.widgets.noninteractive.bg_stroke // dim
|
||||||
};
|
} else {
|
||||||
|
Stroke::NONE
|
||||||
|
};
|
||||||
|
if 0.0 < stroke.width {
|
||||||
|
// Nudged inward, to keep the line inside the panel's own (shifted)
|
||||||
|
// rect: `parent_ui`'s painter sits below the panels that come after
|
||||||
|
// this one, so anything drawn past the fixed edge is covered by them.
|
||||||
// TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done
|
// TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done
|
||||||
let line_pos = side.resize_pos(shifted_outer_rect) + 0.5 * side.sign() * stroke.width;
|
|
||||||
|
// The line goes just _outside_ the frame's outline, in the room `resolve_frame`
|
||||||
|
// reserved for it in the outer margin, i.e.:
|
||||||
|
//
|
||||||
|
// contents | `inner_margin` | outline | separator line | `outer_margin`
|
||||||
|
let outer_margin = f32::from(side.resize_margin(frame.outer_margin));
|
||||||
|
let outline_edge = side.resize_pos(shifted_outer_rect) + side.sign() * outer_margin;
|
||||||
|
let line_pos = outline_edge - 0.5 * side.sign() * stroke.width;
|
||||||
let cross_range = shifted_outer_rect.range_along(side.cross_axis());
|
let cross_range = shifted_outer_rect.range_along(side.cross_axis());
|
||||||
if axis == 0 {
|
if axis == 0 {
|
||||||
parent_ui.painter().vline(line_pos, cross_range, stroke);
|
parent_ui.painter().vline(line_pos, cross_range, stroke);
|
||||||
@@ -857,54 +936,123 @@ impl Panel {
|
|||||||
inner_response
|
inner_response
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The configured [`Frame`], or the default side/top panel frame for this [`Ui`].
|
/// [`Id`] of this panel's resize-handle widget.
|
||||||
fn resolve_frame(&self, ui: &Ui) -> Frame {
|
///
|
||||||
self.frame
|
/// See [`resize_widget_id`] for why open and collapsed panels must share it.
|
||||||
.unwrap_or_else(|| Frame::side_top_panel(ui.style()))
|
fn resize_id(&self) -> Id {
|
||||||
|
resize_widget_id(self.resize_id_source.unwrap_or(self.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Panel is fully closed. If the user is still dragging the resize handle
|
/// The configured [`Frame`], or the default side/top panel frame for this [`Ui`].
|
||||||
/// from the frame the panel closed on, keep its widget id registered so the
|
fn resolve_frame(&self, ui: &Ui) -> Frame {
|
||||||
/// drag survives, and reopen if they drag back past the minimum size.
|
let mut frame = self
|
||||||
fn keep_drag_alive_for_reopen(&self, ui: &Ui, is_expanded: &mut bool) {
|
.frame
|
||||||
let resize_id = self.id.with("__resize");
|
.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
|
||||||
let Some(resize_response) = ui.read_response(resize_id) else {
|
|
||||||
return;
|
if self.show_separator_line {
|
||||||
};
|
// Reserve room for the separator line in the frame's _outer_ margin, so the line
|
||||||
if !resize_response.dragged() {
|
// lands just outside the frame's outline instead of painting on top of it:
|
||||||
return;
|
//
|
||||||
}
|
// contents | `inner_margin` | outline | separator line | `outer_margin`
|
||||||
let Some(pointer) = resize_response.interact_pointer_pos() else {
|
//
|
||||||
return;
|
// We deliberately don't do this for a `resizable` panel that has opted out of the
|
||||||
};
|
// separator line: the line it shows while hovered/dragged is a transient affordance,
|
||||||
|
// and reserving room for it would leave a permanently visible gap.
|
||||||
|
let widgets = &ui.style().visuals.widgets;
|
||||||
|
let stroke_width = widgets.noninteractive.bg_stroke.width.round() as i8;
|
||||||
|
|
||||||
|
let margin_side = self.side.resize_margin_mut(&mut frame.outer_margin);
|
||||||
|
*margin_side = (*margin_side).saturating_add(stroke_width);
|
||||||
|
}
|
||||||
|
|
||||||
|
frame
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The grab handle of a fully collapsed panel: a thin strip along the panel's
|
||||||
|
/// fixed edge, invisible until hovered.
|
||||||
|
///
|
||||||
|
/// Dragging it outward past the minimum size — or double-clicking it —
|
||||||
|
/// reopens the panel. Registering it under the same id as the expanded
|
||||||
|
/// panel's resize handle also keeps an in-progress drag-to-collapse gesture
|
||||||
|
/// alive, so the user can drag the panel straight back out without releasing.
|
||||||
|
fn collapsed_resize_handle(&self, ui: &Ui, is_expanded: &mut bool) {
|
||||||
|
let side = self.side;
|
||||||
|
let axis = side.axis();
|
||||||
|
|
||||||
// Re-register the resize widget at the (now collapsed) fixed edge so its
|
|
||||||
// id stays alive in egui's interaction state.
|
|
||||||
let available_rect = ui.available_rect_before_wrap();
|
let available_rect = ui.available_rect_before_wrap();
|
||||||
let fixed_edge_pos = self.side.fixed_pos(available_rect);
|
let fixed_edge_pos = side.fixed_pos(available_rect);
|
||||||
let cross_range = available_rect.range_along(self.side.cross_axis());
|
let cross_range = available_rect.range_along(side.cross_axis());
|
||||||
let resize_rect = if self.side.axis() == 0 {
|
|
||||||
|
// The strip lies just _inside_ the fixed edge, so it never reaches
|
||||||
|
// outside the area the panel is allowed to occupy.
|
||||||
|
let mut resize_rect = if axis == 0 {
|
||||||
Rect::from_x_y_ranges(Rangef::point(fixed_edge_pos), cross_range)
|
Rect::from_x_y_ranges(Rangef::point(fixed_edge_pos), cross_range)
|
||||||
} else {
|
} else {
|
||||||
Rect::from_x_y_ranges(cross_range, Rangef::point(fixed_edge_pos))
|
Rect::from_x_y_ranges(cross_range, Rangef::point(fixed_edge_pos))
|
||||||
};
|
};
|
||||||
let grab = ui.style().interaction.resize_grab_radius_side;
|
side.set_rect_size(
|
||||||
let resize_rect = resize_rect.expand2(grab * self.side.axis_unit());
|
&mut resize_rect,
|
||||||
ui.interact(resize_rect, resize_id, Sense::drag());
|
ui.style().interaction.resize_grab_radius_side,
|
||||||
|
);
|
||||||
|
|
||||||
// Keep the resize cursor while the user is still holding the drag.
|
let resize_id = self.resize_id();
|
||||||
// Otherwise the cursor would snap back to the default the moment the
|
let response = ui.interact(resize_rect, resize_id, Sense::click_and_drag());
|
||||||
// panel closed, even though the gesture is still ongoing.
|
|
||||||
ui.set_cursor_icon(self.cursor_icon(0.0));
|
|
||||||
|
|
||||||
// Signed distance from the fixed edge to the pointer along the panel's
|
if response.double_clicked() {
|
||||||
// axis. Only counts as "pulled outward" while positive — going past the
|
|
||||||
// fixed edge gives a negative value, NOT a mirrored positive one (no
|
|
||||||
// `.abs()`), so dragging past the screen edge can't spuriously reopen.
|
|
||||||
let dragged_size = -self.side.sign() * (pointer[self.side.axis()] - fixed_edge_pos);
|
|
||||||
if self.outer_size_range.min < dragged_size {
|
|
||||||
*is_expanded = true;
|
*is_expanded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if response.hovered() || response.dragged() {
|
||||||
|
// Advertise that the panel can be pulled out. Also keeps the resize
|
||||||
|
// cursor for a drag that started before the panel closed, instead of
|
||||||
|
// snapping back to the default mid-gesture.
|
||||||
|
ui.set_cursor_icon(self.cursor_icon(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.dragged()
|
||||||
|
&& let Some(pointer) = response.interact_pointer_pos()
|
||||||
|
{
|
||||||
|
// Signed distance from the fixed edge to the pointer along the panel's
|
||||||
|
// axis. Only counts as "pulled outward" while positive — going past the
|
||||||
|
// fixed edge gives a negative value, NOT a mirrored positive one (no
|
||||||
|
// `.abs()`), so dragging past the screen edge can't spuriously reopen.
|
||||||
|
//
|
||||||
|
// We require the full minimum size, so the panel never jumps ahead of
|
||||||
|
// the pointer: it opens exactly when the drag reaches the size it will
|
||||||
|
// open at, and follows the pointer from there.
|
||||||
|
let dragged_size = -side.sign() * (pointer[axis] - fixed_edge_pos);
|
||||||
|
if self.outer_size_range.min < dragged_size {
|
||||||
|
*is_expanded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invisible until hovered, so the handle doesn't read as a stray line at
|
||||||
|
// the edge of the screen.
|
||||||
|
let stroke = if response.dragged() {
|
||||||
|
ui.style().visuals.widgets.active.fg_stroke
|
||||||
|
} else if response.hovered() {
|
||||||
|
ui.style().visuals.widgets.hovered.fg_stroke
|
||||||
|
} else {
|
||||||
|
Stroke::NONE
|
||||||
|
};
|
||||||
|
if 0.0 < stroke.width {
|
||||||
|
// The collapsed panel occupies no space of its own, so the line has to
|
||||||
|
// go _inside_ the area the following panels use — which means painting
|
||||||
|
// in a layer above them, or they would cover it.
|
||||||
|
// TODO(emilk): use the panel's own layer once https://github.com/emilk/egui/issues/1516 is done
|
||||||
|
let painter = ui
|
||||||
|
.ctx()
|
||||||
|
.layer_painter(LayerId::new(Order::Middle, resize_id))
|
||||||
|
.with_clip_rect(resize_rect);
|
||||||
|
|
||||||
|
// Nudge the line inward so it isn't half-clipped by the edge.
|
||||||
|
let line_pos = fixed_edge_pos - 0.5 * side.sign() * stroke.width;
|
||||||
|
if axis == 0 {
|
||||||
|
painter.vline(line_pos, cross_range, stroke);
|
||||||
|
} else {
|
||||||
|
painter.hline(cross_range, line_pos, stroke);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current _outer_ width or height of the panel (from previous frame),
|
/// Get the current _outer_ width or height of the panel (from previous frame),
|
||||||
@@ -939,7 +1087,7 @@ impl Panel {
|
|||||||
|
|
||||||
// Use `resize_id_source` so collapsed/expanded panels in
|
// Use `resize_id_source` so collapsed/expanded panels in
|
||||||
// `show_switched` share one resize widget.
|
// `show_switched` share one resize widget.
|
||||||
let resize_id = self.resize_id_source.unwrap_or(self.id).with("__resize");
|
let resize_id = self.resize_id();
|
||||||
let resize_rect = Rect::from_x_y_ranges(resize_x, resize_y).expand2(amount);
|
let resize_rect = Rect::from_x_y_ranges(resize_x, resize_y).expand2(amount);
|
||||||
ui.interact(resize_rect, resize_id, Sense::click_and_drag())
|
ui.interact(resize_rect, resize_id, Sense::click_and_drag())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::iter::once;
|
use core::iter::once;
|
||||||
|
|
||||||
use emath::{Align, Pos2, Rect, RectAlign, Vec2, vec2};
|
use emath::{Align, Pos2, Rect, RectAlign, Vec2, vec2};
|
||||||
|
|
||||||
@@ -179,7 +179,9 @@ pub struct Popup<'a> {
|
|||||||
|
|
||||||
/// Default width passed to the Area
|
/// Default width passed to the Area
|
||||||
width: Option<f32>,
|
width: Option<f32>,
|
||||||
|
sizing_pass: bool,
|
||||||
sense: Sense,
|
sense: Sense,
|
||||||
|
interactable: bool,
|
||||||
layout: Layout,
|
layout: Layout,
|
||||||
frame: Option<Frame>,
|
frame: Option<Frame>,
|
||||||
style: StyleModifier,
|
style: StyleModifier,
|
||||||
@@ -201,7 +203,9 @@ impl<'a> Popup<'a> {
|
|||||||
alternative_aligns: None,
|
alternative_aligns: None,
|
||||||
gap: 0.0,
|
gap: 0.0,
|
||||||
width: None,
|
width: None,
|
||||||
|
sizing_pass: false,
|
||||||
sense: Sense::click(),
|
sense: Sense::click(),
|
||||||
|
interactable: true,
|
||||||
layout: Layout::default(),
|
layout: Layout::default(),
|
||||||
frame: None,
|
frame: None,
|
||||||
style: StyleModifier::default(),
|
style: StyleModifier::default(),
|
||||||
@@ -369,6 +373,15 @@ impl<'a> Popup<'a> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If `false`, the pointer goes straight through the popup and it's widgets to whatever is behind it.
|
||||||
|
///
|
||||||
|
/// Default: `true`.
|
||||||
|
#[inline]
|
||||||
|
pub fn interactable(mut self, interactable: bool) -> Self {
|
||||||
|
self.interactable = interactable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the sense of the popup.
|
/// Set the sense of the popup.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn sense(mut self, sense: Sense) -> Self {
|
pub fn sense(mut self, sense: Sense) -> Self {
|
||||||
@@ -390,6 +403,19 @@ impl<'a> Popup<'a> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Force the popup's underlying [`Area`] to run an invisible sizing pass.
|
||||||
|
///
|
||||||
|
/// Popups automatically run a sizing pass when they open or reopen. Set this to `true` for
|
||||||
|
/// one frame when the contents of an already open popup change and its cached size may no
|
||||||
|
/// longer fit. Do not leave it enabled continuously, because the popup would remain invisible.
|
||||||
|
///
|
||||||
|
/// Default: `false`.
|
||||||
|
#[inline]
|
||||||
|
pub fn sizing_pass(mut self, sizing_pass: bool) -> Self {
|
||||||
|
self.sizing_pass = sizing_pass;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the id of the Area.
|
/// Set the id of the Area.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn id(mut self, id: Id) -> Self {
|
pub fn id(mut self, id: Id) -> Self {
|
||||||
@@ -472,12 +498,12 @@ impl<'a> Popup<'a> {
|
|||||||
RectAlign::find_best_align(
|
RectAlign::find_best_align(
|
||||||
#[expect(clippy::iter_on_empty_collections)]
|
#[expect(clippy::iter_on_empty_collections)]
|
||||||
#[expect(clippy::or_fun_call)]
|
#[expect(clippy::or_fun_call)]
|
||||||
std::iter::chain(
|
core::iter::chain(
|
||||||
once(self.rect_align),
|
once(self.rect_align),
|
||||||
self.alternative_aligns
|
self.alternative_aligns
|
||||||
// Need the empty slice so the iters have the same type so we can unwrap_or
|
// Need the empty slice so the iters have the same type so we can unwrap_or
|
||||||
.map(|a| std::iter::chain(a.iter().copied(), [].iter().copied()))
|
.map(|a| core::iter::chain(a.iter().copied(), [].iter().copied()))
|
||||||
.unwrap_or(std::iter::chain(
|
.unwrap_or(core::iter::chain(
|
||||||
self.rect_align.symmetries().iter().copied(),
|
self.rect_align.symmetries().iter().copied(),
|
||||||
RectAlign::MENU_ALIGNS.iter().copied(),
|
RectAlign::MENU_ALIGNS.iter().copied(),
|
||||||
)),
|
)),
|
||||||
@@ -545,7 +571,9 @@ impl<'a> Popup<'a> {
|
|||||||
alternative_aligns: _,
|
alternative_aligns: _,
|
||||||
gap,
|
gap,
|
||||||
width,
|
width,
|
||||||
|
sizing_pass,
|
||||||
sense,
|
sense,
|
||||||
|
interactable,
|
||||||
layout,
|
layout,
|
||||||
frame,
|
frame,
|
||||||
style,
|
style,
|
||||||
@@ -570,8 +598,9 @@ impl<'a> Popup<'a> {
|
|||||||
.pivot(pivot)
|
.pivot(pivot)
|
||||||
.fixed_pos(anchor)
|
.fixed_pos(anchor)
|
||||||
.sense(sense)
|
.sense(sense)
|
||||||
|
.interactable(interactable)
|
||||||
.layout(layout)
|
.layout(layout)
|
||||||
.sizing_pass(!was_open_last_frame)
|
.sizing_pass(sizing_pass || !was_open_last_frame)
|
||||||
.info(info.unwrap_or_else(|| {
|
.info(info.unwrap_or_else(|| {
|
||||||
UiStackInfo::new(kind.into()).with_tag_value(
|
UiStackInfo::new(kind.into()).with_tag_value(
|
||||||
MenuConfig::MENU_CONFIG_TAG,
|
MenuConfig::MENU_CONFIG_TAG,
|
||||||
|
|||||||
@@ -289,16 +289,16 @@ impl Resize {
|
|||||||
Rect::from_min_size(position, state.desired_size)
|
Rect::from_min_size(position, state.desired_size)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut content_clip_rect = inner_rect.expand(ui.visuals().clip_rect_margin);
|
let mut content_clip_rect = inner_rect;
|
||||||
|
|
||||||
// If we pull the resize handle to shrink, we want to TRY to shrink it.
|
// If we pull the resize handle to shrink, we want to TRY to shrink it.
|
||||||
// After laying out the contents, we might be much bigger.
|
// After laying out the contents, we might be much bigger.
|
||||||
// In those cases we don't want the clip_rect to be smaller, because
|
// In those cases we don't want the clip_rect to be smaller, because
|
||||||
// then we will clip the contents of the region even thought the result gets larger. This is simply ugly!
|
// then we will clip the contents of the region even thought the result gets larger. This is simply ugly!
|
||||||
// So we use the memory of last_content_size to make the clip rect large enough.
|
// So we use the memory of last_content_size to make the clip rect large enough.
|
||||||
content_clip_rect.max = content_clip_rect.max.max(
|
content_clip_rect.max = content_clip_rect
|
||||||
inner_rect.min + state.last_content_size + Vec2::splat(ui.visuals().clip_rect_margin),
|
.max
|
||||||
);
|
.max(inner_rect.min + state.last_content_size);
|
||||||
|
|
||||||
content_clip_rect = content_clip_rect.intersect(ui.clip_rect()); // Respect parent region
|
content_clip_rect = content_clip_rect.intersect(ui.clip_rect()); // Respect parent region
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#![expect(clippy::needless_range_loop)]
|
#![expect(clippy::needless_range_loop)]
|
||||||
|
|
||||||
use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
|
use core::ops::{Add, AddAssign, BitOr, BitOrAssign};
|
||||||
|
|
||||||
use emath::GuiRounding as _;
|
use emath::GuiRounding as _;
|
||||||
use epaint::{Color32, Direction, Margin, Shape};
|
use epaint::{Color32, Direction, Margin, Shape};
|
||||||
@@ -810,12 +810,11 @@ impl ScrollArea {
|
|||||||
|
|
||||||
{
|
{
|
||||||
// Clip the content, but only when we really need to:
|
// Clip the content, but only when we really need to:
|
||||||
let clip_rect_margin = ui.visuals().clip_rect_margin;
|
|
||||||
let mut content_clip_rect = ui.clip_rect();
|
let mut content_clip_rect = ui.clip_rect();
|
||||||
for d in 0..2 {
|
for d in 0..2 {
|
||||||
if direction_enabled[d] {
|
if direction_enabled[d] {
|
||||||
content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
|
content_clip_rect.min[d] = inner_rect.min[d];
|
||||||
content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
|
content_clip_rect.max[d] = inner_rect.max[d];
|
||||||
} else {
|
} else {
|
||||||
// Nice handling of forced resizing beyond the possible:
|
// Nice handling of forced resizing beyond the possible:
|
||||||
content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
|
content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
|
||||||
@@ -931,7 +930,7 @@ impl ScrollArea {
|
|||||||
|
|
||||||
let saved_scroll_target = content_ui
|
let saved_scroll_target = content_ui
|
||||||
.ctx()
|
.ctx()
|
||||||
.pass_state_mut(|state| std::mem::take(&mut state.scroll_target));
|
.pass_state_mut(|state| core::mem::take(&mut state.scroll_target));
|
||||||
|
|
||||||
Prepared {
|
Prepared {
|
||||||
id,
|
id,
|
||||||
@@ -986,7 +985,7 @@ impl ScrollArea {
|
|||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
row_height_sans_spacing: f32,
|
row_height_sans_spacing: f32,
|
||||||
total_rows: usize,
|
total_rows: usize,
|
||||||
add_contents: impl FnOnce(&mut Ui, std::ops::Range<usize>) -> R,
|
add_contents: impl FnOnce(&mut Ui, core::ops::Range<usize>) -> R,
|
||||||
) -> ScrollAreaOutput<R> {
|
) -> ScrollAreaOutput<R> {
|
||||||
let spacing = ui.spacing().item_spacing;
|
let spacing = ui.spacing().item_spacing;
|
||||||
let row_height_with_spacing = row_height_sans_spacing + spacing.y;
|
let row_height_with_spacing = row_height_sans_spacing + spacing.y;
|
||||||
@@ -1082,17 +1081,9 @@ impl Prepared {
|
|||||||
|
|
||||||
let content_size = content_ui.min_size();
|
let content_size = content_ui.min_size();
|
||||||
|
|
||||||
let scroll_delta = content_ui
|
|
||||||
.ctx()
|
|
||||||
.pass_state_mut(|state| std::mem::take(&mut state.scroll_delta));
|
|
||||||
|
|
||||||
let mut had_explicit_scroll_adjustment = Vec2b::FALSE;
|
let mut had_explicit_scroll_adjustment = Vec2b::FALSE;
|
||||||
|
|
||||||
for d in 0..2 {
|
for d in 0..2 {
|
||||||
// PassState::scroll_delta is inverted from the way we apply the delta, so we need to negate it.
|
|
||||||
let mut delta = -scroll_delta.0[d];
|
|
||||||
let mut animation = scroll_delta.1;
|
|
||||||
|
|
||||||
// We always take both scroll targets regardless of which scroll axes are enabled. This
|
// We always take both scroll targets regardless of which scroll axes are enabled. This
|
||||||
// is to avoid them leaking to other scroll areas.
|
// is to avoid them leaking to other scroll areas.
|
||||||
let scroll_target = content_ui
|
let scroll_target = content_ui
|
||||||
@@ -1100,6 +1091,17 @@ impl Prepared {
|
|||||||
.pass_state_mut(|state| state.scroll_target[d].take());
|
.pass_state_mut(|state| state.scroll_target[d].take());
|
||||||
|
|
||||||
if direction_enabled[d] {
|
if direction_enabled[d] {
|
||||||
|
let (scroll_delta, scroll_animation) = content_ui.ctx().pass_state_mut(|state| {
|
||||||
|
(
|
||||||
|
core::mem::take(&mut state.scroll_delta.0[d]),
|
||||||
|
state.scroll_delta.1,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// PassState::scroll_delta is inverted from the way we apply the delta, so we need to negate it.
|
||||||
|
let mut delta = -scroll_delta;
|
||||||
|
let mut animation = scroll_animation;
|
||||||
|
|
||||||
if let Some(target) = scroll_target {
|
if let Some(target) = scroll_target {
|
||||||
let pass_state::ScrollTarget {
|
let pass_state::ScrollTarget {
|
||||||
range,
|
range,
|
||||||
@@ -1133,8 +1135,8 @@ impl Prepared {
|
|||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
delta += delta_update;
|
|
||||||
animation = animation_update;
|
animation = animation_update;
|
||||||
|
delta += delta_update;
|
||||||
}
|
}
|
||||||
|
|
||||||
if delta != 0.0 {
|
if delta != 0.0 {
|
||||||
@@ -1158,10 +1160,10 @@ impl Prepared {
|
|||||||
}
|
}
|
||||||
ui.request_repaint();
|
ui.request_repaint();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if delta != 0.0 {
|
if delta != 0.0 {
|
||||||
had_explicit_scroll_adjustment[d] = true;
|
had_explicit_scroll_adjustment[d] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1306,8 +1308,6 @@ impl Prepared {
|
|||||||
// * When one ScrollArea is nested inside another, and the outer
|
// * When one ScrollArea is nested inside another, and the outer
|
||||||
// is scrolled so that the scroll-bars of the inner ScrollArea (us)
|
// is scrolled so that the scroll-bars of the inner ScrollArea (us)
|
||||||
// is outside the clip rectangle.
|
// is outside the clip rectangle.
|
||||||
// Really this should use the tighter clip_rect that ignores clip_rect_margin, but we don't store that.
|
|
||||||
// clip_rect_margin is quite a hack. It would be nice to get rid of it.
|
|
||||||
max_cross = ui.clip_rect().max[1 - d] - outer_margin;
|
max_cross = ui.clip_rect().max[1 - d] - outer_margin;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1575,9 +1575,7 @@ fn paint_fade_areas_impl(ui: &Ui, inner_rect: Rect, content_size: Vec2, offset:
|
|||||||
|
|
||||||
let overflow = content_size - inner_rect.size();
|
let overflow = content_size - inner_rect.size();
|
||||||
|
|
||||||
let paint_rect = inner_rect
|
let paint_rect = inner_rect.intersect(ui.min_rect());
|
||||||
.intersect(ui.min_rect())
|
|
||||||
.expand(ui.visuals().clip_rect_margin);
|
|
||||||
|
|
||||||
// Top fade: animate opacity based on how far we've scrolled down.
|
// Top fade: animate opacity based on how far we've scrolled down.
|
||||||
if 0.0 < offset.y {
|
if 0.0 < offset.y {
|
||||||
|
|||||||
@@ -129,7 +129,15 @@ impl Tooltip<'_> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let tooltip_area_id = Self::tooltip_id(parent_widget, state.tooltip_count);
|
let tooltip_area_id = Self::tooltip_id(parent_widget, state.tooltip_count);
|
||||||
popup = popup.anchor(state.bounding_rect).id(tooltip_area_id);
|
|
||||||
|
// Tooltips without interactive contents should not be interactable (hover should pass
|
||||||
|
// through to the widget below).
|
||||||
|
let interactable = Self::had_interactive_widgets(popup.ctx(), tooltip_area_id);
|
||||||
|
|
||||||
|
popup = popup
|
||||||
|
.anchor(state.bounding_rect)
|
||||||
|
.id(tooltip_area_id)
|
||||||
|
.interactable(interactable);
|
||||||
|
|
||||||
let response = popup.show(|ui| {
|
let response = popup.show(|ui| {
|
||||||
// By default, the text in tooltips aren't selectable.
|
// By default, the text in tooltips aren't selectable.
|
||||||
@@ -192,6 +200,20 @@ impl Tooltip<'_> {
|
|||||||
widget_id.with(tooltip_count)
|
widget_id.with(tooltip_count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Did this tooltip contain anything the user can interact with, last pass?
|
||||||
|
///
|
||||||
|
/// Most tooltips are just text. Those should not react to the pointer at all,
|
||||||
|
/// or they would steal the hover from the widget they belong to.
|
||||||
|
fn had_interactive_widgets(ctx: &Context, tooltip_id: Id) -> bool {
|
||||||
|
let tooltip_layer_id = LayerId::new(Order::Tooltip, tooltip_id);
|
||||||
|
ctx.viewport(|vp| {
|
||||||
|
vp.prev_pass
|
||||||
|
.widgets
|
||||||
|
.get_layer(tooltip_layer_id)
|
||||||
|
.any(|w| w.enabled && w.sense.interactive())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Should we show a tooltip for this response?
|
/// Should we show a tooltip for this response?
|
||||||
///
|
///
|
||||||
/// Argument `allow_interactive_tooltip` controls whether mouse can interact with tooltip that
|
/// Argument `allow_interactive_tooltip` controls whether mouse can interact with tooltip that
|
||||||
@@ -247,15 +269,9 @@ impl Tooltip<'_> {
|
|||||||
// Check if we should automatically stay open:
|
// Check if we should automatically stay open:
|
||||||
|
|
||||||
let tooltip_id = Self::next_tooltip_id(&response.ctx, response.id);
|
let tooltip_id = Self::next_tooltip_id(&response.ctx, response.id);
|
||||||
let tooltip_layer_id = LayerId::new(Order::Tooltip, tooltip_id);
|
|
||||||
|
|
||||||
let tooltip_has_interactive_widget = allow_interactive_tooltip
|
let tooltip_has_interactive_widget = allow_interactive_tooltip
|
||||||
&& response.ctx.viewport(|vp| {
|
&& Self::had_interactive_widgets(&response.ctx, tooltip_id);
|
||||||
vp.prev_pass
|
|
||||||
.widgets
|
|
||||||
.get_layer(tooltip_layer_id)
|
|
||||||
.any(|w| w.enabled && w.sense.interactive())
|
|
||||||
});
|
|
||||||
|
|
||||||
if tooltip_has_interactive_widget {
|
if tooltip_has_interactive_widget {
|
||||||
// We keep the tooltip open if hovered,
|
// We keep the tooltip open if hovered,
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ pub struct Window<'a> {
|
|||||||
open: Option<&'a mut bool>,
|
open: Option<&'a mut bool>,
|
||||||
area: Area,
|
area: Area,
|
||||||
frame: Option<Frame>,
|
frame: Option<Frame>,
|
||||||
|
title_frame: Option<Frame>,
|
||||||
resize: Resize,
|
resize: Resize,
|
||||||
scroll: ScrollArea,
|
scroll: ScrollArea,
|
||||||
collapsible: bool,
|
collapsible: bool,
|
||||||
@@ -106,6 +107,7 @@ impl<'a> Window<'a> {
|
|||||||
open: None,
|
open: None,
|
||||||
area,
|
area,
|
||||||
frame: None,
|
frame: None,
|
||||||
|
title_frame: None,
|
||||||
resize: Resize::default()
|
resize: Resize::default()
|
||||||
.with_stroke(false)
|
.with_stroke(false)
|
||||||
.min_size([96.0, 32.0])
|
.min_size([96.0, 32.0])
|
||||||
@@ -265,6 +267,13 @@ impl<'a> Window<'a> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Change the background color, margins, etc. of the title
|
||||||
|
#[inline]
|
||||||
|
pub fn title_frame(mut self, frame: Frame) -> Self {
|
||||||
|
self.title_frame = Some(frame);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set minimum width of the window.
|
/// Set minimum width of the window.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn min_width(mut self, min_width: f32) -> Self {
|
pub fn min_width(mut self, min_width: f32) -> Self {
|
||||||
@@ -549,6 +558,7 @@ impl Window<'_> {
|
|||||||
mut open,
|
mut open,
|
||||||
area,
|
area,
|
||||||
frame,
|
frame,
|
||||||
|
title_frame,
|
||||||
resize,
|
resize,
|
||||||
scroll,
|
scroll,
|
||||||
collapsible,
|
collapsible,
|
||||||
@@ -616,10 +626,12 @@ impl Window<'_> {
|
|||||||
|
|
||||||
let style = ctx.global_style();
|
let style = ctx.global_style();
|
||||||
|
|
||||||
|
// We get or create the Frame for the title and content
|
||||||
let window_frame = frame.unwrap_or_else(|| Frame::window(&style));
|
let window_frame = frame.unwrap_or_else(|| Frame::window(&style));
|
||||||
|
let window_title_frame = title_frame.unwrap_or(window_frame);
|
||||||
|
|
||||||
// We apply the window margin by using the `ScrollArea::content_margin`.
|
// We apply the window margin by using the `ScrollArea::content_margin`.
|
||||||
let window_margin = window_frame.inner_margin;
|
let window_content_margin = window_frame.inner_margin;
|
||||||
let window_frame = window_frame.inner_margin(0.0);
|
let window_frame = window_frame.inner_margin(0.0);
|
||||||
|
|
||||||
let is_explicitly_closed = matches!(open, Some(false));
|
let is_explicitly_closed = matches!(open, Some(false));
|
||||||
@@ -711,7 +723,7 @@ impl Window<'_> {
|
|||||||
title_ui(
|
title_ui(
|
||||||
ui,
|
ui,
|
||||||
title,
|
title,
|
||||||
window_frame.inner_margin(window_margin),
|
window_title_frame,
|
||||||
&mut collapsing,
|
&mut collapsing,
|
||||||
collapsible,
|
collapsible,
|
||||||
on_top,
|
on_top,
|
||||||
@@ -725,12 +737,12 @@ impl Window<'_> {
|
|||||||
.show_body_unindented(ui, |ui| {
|
.show_body_unindented(ui, |ui| {
|
||||||
if scroll.is_any_scroll_enabled() {
|
if scroll.is_any_scroll_enabled() {
|
||||||
scroll
|
scroll
|
||||||
.content_margin(window_margin)
|
.content_margin(window_content_margin)
|
||||||
.show(ui, add_contents)
|
.show(ui, add_contents)
|
||||||
.inner
|
.inner
|
||||||
} else {
|
} else {
|
||||||
crate::Frame::NONE
|
crate::Frame::NONE
|
||||||
.inner_margin(window_margin)
|
.inner_margin(window_content_margin)
|
||||||
.show(ui, add_contents)
|
.show(ui, add_contents)
|
||||||
.inner
|
.inner
|
||||||
}
|
}
|
||||||
@@ -909,7 +921,7 @@ impl SideResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::BitAnd for SideResponse {
|
impl core::ops::BitAnd for SideResponse {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
fn bitand(self, rhs: Self) -> Self::Output {
|
fn bitand(self, rhs: Self) -> Self::Output {
|
||||||
@@ -920,7 +932,7 @@ impl std::ops::BitAnd for SideResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::BitOrAssign for SideResponse {
|
impl core::ops::BitOrAssign for SideResponse {
|
||||||
fn bitor_assign(&mut self, rhs: Self) {
|
fn bitor_assign(&mut self, rhs: Self) {
|
||||||
*self = Self {
|
*self = Self {
|
||||||
hover: self.hover || rhs.hover,
|
hover: self.hover || rhs.hover,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#![warn(missing_docs)] // Let's keep `Context` well-documented.
|
#![warn(missing_docs)] // Let's keep `Context` well-documented.
|
||||||
|
|
||||||
use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration};
|
use core::{cell::RefCell, panic::Location, time::Duration};
|
||||||
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
|
||||||
use emath::GuiRounding as _;
|
use emath::GuiRounding as _;
|
||||||
use epaint::{
|
use epaint::{
|
||||||
@@ -32,7 +33,7 @@ use crate::{
|
|||||||
load::{self, Bytes, Loaders, SizedTexture},
|
load::{self, Bytes, Loaders, SizedTexture},
|
||||||
memory::{Options, Theme},
|
memory::{Options, Theme},
|
||||||
os::OperatingSystem,
|
os::OperatingSystem,
|
||||||
output::FullOutput,
|
output::{FullOutput, LogicOutput},
|
||||||
pass_state::PassState,
|
pass_state::PassState,
|
||||||
plugin::{self, TypedPluginHandle},
|
plugin::{self, TypedPluginHandle},
|
||||||
resize, response, scroll_area, theme,
|
resize, response, scroll_area, theme,
|
||||||
@@ -99,7 +100,7 @@ impl ContextImpl {
|
|||||||
fn begin_pass_repaint_logic(&mut self, viewport_id: ViewportId) {
|
fn begin_pass_repaint_logic(&mut self, viewport_id: ViewportId) {
|
||||||
let viewport = self.viewports.entry(viewport_id).or_default();
|
let viewport = self.viewports.entry(viewport_id).or_default();
|
||||||
|
|
||||||
std::mem::swap(
|
core::mem::swap(
|
||||||
&mut viewport.repaint.prev_causes,
|
&mut viewport.repaint.prev_causes,
|
||||||
&mut viewport.repaint.causes,
|
&mut viewport.repaint.causes,
|
||||||
);
|
);
|
||||||
@@ -244,6 +245,12 @@ pub struct ViewportState {
|
|||||||
// ----------------------
|
// ----------------------
|
||||||
// Cross-frame statistics:
|
// Cross-frame statistics:
|
||||||
pub num_multipass_in_row: usize,
|
pub num_multipass_in_row: usize,
|
||||||
|
|
||||||
|
/// The last theme we sent to the native window via [`ViewportCommand::SetTheme`],
|
||||||
|
/// used to avoid sending redundant commands.
|
||||||
|
///
|
||||||
|
/// See [`crate::Options::sync_window_theme`].
|
||||||
|
pub(crate) last_sent_window_theme: Option<crate::SystemTheme>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What called [`Context::request_repaint`] or [`Context::request_discard`]?
|
/// What called [`Context::request_repaint`] or [`Context::request_discard`]?
|
||||||
@@ -259,14 +266,14 @@ pub struct RepaintCause {
|
|||||||
pub reason: Cow<'static, str>,
|
pub reason: Cow<'static, str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for RepaintCause {
|
impl core::fmt::Debug for RepaintCause {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
write!(f, "{}:{} {}", self.file, self.line, self.reason)
|
write!(f, "{}:{} {}", self.file, self.line, self.reason)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for RepaintCause {
|
impl core::fmt::Display for RepaintCause {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
write!(f, "{}:{} {}", self.file, self.line, self.reason)
|
write!(f, "{}:{} {}", self.file, self.line, self.reason)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -456,7 +463,7 @@ impl ContextImpl {
|
|||||||
|
|
||||||
self.memory.begin_pass(&new_raw_input, &all_viewport_ids);
|
self.memory.begin_pass(&new_raw_input, &all_viewport_ids);
|
||||||
|
|
||||||
viewport.input = std::mem::take(&mut viewport.input).begin_pass(
|
viewport.input = core::mem::take(&mut viewport.input).begin_pass(
|
||||||
new_raw_input,
|
new_raw_input,
|
||||||
viewport.repaint.requested_immediate_repaint_prev_pass(),
|
viewport.repaint.requested_immediate_repaint_prev_pass(),
|
||||||
pixels_per_point,
|
pixels_per_point,
|
||||||
@@ -469,7 +476,13 @@ impl ContextImpl {
|
|||||||
viewport.this_pass.begin_pass();
|
viewport.this_pass.begin_pass();
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut layers: Vec<LayerId> = viewport.prev_pass.widgets.layer_ids().collect();
|
// Areas that are not interactable are click-through: skip them in the hit-test.
|
||||||
|
let mut layers: Vec<LayerId> = viewport
|
||||||
|
.prev_pass
|
||||||
|
.widgets
|
||||||
|
.layer_ids()
|
||||||
|
.filter(|layer_id| self.memory.areas().is_interactable(*layer_id))
|
||||||
|
.collect();
|
||||||
layers.sort_by(|&a, &b| self.memory.areas().compare_order(a, b));
|
layers.sort_by(|&a, &b| self.memory.areas().compare_order(a, b));
|
||||||
|
|
||||||
viewport.hits = if let Some(pos) = viewport.input.pointer.interact_pos() {
|
viewport.hits = if let Some(pos) = viewport.input.pointer.interact_pos() {
|
||||||
@@ -644,7 +657,7 @@ impl ContextImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn all_viewport_ids(&self) -> ViewportIdSet {
|
fn all_viewport_ids(&self) -> ViewportIdSet {
|
||||||
std::iter::chain(self.viewports.keys().copied(), [ViewportId::ROOT]).collect()
|
core::iter::chain(self.viewports.keys().copied(), [ViewportId::ROOT]).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The current active viewport
|
/// The current active viewport
|
||||||
@@ -712,13 +725,13 @@ impl ContextImpl {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Context(Arc<RwLock<ContextImpl>>);
|
pub struct Context(Arc<RwLock<ContextImpl>>);
|
||||||
|
|
||||||
impl std::fmt::Debug for Context {
|
impl core::fmt::Debug for Context {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
f.debug_struct("Context").finish_non_exhaustive()
|
f.debug_struct("Context").finish_non_exhaustive()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::cmp::PartialEq for Context {
|
impl core::cmp::PartialEq for Context {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
Arc::ptr_eq(&self.0, &other.0)
|
Arc::ptr_eq(&self.0, &other.0)
|
||||||
}
|
}
|
||||||
@@ -728,7 +741,7 @@ impl Default for Context {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let ctx_impl = ContextImpl {
|
let ctx_impl = ContextImpl {
|
||||||
embed_viewports: true,
|
embed_viewports: true,
|
||||||
viewports: std::iter::once((ViewportId::ROOT, ViewportState::default())).collect(),
|
viewports: core::iter::once((ViewportId::ROOT, ViewportState::default())).collect(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let ctx = Self(Arc::new(RwLock::new(ctx_impl)));
|
let ctx = Self(Arc::new(RwLock::new(ctx_impl)));
|
||||||
@@ -778,6 +791,7 @@ impl Context {
|
|||||||
/// ui.label("Hello egui!");
|
/// ui.label("Hello egui!");
|
||||||
/// });
|
/// });
|
||||||
/// // handle full_output
|
/// // handle full_output
|
||||||
|
/// # full_output.drop_without_applying_deltas();
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn run_ui(&self, new_input: RawInput, mut run_ui: impl FnMut(&mut Ui)) -> FullOutput {
|
pub fn run_ui(&self, new_input: RawInput, mut run_ui: impl FnMut(&mut Ui)) -> FullOutput {
|
||||||
@@ -837,7 +851,7 @@ impl Context {
|
|||||||
self.write(|ctx| {
|
self.write(|ctx| {
|
||||||
let viewport = ctx.viewport_for(viewport_id);
|
let viewport = ctx.viewport_for(viewport_id);
|
||||||
viewport.output.num_completed_passes =
|
viewport.output.num_completed_passes =
|
||||||
std::mem::take(&mut output.platform_output.num_completed_passes);
|
core::mem::take(&mut output.platform_output.num_completed_passes);
|
||||||
output.platform_output.request_discard_reasons.clear();
|
output.platform_output.request_discard_reasons.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -878,6 +892,57 @@ impl Context {
|
|||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run app logic without showing any ui.
|
||||||
|
///
|
||||||
|
/// Use this instead of [`Self::run_ui`] when nothing will be shown,
|
||||||
|
/// e.g. because the window is minimized or occluded,
|
||||||
|
/// but you still want to let the app tick its logic
|
||||||
|
/// (so that it can e.g. ask to be shown again with [`ViewportCommand::Focus`]).
|
||||||
|
///
|
||||||
|
/// No pass is run, so `f` must not show any ui.
|
||||||
|
/// This means everything egui knows about the ui is left untouched:
|
||||||
|
/// no widget state is garbage-collected, no animation advances,
|
||||||
|
/// and nothing loses focus.
|
||||||
|
///
|
||||||
|
/// Of `new_input`, only the window state ([`RawInput::viewports`] and
|
||||||
|
/// [`RawInput::focused`]) is used, so that `f` can tell that the window is hidden.
|
||||||
|
/// The ui input (events, time, …) is _not_ interpreted, and is left for the next
|
||||||
|
/// call to [`Self::run_ui`]: [`Self::input`] is otherwise still that of the last pass.
|
||||||
|
///
|
||||||
|
/// The returned [`LogicOutput`] is what [`FullOutput`] would have carried:
|
||||||
|
/// anything `f` asked the integration to do.
|
||||||
|
/// There is nothing to paint.
|
||||||
|
#[must_use]
|
||||||
|
pub fn run_logic(&self, new_input: &RawInput, logic: impl FnOnce(&Self)) -> LogicOutput {
|
||||||
|
profiling::function_scope!();
|
||||||
|
|
||||||
|
let viewport_id = new_input.viewport_id;
|
||||||
|
|
||||||
|
self.write(|ctx| {
|
||||||
|
// Consume any outstanding repaint request, so that a new request from `logic`
|
||||||
|
// reaches the integration instead of being considered already served:
|
||||||
|
ctx.begin_pass_repaint_logic(viewport_id);
|
||||||
|
|
||||||
|
// Tell `logic` about the windows, but leave the ui input alone:
|
||||||
|
let raw = &mut ctx.viewport_for(viewport_id).input.raw;
|
||||||
|
raw.viewport_id = viewport_id;
|
||||||
|
raw.viewports = new_input.viewports.clone();
|
||||||
|
raw.focused = new_input.focused;
|
||||||
|
});
|
||||||
|
|
||||||
|
logic(self);
|
||||||
|
|
||||||
|
self.write(|ctx| LogicOutput {
|
||||||
|
platform_output: core::mem::take(&mut ctx.viewport_for(viewport_id).output),
|
||||||
|
viewport_commands: ctx
|
||||||
|
.viewports
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|(_, viewport)| !viewport.commands.is_empty())
|
||||||
|
.map(|(&id, viewport)| (id, core::mem::take(&mut viewport.commands)))
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// An alternative to calling [`Self::run_ui`].
|
/// An alternative to calling [`Self::run_ui`].
|
||||||
///
|
///
|
||||||
/// It is usually better to use [`Self::run_ui`], because
|
/// It is usually better to use [`Self::run_ui`], because
|
||||||
@@ -895,6 +960,7 @@ impl Context {
|
|||||||
///
|
///
|
||||||
/// let full_output = ctx.end_pass();
|
/// let full_output = ctx.end_pass();
|
||||||
/// // handle full_output
|
/// // handle full_output
|
||||||
|
/// # full_output.drop_without_applying_deltas();
|
||||||
/// ```
|
/// ```
|
||||||
pub fn begin_pass(&self, mut new_input: RawInput) {
|
pub fn begin_pass(&self, mut new_input: RawInput) {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
@@ -1698,11 +1764,11 @@ impl Context {
|
|||||||
.get(&id)
|
.get(&id)
|
||||||
.map(|v| v.repaint.cumulative_frame_nr)
|
.map(|v| v.repaint.cumulative_frame_nr)
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
if cfg!(debug_assertions) {
|
debug_assert!(
|
||||||
panic!("cumulative_frame_nr_for failed to find the viewport {id:?}");
|
false,
|
||||||
} else {
|
"cumulative_frame_nr_for failed to find the viewport {id:?}"
|
||||||
0
|
);
|
||||||
}
|
0
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1813,7 +1879,7 @@ impl Context {
|
|||||||
/// See [`Self::request_repaint_after`] for details.
|
/// See [`Self::request_repaint_after`] for details.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn request_repaint_after_secs(&self, seconds: f32) {
|
pub fn request_repaint_after_secs(&self, seconds: f32) {
|
||||||
if let Ok(duration) = std::time::Duration::try_from_secs_f32(seconds) {
|
if let Ok(duration) = core::time::Duration::try_from_secs_f32(seconds) {
|
||||||
self.request_repaint_after(duration);
|
self.request_repaint_after(duration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1996,7 +2062,7 @@ impl Context {
|
|||||||
&self,
|
&self,
|
||||||
f: impl FnOnce(&mut T) -> R,
|
f: impl FnOnce(&mut T) -> R,
|
||||||
) -> Option<R> {
|
) -> Option<R> {
|
||||||
let plugin = self.read(|ctx| ctx.plugins.get(std::any::TypeId::of::<T>()));
|
let plugin = self.read(|ctx| ctx.plugins.get(core::any::TypeId::of::<T>()));
|
||||||
plugin.map(|plugin| f(plugin.lock().typed_plugin_mut()))
|
plugin.map(|plugin| f(plugin.lock().typed_plugin_mut()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2008,13 +2074,13 @@ impl Context {
|
|||||||
if let Some(plugin) = self.plugin_opt() {
|
if let Some(plugin) = self.plugin_opt() {
|
||||||
plugin
|
plugin
|
||||||
} else {
|
} else {
|
||||||
panic!("Plugin of type {:?} not found", std::any::type_name::<T>());
|
panic!("Plugin of type {:?} not found", core::any::type_name::<T>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a handle to the plugin of type `T`, if it was registered.
|
/// Get a handle to the plugin of type `T`, if it was registered.
|
||||||
pub fn plugin_opt<T: plugin::Plugin>(&self) -> Option<TypedPluginHandle<T>> {
|
pub fn plugin_opt<T: plugin::Plugin>(&self) -> Option<TypedPluginHandle<T>> {
|
||||||
let plugin = self.read(|ctx| ctx.plugins.get(std::any::TypeId::of::<T>()));
|
let plugin = self.read(|ctx| ctx.plugins.get(core::any::TypeId::of::<T>()));
|
||||||
plugin.map(TypedPluginHandle::new)
|
plugin.map(TypedPluginHandle::new)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2430,6 +2496,8 @@ impl Context {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.sync_window_theme();
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
self.debug_painting();
|
self.debug_painting();
|
||||||
|
|
||||||
@@ -2441,11 +2509,43 @@ impl Context {
|
|||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Keep the native window theme in sync with the egui [`crate::ThemePreference`],
|
||||||
|
/// if [`crate::Options::sync_window_theme`] is enabled.
|
||||||
|
///
|
||||||
|
/// Sends a [`ViewportCommand::SetTheme`] to the current viewport whenever the
|
||||||
|
/// derived theme changes, so the native window decorations match the egui theme.
|
||||||
|
fn sync_window_theme(&self) {
|
||||||
|
if !self.options(|o| o.sync_window_theme) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::{SystemTheme, ThemePreference};
|
||||||
|
let window_theme = match self.options(|o| o.theme_preference) {
|
||||||
|
ThemePreference::System => SystemTheme::SystemDefault,
|
||||||
|
ThemePreference::Dark => SystemTheme::Dark,
|
||||||
|
ThemePreference::Light => SystemTheme::Light,
|
||||||
|
};
|
||||||
|
|
||||||
|
let changed = self.write(|ctx| {
|
||||||
|
let viewport = ctx.viewport();
|
||||||
|
if viewport.last_sent_window_theme == Some(window_theme) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
viewport.last_sent_window_theme = Some(window_theme);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
self.send_viewport_cmd(ViewportCommand::SetTheme(window_theme));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Called at the end of the pass.
|
/// Called at the end of the pass.
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
fn debug_painting(&self) {
|
fn debug_painting(&self) {
|
||||||
#![expect(clippy::iter_over_hash_type)] // ok to be sloppy in debug painting
|
#![expect(clippy::iter_over_hash_type)] // ok to be sloppy in debug painting
|
||||||
use std::fmt::Write as _;
|
use core::fmt::Write as _;
|
||||||
|
|
||||||
let paint_widget = |widget: &WidgetRect, text: &str, color: Color32| {
|
let paint_widget = |widget: &WidgetRect, text: &str, color: Color32| {
|
||||||
let rect = widget.interact_rect;
|
let rect = widget.interact_rect;
|
||||||
@@ -2626,7 +2726,7 @@ impl ContextImpl {
|
|||||||
// Inform the backend of all textures that have been updated (including font atlas).
|
// Inform the backend of all textures that have been updated (including font atlas).
|
||||||
let textures_delta = self.tex_manager.0.write().take_delta();
|
let textures_delta = self.tex_manager.0.write().take_delta();
|
||||||
|
|
||||||
let mut platform_output: PlatformOutput = std::mem::take(&mut viewport.output);
|
let mut platform_output: PlatformOutput = core::mem::take(&mut viewport.output);
|
||||||
|
|
||||||
if self.memory.should_interrupt_ime()
|
if self.memory.should_interrupt_ime()
|
||||||
&& let Some(ime) = &mut platform_output.ime
|
&& let Some(ime) = &mut platform_output.ime
|
||||||
@@ -2686,7 +2786,7 @@ impl ContextImpl {
|
|||||||
shapes
|
shapes
|
||||||
};
|
};
|
||||||
|
|
||||||
std::mem::swap(&mut viewport.prev_pass, &mut viewport.this_pass);
|
core::mem::swap(&mut viewport.prev_pass, &mut viewport.this_pass);
|
||||||
|
|
||||||
if repaint_needed {
|
if repaint_needed {
|
||||||
self.request_repaint(ended_viewport_id, RepaintCause::new());
|
self.request_repaint(ended_viewport_id, RepaintCause::new());
|
||||||
@@ -2748,7 +2848,7 @@ impl ContextImpl {
|
|||||||
// Let the primary immediate viewport handle the commands of its children too.
|
// Let the primary immediate viewport handle the commands of its children too.
|
||||||
// This can make things easier for the backend, as otherwise we may get commands
|
// This can make things easier for the backend, as otherwise we may get commands
|
||||||
// that affect a viewport while its egui logic is running.
|
// that affect a viewport while its egui logic is running.
|
||||||
std::mem::take(&mut viewport.commands)
|
core::mem::take(&mut viewport.commands)
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
};
|
};
|
||||||
@@ -4233,13 +4333,13 @@ fn warn_if_rect_changes_id(
|
|||||||
struct OrderedRect(Rect);
|
struct OrderedRect(Rect);
|
||||||
|
|
||||||
impl PartialOrd for OrderedRect {
|
impl PartialOrd for OrderedRect {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
|
||||||
Some(self.cmp(other))
|
Some(self.cmp(other))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ord for OrderedRect {
|
impl Ord for OrderedRect {
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
|
||||||
let lhs = self.0;
|
let lhs = self.0;
|
||||||
let rhs = other.0;
|
let rhs = other.0;
|
||||||
lhs.min
|
lhs.min
|
||||||
@@ -4341,6 +4441,7 @@ mod test {
|
|||||||
assert_eq!(num_calls, 1);
|
assert_eq!(num_calls, 1);
|
||||||
assert_eq!(output.platform_output.num_completed_passes, 1);
|
assert_eq!(output.platform_output.num_completed_passes, 1);
|
||||||
assert!(!output.platform_output.requested_discard());
|
assert!(!output.platform_output.requested_discard());
|
||||||
|
output.drop_without_applying_deltas();
|
||||||
}
|
}
|
||||||
|
|
||||||
// A single call, with a denied request to discard:
|
// A single call, with a denied request to discard:
|
||||||
@@ -4366,6 +4467,7 @@ mod test {
|
|||||||
.reason,
|
.reason,
|
||||||
"test"
|
"test"
|
||||||
);
|
);
|
||||||
|
output.drop_without_applying_deltas();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4386,6 +4488,7 @@ mod test {
|
|||||||
assert_eq!(num_calls, 1);
|
assert_eq!(num_calls, 1);
|
||||||
assert_eq!(output.platform_output.num_completed_passes, 1);
|
assert_eq!(output.platform_output.num_completed_passes, 1);
|
||||||
assert!(!output.platform_output.requested_discard());
|
assert!(!output.platform_output.requested_discard());
|
||||||
|
output.drop_without_applying_deltas();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request discard once:
|
// Request discard once:
|
||||||
@@ -4408,6 +4511,7 @@ mod test {
|
|||||||
!output.platform_output.requested_discard(),
|
!output.platform_output.requested_discard(),
|
||||||
"The request should have been cleared when fulfilled"
|
"The request should have been cleared when fulfilled"
|
||||||
);
|
);
|
||||||
|
output.drop_without_applying_deltas();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request discard twice:
|
// Request discard twice:
|
||||||
@@ -4432,6 +4536,7 @@ mod test {
|
|||||||
output.platform_output.requested_discard(),
|
output.platform_output.requested_discard(),
|
||||||
"The unfulfilled request should be reported"
|
"The unfulfilled request should be reported"
|
||||||
);
|
);
|
||||||
|
output.drop_without_applying_deltas();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4460,6 +4565,7 @@ mod test {
|
|||||||
!output.platform_output.requested_discard(),
|
!output.platform_output.requested_discard(),
|
||||||
"The request should have been cleared when fulfilled"
|
"The request should have been cleared when fulfilled"
|
||||||
);
|
);
|
||||||
|
output.drop_without_applying_deltas();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,51 @@
|
|||||||
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use core::{future::Future, pin::Pin};
|
||||||
|
|
||||||
/// A file dropped into egui.
|
/// A file dropped into egui.
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
///
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
/// The integration owns the concrete file handle, letting egui remain independent of windowing
|
||||||
pub struct DroppedFile {
|
/// backends and file APIs.
|
||||||
/// Set by the `egui-winit` backend.
|
pub trait DroppedFile: core::fmt::Debug {
|
||||||
pub path: Option<std::path::PathBuf>,
|
/// The path of the dropped file.
|
||||||
|
///
|
||||||
|
/// This is an absolute path on native platforms. On the web, it is a relative path containing
|
||||||
|
/// only the file name because browsers do not expose the file's local path.
|
||||||
|
fn path(&self) -> &Path;
|
||||||
|
|
||||||
/// Name of the file. Set by the `eframe` web backend.
|
/// Read the file contents.
|
||||||
pub name: String,
|
///
|
||||||
|
/// This is asynchronous because browsers can only read files asynchronously.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the browser cannot read the file.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn bytes_async(&self) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + '_>>;
|
||||||
|
|
||||||
/// With the `eframe` web backend, this is set to the mime-type of the file (if available).
|
/// Read the file contents.
|
||||||
pub mime: String,
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the file cannot be read.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn bytes(&self) -> Result<Vec<u8>, String>;
|
||||||
|
|
||||||
/// Set by the `eframe` web backend.
|
/// The browser file handle, if this file was dropped on the web.
|
||||||
pub last_modified: Option<std::time::SystemTime>,
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn web_file(&self) -> Option<&web_sys::File> {
|
||||||
/// Set by the `eframe` web backend.
|
None
|
||||||
pub bytes: Option<std::sync::Arc<[u8]>>,
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A shared reference to a dropped file.
|
||||||
|
#[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))]
|
||||||
|
pub type DroppedFileHandle = Arc<dyn DroppedFile + Send + Sync>;
|
||||||
|
|
||||||
|
/// A shared reference to a dropped file.
|
||||||
|
///
|
||||||
|
/// This is not necessarily `Send + Sync` when wasm threads are enabled, because
|
||||||
|
/// [`web_sys::File`] is not thread-safe in that configuration.
|
||||||
|
#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))]
|
||||||
|
pub type DroppedFileHandle = Arc<dyn DroppedFile>;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ pub enum ImeEvent {
|
|||||||
/// a non-empty preedit string indicates that the IME is active.
|
/// a non-empty preedit string indicates that the IME is active.
|
||||||
Preedit {
|
Preedit {
|
||||||
text: String,
|
text: String,
|
||||||
active_range_chars: Option<std::ops::Range<usize>>,
|
active_range_chars: Option<core::ops::Range<usize>>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// IME composition ended with this final result.
|
/// IME composition ended with this final result.
|
||||||
@@ -22,6 +22,15 @@ pub enum ImeEvent {
|
|||||||
/// The IME is considered dismissed after this event.
|
/// The IME is considered dismissed after this event.
|
||||||
Commit(String),
|
Commit(String),
|
||||||
|
|
||||||
|
/// Notifies when the text surrounding the cursor should be deleted.
|
||||||
|
///
|
||||||
|
/// `before_chars` and `after_chars` are the number of characters (not
|
||||||
|
/// bytes) to delete before and after the cursor, respectively.
|
||||||
|
DeleteSurrounding {
|
||||||
|
before_chars: usize,
|
||||||
|
after_chars: usize,
|
||||||
|
},
|
||||||
|
|
||||||
/// Notifies when the IME was disabled.
|
/// Notifies when the IME was disabled.
|
||||||
#[deprecated = "No longer used by egui"]
|
#[deprecated = "No longer used by egui"]
|
||||||
Disabled,
|
Disabled,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ mod touch;
|
|||||||
mod viewport_info;
|
mod viewport_info;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
dropped_file::DroppedFile,
|
dropped_file::{DroppedFile, DroppedFileHandle},
|
||||||
event::Event,
|
event::Event,
|
||||||
event_filter::EventFilter,
|
event_filter::EventFilter,
|
||||||
hovered_file::HoveredFile,
|
hovered_file::HoveredFile,
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ pub struct Modifiers {
|
|||||||
pub command: bool,
|
pub command: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Modifiers {
|
impl core::fmt::Debug for Modifiers {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
if self.is_none() {
|
if self.is_none() {
|
||||||
return write!(f, "Modifiers::NONE");
|
return write!(f, "Modifiers::NONE");
|
||||||
}
|
}
|
||||||
@@ -387,7 +387,7 @@ impl Modifiers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::BitOr for Modifiers {
|
impl core::ops::BitOr for Modifiers {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -396,7 +396,7 @@ impl std::ops::BitOr for Modifiers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::BitOrAssign for Modifiers {
|
impl core::ops::BitOrAssign for Modifiers {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn bitor_assign(&mut self, rhs: Self) {
|
fn bitor_assign(&mut self, rhs: Self) {
|
||||||
*self = *self | rhs;
|
*self = *self | rhs;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{OrderedViewportIdMap, Theme, ViewportId, ViewportIdMap, emath::Rect};
|
use crate::{OrderedViewportIdMap, Theme, ViewportId, ViewportIdMap, emath::Rect};
|
||||||
|
|
||||||
use super::{DroppedFile, Event, HoveredFile, SafeAreaInsets, ViewportInfo};
|
use super::{DroppedFileHandle, Event, HoveredFile, SafeAreaInsets, ViewportInfo};
|
||||||
|
|
||||||
/// What the integrations provides to egui at the start of each frame.
|
/// What the integrations provides to egui at the start of each frame.
|
||||||
///
|
///
|
||||||
@@ -13,7 +13,7 @@ use super::{DroppedFile, Event, HoveredFile, SafeAreaInsets, ViewportInfo};
|
|||||||
///
|
///
|
||||||
/// Ii "points" can be calculated from native physical pixels
|
/// Ii "points" can be calculated from native physical pixels
|
||||||
/// using `pixels_per_point` = [`crate::Context::zoom_factor`] * `native_pixels_per_point`;
|
/// using `pixels_per_point` = [`crate::Context::zoom_factor`] * `native_pixels_per_point`;
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct RawInput {
|
pub struct RawInput {
|
||||||
/// The id of the active viewport.
|
/// The id of the active viewport.
|
||||||
@@ -65,9 +65,20 @@ pub struct RawInput {
|
|||||||
|
|
||||||
/// Dragged files dropped into egui.
|
/// Dragged files dropped into egui.
|
||||||
///
|
///
|
||||||
|
/// egui never reads the file contents.
|
||||||
|
#[cfg_attr(
|
||||||
|
not(target_arch = "wasm32"),
|
||||||
|
doc = "Call [`crate::DroppedFile::bytes`] to read a dropped file."
|
||||||
|
)]
|
||||||
|
#[cfg_attr(
|
||||||
|
target_arch = "wasm32",
|
||||||
|
doc = "Call [`crate::DroppedFile::bytes_async`] to read a dropped file."
|
||||||
|
)]
|
||||||
|
///
|
||||||
/// Note: when using `eframe` on Windows, this will always be empty if drag-and-drop support has
|
/// Note: when using `eframe` on Windows, this will always be empty if drag-and-drop support has
|
||||||
/// been disabled in [`crate::viewport::ViewportBuilder`].
|
/// been disabled in [`crate::viewport::ViewportBuilder`].
|
||||||
pub dropped_files: Vec<DroppedFile>,
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
|
pub dropped_files: Vec<DroppedFileHandle>,
|
||||||
|
|
||||||
/// The native window has the keyboard focus (i.e. is receiving key presses).
|
/// The native window has the keyboard focus (i.e. is receiving key presses).
|
||||||
///
|
///
|
||||||
@@ -84,7 +95,7 @@ impl Default for RawInput {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
viewport_id: ViewportId::ROOT,
|
viewport_id: ViewportId::ROOT,
|
||||||
viewports: std::iter::once((ViewportId::ROOT, Default::default())).collect(),
|
viewports: core::iter::once((ViewportId::ROOT, Default::default())).collect(),
|
||||||
screen_rect: None,
|
screen_rect: None,
|
||||||
max_texture_side: None,
|
max_texture_side: None,
|
||||||
time: None,
|
time: None,
|
||||||
@@ -123,9 +134,9 @@ impl RawInput {
|
|||||||
max_texture_side: self.max_texture_side.take(),
|
max_texture_side: self.max_texture_side.take(),
|
||||||
time: self.time,
|
time: self.time,
|
||||||
predicted_dt: self.predicted_dt,
|
predicted_dt: self.predicted_dt,
|
||||||
events: std::mem::take(&mut self.events),
|
events: core::mem::take(&mut self.events),
|
||||||
hovered_files: self.hovered_files.clone(),
|
hovered_files: self.hovered_files.clone(),
|
||||||
dropped_files: std::mem::take(&mut self.dropped_files),
|
dropped_files: core::mem::take(&mut self.dropped_files),
|
||||||
focused: self.focused,
|
focused: self.focused,
|
||||||
system_theme: self.system_theme,
|
system_theme: self.system_theme,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use crate::emath::Rect;
|
|||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct SafeAreaInsets(pub MarginF32);
|
pub struct SafeAreaInsets(pub MarginF32);
|
||||||
|
|
||||||
impl std::ops::Sub<SafeAreaInsets> for Rect {
|
impl core::ops::Sub<SafeAreaInsets> for Rect {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
fn sub(self, rhs: SafeAreaInsets) -> Self::Output {
|
fn sub(self, rhs: SafeAreaInsets) -> Self::Output {
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ impl ViewportInfo {
|
|||||||
Self {
|
Self {
|
||||||
parent: self.parent,
|
parent: self.parent,
|
||||||
title: self.title.clone(),
|
title: self.title.clone(),
|
||||||
events: std::mem::take(&mut self.events),
|
events: core::mem::take(&mut self.events),
|
||||||
native_pixels_per_point: self.native_pixels_per_point,
|
native_pixels_per_point: self.native_pixels_per_point,
|
||||||
monitor_size: self.monitor_size,
|
monitor_size: self.monitor_size,
|
||||||
inner_rect: self.inner_rect,
|
inner_rect: self.inner_rect,
|
||||||
@@ -209,7 +209,7 @@ impl ViewportInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[expect(clippy::ref_option)]
|
#[expect(clippy::ref_option)]
|
||||||
fn opt_as_str<T: std::fmt::Debug>(v: &Option<T>) -> String {
|
fn opt_as_str<T: core::fmt::Debug>(v: &Option<T>) -> String {
|
||||||
v.as_ref().map_or(String::new(), |v| format!("{v:?}"))
|
v.as_ref().map_or(String::new(), |v| format!("{v:?}"))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! All the data egui returns to the backend at the end of each frame.
|
//! All the data egui returns to the backend at the end of each frame.
|
||||||
|
|
||||||
use std::ops::Range;
|
use core::ops::Range;
|
||||||
|
|
||||||
use epaint::text::CharIndex;
|
use epaint::text::CharIndex;
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ pub struct FullOutput {
|
|||||||
|
|
||||||
/// Texture changes since last frame (including the font texture).
|
/// Texture changes since last frame (including the font texture).
|
||||||
///
|
///
|
||||||
/// The backend needs to apply [`crate::TexturesDelta::set`] _before_ painting,
|
/// The backend needs to apply [`crate::TexturesDelta::push`] _before_ painting,
|
||||||
/// and free any texture in [`crate::TexturesDelta::free`] _after_ painting.
|
/// and free any texture in [`crate::TexturesDelta::free`] _after_ painting.
|
||||||
///
|
///
|
||||||
/// It is assumed that all egui viewports share the same painter and texture namespace.
|
/// It is assumed that all egui viewports share the same painter and texture namespace.
|
||||||
@@ -68,6 +68,28 @@ impl FullOutput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [`epaint::textures::TexturesDelta`] will panic when dropped with still unapplied deltas,
|
||||||
|
/// this is a helper to clear the deltas.
|
||||||
|
pub fn drop_without_applying_deltas(mut self) {
|
||||||
|
self.textures_delta.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What egui emits from [`crate::Context::run_logic`], i.e. from a tick where no ui was shown.
|
||||||
|
///
|
||||||
|
/// There is nothing to paint, but the app may still have asked the integration to do things,
|
||||||
|
/// e.g. to show a hidden window again with [`crate::ViewportCommand::Focus`].
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct LogicOutput {
|
||||||
|
/// Non-rendering related output.
|
||||||
|
pub platform_output: PlatformOutput,
|
||||||
|
|
||||||
|
/// The commands sent with [`crate::Context::send_viewport_cmd`] and friends.
|
||||||
|
///
|
||||||
|
/// Note that this contains no information about which viewports exist:
|
||||||
|
/// the integration should leave its viewports as they are.
|
||||||
|
pub viewport_commands: OrderedViewportIdMap<Vec<crate::ViewportCommand>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Information about text being edited.
|
/// Information about text being edited.
|
||||||
@@ -76,6 +98,9 @@ impl FullOutput {
|
|||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct IMEOutput {
|
pub struct IMEOutput {
|
||||||
|
/// IME's purpose.
|
||||||
|
pub purpose: crate::IMEPurpose,
|
||||||
|
|
||||||
/// Where the [`crate::TextEdit`] is located on screen.
|
/// Where the [`crate::TextEdit`] is located on screen.
|
||||||
pub rect: crate::Rect,
|
pub rect: crate::Rect,
|
||||||
|
|
||||||
@@ -217,7 +242,7 @@ impl PlatformOutput {
|
|||||||
/// Take everything ephemeral (everything except `cursor_icon` and
|
/// Take everything ephemeral (everything except `cursor_icon` and
|
||||||
/// `cursor_image` currently)
|
/// `cursor_image` currently)
|
||||||
pub fn take(&mut self) -> Self {
|
pub fn take(&mut self) -> Self {
|
||||||
let taken = std::mem::take(self);
|
let taken = core::mem::take(self);
|
||||||
self.cursor_icon = taken.cursor_icon; // sticky between frames
|
self.cursor_icon = taken.cursor_icon; // sticky between frames
|
||||||
self.cursor_image = taken.cursor_image.clone(); // sticky between frames
|
self.cursor_image = taken.cursor_image.clone(); // sticky between frames
|
||||||
taken
|
taken
|
||||||
@@ -302,8 +327,8 @@ pub struct CustomCursorImage {
|
|||||||
pub hotspot: [u16; 2],
|
pub hotspot: [u16; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for CustomCursorImage {
|
impl core::fmt::Debug for CustomCursorImage {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
f.debug_struct("CustomCursorImage")
|
f.debug_struct("CustomCursorImage")
|
||||||
.field("size", &self.size)
|
.field("size", &self.size)
|
||||||
.field("hotspot", &self.hotspot)
|
.field("hotspot", &self.hotspot)
|
||||||
@@ -519,8 +544,8 @@ impl OutputEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for OutputEvent {
|
impl core::fmt::Debug for OutputEvent {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Clicked(wi) => write!(f, "Clicked({wi:?})"),
|
Self::Clicked(wi) => write!(f, "Clicked({wi:?})"),
|
||||||
Self::DoubleClicked(wi) => write!(f, "DoubleClicked({wi:?})"),
|
Self::DoubleClicked(wi) => write!(f, "DoubleClicked({wi:?})"),
|
||||||
@@ -566,8 +591,8 @@ pub struct WidgetInfo {
|
|||||||
pub hint_text: Option<String>,
|
pub hint_text: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for WidgetInfo {
|
impl core::fmt::Debug for WidgetInfo {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
let Self {
|
let Self {
|
||||||
typ,
|
typ,
|
||||||
enabled,
|
enabled,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::{any::Any, sync::Arc};
|
use core::any::Any;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// A wrapper around `dyn Any`, used for passing custom user data
|
/// A wrapper around `dyn Any`, used for passing custom user data
|
||||||
/// to [`crate::ViewportCommand::Screenshot`].
|
/// to [`crate::ViewportCommand::Screenshot`].
|
||||||
@@ -30,8 +31,8 @@ impl PartialEq for UserData {
|
|||||||
|
|
||||||
impl Eq for UserData {}
|
impl Eq for UserData {}
|
||||||
|
|
||||||
impl std::hash::Hash for UserData {
|
impl core::hash::Hash for UserData {
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||||
self.data.as_ref().map(Arc::as_ptr).hash(state);
|
self.data.as_ref().map(Arc::as_ptr).hash(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +58,7 @@ impl<'de> serde::Deserialize<'de> for UserData {
|
|||||||
impl serde::de::Visitor<'_> for UserDataVisitor {
|
impl serde::de::Visitor<'_> for UserDataVisitor {
|
||||||
type Value = UserData;
|
type Value = UserData;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
formatter.write_str("a None value")
|
formatter.write_str("a None value")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ pub fn print(ctx: &Context, text: impl Into<WidgetText>) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let location = std::panic::Location::caller();
|
let location = core::panic::Location::caller();
|
||||||
let location = format!("{}:{}", location.file(), location.line());
|
let location = format!("{}:{}", location.file(), location.line());
|
||||||
|
|
||||||
let plugin = ctx.plugin::<DebugTextPlugin>();
|
let plugin = ctx.plugin::<DebugTextPlugin>();
|
||||||
@@ -58,7 +58,7 @@ impl Plugin for DebugTextPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_end_pass(&mut self, ui: &mut Ui) {
|
fn on_end_pass(&mut self, ui: &mut Ui) {
|
||||||
let entries = std::mem::take(&mut self.entries);
|
let entries = core::mem::take(&mut self.entries);
|
||||||
Self::paint_entries(ui, entries);
|
Self::paint_entries(ui, entries);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::{any::Any, sync::Arc};
|
use core::any::Any;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{Context, CursorIcon, Plugin, Ui};
|
use crate::{Context, CursorIcon, Plugin, Ui};
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ pub(crate) struct GridLayout {
|
|||||||
curr_state: State,
|
curr_state: State,
|
||||||
initial_available: Rect,
|
initial_available: Rect,
|
||||||
|
|
||||||
|
/// Are we inside an enclosing sizing pass (e.g. [`crate::Resize`] measuring
|
||||||
|
/// the minimum content width)? If so we must not remember the (narrow) sizes
|
||||||
|
/// we measure during it.
|
||||||
|
sizing_pass: bool,
|
||||||
|
|
||||||
// Options:
|
// Options:
|
||||||
num_columns: Option<usize>,
|
num_columns: Option<usize>,
|
||||||
spacing: Vec2,
|
spacing: Vec2,
|
||||||
@@ -90,6 +95,10 @@ pub(crate) struct GridLayout {
|
|||||||
impl GridLayout {
|
impl GridLayout {
|
||||||
pub(crate) fn new(ui: &Ui, id: Id, prev_state: Option<State>) -> Self {
|
pub(crate) fn new(ui: &Ui, id: Id, prev_state: Option<State>) -> Self {
|
||||||
let is_first_frame = prev_state.is_none();
|
let is_first_frame = prev_state.is_none();
|
||||||
|
|
||||||
|
// An outer sizing pass, we should render as small as possible.
|
||||||
|
let sizing_pass = ui.is_sizing_pass();
|
||||||
|
|
||||||
let prev_state = prev_state.unwrap_or_default();
|
let prev_state = prev_state.unwrap_or_default();
|
||||||
|
|
||||||
// TODO(emilk): respect current layout
|
// TODO(emilk): respect current layout
|
||||||
@@ -110,6 +119,7 @@ impl GridLayout {
|
|||||||
prev_state,
|
prev_state,
|
||||||
curr_state: State::default(),
|
curr_state: State::default(),
|
||||||
initial_available,
|
initial_available,
|
||||||
|
sizing_pass,
|
||||||
|
|
||||||
num_columns: None,
|
num_columns: None,
|
||||||
spacing: ui.spacing().item_spacing,
|
spacing: ui.spacing().item_spacing,
|
||||||
@@ -180,7 +190,11 @@ impl GridLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn next_cell(&self, cursor: Rect, child_size: Vec2) -> Rect {
|
pub(crate) fn next_cell(&self, cursor: Rect, child_size: Vec2) -> Rect {
|
||||||
let width = self.prev_state.col_width(self.col).unwrap_or(0.0);
|
let width = if self.sizing_pass {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
self.prev_state.col_width(self.col).unwrap_or(0.0)
|
||||||
|
};
|
||||||
let height = self.prev_row_height(self.row);
|
let height = self.prev_row_height(self.row);
|
||||||
let size = child_size.max(vec2(width, height));
|
let size = child_size.max(vec2(width, height));
|
||||||
Rect::from_min_size(cursor.min, size).round_ui()
|
Rect::from_min_size(cursor.min, size).round_ui()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// TODO(emilk): have separate types `PositionId` and `UniqueId`. ?
|
// TODO(emilk): have separate types `PositionId` and `UniqueId`. ?
|
||||||
|
|
||||||
use std::num::NonZeroU64;
|
use core::num::NonZeroU64;
|
||||||
|
|
||||||
use crate::{AsIdSalt, IdSalt};
|
use crate::{AsIdSalt, IdSalt};
|
||||||
|
|
||||||
@@ -8,9 +8,9 @@ use crate::{AsIdSalt, IdSalt};
|
|||||||
///
|
///
|
||||||
/// This is all types implementing `Hash` and `Debug`,
|
/// This is all types implementing `Hash` and `Debug`,
|
||||||
/// which includes things like string, integers, tuples of those, etc.
|
/// which includes things like string, integers, tuples of those, etc.
|
||||||
pub trait AsId: std::hash::Hash + std::fmt::Debug {}
|
pub trait AsId: core::hash::Hash + core::fmt::Debug {}
|
||||||
|
|
||||||
impl<T: std::hash::Hash + std::fmt::Debug> AsId for T {}
|
impl<T: core::hash::Hash + core::fmt::Debug> AsId for T {}
|
||||||
|
|
||||||
/// egui tracks widgets frame-to-frame using [`Id`]s.
|
/// egui tracks widgets frame-to-frame using [`Id`]s.
|
||||||
///
|
///
|
||||||
@@ -41,6 +41,13 @@ impl<T: std::hash::Hash + std::fmt::Debug> AsId for T {}
|
|||||||
/// This is niche-optimized to that `Option<Id>` is the same size as `Id`.
|
/// This is niche-optimized to that `Option<Id>` is the same size as `Id`.
|
||||||
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
|
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "serde",
|
||||||
|
expect(
|
||||||
|
clippy::unsafe_derive_deserialize,
|
||||||
|
reason = "`from_high_entropy_bits` is only `unsafe` about entropy, not memory safety"
|
||||||
|
)
|
||||||
|
)]
|
||||||
pub struct Id(NonZeroU64);
|
pub struct Id(NonZeroU64);
|
||||||
|
|
||||||
impl nohash_hasher::IsEnabled for Id {}
|
impl nohash_hasher::IsEnabled for Id {}
|
||||||
@@ -75,7 +82,7 @@ impl Id {
|
|||||||
|
|
||||||
/// Generate a child [`Id`] by salting the parent [`Id`] with the given argument.
|
/// Generate a child [`Id`] by salting the parent [`Id`] with the given argument.
|
||||||
pub fn with(self, salt: impl AsIdSalt) -> Self {
|
pub fn with(self, salt: impl AsIdSalt) -> Self {
|
||||||
use std::hash::{BuildHasher as _, Hasher as _};
|
use core::hash::{BuildHasher as _, Hasher as _};
|
||||||
let mut hasher = ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher();
|
let mut hasher = ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher();
|
||||||
hasher.write_u64(self.value());
|
hasher.write_u64(self.value());
|
||||||
hasher.write_u64(IdSalt::new(&salt).value());
|
hasher.write_u64(IdSalt::new(&salt).value());
|
||||||
@@ -124,8 +131,8 @@ impl Id {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Id {
|
impl core::fmt::Debug for Id {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
if *self == Self::NULL {
|
if *self == Self::NULL {
|
||||||
return write!(f, "Id::NULL");
|
return write!(f, "Id::NULL");
|
||||||
}
|
}
|
||||||
@@ -204,8 +211,8 @@ mod id_source {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn id_size() {
|
fn id_size() {
|
||||||
assert_eq!(std::mem::size_of::<Id>(), 8);
|
assert_eq!(core::mem::size_of::<Id>(), 8);
|
||||||
assert_eq!(std::mem::size_of::<Option<Id>>(), 8);
|
assert_eq!(core::mem::size_of::<Option<Id>>(), 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use std::num::NonZeroU64;
|
use core::num::NonZeroU64;
|
||||||
|
|
||||||
/// Types that can be converted to an [`IdSalt`].
|
/// Types that can be converted to an [`IdSalt`].
|
||||||
///
|
///
|
||||||
/// This is all types implementing `Hash` and `Debug`,
|
/// This is all types implementing `Hash` and `Debug`,
|
||||||
/// which includes things like string, integers, tuples of those, etc.
|
/// which includes things like string, integers, tuples of those, etc.
|
||||||
pub trait AsIdSalt: std::hash::Hash + std::fmt::Debug {}
|
pub trait AsIdSalt: core::hash::Hash + core::fmt::Debug {}
|
||||||
|
|
||||||
impl<T: std::hash::Hash + std::fmt::Debug> AsIdSalt for T {}
|
impl<T: core::hash::Hash + core::fmt::Debug> AsIdSalt for T {}
|
||||||
|
|
||||||
/// Uniquely identifies a child widget within a parent widget.
|
/// Uniquely identifies a child widget within a parent widget.
|
||||||
///
|
///
|
||||||
@@ -57,8 +57,8 @@ impl IdSalt {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for IdSalt {
|
impl core::fmt::Debug for IdSalt {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
if let Some(source) = id_salt_source::get(*self) {
|
if let Some(source) = id_salt_source::get(*self) {
|
||||||
return write!(f, "IdSalt::new({source})");
|
return write!(f, "IdSalt::new({source})");
|
||||||
|
|||||||
@@ -13,10 +13,8 @@ use crate::{
|
|||||||
},
|
},
|
||||||
input_state::wheel_state::WheelState,
|
input_state::wheel_state::WheelState,
|
||||||
};
|
};
|
||||||
use std::{
|
use core::time::Duration;
|
||||||
collections::{BTreeMap, HashSet},
|
use std::collections::{BTreeMap, HashSet};
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub use crate::Key;
|
pub use crate::Key;
|
||||||
pub use touch_state::MultiTouchInfo;
|
pub use touch_state::MultiTouchInfo;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::{collections::BTreeMap, fmt::Debug};
|
use core::fmt::Debug;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Event, RawInput, TouchId, TouchPhase,
|
Event, RawInput, TouchId, TouchPhase,
|
||||||
@@ -305,7 +306,7 @@ impl TouchState {
|
|||||||
|
|
||||||
impl Debug for TouchState {
|
impl Debug for TouchState {
|
||||||
// This outputs less clutter than `#[derive(Debug)]`:
|
// This outputs less clutter than `#[derive(Debug)]`:
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
for (id, touch) in &self.active_touches {
|
for (id, touch) in &self.active_touches {
|
||||||
f.write_fmt(format_args!("#{id:?}: {touch:#?}\n"))?;
|
f.write_fmt(format_args!("#{id:?}: {touch:#?}\n"))?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,7 +197,24 @@ pub(crate) fn interact(
|
|||||||
// This widget is sensitive to both clicks and drags.
|
// This widget is sensitive to both clicks and drags.
|
||||||
// When the mouse first is pressed, it could be either,
|
// When the mouse first is pressed, it could be either,
|
||||||
// so we postpone the decision until we know.
|
// so we postpone the decision until we know.
|
||||||
input.pointer.is_decidedly_dragging()
|
//
|
||||||
|
// …unless the pointer has left the widget: a click has to be
|
||||||
|
// released on the widget, so once the pointer is outside there is
|
||||||
|
// nothing left to wait for.
|
||||||
|
//
|
||||||
|
// Deciding here means a thin drag handle (narrower than
|
||||||
|
// `max_click_dist`) doesn't spend the decision window as neither
|
||||||
|
// hovered nor dragged, which would make its highlight blink out.
|
||||||
|
// The hit-test picks up widgets within `interact_radius`, so
|
||||||
|
// `hits.click` can name such a handle even when the pointer is a
|
||||||
|
// few points outside it.
|
||||||
|
//
|
||||||
|
// A widget on top might "steal" the click hit, but then the pointer is still inside
|
||||||
|
// us, and pressing that button must not start a drag. So we check both.
|
||||||
|
let pointer_is_inside = hits.contains_pointer.iter().any(|w| w.id == widget.id);
|
||||||
|
let could_still_be_clicked =
|
||||||
|
pointer_is_inside || hits.click.is_some_and(|hit| hit.id == widget.id);
|
||||||
|
input.pointer.is_decidedly_dragging() || !could_still_be_clicked
|
||||||
} else {
|
} else {
|
||||||
// This widget is just sensitive to drags, so we can mark it as dragged right away:
|
// This widget is just sensitive to drags, so we can mark it as dragged right away:
|
||||||
widget.sense.senses_drag()
|
widget.sense.senses_drag()
|
||||||
@@ -262,7 +279,7 @@ pub(crate) fn interact(
|
|||||||
let drag_order = hits.drag.and_then(|w| order(w.id)).unwrap_or(0);
|
let drag_order = hits.drag.and_then(|w| order(w.id)).unwrap_or(0);
|
||||||
let top_interactive_order = click_order.max(drag_order);
|
let top_interactive_order = click_order.max(drag_order);
|
||||||
|
|
||||||
let mut hovered: IdSet = std::iter::chain(&hits.click, &hits.drag)
|
let mut hovered: IdSet = core::iter::chain(&hits.click, &hits.drag)
|
||||||
.map(|w| w.id)
|
.map(|w| w.id)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ impl LayerId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for LayerId {
|
impl core::fmt::Debug for LayerId {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
let Self { order, id } = self;
|
let Self { order, id } = self;
|
||||||
write!(f, "LayerId {{ {order:?} {id:?} }}")
|
write!(f, "LayerId {{ {order:?} {id:?} }}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use emath::GuiRounding as _;
|
use emath::{GuiRounding as _, fast_midpoint};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Align, Direction,
|
Align, Direction,
|
||||||
@@ -477,12 +477,12 @@ impl Layout {
|
|||||||
|
|
||||||
// Make sure it isn't negative:
|
// Make sure it isn't negative:
|
||||||
if avail.max.x < avail.min.x {
|
if avail.max.x < avail.min.x {
|
||||||
let x = 0.5 * (avail.min.x + avail.max.x);
|
let x = fast_midpoint(avail.min.x, avail.max.x);
|
||||||
avail.min.x = x;
|
avail.min.x = x;
|
||||||
avail.max.x = x;
|
avail.max.x = x;
|
||||||
}
|
}
|
||||||
if avail.max.y < avail.min.y {
|
if avail.max.y < avail.min.y {
|
||||||
let y = 0.5 * (avail.min.y + avail.max.y);
|
let y = fast_midpoint(avail.min.y, avail.max.y);
|
||||||
avail.min.y = y;
|
avail.min.y = y;
|
||||||
avail.max.y = y;
|
avail.max.y = y;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -474,7 +474,7 @@ pub use self::{
|
|||||||
Key, UserData,
|
Key, UserData,
|
||||||
input::*,
|
input::*,
|
||||||
output::{
|
output::{
|
||||||
self, CursorIcon, CustomCursorImage, FullOutput, OpenUrl, OutputCommand,
|
self, CursorIcon, CustomCursorImage, FullOutput, LogicOutput, OpenUrl, OutputCommand,
|
||||||
PlatformOutput, UserAttentionType, WidgetInfo,
|
PlatformOutput, UserAttentionType, WidgetInfo,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -680,18 +680,20 @@ pub enum WidgetType {
|
|||||||
pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) {
|
pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) {
|
||||||
let ctx = Context::default();
|
let ctx = Context::default();
|
||||||
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
|
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
|
||||||
let _ = ctx.run_ui(Default::default(), |ui| {
|
let output = ctx.run_ui(Default::default(), |ui| {
|
||||||
run_ui(ui.ctx());
|
run_ui(ui.ctx());
|
||||||
});
|
});
|
||||||
|
output.drop_without_applying_deltas();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For use in tests; especially doctests.
|
/// For use in tests; especially doctests.
|
||||||
pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
|
pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
|
||||||
let ctx = Context::default();
|
let ctx = Context::default();
|
||||||
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
|
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
|
||||||
let _ = ctx.run_ui(Default::default(), |ui| {
|
let output = ctx.run_ui(Default::default(), |ui| {
|
||||||
add_contents(ui);
|
add_contents(ui);
|
||||||
});
|
});
|
||||||
|
output.drop_without_applying_deltas();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn accesskit_root_id() -> Id {
|
pub fn accesskit_root_id() -> Id {
|
||||||
|
|||||||
@@ -55,12 +55,11 @@
|
|||||||
mod bytes_loader;
|
mod bytes_loader;
|
||||||
mod texture_loader;
|
mod texture_loader;
|
||||||
|
|
||||||
use std::{
|
use core::{
|
||||||
borrow::Cow,
|
|
||||||
fmt::{Debug, Display},
|
fmt::{Debug, Display},
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
sync::Arc,
|
|
||||||
};
|
};
|
||||||
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
|
||||||
use ahash::HashMap;
|
use ahash::HashMap;
|
||||||
|
|
||||||
@@ -108,13 +107,13 @@ impl LoadError {
|
|||||||
detected_format.as_ref().map_or(0, |s| s.len())
|
detected_format.as_ref().map_or(0, |s| s.len())
|
||||||
}
|
}
|
||||||
Self::Loading(message) => message.len(),
|
Self::Loading(message) => message.len(),
|
||||||
_ => std::mem::size_of::<Self>(),
|
_ => core::mem::size_of::<Self>(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for LoadError {
|
impl Display for LoadError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::NoImageLoaders => f.write_str(
|
Self::NoImageLoaders => f.write_str(
|
||||||
"No image loaders are installed. If you're trying to load some images \
|
"No image loaders are installed. If you're trying to load some images \
|
||||||
@@ -136,9 +135,9 @@ impl Display for LoadError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for LoadError {}
|
impl core::error::Error for LoadError {}
|
||||||
|
|
||||||
pub type Result<T, E = LoadError> = std::result::Result<T, E>;
|
pub type Result<T, E = LoadError> = core::result::Result<T, E>;
|
||||||
|
|
||||||
/// Given as a hint for image loading requests.
|
/// Given as a hint for image loading requests.
|
||||||
///
|
///
|
||||||
@@ -209,7 +208,7 @@ pub enum Bytes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for Bytes {
|
impl Debug for Bytes {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Static(arg0) => f.debug_tuple("Static").field(&arg0.len()).finish(),
|
Self::Static(arg0) => f.debug_tuple("Static").field(&arg0.len()).finish(),
|
||||||
Self::Shared(arg0) => f.debug_tuple("Shared").field(&arg0.len()).finish(),
|
Self::Shared(arg0) => f.debug_tuple("Shared").field(&arg0.len()).finish(),
|
||||||
@@ -307,6 +306,19 @@ macro_rules! generate_loader_id {
|
|||||||
}
|
}
|
||||||
pub use crate::generate_loader_id;
|
pub use crate::generate_loader_id;
|
||||||
|
|
||||||
|
/// Does the given URI end with the given file extension?
|
||||||
|
///
|
||||||
|
/// The comparison ignores ASCII case and any `#fragment` at the end of the URI,
|
||||||
|
/// so `has_extension("cat.GIF#frame=2", "gif")` is `true`.
|
||||||
|
///
|
||||||
|
/// This is useful when implementing an [`ImageLoader`].
|
||||||
|
pub fn has_extension(uri: &str, extension: &str) -> bool {
|
||||||
|
let path = uri.split('#').next().unwrap_or(uri);
|
||||||
|
std::path::Path::new(path)
|
||||||
|
.extension()
|
||||||
|
.is_some_and(|found| found.eq_ignore_ascii_case(extension))
|
||||||
|
}
|
||||||
|
|
||||||
pub type BytesLoadResult = Result<BytesPoll>;
|
pub type BytesLoadResult = Result<BytesPoll>;
|
||||||
|
|
||||||
/// Represents a loader capable of loading raw unstructured bytes from somewhere,
|
/// Represents a loader capable of loading raw unstructured bytes from somewhere,
|
||||||
@@ -387,7 +399,7 @@ pub type ImageLoadResult = Result<ImagePoll>;
|
|||||||
/// An `ImageLoader` decodes raw bytes into a [`ColorImage`].
|
/// An `ImageLoader` decodes raw bytes into a [`ColorImage`].
|
||||||
///
|
///
|
||||||
/// Implementations are expected to cache at least each `URI`.
|
/// Implementations are expected to cache at least each `URI`.
|
||||||
pub trait ImageLoader: std::any::Any {
|
pub trait ImageLoader: core::any::Any {
|
||||||
/// Unique ID of this loader.
|
/// Unique ID of this loader.
|
||||||
///
|
///
|
||||||
/// To reduce the chance of collisions, include `module_path!()` as part of this ID.
|
/// To reduce the chance of collisions, include `module_path!()` as part of this ID.
|
||||||
@@ -640,3 +652,14 @@ impl Loaders {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_has_extension() {
|
||||||
|
assert!(has_extension("cat.svg", "svg"));
|
||||||
|
assert!(has_extension("cat.SVG", "svg"));
|
||||||
|
assert!(has_extension("http://example.com/cat.gif#frame=2", "gif"));
|
||||||
|
assert!(!has_extension("cat.svg.png", "svg"));
|
||||||
|
assert!(!has_extension("svg", "svg"));
|
||||||
|
assert!(!has_extension("cat.jpeg", "jpg"));
|
||||||
|
assert!(!has_extension("cat.svg?v=1", "svg"));
|
||||||
|
}
|
||||||
|
|||||||
@@ -150,5 +150,5 @@ impl TextureLoader for DefaultTextureLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_svg(uri: &str) -> bool {
|
fn is_svg(uri: &str) -> bool {
|
||||||
uri.ends_with(".svg")
|
super::has_extension(uri, "svg")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#![warn(missing_docs)] // Let's keep this file well-documented.` to memory.rs
|
#![warn(missing_docs)] // Let's keep this file well-documented.` to memory.rs
|
||||||
|
|
||||||
use std::num::NonZeroUsize;
|
use core::num::NonZeroUsize;
|
||||||
|
|
||||||
use ahash::{HashMap, HashSet};
|
use ahash::{HashMap, HashSet};
|
||||||
use epaint::emath::TSTransform;
|
use epaint::emath::TSTransform;
|
||||||
@@ -216,6 +216,18 @@ pub struct Options {
|
|||||||
#[cfg_attr(feature = "serde", serde(skip))]
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
pub(crate) system_theme: Option<Theme>,
|
pub(crate) system_theme: Option<Theme>,
|
||||||
|
|
||||||
|
/// If `true`, egui will keep the native window theme in sync with
|
||||||
|
/// [`Self::theme_preference`] by sending a [`crate::ViewportCommand::SetTheme`]
|
||||||
|
/// to the root viewport whenever the preference changes.
|
||||||
|
///
|
||||||
|
/// This makes the native window decorations (title bar, borders, …) match the
|
||||||
|
/// theme selected inside egui.
|
||||||
|
///
|
||||||
|
/// Set this to `false` if you want to manage the native window theme yourself.
|
||||||
|
///
|
||||||
|
/// This is `true` by default.
|
||||||
|
pub sync_window_theme: bool,
|
||||||
|
|
||||||
/// Global zoom factor of the UI.
|
/// Global zoom factor of the UI.
|
||||||
///
|
///
|
||||||
/// This is used to calculate the `pixels_per_point`
|
/// This is used to calculate the `pixels_per_point`
|
||||||
@@ -318,6 +330,7 @@ impl Default for Options {
|
|||||||
theme_preference: Default::default(),
|
theme_preference: Default::default(),
|
||||||
fallback_theme: Theme::Dark,
|
fallback_theme: Theme::Dark,
|
||||||
system_theme: None,
|
system_theme: None,
|
||||||
|
sync_window_theme: true,
|
||||||
zoom_factor: 1.0,
|
zoom_factor: 1.0,
|
||||||
zoom_with_keyboard: true,
|
zoom_with_keyboard: true,
|
||||||
quit_shortcuts: vec![crate::KeyboardShortcut::new(
|
quit_shortcuts: vec![crate::KeyboardShortcut::new(
|
||||||
@@ -381,6 +394,7 @@ impl Options {
|
|||||||
theme_preference,
|
theme_preference,
|
||||||
fallback_theme: _,
|
fallback_theme: _,
|
||||||
system_theme: _,
|
system_theme: _,
|
||||||
|
sync_window_theme,
|
||||||
zoom_factor,
|
zoom_factor,
|
||||||
zoom_with_keyboard,
|
zoom_with_keyboard,
|
||||||
quit_shortcuts: _, // not shown in ui
|
quit_shortcuts: _, // not shown in ui
|
||||||
@@ -429,6 +443,8 @@ impl Options {
|
|||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
theme_preference.radio_buttons(ui);
|
theme_preference.radio_buttons(ui);
|
||||||
|
|
||||||
|
ui.checkbox(sync_window_theme, "Sync window theme with egui theme");
|
||||||
|
|
||||||
let style = std::sync::Arc::make_mut(match theme {
|
let style = std::sync::Arc::make_mut(match theme {
|
||||||
Theme::Dark => dark_style,
|
Theme::Dark => dark_style,
|
||||||
Theme::Light => light_style,
|
Theme::Light => light_style,
|
||||||
@@ -926,7 +942,7 @@ impl Memory {
|
|||||||
if let Some(modal_layer) = self.focus().and_then(|f| f.top_modal_layer) {
|
if let Some(modal_layer) = self.focus().and_then(|f| f.top_modal_layer) {
|
||||||
matches!(
|
matches!(
|
||||||
self.areas().compare_order(layer_id, modal_layer),
|
self.areas().compare_order(layer_id, modal_layer),
|
||||||
std::cmp::Ordering::Equal | std::cmp::Ordering::Greater
|
core::cmp::Ordering::Equal | core::cmp::Ordering::Greater
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
@@ -966,7 +982,7 @@ impl Memory {
|
|||||||
if let Some(current) = self.focus().and_then(|f| f.top_modal_layer_current_frame)
|
if let Some(current) = self.focus().and_then(|f| f.top_modal_layer_current_frame)
|
||||||
&& matches!(
|
&& matches!(
|
||||||
self.areas().compare_order(layer_id, current),
|
self.areas().compare_order(layer_id, current),
|
||||||
std::cmp::Ordering::Less
|
core::cmp::Ordering::Less
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -1194,6 +1210,11 @@ impl Areas {
|
|||||||
self.areas.get_mut(&id)
|
self.areas.get_mut(&id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Can the user interact with this layer or it's widgets, or do clicks go straight through it?
|
||||||
|
pub(crate) fn is_interactable(&self, layer_id: LayerId) -> bool {
|
||||||
|
self.get(layer_id.id).is_none_or(|area| area.interactable)
|
||||||
|
}
|
||||||
|
|
||||||
/// All layers back-to-front, top is last.
|
/// All layers back-to-front, top is last.
|
||||||
pub(crate) fn order(&self) -> &[LayerId] {
|
pub(crate) fn order(&self) -> &[LayerId] {
|
||||||
&self.order
|
&self.order
|
||||||
@@ -1202,12 +1223,12 @@ impl Areas {
|
|||||||
/// Compare the order of two layers, based on the order list from last frame.
|
/// Compare the order of two layers, based on the order list from last frame.
|
||||||
///
|
///
|
||||||
/// May return [`std::cmp::Ordering::Equal`] if the layers are not in the order list.
|
/// May return [`std::cmp::Ordering::Equal`] if the layers are not in the order list.
|
||||||
pub(crate) fn compare_order(&self, a: LayerId, b: LayerId) -> std::cmp::Ordering {
|
pub(crate) fn compare_order(&self, a: LayerId, b: LayerId) -> core::cmp::Ordering {
|
||||||
// Sort by layer `order` first and use `order_map` to resolve disputes.
|
// Sort by layer `order` first and use `order_map` to resolve disputes.
|
||||||
// If `order_map` only contains one layer ID, then the other one will be
|
// If `order_map` only contains one layer ID, then the other one will be
|
||||||
// lower because `None < Some(x)`.
|
// lower because `None < Some(x)`.
|
||||||
match a.order.cmp(&b.order) {
|
match a.order.cmp(&b.order) {
|
||||||
std::cmp::Ordering::Equal => self.order_map.get(&a).cmp(&self.order_map.get(&b)),
|
core::cmp::Ordering::Equal => self.order_map.get(&a).cmp(&self.order_map.get(&b)),
|
||||||
cmp => cmp,
|
cmp => cmp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1255,7 +1276,7 @@ impl Areas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn visible_layer_ids(&self) -> ahash::HashSet<LayerId> {
|
pub fn visible_layer_ids(&self) -> ahash::HashSet<LayerId> {
|
||||||
std::iter::chain(
|
core::iter::chain(
|
||||||
&self.visible_areas_last_frame,
|
&self.visible_areas_last_frame,
|
||||||
&self.visible_areas_current_frame,
|
&self.visible_areas_current_frame,
|
||||||
)
|
)
|
||||||
@@ -1344,7 +1365,7 @@ impl Areas {
|
|||||||
..
|
..
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
std::mem::swap(visible_areas_last_frame, visible_areas_current_frame);
|
core::mem::swap(visible_areas_last_frame, visible_areas_current_frame);
|
||||||
visible_areas_current_frame.clear();
|
visible_areas_current_frame.clear();
|
||||||
|
|
||||||
order.sort_by_key(|layer| (layer.order, wants_to_be_on_top.contains(layer)));
|
order.sort_by_key(|layer| (layer.order, wants_to_be_on_top.contains(layer)));
|
||||||
@@ -1353,7 +1374,7 @@ impl Areas {
|
|||||||
// For all layers with sublayers, put the sublayers directly after the parent layer:
|
// For all layers with sublayers, put the sublayers directly after the parent layer:
|
||||||
// (it doesn't matter in which order we replace parents with their children)
|
// (it doesn't matter in which order we replace parents with their children)
|
||||||
#[expect(clippy::iter_over_hash_type)]
|
#[expect(clippy::iter_over_hash_type)]
|
||||||
for (parent, children) in std::mem::take(sublayers) {
|
for (parent, children) in core::mem::take(sublayers) {
|
||||||
let mut moved_layers = vec![parent]; // parent first…
|
let mut moved_layers = vec![parent]; // parent first…
|
||||||
|
|
||||||
order.retain(|l| {
|
order.retain(|l| {
|
||||||
@@ -1462,14 +1483,14 @@ fn order_map_total_ordering() {
|
|||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
for &[a, b] in layers.array_windows() {
|
for &[a, b] in layers.array_windows() {
|
||||||
assert!(a.order <= b.order, "does not follow LayerId.order");
|
assert!(a.order <= b.order, "does not follow LayerId.order");
|
||||||
if areas.compare_order(a, b) != std::cmp::Ordering::Equal {
|
if areas.compare_order(a, b) != core::cmp::Ordering::Equal {
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
equivalence_classes.push(i);
|
equivalence_classes.push(i);
|
||||||
}
|
}
|
||||||
assert_eq!(layers.len(), equivalence_classes.len());
|
assert_eq!(layers.len(), equivalence_classes.len());
|
||||||
for (&l1, c1) in std::iter::zip(&layers, &equivalence_classes) {
|
for (&l1, c1) in core::iter::zip(&layers, &equivalence_classes) {
|
||||||
for (&l2, c2) in std::iter::zip(&layers, &equivalence_classes) {
|
for (&l2, c2) in core::iter::zip(&layers, &equivalence_classes) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
c1.cmp(c2),
|
c1.cmp(c2),
|
||||||
areas.compare_order(l1, l2),
|
areas.compare_order(l1, l2),
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ impl Painter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn error(&self, pos: Pos2, text: impl std::fmt::Display) -> Rect {
|
pub fn error(&self, pos: Pos2, text: impl core::fmt::Display) -> Rect {
|
||||||
let color = self.ctx.global_style().visuals.error_fg_color;
|
let color = self.ctx.global_style().visuals.error_fg_color;
|
||||||
self.debug_text(pos, Align2::LEFT_TOP, color, format!("🔥 {text}"))
|
self.debug_text(pos, Align2::LEFT_TOP, color, format!("🔥 {text}"))
|
||||||
}
|
}
|
||||||
@@ -416,7 +416,7 @@ impl Painter {
|
|||||||
/// Show an arrow starting at `origin` and going in the direction of `vec`, with the length `vec.length()`.
|
/// Show an arrow starting at `origin` and going in the direction of `vec`, with the length `vec.length()`.
|
||||||
pub fn arrow(&self, origin: Pos2, vec: Vec2, stroke: impl Into<Stroke>) {
|
pub fn arrow(&self, origin: Pos2, vec: Vec2, stroke: impl Into<Stroke>) {
|
||||||
use crate::emath::Rot2;
|
use crate::emath::Rot2;
|
||||||
let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0);
|
let rot = Rot2::from_angle(core::f32::consts::TAU / 10.0);
|
||||||
let tip_length = vec.length() / 4.0;
|
let tip_length = vec.length() / 4.0;
|
||||||
let tip = origin + vec;
|
let tip = origin + vec;
|
||||||
let dir = vec.normalized();
|
let dir = vec.normalized();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use std::sync::Arc;
|
|||||||
/// Plugins should not hold a reference to the [`Context`], since this would create a cycle
|
/// Plugins should not hold a reference to the [`Context`], since this would create a cycle
|
||||||
/// (which would prevent the [`Context`] from being dropped).
|
/// (which would prevent the [`Context`] from being dropped).
|
||||||
#[expect(unused_variables)]
|
#[expect(unused_variables)]
|
||||||
pub trait Plugin: Send + Sync + std::any::Any + 'static {
|
pub trait Plugin: Send + Sync + core::any::Any + 'static {
|
||||||
/// Plugin name.
|
/// Plugin name.
|
||||||
///
|
///
|
||||||
/// Used when profiling.
|
/// Used when profiling.
|
||||||
@@ -60,14 +60,14 @@ pub(crate) struct PluginHandle {
|
|||||||
/// Use [`Self::lock`] to access the plugin.
|
/// Use [`Self::lock`] to access the plugin.
|
||||||
pub struct TypedPluginHandle<P: Plugin> {
|
pub struct TypedPluginHandle<P: Plugin> {
|
||||||
handle: Arc<Mutex<PluginHandle>>,
|
handle: Arc<Mutex<PluginHandle>>,
|
||||||
_type: std::marker::PhantomData<P>,
|
_type: core::marker::PhantomData<P>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P: Plugin> TypedPluginHandle<P> {
|
impl<P: Plugin> TypedPluginHandle<P> {
|
||||||
pub(crate) fn new(handle: Arc<Mutex<PluginHandle>>) -> Self {
|
pub(crate) fn new(handle: Arc<Mutex<PluginHandle>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
handle,
|
handle,
|
||||||
_type: std::marker::PhantomData,
|
_type: core::marker::PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ impl<P: Plugin> TypedPluginHandle<P> {
|
|||||||
pub fn lock(&self) -> TypedPluginGuard<'_, P> {
|
pub fn lock(&self) -> TypedPluginGuard<'_, P> {
|
||||||
TypedPluginGuard {
|
TypedPluginGuard {
|
||||||
guard: self.handle.lock(),
|
guard: self.handle.lock(),
|
||||||
_type: std::marker::PhantomData,
|
_type: core::marker::PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,12 +85,12 @@ impl<P: Plugin> TypedPluginHandle<P> {
|
|||||||
/// A guard that provides access to a [`Plugin`].
|
/// A guard that provides access to a [`Plugin`].
|
||||||
pub struct TypedPluginGuard<'a, P: Plugin> {
|
pub struct TypedPluginGuard<'a, P: Plugin> {
|
||||||
guard: MutexGuard<'a, PluginHandle>,
|
guard: MutexGuard<'a, PluginHandle>,
|
||||||
_type: std::marker::PhantomData<P>,
|
_type: core::marker::PhantomData<P>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P: Plugin> TypedPluginGuard<'_, P> {}
|
impl<P: Plugin> TypedPluginGuard<'_, P> {}
|
||||||
|
|
||||||
impl<P: Plugin> std::ops::Deref for TypedPluginGuard<'_, P> {
|
impl<P: Plugin> core::ops::Deref for TypedPluginGuard<'_, P> {
|
||||||
type Target = P;
|
type Target = P;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
@@ -98,7 +98,7 @@ impl<P: Plugin> std::ops::Deref for TypedPluginGuard<'_, P> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P: Plugin> std::ops::DerefMut for TypedPluginGuard<'_, P> {
|
impl<P: Plugin> core::ops::DerefMut for TypedPluginGuard<'_, P> {
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
self.guard.typed_plugin_mut()
|
self.guard.typed_plugin_mut()
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ impl PluginHandle {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn plugin_type_id(&self) -> std::any::TypeId {
|
fn plugin_type_id(&self) -> core::any::TypeId {
|
||||||
(*self.plugin).type_id()
|
(*self.plugin).type_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,13 +120,13 @@ impl PluginHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn typed_plugin<P: Plugin + 'static>(&self) -> &P {
|
fn typed_plugin<P: Plugin + 'static>(&self) -> &P {
|
||||||
(self.plugin.as_ref() as &dyn std::any::Any)
|
(self.plugin.as_ref() as &dyn core::any::Any)
|
||||||
.downcast_ref::<P>()
|
.downcast_ref::<P>()
|
||||||
.expect("PluginHandle: plugin is not of the expected type")
|
.expect("PluginHandle: plugin is not of the expected type")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn typed_plugin_mut<P: Plugin + 'static>(&mut self) -> &mut P {
|
pub fn typed_plugin_mut<P: Plugin + 'static>(&mut self) -> &mut P {
|
||||||
(self.plugin.as_mut() as &mut dyn std::any::Any)
|
(self.plugin.as_mut() as &mut dyn core::any::Any)
|
||||||
.downcast_mut::<P>()
|
.downcast_mut::<P>()
|
||||||
.expect("PluginHandle: plugin is not of the expected type")
|
.expect("PluginHandle: plugin is not of the expected type")
|
||||||
}
|
}
|
||||||
@@ -135,7 +135,7 @@ impl PluginHandle {
|
|||||||
/// User-registered plugins.
|
/// User-registered plugins.
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub(crate) struct Plugins {
|
pub(crate) struct Plugins {
|
||||||
plugins: HashMap<std::any::TypeId, Arc<Mutex<PluginHandle>>>,
|
plugins: HashMap<core::any::TypeId, Arc<Mutex<PluginHandle>>>,
|
||||||
plugins_ordered: PluginsOrdered,
|
plugins_ordered: PluginsOrdered,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ impl Plugins {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, type_id: std::any::TypeId) -> Option<Arc<Mutex<PluginHandle>>> {
|
pub fn get(&self, type_id: core::any::TypeId) -> Option<Arc<Mutex<PluginHandle>>> {
|
||||||
self.plugins.get(&type_id).cloned()
|
self.plugins.get(&type_id).cloned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::{any::Any, sync::Arc};
|
use core::any::Any;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Context, CursorIcon, Id, LayerId, PointerButton, Popup, PopupKind, Sense, Tooltip, Ui,
|
Context, CursorIcon, Id, LayerId, PointerButton, Popup, PopupKind, Sense, Tooltip, Ui,
|
||||||
@@ -77,7 +78,7 @@ pub struct Response {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_response_size() {
|
fn test_response_size() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
std::mem::size_of::<Response>(),
|
core::mem::size_of::<Response>(),
|
||||||
88,
|
88,
|
||||||
"Keep Response small, because we create them often, and we want to keep it lean and fast"
|
"Keep Response small, because we create them often, and we want to keep it lean and fast"
|
||||||
);
|
);
|
||||||
@@ -309,6 +310,12 @@ impl Response {
|
|||||||
///
|
///
|
||||||
/// In contrast to [`Self::contains_pointer`], this will be `false` whenever some other widget is being dragged.
|
/// In contrast to [`Self::contains_pointer`], this will be `false` whenever some other widget is being dragged.
|
||||||
/// `hovered` is always `false` for disabled widgets.
|
/// `hovered` is always `false` for disabled widgets.
|
||||||
|
///
|
||||||
|
/// While a widget is being clicked or dragged it is the only hovered widget,
|
||||||
|
/// so this stays `true` even after the pointer moves off it. Together with
|
||||||
|
/// how [`Self::dragged`] resolves a press that leaves the widget, that means
|
||||||
|
/// `hovered() || dragged()` holds for a whole press-drag-release gesture,
|
||||||
|
/// which is what you want for highlighting something like a drag handle.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn hovered(&self) -> bool {
|
pub fn hovered(&self) -> bool {
|
||||||
self.flags.contains(Flags::HOVERED)
|
self.flags.contains(Flags::HOVERED)
|
||||||
@@ -403,11 +410,21 @@ impl Response {
|
|||||||
/// To find out which button(s), use [`Self::dragged_by`].
|
/// To find out which button(s), use [`Self::dragged_by`].
|
||||||
///
|
///
|
||||||
/// If the widget is only sensitive to drags, this is `true` as soon as the pointer presses down on it.
|
/// If the widget is only sensitive to drags, this is `true` as soon as the pointer presses down on it.
|
||||||
/// If the widget also senses clicks, this won't be true until the pointer has moved a bit,
|
///
|
||||||
/// or the user has pressed down for long enough.
|
/// If the widget also senses clicks, the press could be either, so the
|
||||||
|
/// decision is postponed until whichever of these comes first:
|
||||||
|
/// * the pointer moves further than [`crate::InputOptions::max_click_dist`],
|
||||||
|
/// * it is held longer than [`crate::InputOptions::max_click_duration`],
|
||||||
|
/// * or it leaves the widget — a click has to be released on the widget, so
|
||||||
|
/// once the pointer is outside, the gesture can only be a drag. This is what
|
||||||
|
/// keeps a handle thinner than `max_click_dist` from spending the decision
|
||||||
|
/// window as neither hovered nor dragged.
|
||||||
|
///
|
||||||
/// See [`crate::input_state::PointerState::is_decidedly_dragging`] for details.
|
/// See [`crate::input_state::PointerState::is_decidedly_dragging`] for details.
|
||||||
///
|
///
|
||||||
/// If you want to avoid the delay, use [`Self::is_pointer_button_down_on`] instead.
|
/// While the decision is pending the pointer is still on the widget, so
|
||||||
|
/// [`Self::hovered`] is `true` throughout. If you want neither the delay nor
|
||||||
|
/// the distinction, use [`Self::is_pointer_button_down_on`].
|
||||||
///
|
///
|
||||||
/// If the widget is NOT sensitive to drags, this will always be `false`.
|
/// If the widget is NOT sensitive to drags, this will always be `false`.
|
||||||
/// [`crate::DragValue`] senses drags; [`crate::Label`] does not (unless you call [`crate::Label::sense`]).
|
/// [`crate::DragValue`] senses drags; [`crate::Label`] does not (unless you call [`crate::Label::sense`]).
|
||||||
@@ -571,6 +588,9 @@ impl Response {
|
|||||||
/// even when dragging outside the widget.
|
/// even when dragging outside the widget.
|
||||||
///
|
///
|
||||||
/// This could also be thought of as "is this widget being interacted with?".
|
/// This could also be thought of as "is this widget being interacted with?".
|
||||||
|
///
|
||||||
|
/// Unlike [`Self::dragged`], this is `true` from the press frame onwards, with
|
||||||
|
/// no click-versus-drag decision window.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn is_pointer_button_down_on(&self) -> bool {
|
pub fn is_pointer_button_down_on(&self) -> bool {
|
||||||
self.flags.contains(Flags::IS_POINTER_BUTTON_DOWN_ON)
|
self.flags.contains(Flags::IS_POINTER_BUTTON_DOWN_ON)
|
||||||
@@ -1093,7 +1113,7 @@ impl Response {
|
|||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// Now `draw_vec2(ui, foo).hovered` is true if either [`DragValue`](crate::DragValue) were hovered.
|
/// Now `draw_vec2(ui, foo).hovered` is true if either [`DragValue`](crate::DragValue) were hovered.
|
||||||
impl std::ops::BitOr for Response {
|
impl core::ops::BitOr for Response {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
fn bitor(self, rhs: Self) -> Self {
|
fn bitor(self, rhs: Self) -> Self {
|
||||||
@@ -1114,7 +1134,7 @@ impl std::ops::BitOr for Response {
|
|||||||
/// if response.hovered() { ui.label("You hovered at least one of the widgets"); }
|
/// if response.hovered() { ui.label("You hovered at least one of the widgets"); }
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
impl std::ops::BitOrAssign for Response {
|
impl core::ops::BitOrAssign for Response {
|
||||||
fn bitor_assign(&mut self, rhs: Self) {
|
fn bitor_assign(&mut self, rhs: Self) {
|
||||||
*self = self.union(rhs);
|
*self = self.union(rhs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ bitflags::bitflags! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Sense {
|
impl core::fmt::Debug for Sense {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
write!(f, "Sense {{")?;
|
write!(f, "Sense {{")?;
|
||||||
if self.senses_click() {
|
if self.senses_click() {
|
||||||
write!(f, " click")?;
|
write!(f, " click")?;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
//! egui theme (spacing, colors, etc).
|
//! egui theme (spacing, colors, etc).
|
||||||
|
|
||||||
|
use core::ops::RangeInclusive;
|
||||||
use emath::Align;
|
use emath::Align;
|
||||||
use epaint::{
|
use epaint::{
|
||||||
CornerRadius, FontColorTransferFunction, Shadow, Stroke, TextOptions,
|
CornerRadius, FontColorTransferFunction, Shadow, Stroke, TextOptions,
|
||||||
text::{FontTweak, FontVariationAxis, HintingTarget, SmoothHinting},
|
text::{FontTweak, FontVariationAxis, HintingTarget, SmoothHinting},
|
||||||
};
|
};
|
||||||
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
|
use std::{collections::BTreeMap, sync::Arc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ComboBox, CursorIcon, FontFamily, FontId, Grid, Margin, Response, RichText, TextWrapMode,
|
ComboBox, CursorIcon, FontFamily, FontId, Grid, Margin, Response, RichText, TextWrapMode,
|
||||||
@@ -47,8 +48,8 @@ impl NumberFormatter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for NumberFormatter {
|
impl core::fmt::Debug for NumberFormatter {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
f.write_str("NumberFormatter")
|
f.write_str("NumberFormatter")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,8 +94,8 @@ pub enum TextStyle {
|
|||||||
Name(std::sync::Arc<str>),
|
Name(std::sync::Arc<str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for TextStyle {
|
impl core::fmt::Display for TextStyle {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Small => "Small".fmt(f),
|
Self::Small => "Small".fmt(f),
|
||||||
Self::Body => "Body".fmt(f),
|
Self::Body => "Body".fmt(f),
|
||||||
@@ -192,8 +193,8 @@ impl From<TextStyle> for FontSelection {
|
|||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct StyleModifier(Option<Arc<dyn Fn(&mut Style) + Send + Sync>>);
|
pub struct StyleModifier(Option<Arc<dyn Fn(&mut Style) + Send + Sync>>);
|
||||||
|
|
||||||
impl std::fmt::Debug for StyleModifier {
|
impl core::fmt::Debug for StyleModifier {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
f.write_str("StyleModifier")
|
f.write_str("StyleModifier")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -419,6 +420,9 @@ pub struct Spacing {
|
|||||||
/// Default width of a [`crate::TextEdit`].
|
/// Default width of a [`crate::TextEdit`].
|
||||||
pub text_edit_width: f32,
|
pub text_edit_width: f32,
|
||||||
|
|
||||||
|
/// Additional vertical spacing between lines of text.
|
||||||
|
pub extra_text_line_spacing: f32,
|
||||||
|
|
||||||
/// Checkboxes, radio button and collapsing headers have an icon at the start.
|
/// Checkboxes, radio button and collapsing headers have an icon at the start.
|
||||||
/// This is the width/height of the outer part of this icon (e.g. the BOX of the checkbox).
|
/// This is the width/height of the outer part of this icon (e.g. the BOX of the checkbox).
|
||||||
pub icon_width: f32,
|
pub icon_width: f32,
|
||||||
@@ -1074,10 +1078,12 @@ pub struct Visuals {
|
|||||||
/// How the text cursor acts.
|
/// How the text cursor acts.
|
||||||
pub text_cursor: TextCursorStyle,
|
pub text_cursor: TextCursorStyle,
|
||||||
|
|
||||||
/// Allow widgets to paint this much outside the scroll area rect.
|
/// Unused. Kept only for backwards compatibility.
|
||||||
///
|
///
|
||||||
/// Legacy. Should not be used anymore.
|
/// Used to allow widgets to paint this much outside the scroll area rect.
|
||||||
|
/// Setting it now has no effect.
|
||||||
/// Use [`crate::ScrollArea::content_margin`] instead.
|
/// Use [`crate::ScrollArea::content_margin`] instead.
|
||||||
|
#[deprecated(note = "This is now unused and has no effect")]
|
||||||
pub clip_rect_margin: f32,
|
pub clip_rect_margin: f32,
|
||||||
|
|
||||||
/// Show a background behind buttons.
|
/// Show a background behind buttons.
|
||||||
@@ -1456,6 +1462,7 @@ impl Default for Spacing {
|
|||||||
slider_rail_height: 8.0,
|
slider_rail_height: 8.0,
|
||||||
combo_width: 100.0,
|
combo_width: 100.0,
|
||||||
text_edit_width: 280.0,
|
text_edit_width: 280.0,
|
||||||
|
extra_text_line_spacing: 0.0,
|
||||||
icon_width: 14.0,
|
icon_width: 14.0,
|
||||||
icon_width_inner: 8.0,
|
icon_width_inner: 8.0,
|
||||||
icon_spacing: 4.0,
|
icon_spacing: 4.0,
|
||||||
@@ -1487,6 +1494,7 @@ impl Default for Interaction {
|
|||||||
|
|
||||||
impl Visuals {
|
impl Visuals {
|
||||||
/// Default dark theme.
|
/// Default dark theme.
|
||||||
|
#[expect(deprecated)]
|
||||||
pub fn dark() -> Self {
|
pub fn dark() -> Self {
|
||||||
Self {
|
Self {
|
||||||
dark_mode: true,
|
dark_mode: true,
|
||||||
@@ -1945,6 +1953,7 @@ impl Spacing {
|
|||||||
slider_rail_height,
|
slider_rail_height,
|
||||||
combo_width,
|
combo_width,
|
||||||
text_edit_width,
|
text_edit_width,
|
||||||
|
extra_text_line_spacing,
|
||||||
icon_width,
|
icon_width,
|
||||||
icon_width_inner,
|
icon_width_inner,
|
||||||
icon_spacing,
|
icon_spacing,
|
||||||
@@ -2011,6 +2020,10 @@ impl Spacing {
|
|||||||
ui.add(DragValue::new(text_edit_width).range(0.0..=1000.0));
|
ui.add(DragValue::new(text_edit_width).range(0.0..=1000.0));
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("Extra text line spacing");
|
||||||
|
ui.add(DragValue::new(extra_text_line_spacing).range(0.0..=20.0));
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
ui.label("Tooltip wrap width");
|
ui.label("Tooltip wrap width");
|
||||||
ui.add(DragValue::new(tooltip_width).range(0.0..=1000.0));
|
ui.add(DragValue::new(tooltip_width).range(0.0..=1000.0));
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
@@ -2263,6 +2276,7 @@ impl WidgetVisuals {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Visuals {
|
impl Visuals {
|
||||||
|
#[expect(deprecated)]
|
||||||
pub fn ui(&mut self, ui: &mut crate::Ui) {
|
pub fn ui(&mut self, ui: &mut crate::Ui) {
|
||||||
let Self {
|
let Self {
|
||||||
dark_mode,
|
dark_mode,
|
||||||
@@ -2297,7 +2311,7 @@ impl Visuals {
|
|||||||
|
|
||||||
text_cursor,
|
text_cursor,
|
||||||
|
|
||||||
clip_rect_margin,
|
clip_rect_margin: _,
|
||||||
button_frame,
|
button_frame,
|
||||||
collapsing_header_frame,
|
collapsing_header_frame,
|
||||||
indent_has_left_vline,
|
indent_has_left_vline,
|
||||||
@@ -2484,8 +2498,6 @@ impl Visuals {
|
|||||||
|
|
||||||
ui.collapsing("Misc", |ui| {
|
ui.collapsing("Misc", |ui| {
|
||||||
ui.add(Slider::new(resize_corner_size, 0.0..=20.0).text("resize_corner_size"));
|
ui.add(Slider::new(resize_corner_size, 0.0..=20.0).text("resize_corner_size"));
|
||||||
ui.add(Slider::new(clip_rect_margin, 0.0..=20.0).text("clip_rect_margin"));
|
|
||||||
|
|
||||||
ui.checkbox(button_frame, "Button has a frame");
|
ui.checkbox(button_frame, "Button has a frame");
|
||||||
ui.checkbox(collapsing_header_frame, "Collapsing header has a frame");
|
ui.checkbox(collapsing_header_frame, "Collapsing header has a frame");
|
||||||
ui.checkbox(
|
ui.checkbox(
|
||||||
@@ -2684,7 +2696,7 @@ impl DebugOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO(emilk): improve and standardize
|
// TODO(emilk): improve and standardize
|
||||||
fn two_drag_values(value: &mut Vec2, range: std::ops::RangeInclusive<f32>) -> impl Widget + '_ {
|
fn two_drag_values(value: &mut Vec2, range: core::ops::RangeInclusive<f32>) -> impl Widget + '_ {
|
||||||
move |ui: &mut crate::Ui| {
|
move |ui: &mut crate::Ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.add(
|
ui.add(
|
||||||
@@ -2753,8 +2765,8 @@ impl NumericColorSpace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for NumericColorSpace {
|
impl core::fmt::Display for NumericColorSpace {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::GammaByte => write!(f, "U8"),
|
Self::GammaByte => write!(f, "U8"),
|
||||||
Self::Linear => write!(f, "F"),
|
Self::Linear => write!(f, "F"),
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ impl CCursorRange {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The range of selected character indices.
|
/// The range of selected character indices.
|
||||||
pub fn as_sorted_char_range(&self) -> std::ops::Range<CharIndex> {
|
pub fn as_sorted_char_range(&self) -> core::ops::Range<CharIndex> {
|
||||||
let [start, end] = self.sorted_cursors();
|
let [start, end] = self.sorted_cursors();
|
||||||
std::ops::Range {
|
core::ops::Range {
|
||||||
start: start.index,
|
start: start.index,
|
||||||
end: end.index,
|
end: end.index,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ fn pos_in_galley(galley: &Galley, ccursor: CCursor) -> Pos2 {
|
|||||||
galley.pos_from_cursor(ccursor).center()
|
galley.pos_from_cursor(ccursor).center()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for WidgetTextCursor {
|
impl core::fmt::Debug for WidgetTextCursor {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
let Self {
|
let Self {
|
||||||
widget_id,
|
widget_id,
|
||||||
ccursor,
|
ccursor,
|
||||||
@@ -271,7 +271,7 @@ impl ViewportLabelSelectionState {
|
|||||||
self.is_dragging = false;
|
self.is_dragging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let text_to_copy = std::mem::take(&mut self.text_to_copy);
|
let text_to_copy = core::mem::take(&mut self.text_to_copy);
|
||||||
if !text_to_copy.is_empty() {
|
if !text_to_copy.is_empty() {
|
||||||
ui.copy_text(text_to_copy);
|
ui.copy_text(text_to_copy);
|
||||||
}
|
}
|
||||||
@@ -773,7 +773,7 @@ mod tests {
|
|||||||
.or_default()
|
.or_default()
|
||||||
.selection = Some(test_selection());
|
.selection = Some(test_selection());
|
||||||
|
|
||||||
let _ = ctx.run_ui(RawInput::default(), |_| {});
|
let output = ctx.run_ui(RawInput::default(), |_| {});
|
||||||
assert!(
|
assert!(
|
||||||
plugin
|
plugin
|
||||||
.lock()
|
.lock()
|
||||||
@@ -782,11 +782,13 @@ mod tests {
|
|||||||
.is_some_and(ViewportLabelSelectionState::has_selection),
|
.is_some_and(ViewportLabelSelectionState::has_selection),
|
||||||
"a pass in another viewport must not clear the child viewport selection"
|
"a pass in another viewport must not clear the child viewport selection"
|
||||||
);
|
);
|
||||||
|
output.drop_without_applying_deltas();
|
||||||
|
|
||||||
let _ = ctx.run_ui(child_viewport_input(child_viewport_id), |_| {});
|
let output = ctx.run_ui(child_viewport_input(child_viewport_id), |_| {});
|
||||||
assert!(
|
assert!(
|
||||||
!plugin.lock().has_selection(),
|
!plugin.lock().has_selection(),
|
||||||
"the selection must be cleared when its labels disappear from the same viewport"
|
"the selection must be cleared when its labels disappear from the same viewport"
|
||||||
);
|
);
|
||||||
|
output.drop_without_applying_deltas();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ pub fn char_index_from_byte_index(input: &str, byte_index: ByteIndex) -> CharInd
|
|||||||
CharIndex(input.chars().count())
|
CharIndex(input.chars().count())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn slice_char_range(s: &str, char_range: std::ops::Range<CharIndex>) -> &str {
|
pub fn slice_char_range(s: &str, char_range: core::ops::Range<CharIndex>) -> &str {
|
||||||
assert!(
|
assert!(
|
||||||
char_range.start <= char_range.end,
|
char_range.start <= char_range.end,
|
||||||
"Invalid range, start must be less than end, but start = {}, end = {}",
|
"Invalid range, start must be less than end, but start = {}, end = {}",
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ pub(crate) fn paint_ime_preedit_text_visuals(
|
|||||||
painter: &Painter,
|
painter: &Painter,
|
||||||
galley: &Arc<Galley>,
|
galley: &Arc<Galley>,
|
||||||
row_height: f32,
|
row_height: f32,
|
||||||
preedit_range: std::ops::Range<CCursor>,
|
preedit_range: core::ops::Range<CCursor>,
|
||||||
mut relative_active_range: Option<std::ops::Range<CCursor>>,
|
mut relative_active_range: Option<core::ops::Range<CCursor>>,
|
||||||
time_since_last_interaction: f64,
|
time_since_last_interaction: f64,
|
||||||
) {
|
) {
|
||||||
/// Instead of implementing [`PartialOrd`] and [`Ord`] for [`CCursor`] to
|
/// Instead of implementing [`PartialOrd`] and [`Ord`] for [`CCursor`] to
|
||||||
@@ -150,7 +150,7 @@ pub(crate) fn paint_ime_preedit_text_visuals(
|
|||||||
/// These traits are intentionally not implemented because
|
/// These traits are intentionally not implemented because
|
||||||
/// [`CCursor::prefer_next_row`] makes it difficult to define a clear
|
/// [`CCursor::prefer_next_row`] makes it difficult to define a clear
|
||||||
/// ordering between two [`CCursor`]s.
|
/// ordering between two [`CCursor`]s.
|
||||||
fn is_cursor_range_empty(range: &std::ops::Range<CCursor>) -> bool {
|
fn is_cursor_range_empty(range: &core::ops::Range<CCursor>) -> bool {
|
||||||
range.start.index == range.end.index
|
range.start.index == range.end.index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
#![warn(missing_docs)] // Let's keep `Ui` well-documented.
|
#![warn(missing_docs)] // Let's keep `Ui` well-documented.
|
||||||
#![expect(clippy::use_self)]
|
#![expect(clippy::use_self)]
|
||||||
|
|
||||||
use std::{any::Any, ops::Deref, sync::Arc};
|
use core::{any::Any, ops::Deref};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::containers::menu;
|
use crate::containers::menu;
|
||||||
use crate::widget_style::{HasClasses as _, ROOT_CLASS};
|
use crate::widget_style::{HasClasses as _, ROOT_CLASS};
|
||||||
@@ -1984,7 +1985,7 @@ impl Ui {
|
|||||||
/// but is shown to the user in fractions of one Tau (i.e. fractions of one turn).
|
/// but is shown to the user in fractions of one Tau (i.e. fractions of one turn).
|
||||||
/// The angle is NOT wrapped, so the user may select, for instance 2𝞃 (720°)
|
/// The angle is NOT wrapped, so the user may select, for instance 2𝞃 (720°)
|
||||||
pub fn drag_angle_tau(&mut self, radians: &mut f32) -> Response {
|
pub fn drag_angle_tau(&mut self, radians: &mut f32) -> Response {
|
||||||
use std::f32::consts::TAU;
|
use core::f32::consts::TAU;
|
||||||
|
|
||||||
let mut taus = *radians / TAU;
|
let mut taus = *radians / TAU;
|
||||||
let mut response = self.add(DragValue::new(&mut taus).speed(0.01).suffix("τ"));
|
let mut response = self.add(DragValue::new(&mut taus).speed(0.01).suffix("τ"));
|
||||||
@@ -2599,7 +2600,7 @@ impl Ui {
|
|||||||
let column_width = (self.available_width() - total_spacing) / (NUM_COL as f32);
|
let column_width = (self.available_width() - total_spacing) / (NUM_COL as f32);
|
||||||
let top_left = self.cursor().min;
|
let top_left = self.cursor().min;
|
||||||
|
|
||||||
let mut columns = std::array::from_fn(|col_idx| {
|
let mut columns = core::array::from_fn(|col_idx| {
|
||||||
let pos = top_left + vec2((col_idx as f32) * (column_width + spacing), 0.0);
|
let pos = top_left + vec2((col_idx as f32) * (column_width + spacing), 0.0);
|
||||||
let child_rect = Rect::from_min_max(
|
let child_rect = Rect::from_min_max(
|
||||||
pos,
|
pos,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
use core::{any::Any, iter::FusedIterator};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{any::Any, iter::FusedIterator};
|
|
||||||
|
|
||||||
use crate::widget_style::Classes;
|
use crate::widget_style::Classes;
|
||||||
use epaint::Color32;
|
use epaint::Color32;
|
||||||
|
|||||||
@@ -16,15 +16,15 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<K, V> std::fmt::Debug for FixedCache<K, V> {
|
impl<K, V> core::fmt::Debug for FixedCache<K, V> {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
write!(f, "Cache")
|
write!(f, "Cache")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<K, V> FixedCache<K, V>
|
impl<K, V> FixedCache<K, V>
|
||||||
where
|
where
|
||||||
K: std::hash::Hash + PartialEq,
|
K: core::hash::Hash + PartialEq,
|
||||||
{
|
{
|
||||||
pub fn get(&self, key: &K) -> Option<&V> {
|
pub fn get(&self, key: &K) -> Option<&V> {
|
||||||
let bucket = (hash(key) % (FIXED_CACHE_SIZE as u64)) as usize;
|
let bucket = (hash(key) % (FIXED_CACHE_SIZE as u64)) as usize;
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
// For non-serializable types, these simply return `None`.
|
// For non-serializable types, these simply return `None`.
|
||||||
// This will also allow users to pick their own serialization format per type.
|
// This will also allow users to pick their own serialization format per type.
|
||||||
|
|
||||||
use std::{any::Any, sync::Arc};
|
use core::any::Any;
|
||||||
|
use std::sync::Arc;
|
||||||
// -----------------------------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Like [`std::any::TypeId`], but can be serialized and deserialized.
|
/// Like [`std::any::TypeId`], but can be serialized and deserialized.
|
||||||
@@ -14,7 +15,7 @@ pub struct TypeId(u64);
|
|||||||
impl TypeId {
|
impl TypeId {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn of<T: Any + 'static>() -> Self {
|
pub fn of<T: Any + 'static>() -> Self {
|
||||||
std::any::TypeId::of::<T>().into()
|
core::any::TypeId::of::<T>().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
@@ -23,9 +24,9 @@ impl TypeId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<std::any::TypeId> for TypeId {
|
impl From<core::any::TypeId> for TypeId {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from(id: std::any::TypeId) -> Self {
|
fn from(id: core::any::TypeId) -> Self {
|
||||||
Self(epaint::util::hash(id))
|
Self(epaint::util::hash(id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,8 +114,8 @@ impl Clone for Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for Element {
|
impl core::fmt::Debug for Element {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
match &self {
|
match &self {
|
||||||
Self::Value { value, .. } => f
|
Self::Value { value, .. } => f
|
||||||
.debug_struct("Element::Value")
|
.debug_struct("Element::Value")
|
||||||
@@ -314,7 +315,7 @@ fn from_ron_str<T: serde::de::DeserializeOwned>(ron: &str) -> Option<T> {
|
|||||||
Err(_err) => {
|
Err(_err) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"egui: Failed to deserialize {} from memory: {}, ron error: {:?}",
|
"egui: Failed to deserialize {} from memory: {}, ron error: {:?}",
|
||||||
std::any::type_name::<T>(),
|
core::any::type_name::<T>(),
|
||||||
_err,
|
_err,
|
||||||
ron
|
ron
|
||||||
);
|
);
|
||||||
@@ -578,7 +579,7 @@ impl IdTypeMap {
|
|||||||
pub fn remove_temp<T: 'static + Default>(&mut self, id: Id) -> Option<T> {
|
pub fn remove_temp<T: 'static + Default>(&mut self, id: Id) -> Option<T> {
|
||||||
let key = RawKey::new::<T>(id);
|
let key = RawKey::new::<T>(id);
|
||||||
let mut element = self.map.remove(&key)?;
|
let mut element = self.map.remove(&key)?;
|
||||||
Some(std::mem::take(element.get_mut_temp()?))
|
Some(core::mem::take(element.get_mut_temp()?))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a temporary value given a raw key.
|
/// Remove a temporary value given a raw key.
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ pub struct Undoer<State> {
|
|||||||
flux: Option<Flux<State>>,
|
flux: Option<Flux<State>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<State> std::fmt::Debug for Undoer<State> {
|
impl<State> core::fmt::Debug for Undoer<State> {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
let Self { undos, redos, .. } = self;
|
let Self { undos, redos, .. } = self;
|
||||||
f.debug_struct("Undoer")
|
f.debug_struct("Undoer")
|
||||||
.field("undo count", &undos.len())
|
.field("undo count", &undos.len())
|
||||||
|
|||||||
@@ -71,9 +71,8 @@
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use epaint::{Pos2, Vec2};
|
|
||||||
|
|
||||||
use crate::{AsId, Context, Id, Ui};
|
use crate::{AsId, Context, Id, Ui};
|
||||||
|
use epaint::{Pos2, Vec2};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -121,13 +120,13 @@ pub struct ViewportId(pub Id);
|
|||||||
// We implement `PartialOrd` and `Ord` so we can use `ViewportId` in a `BTreeMap`,
|
// We implement `PartialOrd` and `Ord` so we can use `ViewportId` in a `BTreeMap`,
|
||||||
// which allows predicatable iteration order, frame-to-frame.
|
// which allows predicatable iteration order, frame-to-frame.
|
||||||
impl PartialOrd for ViewportId {
|
impl PartialOrd for ViewportId {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
|
||||||
Some(self.cmp(other))
|
Some(self.cmp(other))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ord for ViewportId {
|
impl Ord for ViewportId {
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
|
||||||
self.0.value().cmp(&other.0.value())
|
self.0.value().cmp(&other.0.value())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,8 +138,8 @@ impl Default for ViewportId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for ViewportId {
|
impl core::fmt::Debug for ViewportId {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
self.0.short_debug_format().fmt(f)
|
self.0.short_debug_format().fmt(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,8 +198,8 @@ impl IconData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for IconData {
|
impl core::fmt::Debug for IconData {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
f.debug_struct("IconData")
|
f.debug_struct("IconData")
|
||||||
.field("width", &self.width)
|
.field("width", &self.width)
|
||||||
.field("height", &self.height)
|
.field("height", &self.height)
|
||||||
@@ -1276,7 +1275,7 @@ pub struct ViewportOutput {
|
|||||||
/// but if you haven't, you can use this instead.
|
/// but if you haven't, you can use this instead.
|
||||||
///
|
///
|
||||||
/// If the duration is zero, schedule a repaint immediately.
|
/// If the duration is zero, schedule a repaint immediately.
|
||||||
pub repaint_delay: std::time::Duration,
|
pub repaint_delay: core::time::Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ViewportOutput {
|
impl ViewportOutput {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user