1
0
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:
Lucas Meurer
2026-08-21 11:33:30 +02:00
252 changed files with 4993 additions and 1936 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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));
}
}
}
} }

View File

@@ -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);

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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:?}");
} }
} }

View File

@@ -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)

View File

@@ -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",

View File

@@ -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> {

View File

@@ -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>;

View File

@@ -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,
} );
} }
} }

View File

@@ -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);
} }

View File

@@ -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
} }

View File

@@ -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,
) )
}; };

View File

@@ -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);

View File

@@ -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};

View File

@@ -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(),
}) })
} }

View File

@@ -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!();

View File

@@ -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: {}",

View 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)
}
}

View 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();
} }
})?; })?;

View File

@@ -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)),
) )

View File

@@ -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);
} }
} }

View File

@@ -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()
} }

View File

@@ -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>;

View File

@@ -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);
} }

View File

@@ -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);
} }
} }

View File

@@ -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

View File

@@ -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)

View File

@@ -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);

View File

@@ -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);

View File

@@ -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:?}");

View File

@@ -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())

View File

@@ -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,

View File

@@ -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);
} }
} }

View File

@@ -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)

View 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())
}
}

View File

@@ -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`.

View File

@@ -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"] }

View 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:?})"),

View File

@@ -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));
} }
} }

View File

@@ -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));
} }
} }

View File

@@ -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]",

View File

@@ -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);

View File

@@ -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);

View File

@@ -1,4 +1,4 @@
use std::hash::Hash; use core::hash::Hash;
use super::CacheTrait; use super::CacheTrait;

View File

@@ -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);
} }

View File

@@ -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));

View File

@@ -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)
} }
} }

View File

@@ -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());

View File

@@ -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!"
); );
} }

View File

@@ -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())
} }

View File

@@ -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,

View File

@@ -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

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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();
} }
} }
} }

View File

@@ -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>;

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;

View File

@@ -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,
} }

View File

@@ -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 {

View File

@@ -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:?}"))
} }
}); });

View File

@@ -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,

View File

@@ -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")
} }

View File

@@ -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);
} }
} }

View File

@@ -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};

View File

@@ -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()

View File

@@ -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)]

View File

@@ -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})");

View File

@@ -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;

View File

@@ -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"))?;
} }

View File

@@ -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();

View File

@@ -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:?} }}")
} }

View File

@@ -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;
} }

View File

@@ -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 {

View File

@@ -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"));
}

View File

@@ -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")
} }

View File

@@ -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),

View File

@@ -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();

View File

@@ -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()
} }
} }

View File

@@ -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);
} }

View File

@@ -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")?;

View File

@@ -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"),

View File

@@ -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,
} }

View File

@@ -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();
} }
} }

View File

@@ -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 = {}",

View File

@@ -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
} }

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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.

View File

@@ -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())

View File

@@ -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